Skip to content

fix(scheduler): preserve recurring schedule progress#289

Open
yordis wants to merge 3 commits into
mainfrom
yordis/fix-rrule-scheduling
Open

fix(scheduler): preserve recurring schedule progress#289
yordis wants to merge 3 commits into
mainfrom
yordis/fix-rrule-scheduling

Conversation

@yordis

@yordis yordis commented Jun 15, 2026

Copy link
Copy Markdown
Member
  • Prevent recurring schedules from silently stopping after creation or a fired occurrence.
  • Protect scheduler durability at the recurrence boundary that must advance through persisted state and live NATS scheduling.
  • Give read models and external links a stable accepted-occurrence identity without coupling them to retry timing.

@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Implements end-to-end RRULE recurring schedule lifecycle: adds three new protobuf event messages and two State progress fields, a non-zero occurrence-sequence domain type, an RRULE expansion cursor engine, RecordScheduleOccurrence and ScheduleNextOccurrence commands, DispatchRequest and RRuleWakeupProcessor, reconciler branching for occurrence/completion events, ExecutionScheduleWriter::dispatch, a new event_store generic dependency on ScheduleProcessor, and a live NATS integration test.

Changes

RRULE Schedule Occurrence Lifecycle

Layer / File(s) Summary
New protobuf schemas and codec wiring
proto/trogonai/scheduler/schedules/v1/schedule_occurrence_recorded.proto, proto/.../schedule_occurrence_scheduled.proto, proto/.../schedule_completed.proto, proto/.../events.proto, proto/.../state/v1/state.proto, rsworkspace/crates/trogonai-proto/src/scheduler/schedules/codec.rs
Defines ScheduleOccurrenceRecorded, ScheduleOccurrenceScheduled, and ScheduleCompleted proto messages; adds last_occurrence_at/last_occurrence_sequence to State; wires new cases into encode/decode/event_type codec functions and updates the snapshot round-trip fixture.
ScheduleOccurrenceSequence domain type
rsworkspace/crates/trogon-scheduler/src/commands/domain/schedule_occurrence_sequence.rs, rsworkspace/crates/trogon-scheduler/src/commands/domain/mod.rs
Adds ScheduleOccurrenceSequence non-zero u64 newtype with try_new/next_after (overflow-checked) and ScheduleOccurrenceSequenceError (Zero/Overflow).
State evolve() for occurrence events
rsworkspace/crates/trogon-scheduler/src/commands/state.rs
Extends EvolveError with MissingOccurrenceAt/MissingOccurrenceSequence; evolve() now carries occurrence-progress fields forward, resets them on ScheduleCreated, updates on ScheduleOccurrenceRecorded, sets pending on ScheduleOccurrenceScheduled, and clears pending on remove/pause.
RRULE expansion engine and wakeup payload
rsworkspace/crates/trogon-scheduler/src/commands/rrule.rs, rsworkspace/.../reconciliation/rrule_wakeup_payload.rs, rsworkspace/.../reconciliation/schedule_subject.rs, rsworkspace/.../reconciliation/mod.rs
Adds RRuleCursor (AtOrAfter/After), next_rrule_occurrence building RRuleSet with rdate/exdate, schedule_or_complete_event, rrule_timezone; introduces RRuleWakeupPayload with RFC3339 storage and JSON encode/decode; adds RRULE_WAKEUP_SUBJECT_PREFIX and ScheduleSubject::rrule_wakeup.
ScheduleNextOccurrence command
rsworkspace/crates/trogon-scheduler/src/commands/schedule_next_occurrence.rs
Decider that reads durable state, rejects already-armed schedules, applies a 5-minute past-occurrence grace window for cursor selection, computes next RRULE occurrence, and emits ScheduleOccurrenceScheduled or ScheduleCompleted.
RecordScheduleOccurrence command
rsworkspace/crates/trogon-scheduler/src/commands/record_schedule_occurrence.rs, rsworkspace/.../commands/mod.rs, rsworkspace/crates/trogon-scheduler/src/lib.rs
Decider enforcing occurrence monotonicity, pending-occurrence match, sequence advancement via next_after, and RRULE next-occurrence expansion; emits ScheduleOccurrenceRecorded plus follow-up. Exports new command types publicly.
Existing command test fixture updates
rsworkspace/crates/trogon-scheduler/src/commands/create_schedule.rs, pause_schedule.rs, remove_schedule.rs, resume_schedule.rs
Updates decide_rejects_invalid_state_values fixtures in existing commands to supply the expanded state_v1::State shape with last_occurrence_at: MessageField::default().
DispatchRequest type and ScheduleRequest refactor
rsworkspace/crates/trogon-scheduler/src/processor/execution/reconciliation/request.rs
Adds DispatchRequest with build/build_occurrence constructors (source rejection, occurrence-metadata headers, reserved-header filtering); refactors ScheduleRequest builders extracting centralized at_schedule_value helper and explicit timezone precomputation.
Reconciler occurrence/completion handling and decode extensions
rsworkspace/.../reconciliation/reconcile.rs, rsworkspace/.../reconciliation/recorded_events.rs
Extends ScheduleChange with OccurrenceRecorded/OccurrenceScheduled/Completed and ReconcileAction with Dispatch(DispatchRequest); changes Schedule::RRule branch to ArmNext; adds reconciliation arms for all new variants; extends decode_schedule_change and adds OccurrenceSequence error variant.
ExecutionScheduleWriter dispatch
rsworkspace/crates/trogon-scheduler/src/processor/execution/execution_schedules/mod.rs, rsworkspace/.../execution/mod.rs
Adds dispatch() async method publishing a single message with build_dispatch_headers (trace headers + request headers + Nats-Msg-Id), reusing existing publish/ack error model. Exposes wakeup submodule.
RRuleWakeupProcessor and consumer config
rsworkspace/crates/trogon-scheduler/src/processor/execution/wakeup.rs
Adds RRuleWakeupProcessor::process decoding payload, validating subject, executing RecordScheduleOccurrence, mapping outcomes; adds rrule_wakeup_consumer_config returning a JetStream pull consumer with RRULE filter and durable name.
ScheduleProcessor event-store dependency and dispatcher propagation
rsworkspace/.../worker/processor.rs, rsworkspace/.../worker/dispatcher.rs, rsworkspace/.../worker/testkit.rs
Adds event_store: E generic to ScheduleProcessor and ScheduleProcessor::new; introduces apply_action helper routing Publish/Dispatch/Purge/ArmNext/CheckpointOnly; propagates E through spawn_dispatcher/run/process_one/poison_record; adds MemoryEventStore to testkit.
Live NATS RRULE integration test
rsworkspace/crates/trogon-scheduler/src/processor/execution/nats_execution_tests.rs
Adds ignored integration test driving the full RRULE lifecycle end-to-end against a live NATS server, asserting wakeup publication, user dispatch headers, and final Expired checkpoint state.

Sequence Diagram

sequenceDiagram
  participant EventStream
  participant ScheduleProcessor
  participant Reconciler
  participant ExecutionScheduleWriter
  participant RRuleWakeupProcessor
  participant RecordScheduleOccurrence
  participant UserSubject

  EventStream->>ScheduleProcessor: ScheduleCreated (RRule)
  ScheduleProcessor->>Reconciler: reconcile(Created)
  Reconciler-->>ScheduleProcessor: ArmNext
  ScheduleProcessor->>ScheduleNextOccurrence: execute via event_store
  ScheduleNextOccurrence-->>EventStream: ScheduleOccurrenceScheduled appended

  EventStream->>ScheduleProcessor: ScheduleOccurrenceScheduled
  ScheduleProcessor->>Reconciler: reconcile(OccurrenceScheduled)
  Reconciler-->>ScheduleProcessor: Publish(RRuleWakeupRequest)
  ScheduleProcessor->>ExecutionScheduleWriter: upsert RRULE wakeup `@at` message

  Note over RRuleWakeupProcessor: NATS JetStream delivers wakeup
  RRuleWakeupProcessor->>RecordScheduleOccurrence: execute via event_store
  RecordScheduleOccurrence-->>EventStream: ScheduleOccurrenceRecorded + ScheduleOccurrenceScheduled appended

  EventStream->>ScheduleProcessor: ScheduleOccurrenceRecorded
  ScheduleProcessor->>Reconciler: reconcile(OccurrenceRecorded)
  Reconciler-->>ScheduleProcessor: Dispatch(DispatchRequest)
  ScheduleProcessor->>ExecutionScheduleWriter: dispatch user message
  ExecutionScheduleWriter->>UserSubject: publish with Trogon-Schedule-Occurrence-* headers

  EventStream->>ScheduleProcessor: ScheduleCompleted (final occurrence)
  ScheduleProcessor->>Reconciler: reconcile(Completed)
  Reconciler-->>ScheduleProcessor: Purge + Expired checkpoint
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • TrogonStack/trogonai#209: Modifies the same scheduler execution processor stack (worker/processor and reconciliation/checkpoint-based execution-schedule publishing) that this PR extends for RRULE/wakeup dispatch behavior.
  • TrogonStack/trogonai#192: Introduced the scheduler command/state layer that this PR builds on, including commands/state.rs evolve logic and the existing decider test shape that is updated here for the new State fields.
  • TrogonStack/trogonai#194: Adds the ResumeSchedule command whose test fixtures are updated in this PR to account for the expanded persisted state_v1::State shape.

Poem

🐇 Hop, hop, the wakeup arrives on time,
An RRule set blooming, each tick in rhyme.
The occurrence recorded, the sequence ticks up,
A dispatch flies out to fill every cup.
When the last recurrence fades into Expired,
This rabbit declares: the scheduler's admired! 🌿

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch yordis/fix-rrule-scheduling

@cursor

cursor Bot commented Jun 15, 2026

Copy link
Copy Markdown

PR Summary

High Risk
Changes core schedule event sourcing, RRULE expansion, NATS scheduling/wakeup subjects, and checkpoint reconciliation—any bug can drop occurrences, double-dispatch, or leave stale timers.

Overview
RRULE schedules no longer stop after create or a fired tick. The aggregate owns recurrence math and durable progress; execution only arms, publishes @at wakeups, records firings, and dispatches user messages.

Schedule state gains last_occurrence_*, embedded schedule, and pending_occurrence_at. The event stream adds ScheduleOccurrenceRecorded, ScheduleOccurrenceScheduled, and ScheduleCompleted, folding occurrence planning into the same aggregate history as create/pause/resume.

New commands ScheduleNextOccurrence (arm first/next occurrence) and RecordScheduleOccurrence (accept a wakeup, emit next plan or completion) use shared rrule expansion. RRuleWakeupProcessor consumes internal wakeup subjects and appends RecordScheduleOccurrence with idempotent handling for stale/paused schedules.

The execution processor arms RRULE on create/resume (ArmNext), publishes planned @at timers to scheduler-owned wakeup targets, dispatches user payloads on recorded occurrences (with Trogon-Schedule-Occurrence-* headers), and purges on completion. Live NATS integration tests cover the full loop.

Reviewed by Cursor Bugbot for commit 1c1248b. Bugbot is set up for automated code reviews on this repo. Configure here.

@github-actions

github-actions Bot commented Jun 15, 2026

Copy link
Copy Markdown

badge

Code Coverage Summary

Details
Filename                                                                                  Stmts    Miss  Cover    Missing
--------------------------------------------------------------------------------------  -------  ------  -------  ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
crates/trogon-scheduler/src/processor/execution/reconciliation/go_duration.rs                59       0  100.00%
crates/trogon-scheduler/src/processor/execution/reconciliation/reconcile.rs                 739      13  98.24%   247-256, 304-306
crates/trogon-scheduler/src/processor/execution/reconciliation/rrule_wakeup_payload.rs       18       0  100.00%
crates/trogon-scheduler/src/processor/execution/reconciliation/recorded_events.rs           663      11  98.34%   200-205, 250, 265, 284, 290, 296, 329, 339, 357, 441
crates/trogon-scheduler/src/processor/execution/reconciliation/request.rs                   540       2  99.63%   273, 278
crates/trogon-scheduler/src/processor/execution/reconciliation/schedule_subject.rs           58       0  100.00%
crates/trogon-scheduler/src/processor/execution/reconciliation/schedule_key.rs               67       0  100.00%
crates/trogon-std/src/dirs/fixed.rs                                                          80       0  100.00%
crates/trogon-std/src/dirs/system.rs                                                         71       0  100.00%
crates/trogon-gateway/src/source/linear/server.rs                                           386       0  100.00%
crates/trogon-gateway/src/source/linear/signature.rs                                         54       1  98.15%   16
crates/trogon-gateway/src/source/linear/config.rs                                            17       0  100.00%
crates/mcp-nats/src/nats/parsing.rs                                                         191       0  100.00%
crates/mcp-nats/src/nats/mod.rs                                                              99       0  100.00%
crates/trogon-nats/src/lease/renew_interval.rs                                               57       0  100.00%
crates/trogon-nats/src/lease/ttl.rs                                                          68       0  100.00%
crates/trogon-nats/src/lease/acquire.rs                                                       5       5  0.00%    9-14
crates/trogon-nats/src/lease/lease_bucket.rs                                                 19       0  100.00%
crates/trogon-nats/src/lease/lease_key.rs                                                    19       0  100.00%
crates/trogon-nats/src/lease/mod.rs                                                         523      13  97.51%   113-126
crates/trogon-nats/src/lease/nats_kv_lease_config.rs                                         26       0  100.00%
crates/trogon-nats/src/lease/lease_timing.rs                                                 15       0  100.00%
crates/trogon-nats/src/lease/release.rs                                                       5       5  0.00%    8-12
crates/trogon-nats/src/lease/provision.rs                                                   187      10  94.65%   82-92
crates/trogon-nats/src/lease/renew.rs                                                       246      19  92.28%   23-29, 48-59
crates/mcp-nats/src/nats/subjects/client/create_elicitation.rs                               12       0  100.00%
crates/mcp-nats/src/nats/subjects/client/progress.rs                                         12       0  100.00%
crates/mcp-nats/src/nats/subjects/client/ping.rs                                              9       0  100.00%
crates/mcp-nats/src/nats/subjects/client/cancelled.rs                                        12       0  100.00%
crates/mcp-nats/src/nats/subjects/client/create_message.rs                                   12       0  100.00%
crates/mcp-nats/src/nats/subjects/client/roots_list_changed.rs                               12       0  100.00%
crates/mcp-nats/src/nats/subjects/client/initialized.rs                                      12       0  100.00%
crates/mcp-nats/src/nats/subjects/client/list_roots.rs                                       12       0  100.00%
crates/mcp-nats/src/nats/subjects/subscriptions/one_server.rs                                 9       0  100.00%
crates/mcp-nats/src/nats/subjects/subscriptions/all_client.rs                                 6       0  100.00%
crates/mcp-nats/src/nats/subjects/subscriptions/all_server.rs                                 6       0  100.00%
crates/mcp-nats/src/nats/subjects/subscriptions/one_client.rs                                 9       0  100.00%
crates/acp-nats-server/src/connection.rs                                                    182      36  80.22%   95-102, 107-122, 138, 140-141, 146, 155-156, 161, 165, 169, 172, 180, 184, 187, 190-194, 232
crates/acp-nats-server/src/transport.rs                                                    1915     106  94.46%   253, 512, 530, 557, 611, 616, 635, 647, 766, 789-791, 843, 860-863, 958-961, 1035, 1038, 1041, 1050, 1054, 1057, 1060-1063, 1082, 1114-1117, 1125-1130, 1142-1146, 1150-1159, 1171-1172, 1190-1191, 1201, 1217-1221, 1249-1255, 1275-1277, 1282-1286, 1289-1294, 1311, 1313-1314, 1396-1397, 1409-1410, 1430-1431, 1483-1499, 2204, 2248, 2301, 2357, 2370
crates/acp-nats-server/src/acp_connection_id.rs                                              37       0  100.00%
crates/acp-nats-server/src/config.rs                                                        126       3  97.62%   41-43
crates/acp-nats-server/src/main.rs                                                          896      10  98.88%   100, 231-238, 437
crates/mcp-nats/src/mcp_prefix.rs                                                            34       0  100.00%
crates/mcp-nats/src/server.rs                                                                31       0  100.00%
crates/mcp-nats/src/client.rs                                                                31       0  100.00%
crates/mcp-nats/src/jsonrpc.rs                                                               22       0  100.00%
crates/mcp-nats/src/transport.rs                                                            698       0  100.00%
crates/mcp-nats/src/config.rs                                                               110       0  100.00%
crates/mcp-nats/src/mcp_peer_id.rs                                                           31       0  100.00%
crates/trogon-std/src/telemetry/http.rs                                                     217       0  100.00%
crates/trogon-std/src/duration.rs                                                            42       0  100.00%
crates/trogon-std/src/json.rs                                                                30       0  100.00%
crates/trogon-std/src/args.rs                                                                19       9  52.63%   11-28
crates/trogon-std/src/secret_string.rs                                                       32       0  100.00%
crates/trogon-std/src/signal.rs                                                              26      12  53.85%   6-11, 18-25, 34
crates/trogon-std/src/uuid.rs                                                                 7       0  100.00%
crates/trogon-std/src/http.rs                                                                19       0  100.00%
crates/acp-nats/src/agent/list_sessions.rs                                                   47       0  100.00%
crates/acp-nats/src/agent/set_session_model.rs                                               67       0  100.00%
crates/acp-nats/src/agent/initialize.rs                                                      79       0  100.00%
crates/acp-nats/src/agent/cancel.rs                                                         101       0  100.00%
crates/acp-nats/src/agent/prompt.rs                                                         471       0  100.00%
crates/acp-nats/src/agent/bridge.rs                                                         123       4  96.75%   108-111
crates/acp-nats/src/agent/fork_session.rs                                                    94       0  100.00%
crates/acp-nats/src/agent/js_request.rs                                                     283       0  100.00%
crates/acp-nats/src/agent/set_session_config_option.rs                                       67       0  100.00%
crates/acp-nats/src/agent/authenticate.rs                                                    49       0  100.00%
crates/acp-nats/src/agent/test_support.rs                                                   267       0  100.00%
crates/acp-nats/src/agent/set_session_mode.rs                                                67       0  100.00%
crates/acp-nats/src/agent/logout.rs                                                          49       0  100.00%
crates/acp-nats/src/agent/resume_session.rs                                                  90       0  100.00%
crates/acp-nats/src/agent/ext_method.rs                                                      82       0  100.00%
crates/acp-nats/src/agent/load_session.rs                                                    89       0  100.00%
crates/acp-nats/src/agent/ext_notification.rs                                                82       0  100.00%
crates/acp-nats/src/agent/new_session.rs                                                     82       0  100.00%
crates/acp-nats/src/agent/mod.rs                                                             65       0  100.00%
crates/acp-nats/src/agent/close_session.rs                                                   63       0  100.00%
crates/acp-nats/src/nats/subjects/global/authenticate.rs                                      6       0  100.00%
crates/acp-nats/src/nats/subjects/global/logout.rs                                            6       0  100.00%
crates/acp-nats/src/nats/subjects/global/session_new.rs                                       6       0  100.00%
crates/acp-nats/src/nats/subjects/global/initialize.rs                                        6       0  100.00%
crates/acp-nats/src/nats/subjects/global/ext_notify.rs                                        9       0  100.00%
crates/acp-nats/src/nats/subjects/global/session_list.rs                                      6       0  100.00%
crates/acp-nats/src/nats/subjects/global/ext.rs                                               9       0  100.00%
crates/trogon-gateway/src/source/sentry/sentry_client_secret.rs                              17       0  100.00%
crates/trogon-gateway/src/source/sentry/server.rs                                           308       0  100.00%
crates/trogon-gateway/src/source/sentry/signature.rs                                         42       0  100.00%
crates/trogon-decider/src/events.rs                                                          49       0  100.00%
crates/trogon-decider/src/act.rs                                                             62       0  100.00%
crates/trogon-decider/src/testing.rs                                                        654       0  100.00%
crates/trogon-decider/src/decision.rs                                                        27       0  100.00%
crates/trogon-decider/src/lib.rs                                                            138       0  100.00%
crates/trogon-gateway/src/source/github/server.rs                                           328       0  100.00%
crates/trogon-gateway/src/source/github/signature.rs                                         50       0  100.00%
crates/trogon-gateway/src/source/github/config.rs                                            17       0  100.00%
crates/trogon-gateway/src/source/discord/config.rs                                          105       0  100.00%
crates/trogon-gateway/src/source/discord/gateway.rs                                         426       1  99.77%   137
crates/trogon-scheduler/src/processor/execution/worker/testkit.rs                           330       4  98.79%   459, 490-491, 496
crates/trogon-scheduler/src/processor/execution/worker/dispatcher.rs                        941      12  98.72%   185-186, 197, 201-202, 609, 949-955
crates/trogon-scheduler/src/processor/execution/worker/processor.rs                        1165      17  98.54%   279, 339, 437-438, 444, 495-506, 538-541
crates/trogon-scheduler/src/processor/execution/worker/consumer.rs                          203       0  100.00%
crates/trogon-scheduler/src/telemetry/metrics.rs                                             52       0  100.00%
crates/trogon-scheduler/src/telemetry/trace.rs                                               41       0  100.00%
crates/trogon-std/src/env/system.rs                                                          17       0  100.00%
crates/trogon-std/src/env/in_memory.rs                                                       73       0  100.00%
crates/mcp-nats/src/telemetry/transport.rs                                                    6       0  100.00%
crates/trogon-nats/src/jetstream/traits.rs                                                   46      40  13.04%   181-251
crates/trogon-nats/src/jetstream/stream_max_age.rs                                           18       0  100.00%
crates/trogon-nats/src/jetstream/mocks.rs                                                  1686       1  99.94%   505
crates/trogon-nats/src/jetstream/create_conflicts.rs                                         24       0  100.00%
crates/trogon-nats/src/jetstream/publish.rs                                                  64       0  100.00%
crates/trogon-nats/src/jetstream/claim_check.rs                                             405      10  97.53%   45-47, 116-122
crates/trogon-std/src/time/system.rs                                                         31       0  100.00%
crates/trogon-std/src/time/mock.rs                                                          125       0  100.00%
crates/acp-nats/src/nats/subjects/mod.rs                                                    362       0  100.00%
crates/acp-nats/src/nats/subjects/stream.rs                                                  56       0  100.00%
crates/mcp-nats/src/nats/subjects/server/cancel_task.rs                                      12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/get_task.rs                                         12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/elicitation_completed.rs                            12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/set_logging_level.rs                                12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/logging_message.rs                                  12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/prompt_list_changed.rs                              12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/call_tool.rs                                        12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/list_tools.rs                                       12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/read_resource.rs                                    12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/list_prompts.rs                                     12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/cancelled.rs                                        12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/ping.rs                                              9       0  100.00%
crates/mcp-nats/src/nats/subjects/server/resource_list_changed.rs                            12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/get_prompt.rs                                       12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/resource_updated.rs                                 12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/tool_list_changed.rs                                12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/list_resource_templates.rs                          12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/list_tasks.rs                                       12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/get_task_result.rs                                  12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/list_resources.rs                                   12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/initialize.rs                                       12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/complete.rs                                         12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/unsubscribe_resource.rs                             12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/subscribe_resource.rs                               12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/progress.rs                                         12       0  100.00%
crates/trogon-gateway/src/source/incidentio/server.rs                                       343       0  100.00%
crates/trogon-gateway/src/source/incidentio/incidentio_event_type.rs                         62       0  100.00%
crates/trogon-gateway/src/source/incidentio/config.rs                                        16       0  100.00%
crates/trogon-gateway/src/source/incidentio/incidentio_signing_secret.rs                     56       0  100.00%
crates/trogon-gateway/src/source/incidentio/signature.rs                                    206       0  100.00%
crates/trogon-scheduler/src/processor/execution/wakeup.rs                                   335       4  98.81%   104, 367, 379, 559
crates/trogon-scheduler/src/processor/execution/execution_schedules/mod.rs                  270       0  100.00%
crates/trogon-decider-runtime/src/snapshot/codec/snapshot_envelope_encode_error.rs           14       0  100.00%
crates/trogon-decider-runtime/src/snapshot/codec/snapshot_decode_error.rs                    49       0  100.00%
crates/trogon-decider-runtime/src/snapshot/codec/snapshot_payload_decode.rs                   3       0  100.00%
crates/trogon-decider-runtime/src/snapshot/codec/snapshot_encode_error.rs                    36       0  100.00%
crates/trogon-decider-runtime/src/snapshot/codec/snapshot_envelope_decode_error.rs           28       0  100.00%
crates/trogon-decider-runtime/src/snapshot/codec/encoded_snapshot.rs                        117       0  100.00%
crates/acp-nats-stdio/src/main.rs                                                           135      25  81.48%   65, 113-120, 126-128, 145, 174-193
crates/acp-nats-stdio/src/config.rs                                                          66       0  100.00%
crates/acp-nats/src/telemetry/metrics.rs                                                     53       0  100.00%
crates/acp-nats/src/jsonrpc.rs                                                                6       0  100.00%
crates/acp-nats/src/req_id.rs                                                                39       0  100.00%
crates/acp-nats/src/config.rs                                                               203       0  100.00%
crates/acp-nats/src/ext_method_name.rs                                                       65       0  100.00%
crates/acp-nats/src/client_proxy.rs                                                         181       0  100.00%
crates/acp-nats/src/lib.rs                                                                   69       0  100.00%
crates/acp-nats/src/pending_prompt_waiters.rs                                               131       0  100.00%
crates/acp-nats/src/session_id.rs                                                            68       0  100.00%
crates/acp-nats/src/in_flight_slot_guard.rs                                                  32       0  100.00%
crates/acp-nats/src/error.rs                                                                 82       0  100.00%
crates/acp-nats/src/acp_prefix.rs                                                            46       0  100.00%
crates/acp-nats/src/nats/subjects/commands/prompt.rs                                         15       0  100.00%
crates/acp-nats/src/nats/subjects/commands/cancel.rs                                         15       0  100.00%
crates/acp-nats/src/nats/subjects/commands/set_mode.rs                                       15       0  100.00%
crates/acp-nats/src/nats/subjects/commands/fork.rs                                           15       0  100.00%
crates/acp-nats/src/nats/subjects/commands/close.rs                                          15       0  100.00%
crates/acp-nats/src/nats/subjects/commands/load.rs                                           15       0  100.00%
crates/acp-nats/src/nats/subjects/commands/resume.rs                                         15       0  100.00%
crates/acp-nats/src/nats/subjects/commands/set_config_option.rs                              15       0  100.00%
crates/acp-nats/src/nats/subjects/commands/set_model.rs                                      15       0  100.00%
crates/trogon-gateway/src/source/notion/verification_token.rs                               220       0  100.00%
crates/trogon-gateway/src/source/notion/signature.rs                                         45       0  100.00%
crates/trogon-gateway/src/source/notion/notion_verification_token.rs                         17       0  100.00%
crates/trogon-gateway/src/source/notion/server.rs                                           310       4  98.71%   115-116, 135-136
crates/trogon-gateway/src/source/notion/notion_event_type.rs                                 46       3  93.48%   50-52
crates/trogon-scheduler/src/commands/domain/schedule_occurrence_sequence.rs                  27       3  88.89%   34-36
crates/trogon-scheduler/src/commands/domain/schedule_id.rs                                   81       0  100.00%
crates/trogon-scheduler/src/commands/domain/schedule.rs                                     638       0  100.00%
crates/trogon-scheduler/src/commands/domain/schedule_event_schedule.rs                       83       0  100.00%
crates/trogon-scheduler/src/commands/domain/schedule_event_status.rs                         10       0  100.00%
crates/trogon-scheduler/src/commands/domain/message.rs                                      219       0  100.00%
crates/trogon-scheduler/src/commands/domain/schedule_event_delivery.rs                       25       0  100.00%
crates/trogon-scheduler/src/commands/domain/schedule_event_sampling_source.rs                20       0  100.00%
crates/trogonai-proto/src/scheduler/schedules/codec.rs                                      264       0  100.00%
crates/trogon-service-config/src/lib.rs                                                      92       0  100.00%
crates/trogon-gateway/src/source/slack/socket_mode.rs                                       716       0  100.00%
crates/trogon-gateway/src/source/slack/server.rs                                            854       0  100.00%
crates/trogon-gateway/src/source/slack/config.rs                                             58       0  100.00%
crates/trogon-gateway/src/source/slack/signature.rs                                          66       0  100.00%
crates/trogon-decider-runtime/src/snapshot/read_snapshot.rs                                  11       0  100.00%
crates/trogon-decider-runtime/src/snapshot/snapshot_type.rs                                  73       0  100.00%
crates/trogon-decider-runtime/src/snapshot/mod.rs                                             3       0  100.00%
crates/trogonai-proto/src/codec.rs                                                           16       0  100.00%
crates/trogonai-proto/src/convert.rs                                                        120       0  100.00%
crates/trogon-decider-runtime/src/event/codec/event_decode.rs                                29       0  100.00%
crates/trogon-decider-runtime/src/event/codec/event_payload_error.rs                         36       0  100.00%
crates/trogon-scheduler/src/processor/execution/checkpoints/codec.rs                        641      68  89.39%   134, 140, 149, 192, 208-210, 227, 244-246, 415, 417-419, 453-464, 480-481, 486-487, 493-494, 507-508, 513-514, 519-523, 529-530, 545-546, 551-552, 558-559, 566-567, 572-573, 585-589, 595-597, 612-618, 626, 631-633, 643, 648
crates/trogon-scheduler/src/processor/execution/checkpoints/record.rs                         6       0  100.00%
crates/trogon-scheduler/src/processor/execution/checkpoints/failure.rs                       38       0  100.00%
crates/trogon-scheduler/src/processor/execution/checkpoints/store.rs                        407      17  95.82%   102, 120, 124, 132, 224-230, 236, 279-283
crates/mcp-nats-server/src/allowed_host.rs                                                   87       0  100.00%
crates/mcp-nats-server/src/main.rs                                                          357     127  64.43%   149-166, 202-204, 214, 220-221, 228-231, 255-257, 261-270, 292-305, 310-358, 489, 492, 500-542
crates/mcp-nats-server/src/config.rs                                                        257       0  100.00%
crates/trogon-decider-nats/src/stream_store.rs                                              659      18  97.27%   70-72, 245, 273-274, 277, 293-297, 464-465, 506, 519-523
crates/trogon-decider-nats/src/snapshot_store.rs                                            861      27  96.86%   208-210, 248-250, 361-367, 449, 585, 590, 686-688, 694-696, 730-731, 741-742, 761, 789-790
crates/trogon-decider-nats/src/store.rs                                                     127      47  62.99%   35-160
crates/mcp-nats-stdio/src/main.rs                                                           204       0  100.00%
crates/mcp-nats-stdio/src/config.rs                                                         149       0  100.00%
crates/acp-nats/src/client/terminal_output.rs                                               206       0  100.00%
crates/acp-nats/src/client/ext_session_prompt_response.rs                                   135       0  100.00%
crates/acp-nats/src/client/terminal_wait_for_exit.rs                                        364       0  100.00%
crates/acp-nats/src/client/terminal_release.rs                                              335       0  100.00%
crates/acp-nats/src/client/fs_write_text_file.rs                                            408       0  100.00%
crates/acp-nats/src/client/ext.rs                                                           296       8  97.30%   146-155, 172-181
crates/acp-nats/src/client/request_permission.rs                                            298       0  100.00%
crates/acp-nats/src/client/rpc_reply.rs                                                      64       0  100.00%
crates/acp-nats/src/client/fs_read_text_file.rs                                             346       0  100.00%
crates/acp-nats/src/client/session_update.rs                                                 55       0  100.00%
crates/acp-nats/src/client/terminal_create.rs                                               264       0  100.00%
crates/acp-nats/src/client/terminal_kill.rs                                                 278       0  100.00%
crates/acp-nats/src/client/mod.rs                                                          2851       0  100.00%
crates/trogon-decider-runtime/src/headers/header_name.rs                                     28       0  100.00%
crates/trogon-decider-runtime/src/headers/header_map.rs                                      54       3  94.44%   20-22
crates/trogon-decider-runtime/src/headers/header_value.rs                                    34       0  100.00%
crates/trogon-decider-runtime/src/headers/mod.rs                                             74       0  100.00%
crates/acp-nats/src/jetstream/ext_policy.rs                                                  26       0  100.00%
crates/acp-nats/src/jetstream/provision.rs                                                   52       0  100.00%
crates/acp-nats/src/jetstream/streams.rs                                                    163       4  97.55%   206-208, 218
crates/acp-nats/src/jetstream/consumers.rs                                                   91       0  100.00%
crates/trogon-gateway/src/source/gitlab/signature.rs                                        165       0  100.00%
crates/trogon-gateway/src/source/gitlab/gitlab_signing_token.rs                              61       0  100.00%
crates/trogon-gateway/src/source/gitlab/server.rs                                           460       0  100.00%
crates/trogon-gateway/src/source/twitter/config.rs                                           17       0  100.00%
crates/trogon-gateway/src/source/twitter/server.rs                                          525       0  100.00%
crates/trogon-gateway/src/source/twitter/signature.rs                                        58       0  100.00%
crates/acp-nats/src/nats/subjects/responses/response.rs                                      20       0  100.00%
crates/acp-nats/src/nats/subjects/responses/ext_ready.rs                                     12       0  100.00%
crates/acp-nats/src/nats/subjects/responses/prompt_response.rs                               27       0  100.00%
crates/acp-nats/src/nats/subjects/responses/update.rs                                        27       0  100.00%
crates/acp-nats/src/nats/subjects/responses/cancelled.rs                                     15       0  100.00%
crates/acp-nats/src/nats/subjects/client_ops/terminal_output.rs                              12       0  100.00%
crates/acp-nats/src/nats/subjects/client_ops/session_update.rs                               12       0  100.00%
crates/acp-nats/src/nats/subjects/client_ops/fs_write_text_file.rs                           12       0  100.00%
crates/acp-nats/src/nats/subjects/client_ops/fs_read_text_file.rs                            12       0  100.00%
crates/acp-nats/src/nats/subjects/client_ops/terminal_create.rs                              12       0  100.00%
crates/acp-nats/src/nats/subjects/client_ops/terminal_kill.rs                                12       0  100.00%
crates/acp-nats/src/nats/subjects/client_ops/terminal_release.rs                             12       0  100.00%
crates/acp-nats/src/nats/subjects/client_ops/terminal_wait_for_exit.rs                       12       0  100.00%
crates/acp-nats/src/nats/subjects/client_ops/session_request_permission.rs                   12       0  100.00%
crates/trogon-decider-runtime/src/stream/mod.rs                                              38       0  100.00%
crates/trogon-decider-runtime/src/stream/read_stream.rs                                       7       0  100.00%
crates/trogon-decider-runtime/src/stream/append_stream.rs                                     5       0  100.00%
crates/trogon-decider-runtime/src/stream/stream_position.rs                                  26       0  100.00%
crates/trogon-gateway/src/config.rs                                                        2591      42  98.38%   84, 668, 671, 831, 888, 971, 974, 977, 981, 1065-1072, 1149, 1152, 1155, 1160, 1218, 1221, 1224, 1303, 1306, 1309, 1313, 1377, 1380, 1383, 1446, 1449, 1452, 1457, 1532, 1535, 1538, 1543, 1601, 1604, 1607, 1820-1822
crates/trogon-gateway/src/source_integration_id.rs                                           55       2  96.36%   58, 60
crates/trogon-gateway/src/streams.rs                                                        129       0  100.00%
crates/trogon-gateway/src/source_plugin.rs                                                  269       3  98.88%   82, 141-142
crates/trogon-gateway/src/http.rs                                                           145       0  100.00%
crates/trogon-gateway/src/source_status.rs                                                   24       0  100.00%
crates/trogon-gateway/src/main.rs                                                           111       0  100.00%
crates/trogon-gateway/src/source/telegram/signature.rs                                       27       0  100.00%
crates/trogon-gateway/src/source/telegram/server.rs                                         339       0  100.00%
crates/trogon-gateway/src/source/telegram/config.rs                                          89       0  100.00%
crates/trogon-gateway/src/source/telegram/registration.rs                                   313       0  100.00%
crates/trogon-nats/src/telemetry/messaging.rs                                                82       0  100.00%
crates/trogon-gateway/src/source/standard_webhooks.rs                                       138       0  100.00%
crates/trogon-scheduler/src/commands/pause_schedule.rs                                      175       0  100.00%
crates/trogon-scheduler/src/commands/record_schedule_occurrence.rs                          299       2  99.33%   289, 313
crates/trogon-scheduler/src/commands/schedule_next_occurrence.rs                            160       9  94.38%   80, 83, 107-109, 116, 134, 192, 221
crates/trogon-scheduler/src/commands/state.rs                                               441       0  100.00%
crates/trogon-scheduler/src/commands/rrule.rs                                               125       5  96.00%   73, 78-79, 136-137
crates/trogon-scheduler/src/commands/create_schedule.rs                                     199       0  100.00%
crates/trogon-scheduler/src/commands/remove_schedule.rs                                     190       0  100.00%
crates/trogon-scheduler/src/commands/snapshot.rs                                              4       0  100.00%
crates/trogon-scheduler/src/commands/resume_schedule.rs                                     182       0  100.00%
crates/trogon-telemetry/src/trace.rs                                                         23       1  95.65%   24
crates/trogon-telemetry/src/resource_attribute.rs                                            23       0  100.00%
crates/trogon-telemetry/src/log.rs                                                           70       1  98.57%   35
crates/trogon-telemetry/src/lib.rs                                                          208      24  88.46%   54, 118, 123, 128, 138-139, 145-163, 199, 202, 205, 211
crates/trogon-telemetry/src/metric.rs                                                        26       1  96.15%   30
crates/trogon-telemetry/src/service_name.rs                                                  44       0  100.00%
crates/trogon-std/src/fs/mem.rs                                                             216      10  95.37%   61-63, 77-79, 132-134, 157
crates/trogon-std/src/fs/system.rs                                                           92       0  100.00%
crates/trogon-decider-runtime/src/event/event_id.rs                                          32       0  100.00%
crates/trogon-decider-runtime/src/event/mod.rs                                              170       0  100.00%
crates/trogon-decider-runtime/src/event/stream_event.rs                                       8       0  100.00%
crates/trogon-decider-runtime/src/event/event_identity.rs                                     3       0  100.00%
crates/trogon-gateway/src/source/microsoft_graph/client_state.rs                             30       0  100.00%
crates/trogon-gateway/src/source/microsoft_graph/server.rs                                  325       0  100.00%
crates/trogon-decider-runtime/src/execution.rs                                             1432       0  100.00%
crates/mcp-nats/src/nats/subjects/mod.rs                                                     89       0  100.00%
crates/acp-nats-agent/src/connection.rs                                                    1260       1  99.92%   590
crates/trogon-nats/src/messaging.rs                                                         534       2  99.63%   144, 154
crates/trogon-nats/src/token.rs                                                               6       0  100.00%
crates/trogon-nats/src/subject_token_violation.rs                                            11       0  100.00%
crates/trogon-nats/src/nats_token.rs                                                        157       0  100.00%
crates/trogon-nats/src/mocks.rs                                                             314       0  100.00%
crates/trogon-nats/src/client.rs                                                             22      22  0.00%    50-86
crates/trogon-nats/src/connect.rs                                                            82       6  92.68%   41-46
crates/trogon-nats/src/auth.rs                                                              114       0  100.00%
crates/acp-nats/src/nats/subjects/subscriptions/prompt_wildcard.rs                            9       0  100.00%
crates/acp-nats/src/nats/subjects/subscriptions/all_session.rs                                9       0  100.00%
crates/acp-nats/src/nats/subjects/subscriptions/one_agent.rs                                 15       0  100.00%
crates/acp-nats/src/nats/subjects/subscriptions/one_client.rs                                15       0  100.00%
crates/acp-nats/src/nats/subjects/subscriptions/all_agent.rs                                  9       0  100.00%
crates/acp-nats/src/nats/subjects/subscriptions/all_client.rs                                 9       0  100.00%
crates/acp-nats/src/nats/subjects/subscriptions/one_session.rs                               12       0  100.00%
crates/acp-nats/src/nats/subjects/subscriptions/global_all.rs                                 9       0  100.00%
crates/acp-nats/src/nats/subjects/subscriptions/all_agent_ext.rs                              9       0  100.00%
crates/acp-nats/src/nats/mod.rs                                                              23       0  100.00%
crates/acp-nats/src/nats/parsing.rs                                                         278       1  99.64%   151
crates/acp-nats/src/nats/extensions.rs                                                        3       0  100.00%
TOTAL                                                                                     49859     834  98.33%

Diff against main

Filename                                                                                  Stmts    Miss  Cover
--------------------------------------------------------------------------------------  -------  ------  --------
crates/trogon-scheduler/src/processor/execution/reconciliation/reconcile.rs                +237     +13  -1.76%
crates/trogon-scheduler/src/processor/execution/reconciliation/rrule_wakeup_payload.rs      +18       0  +100.00%
crates/trogon-scheduler/src/processor/execution/reconciliation/recorded_events.rs           +98      -2  +0.64%
crates/trogon-scheduler/src/processor/execution/reconciliation/request.rs                  +214      -1  +0.55%
crates/trogon-scheduler/src/processor/execution/reconciliation/schedule_subject.rs          +10       0  +100.00%
crates/trogon-scheduler/src/processor/execution/worker/testkit.rs                           +52      +4  -1.21%
crates/trogon-scheduler/src/processor/execution/worker/dispatcher.rs                        +30       0  +0.04%
crates/trogon-scheduler/src/processor/execution/worker/processor.rs                        +252      +7  -0.36%
crates/trogon-scheduler/src/processor/execution/wakeup.rs                                  +335      +4  +98.81%
crates/trogon-scheduler/src/processor/execution/execution_schedules/mod.rs                  +80       0  +100.00%
crates/trogon-scheduler/src/commands/domain/schedule_occurrence_sequence.rs                 +27      +3  +88.89%
crates/trogonai-proto/src/scheduler/schedules/codec.rs                                      +16       0  +100.00%
crates/trogon-scheduler/src/commands/pause_schedule.rs                                      +17       0  +100.00%
crates/trogon-scheduler/src/commands/record_schedule_occurrence.rs                         +299      +2  +99.33%
crates/trogon-scheduler/src/commands/schedule_next_occurrence.rs                           +160      +9  +94.38%
crates/trogon-scheduler/src/commands/state.rs                                              +253       0  +100.00%
crates/trogon-scheduler/src/commands/rrule.rs                                              +125      +5  +96.00%
crates/trogon-scheduler/src/commands/create_schedule.rs                                     +17       0  +100.00%
crates/trogon-scheduler/src/commands/remove_schedule.rs                                     +21       0  +100.00%
crates/trogon-scheduler/src/commands/resume_schedule.rs                                     +17       0  +100.00%
TOTAL                                                                                     +2278     +44  -0.01%

Results for commit: 1c1248b

Minimum allowed coverage is 95%

♻️ This comment has been updated with latest results

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
rsworkspace/crates/trogon-scheduler/src/processor/execution/reconciliation/rrule.rs (1)

97-113: ⚡ Quick win

Add an explicit equality-boundary test for RRuleCursor::AtOrAfter.

The suite validates “before” and strict “after”, but not the exact-match boundary (cursor == occurrence). Adding that case protects the core recurrence continuation contract from regressions.

✅ Suggested test addition
 #[test]
 fn strict_after_cursor_does_not_repeat_the_fired_occurrence() {
@@
     assert_eq!(next, Some(instant("2026-01-02T00:00:00Z")));
 }
+
+#[test]
+fn at_or_after_cursor_includes_an_occurrence_at_the_exact_cursor() {
+    let schedule = Schedule::rrule("2026-01-01T00:00:00Z", "FREQ=DAILY;COUNT=2", None).unwrap();
+
+    let next = next_rrule_occurrence(&schedule, RRuleCursor::at_or_after(instant("2026-01-01T00:00:00Z"))).unwrap();
+
+    assert_eq!(next, Some(instant("2026-01-01T00:00:00Z")));
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@rsworkspace/crates/trogon-scheduler/src/processor/execution/reconciliation/rrule.rs`
around lines 97 - 113, Add a new test function to validate the exact-match
boundary case for RRuleCursor::at_or_after. The test should create an rrule
schedule and call next_rrule_occurrence with RRuleCursor::at_or_after pointing
to a time that exactly matches one of the recurrence occurrences, then assert
that the function returns that exact occurrence (not skipping to the next one).
This ensures the "at or after" semantics are properly tested alongside the
existing "before" and strict "after" test cases.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@rsworkspace/crates/trogon-scheduler/src/processor/execution/worker/processor.rs`:
- Around line 280-281: The metrics recording calls (record_publish and
record_purge) are being executed before checkpoint persistence, which causes
double-counting if the checkpoint save fails and the continuation is retried.
Move the self.metrics.record_publish() call at line 280-281 to execute after the
checkpoints.save() call completes successfully. Apply the same fix to the
sibling locations at lines 287-288 (move record_purge after checkpoints.save)
and lines 292-299 (move the metrics recording after checkpoints.save). Ensure
metrics are only recorded once the checkpoint has been successfully persisted.

---

Nitpick comments:
In
`@rsworkspace/crates/trogon-scheduler/src/processor/execution/reconciliation/rrule.rs`:
- Around line 97-113: Add a new test function to validate the exact-match
boundary case for RRuleCursor::at_or_after. The test should create an rrule
schedule and call next_rrule_occurrence with RRuleCursor::at_or_after pointing
to a time that exactly matches one of the recurrence occurrences, then assert
that the function returns that exact occurrence (not skipping to the next one).
This ensures the "at or after" semantics are properly tested alongside the
existing "before" and strict "after" test cases.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 507b9e2b-fe44-4c74-b1d4-0dd3f2643302

📥 Commits

Reviewing files that changed from the base of the PR and between 7bdcf72 and e34c609.

📒 Files selected for processing (8)
  • rsworkspace/crates/trogon-scheduler/src/processor/execution/nats_execution_tests.rs
  • rsworkspace/crates/trogon-scheduler/src/processor/execution/reconciliation/mod.rs
  • rsworkspace/crates/trogon-scheduler/src/processor/execution/reconciliation/reconcile.rs
  • rsworkspace/crates/trogon-scheduler/src/processor/execution/reconciliation/request.rs
  • rsworkspace/crates/trogon-scheduler/src/processor/execution/reconciliation/rrule.rs
  • rsworkspace/crates/trogon-scheduler/src/processor/execution/worker/mod.rs
  • rsworkspace/crates/trogon-scheduler/src/processor/execution/worker/processor.rs
  • rsworkspace/crates/trogon-scheduler/src/processor/mod.rs

Comment thread rsworkspace/crates/trogon-scheduler/src/processor/execution/worker/processor.rs Outdated
@yordis yordis force-pushed the yordis/fix-rrule-scheduling branch 2 times, most recently from 4783111 to 5ccf0da Compare June 15, 2026 18:19
backoff: Vec::new(),
..Default::default()
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RRULE wakeups lack runtime consumer

High Severity

The PR schedules RRULE continuations via internal wakeup subjects and adds RRuleWakeupProcessor plus rrule_wakeup_consumer_config, but nothing in the scheduler worker starts a durable consumer or calls process outside ignored NATS integration tests. After the first NATS @at fires, ScheduleOccurrenceFired events are never appended, so the execution processor cannot dispatch or schedule the next occurrence.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 5ccf0da. Configure here.

RRuleCursor::After(_) => candidates.into_iter().find(|date| date.with_timezone(&Utc) > instant),
};

Ok(occurrence.map(|date| date.with_timezone(&Utc)))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AtOrAfter skips exact instant

Medium Severity

RRuleCursor::AtOrAfter takes the first date from RRuleSet::after(cursor), which is typically strict-after the cursor. When reconciliation uses now - PAST_AT_GRACE and that value equals a recurrence instant, expansion can return no next occurrence and mark the schedule expired while that occurrence was never scheduled or dispatched.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 5ccf0da. Configure here.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (4)
rsworkspace/crates/trogonai-proto/src/scheduler/schedules/codec.rs (1)

58-82: ⚡ Quick win

Add lifecycle codec assertions for ScheduleOccurrenceFired.

The new branch is wired in encode/decode/type dispatch, but the lifecycle tests still don’t explicitly cover this case. Add one assertion per lifecycle test path so this event can’t regress silently.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rsworkspace/crates/trogonai-proto/src/scheduler/schedules/codec.rs` around
lines 58 - 82, The ScheduleOccurrenceFired variant has been wired into the
encode/decode/type dispatch functions (encode_schedule_event_case,
decode_schedule_event_case, and schedule_event_case_type), but the lifecycle
codec tests do not explicitly cover this new case. Add one assertion per
lifecycle test path to verify the ScheduleOccurrenceFired variant encodes,
decodes, and dispatches correctly so that future changes don't cause silent
regressions. Include assertions that test the encode/decode round-trip and type
dispatch for this variant alongside the existing test cases for other
ScheduleEventCase variants.
rsworkspace/crates/trogon-scheduler/src/processor/execution/reconciliation/request.rs (1)

114-143: ⚡ Quick win

Add direct unit coverage for the new wakeup/dispatch builders.

Lines 114 and 213 introduce new construction paths, but the test module currently exercises only ScheduleRequest::build paths. Please add explicit tests for build_rrule_wakeup (target/TTL/correlation headers + payload decode) and DispatchRequest::build (UnsupportedDispatchSource, internal-target rejection, user-header passthrough).

Also applies to: 213-265, 317-616

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@rsworkspace/crates/trogon-scheduler/src/processor/execution/reconciliation/request.rs`
around lines 114 - 143, Add explicit unit tests for the new builder methods
introduced in this code. Create tests for the `build_rrule_wakeup` method that
verify correct setting of target headers, TTL header formatting, correlation
headers (TROGON_SCHEDULE_KEY_HEADER and TROGON_SCHEDULE_ID_B64_HEADER), and
correct payload encoding/decoding. Additionally, create tests for the
`DispatchRequest::build` method (at line 213) that verify handling of
UnsupportedDispatchSource errors, rejection of internal-target scenarios, and
correct passthrough of user-supplied headers. These new test cases should
complement the existing test coverage which currently only exercises the basic
`ScheduleRequest::build` path.
rsworkspace/crates/trogon-scheduler/src/processor/execution/reconciliation/recorded_events.rs (1)

230-240: ⚡ Quick win

Add focused tests for the new occurrence-fired decode branch.

This branch is the only path from persisted ScheduleOccurrenceFired events into reconciliation. Please add one success test and one missing-fired_at rejection test so regressions here do not silently stop RRULE continuation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@rsworkspace/crates/trogon-scheduler/src/processor/execution/reconciliation/recorded_events.rs`
around lines 230 - 240, Add two focused unit tests for the
ScheduleOccurrenceFired branch in the reconciliation event decoding logic.
First, create a success test that verifies a valid ScheduleOccurrenceFired event
with all required fields (schedule_id and fired_at) correctly decodes into a
ScheduleChange::OccurrenceFired variant with the expected schedule_id and
fired_at values. Second, create a rejection test that verifies when the fired_at
field is missing or None, the decoding fails with the
ScheduleEventDecodeError::MissingField error for "fired_at". These tests ensure
regressions in this critical event processing path are caught before they
silently break RRULE continuation.
rsworkspace/crates/trogon-scheduler/src/processor/execution/nats_execution_tests.rs (1)

174-191: Route test NATS interactions through trogon-nats trait abstractions instead of binding directly to concrete async_nats types.

The fetch_wakeup() helper (lines 174-191) and related test setup functions like execution_stream() directly depend on concrete async_nats::jetstream types. Per repository policy, NATS test code should use trogon-nats abstractions for testability and swappability. Replace the concrete consumer parameter with trogon_nats::jetstream::JetStreamConsumer trait bounds and use the mock implementations available in trogon_nats for testing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@rsworkspace/crates/trogon-scheduler/src/processor/execution/nats_execution_tests.rs`
around lines 174 - 191, Refactor the fetch_wakeup() helper function to accept a
generic parameter bound by trogon_nats::jetstream::JetStreamConsumer trait
instead of the concrete async_nats::jetstream::consumer::Consumer type. Update
the function signature to use trait bounds that allow for mock implementations
from trogon_nats, making the test code compatible with the repository's
abstraction policy. Additionally, review and update related test setup functions
like execution_stream() to similarly use trogon_nats trait abstractions instead
of concrete async_nats types, ensuring all NATS interactions in the test file go
through trogon_nats abstractions for consistency.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@rsworkspace/crates/trogon-scheduler/src/commands/record_schedule_occurrence.rs`:
- Around line 9-12: The RecordScheduleOccurrence struct and other
occurrence-related types expose raw DateTime<Utc> primitives, which bypasses
invariant enforcement and weakens domain guarantees. Create a domain-specific
value object for the occurrence timestamp (similar to how AcpPrefix is used
instead of String), then update all affected locations: replace the
DateTime<Utc> field in RecordScheduleOccurrence (lines 9-12), update the
OccurrenceAlreadyRecorded error type (lines 23-27), and update any other
occurrence timestamp field (lines 40-41) to use this new value object instead of
the raw primitive. Ensure the value object's factory method guarantees
correctness at construction so invalid instances are unrepresentable.

In
`@rsworkspace/crates/trogon-scheduler/src/processor/execution/reconciliation/reconcile.rs`:
- Around line 277-284: The RRULE grace-boundary handling is inconsistent with
the one-shot schedule (At path) boundary handling. The `decide_rrule` function
call uses `RRuleCursor::at_or_after(now - PAST_AT_GRACE)` which includes
occurrences at the boundary, while the At path on line 288 excludes that same
boundary with `<=`. Either change the RRULE cursor to use an exclusive boundary
(after instead of at_or_after) to match the At path behavior, or update both
code paths and their tests to use the same inclusive boundary rule consistently.

In
`@rsworkspace/crates/trogon-scheduler/src/processor/execution/reconciliation/rrule_wakeup_payload.rs`:
- Around line 5-8: The RRuleWakeupPayload struct currently uses primitive String
fields that can be invalid at construction time, with validation deferred to
Line 22. Split the representation into wire and domain types following the
coding guidelines. At
rsworkspace/crates/trogon-scheduler/src/processor/execution/reconciliation/rrule_wakeup_payload.rs
lines 5-8, replace the primitive String fields with domain-specific value
objects. At lines 30-31 where deserialization occurs, deserialize into a
temporary wire type (with primitive strings) instead of directly into
RRuleWakeupPayload. At lines 22-24 where validation currently happens, implement
a conversion function (e.g., impl From or a try_into method) that validates
schedule_id and fired_at exactly once when converting from wire to domain type,
ensuring RRuleWakeupPayload cannot be constructed with invalid values.

In `@rsworkspace/crates/trogon-scheduler/src/processor/execution/wakeup.rs`:
- Around line 47-48: The `SubjectMismatch` error struct at lines 47-48 stores
untrusted subject input as raw strings, violating the coding guideline that
untrusted input must use distinct boundary types. Create a new
`RRuleWakeupSubjectInput` boundary type to wrap and validate the subject string
input, then update the `SubjectMismatch` error definition to use typed fields
from this boundary type instead of raw String fields. Apply the same boundary
type wrapper when creating `SubjectMismatch` errors at all sites where subject
validation occurs (lines 89-96 and 128-137), ensuring the subject is wrapped
before comparison rather than carrying raw strings in the error.
- Around line 90-98: The `RecordScheduleOccurrence::decide` method validates
ordering but does not confirm that the `fired_at` timestamp is a valid RRULE
occurrence for the schedule. Add validation in
`RecordScheduleOccurrence::decide` to call `next_rrule_occurrence` (or
equivalent method) with the schedule's current RRULE and exception dates to
verify that `fired_at` matches a valid occurrence. If `fired_at` does not
correspond to a valid occurrence, reject the command with an appropriate error.
This ensures that if the schedule's RRULE or exception dates change between
wakeup publication and processing, invalid timestamps are not accepted and valid
occurrences are not skipped.

In
`@rsworkspace/crates/trogon-scheduler/src/processor/execution/worker/processor.rs`:
- Around line 505-506: The match arm handling ReconcileAction::Multiple(_) at
line 505 currently returns Ok(()) without processing the nested actions
contained within the Multiple variant, which causes those actions to be skipped
while allowing checkpoints to be persisted anyway. Extract the nested actions
from the Multiple variant and process each one appropriately (either recursively
through the same processing logic or by iterating through and handling them
sequentially) to ensure all work is actually executed before any checkpoint is
acknowledged. Do not simply return Ok(()) for the Multiple case.

---

Nitpick comments:
In
`@rsworkspace/crates/trogon-scheduler/src/processor/execution/nats_execution_tests.rs`:
- Around line 174-191: Refactor the fetch_wakeup() helper function to accept a
generic parameter bound by trogon_nats::jetstream::JetStreamConsumer trait
instead of the concrete async_nats::jetstream::consumer::Consumer type. Update
the function signature to use trait bounds that allow for mock implementations
from trogon_nats, making the test code compatible with the repository's
abstraction policy. Additionally, review and update related test setup functions
like execution_stream() to similarly use trogon_nats trait abstractions instead
of concrete async_nats types, ensuring all NATS interactions in the test file go
through trogon_nats abstractions for consistency.

In
`@rsworkspace/crates/trogon-scheduler/src/processor/execution/reconciliation/recorded_events.rs`:
- Around line 230-240: Add two focused unit tests for the
ScheduleOccurrenceFired branch in the reconciliation event decoding logic.
First, create a success test that verifies a valid ScheduleOccurrenceFired event
with all required fields (schedule_id and fired_at) correctly decodes into a
ScheduleChange::OccurrenceFired variant with the expected schedule_id and
fired_at values. Second, create a rejection test that verifies when the fired_at
field is missing or None, the decoding fails with the
ScheduleEventDecodeError::MissingField error for "fired_at". These tests ensure
regressions in this critical event processing path are caught before they
silently break RRULE continuation.

In
`@rsworkspace/crates/trogon-scheduler/src/processor/execution/reconciliation/request.rs`:
- Around line 114-143: Add explicit unit tests for the new builder methods
introduced in this code. Create tests for the `build_rrule_wakeup` method that
verify correct setting of target headers, TTL header formatting, correlation
headers (TROGON_SCHEDULE_KEY_HEADER and TROGON_SCHEDULE_ID_B64_HEADER), and
correct payload encoding/decoding. Additionally, create tests for the
`DispatchRequest::build` method (at line 213) that verify handling of
UnsupportedDispatchSource errors, rejection of internal-target scenarios, and
correct passthrough of user-supplied headers. These new test cases should
complement the existing test coverage which currently only exercises the basic
`ScheduleRequest::build` path.

In `@rsworkspace/crates/trogonai-proto/src/scheduler/schedules/codec.rs`:
- Around line 58-82: The ScheduleOccurrenceFired variant has been wired into the
encode/decode/type dispatch functions (encode_schedule_event_case,
decode_schedule_event_case, and schedule_event_case_type), but the lifecycle
codec tests do not explicitly cover this new case. Add one assertion per
lifecycle test path to verify the ScheduleOccurrenceFired variant encodes,
decodes, and dispatches correctly so that future changes don't cause silent
regressions. Include assertions that test the encode/decode round-trip and type
dispatch for this variant alongside the existing test cases for other
ScheduleEventCase variants.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 30a0afaf-a167-4781-9db9-6c091c149106

📥 Commits

Reviewing files that changed from the base of the PR and between e34c609 and 5ccf0da.

⛔ Files ignored due to path filters (9)
  • rsworkspace/crates/trogonai-proto/src/gen/trogonai.scheduler.schedules.state.v1.state.__view.rs is excluded by !**/gen/**
  • rsworkspace/crates/trogonai-proto/src/gen/trogonai.scheduler.schedules.state.v1.state.rs is excluded by !**/gen/**
  • rsworkspace/crates/trogonai-proto/src/gen/trogonai.scheduler.schedules.v1.events.__oneof.rs is excluded by !**/gen/**
  • rsworkspace/crates/trogonai-proto/src/gen/trogonai.scheduler.schedules.v1.events.__view.rs is excluded by !**/gen/**
  • rsworkspace/crates/trogonai-proto/src/gen/trogonai.scheduler.schedules.v1.events.__view_oneof.rs is excluded by !**/gen/**
  • rsworkspace/crates/trogonai-proto/src/gen/trogonai.scheduler.schedules.v1.events.rs is excluded by !**/gen/**
  • rsworkspace/crates/trogonai-proto/src/gen/trogonai.scheduler.schedules.v1.mod.rs is excluded by !**/gen/**
  • rsworkspace/crates/trogonai-proto/src/gen/trogonai.scheduler.schedules.v1.schedule_occurrence_fired.__view.rs is excluded by !**/gen/**
  • rsworkspace/crates/trogonai-proto/src/gen/trogonai.scheduler.schedules.v1.schedule_occurrence_fired.rs is excluded by !**/gen/**
📒 Files selected for processing (25)
  • proto/trogonai/scheduler/schedules/state/v1/state.proto
  • proto/trogonai/scheduler/schedules/v1/events.proto
  • proto/trogonai/scheduler/schedules/v1/schedule_occurrence_fired.proto
  • rsworkspace/crates/trogon-scheduler/src/commands/create_schedule.rs
  • rsworkspace/crates/trogon-scheduler/src/commands/mod.rs
  • rsworkspace/crates/trogon-scheduler/src/commands/pause_schedule.rs
  • rsworkspace/crates/trogon-scheduler/src/commands/record_schedule_occurrence.rs
  • rsworkspace/crates/trogon-scheduler/src/commands/remove_schedule.rs
  • rsworkspace/crates/trogon-scheduler/src/commands/resume_schedule.rs
  • rsworkspace/crates/trogon-scheduler/src/commands/state.rs
  • rsworkspace/crates/trogon-scheduler/src/lib.rs
  • rsworkspace/crates/trogon-scheduler/src/processor/execution/execution_schedules/mod.rs
  • rsworkspace/crates/trogon-scheduler/src/processor/execution/mod.rs
  • rsworkspace/crates/trogon-scheduler/src/processor/execution/nats_execution_tests.rs
  • rsworkspace/crates/trogon-scheduler/src/processor/execution/reconciliation/mod.rs
  • rsworkspace/crates/trogon-scheduler/src/processor/execution/reconciliation/reconcile.rs
  • rsworkspace/crates/trogon-scheduler/src/processor/execution/reconciliation/recorded_events.rs
  • rsworkspace/crates/trogon-scheduler/src/processor/execution/reconciliation/request.rs
  • rsworkspace/crates/trogon-scheduler/src/processor/execution/reconciliation/rrule.rs
  • rsworkspace/crates/trogon-scheduler/src/processor/execution/reconciliation/rrule_wakeup_payload.rs
  • rsworkspace/crates/trogon-scheduler/src/processor/execution/reconciliation/schedule_subject.rs
  • rsworkspace/crates/trogon-scheduler/src/processor/execution/wakeup.rs
  • rsworkspace/crates/trogon-scheduler/src/processor/execution/worker/processor.rs
  • rsworkspace/crates/trogon-scheduler/src/processor/mod.rs
  • rsworkspace/crates/trogonai-proto/src/scheduler/schedules/codec.rs
✅ Files skipped from review due to trivial changes (7)
  • proto/trogonai/scheduler/schedules/v1/events.proto
  • proto/trogonai/scheduler/schedules/v1/schedule_occurrence_fired.proto
  • rsworkspace/crates/trogon-scheduler/src/processor/execution/mod.rs
  • rsworkspace/crates/trogon-scheduler/src/lib.rs
  • rsworkspace/crates/trogon-scheduler/src/processor/mod.rs
  • rsworkspace/crates/trogon-scheduler/src/commands/remove_schedule.rs
  • rsworkspace/crates/trogon-scheduler/src/commands/pause_schedule.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • rsworkspace/crates/trogon-scheduler/src/processor/execution/reconciliation/mod.rs
  • rsworkspace/crates/trogon-scheduler/src/processor/execution/reconciliation/rrule.rs

Comment on lines +9 to +12
pub struct RecordScheduleOccurrence {
pub id: ScheduleId,
pub fired_at: DateTime<Utc>,
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

Use a domain value object instead of raw DateTime<Utc> in command/error surfaces.

RecordScheduleOccurrence and OccurrenceAlreadyRecorded currently expose primitive timestamps directly. This bypasses invariant enforcement at construction and weakens domain guarantees for occurrence semantics.

As per coding guidelines, “Prefer domain-specific value objects over primitives (e.g., AcpPrefix instead of String). Each type's factory must guarantee correctness at construction—invalid instances should be unrepresentable.”

Also applies to: 23-27, 40-41

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@rsworkspace/crates/trogon-scheduler/src/commands/record_schedule_occurrence.rs`
around lines 9 - 12, The RecordScheduleOccurrence struct and other
occurrence-related types expose raw DateTime<Utc> primitives, which bypasses
invariant enforcement and weakens domain guarantees. Create a domain-specific
value object for the occurrence timestamp (similar to how AcpPrefix is used
instead of String), then update all affected locations: replace the
DateTime<Utc> field in RecordScheduleOccurrence (lines 9-12), update the
OccurrenceAlreadyRecorded error type (lines 23-27), and update any other
occurrence timestamp field (lines 40-41) to use this new value object instead of
the raw primitive. Ensure the value object's factory method guarantees
correctness at construction so invalid instances are unrepresentable.

Source: Coding guidelines

Comment on lines +5 to +8
pub(crate) struct RRuleWakeupPayload {
schedule_id: String,
fired_at: String,
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

Make wakeup payload validity explicit at construction time.

Line 30 currently deserializes into primitive String fields (Lines 6-7), so malformed schedule_id/fired_at can exist inside RRuleWakeupPayload and only fail later at Line 22. Please split wire and domain representations so conversion validates once and the domain payload cannot be invalid after construction.

As per coding guidelines, “Prefer domain-specific value objects over primitives … invalid instances should be unrepresentable” and “Untrusted input must use distinct *Input/*Wire/*Request types. Convert those boundary types into domain types exactly once.”

Also applies to: 22-24, 30-31

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@rsworkspace/crates/trogon-scheduler/src/processor/execution/reconciliation/rrule_wakeup_payload.rs`
around lines 5 - 8, The RRuleWakeupPayload struct currently uses primitive
String fields that can be invalid at construction time, with validation deferred
to Line 22. Split the representation into wire and domain types following the
coding guidelines. At
rsworkspace/crates/trogon-scheduler/src/processor/execution/reconciliation/rrule_wakeup_payload.rs
lines 5-8, replace the primitive String fields with domain-specific value
objects. At lines 30-31 where deserialization occurs, deserialize into a
temporary wire type (with primitive strings) instead of directly into
RRuleWakeupPayload. At lines 22-24 where validation currently happens, implement
a conversion function (e.g., impl From or a try_into method) that validates
schedule_id and fired_at exactly once when converting from wire to domain type,
ensuring RRuleWakeupPayload cannot be constructed with invalid values.

Source: Coding guidelines

Comment on lines +47 to +48
#[error("RRULE wakeup subject '{actual}' does not match expected subject '{expected}'")]
SubjectMismatch { expected: String, actual: String },

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Wrap the inbound NATS subject in a boundary type before validation.

subject: &str is untrusted transport input, and SubjectMismatch persists the raw values as String. Introduce a small RRuleWakeupSubjectInput/Wire type and carry typed fields in the error instead of raw strings. As per coding guidelines, "Untrusted input must use distinct *Input / *Wire / *Request types" and "Errors must be typed—use structs or enums, never String or format!()."

Also applies to: 89-96, 128-137

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rsworkspace/crates/trogon-scheduler/src/processor/execution/wakeup.rs` around
lines 47 - 48, The `SubjectMismatch` error struct at lines 47-48 stores
untrusted subject input as raw strings, violating the coding guideline that
untrusted input must use distinct boundary types. Create a new
`RRuleWakeupSubjectInput` boundary type to wrap and validate the subject string
input, then update the `SubjectMismatch` error definition to use typed fields
from this boundary type instead of raw String fields. Apply the same boundary
type wrapper when creating `SubjectMismatch` errors at all sites where subject
validation occurs (lines 89-96 and 128-137), ensuring the subject is wrapped
before comparison rather than carrying raw strings in the error.

Source: Coding guidelines

Comment on lines +90 to +98
let wakeup = RRuleWakeupPayload::decode(payload).map_err(|source| RRuleWakeupError::PayloadJson { source })?;
let schedule_id =
ScheduleId::parse(wakeup.schedule_id()).map_err(|source| RRuleWakeupError::ScheduleId { source })?;
let fired_at = wakeup
.fired_at()
.map_err(|source| RRuleWakeupError::FiredAt { source })?;
ensure_subject_matches_payload(subject, &schedule_id)?;

let command = RecordScheduleOccurrence::new(schedule_id, fired_at);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect whether RecordScheduleOccurrence validates fired_at against the RRULE schedule,
# not only duplicate/stale ordering.
# Expected: The command path should prove fired_at is a valid/expected occurrence for the current schedule
# before appending ScheduleOccurrenceFired.

set -euo pipefail

rg -n -C 6 \
  'RecordScheduleOccurrence|RecordScheduleOccurrenceError|OccurrenceAlreadyRecorded|last_occurrence_fired_at|next_rrule_occurrence|RRuleCursor|Schedule::RRule' \
  rsworkspace/crates/trogon-scheduler/src/commands/record_schedule_occurrence.rs \
  rsworkspace/crates/trogon-scheduler/src/commands/state.rs \
  rsworkspace/crates/trogon-scheduler/src/processor/execution/reconciliation/rrule.rs

Repository: TrogonStack/trogonai

Length of output: 43270


🏁 Script executed:

cat -n rsworkspace/crates/trogon-scheduler/src/processor/execution/wakeup.rs | head -120

Repository: TrogonStack/trogonai

Length of output: 5232


🏁 Script executed:

# Check if there's any RRULE validation in the wakeup generation or elsewhere in the processor
rg -n 'next_rrule_occurrence|validate.*fired|valid.*occurrence' rsworkspace/crates/trogon-scheduler/src/processor/execution/ --type rust

Repository: TrogonStack/trogonai

Length of output: 1778


🏁 Script executed:

# Check what the reconciliation does and how it relates to wakeup generation
rg -n 'wakeup|WakeupPayload|generate.*wakeup|publish.*wakeup' rsworkspace/crates/trogon-scheduler/src/processor/execution/reconciliation/ --type rust -B 2 -A 2

Repository: TrogonStack/trogonai

Length of output: 9400


🏁 Script executed:

# Check the reconcile logic that calls build_rrule_wakeup and how `at` is determined
sed -n '300,340p' rsworkspace/crates/trogon-scheduler/src/processor/execution/reconciliation/reconcile.rs

Repository: TrogonStack/trogonai

Length of output: 1389


RecordScheduleOccurrence must validate that fired_at matches a valid RRULE occurrence for the schedule.

The wakeup processor validates the subject matches the payload schedule ID, then records the payload's fired_at directly. However, RecordScheduleOccurrence::decide only validates ordering (no duplicates/stale timestamps); it does not call next_rrule_occurrence to confirm fired_at is a valid occurrence in the current schedule. If the schedule's RRULE or exception dates change between wakeup publication and processing, an invalid fired_at could be accepted, potentially skipping valid scheduled occurrences.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rsworkspace/crates/trogon-scheduler/src/processor/execution/wakeup.rs` around
lines 90 - 98, The `RecordScheduleOccurrence::decide` method validates ordering
but does not confirm that the `fired_at` timestamp is a valid RRULE occurrence
for the schedule. Add validation in `RecordScheduleOccurrence::decide` to call
`next_rrule_occurrence` (or equivalent method) with the schedule's current RRULE
and exception dates to verify that `fired_at` matches a valid occurrence. If
`fired_at` does not correspond to a valid occurrence, reject the command with an
appropriate error. This ensures that if the schedule's RRULE or exception dates
change between wakeup publication and processing, invalid timestamps are not
accepted and valid occurrences are not skipped.

Comment thread rsworkspace/crates/trogon-scheduler/src/processor/execution/worker/processor.rs Outdated
@yordis yordis added the rust:coverage-baseline-reset Relax Rust coverage gate to establish a new baseline label Jun 15, 2026
@yordis yordis force-pushed the yordis/fix-rrule-scheduling branch from 5ccf0da to 33a9536 Compare June 15, 2026 18:37
pub(crate) mod checkpoints;
pub(crate) mod execution_schedules;
pub(crate) mod reconciliation;
pub mod wakeup;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RRULE wakeup consumer unwired

High Severity

RRULE schedules now publish one-shot NATS wakeups to scheduler.schedules.execution.v1.rrule.*, but nothing in the production worker consumes those messages. RRuleWakeupProcessor and rrule_wakeup_consumer_config exist only in integration tests, so ScheduleOccurrenceFired events are never appended and the execution processor never dispatches user payloads or schedules the next occurrence.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 33a9536. Configure here.

@yordis yordis force-pushed the yordis/fix-rrule-scheduling branch from 33a9536 to dfcd0af Compare June 15, 2026 19:15
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
@yordis yordis force-pushed the yordis/fix-rrule-scheduling branch from dfcd0af to bad4e3d Compare June 15, 2026 19:23

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
rsworkspace/crates/trogon-scheduler/src/processor/execution/nats_execution_tests.rs (1)

134-151: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Avoid deleting fixed live-test streams without serializing the tests.

execution_stream() and event_stream() now delete shared constant streams. When the ignored live tests are run together, one test can delete a stream another test is actively using, causing flaky failures. Use per-test stream names or guard these live tests with a shared async mutex.

🧪 One low-impact fix: serialize the live tests
+static LIVE_NATS_TEST_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
+
 #[tokio::test]
 #[ignore = "requires NATS 2.14+ with AllowAtomicPublish and AllowMsgSchedules; set SCHEDULER_NATS_URL"]
 async fn rrule_command_event_is_processed_and_continued_against_live_nats() {
+    let _guard = LIVE_NATS_TEST_LOCK.lock().await;
     let Some(context) = context().await else {

Apply the same guard at the start of the other live tests that share EXECUTION_STREAM / EVENT_STREAM.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@rsworkspace/crates/trogon-scheduler/src/processor/execution/nats_execution_tests.rs`
around lines 134 - 151, The execution_stream() and event_stream() functions
delete shared constant streams (EXECUTION_STREAM and EVENT_STREAM) without
synchronization, causing race conditions when multiple live tests run
concurrently. Implement a shared async mutex guard to serialize access across
all live tests that use these streams. Add the mutex guard at the start of each
live test function that calls execution_stream() or event_stream() to ensure
only one test accesses these shared streams at a time, preventing one test from
deleting a stream while another is actively using it.
♻️ Duplicate comments (1)
rsworkspace/crates/trogon-scheduler/src/commands/record_schedule_occurrence.rs (1)

9-12: 🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

Replace raw occurrence timestamps with a domain value object across command/error surfaces.

RecordScheduleOccurrence and OccurrenceAlreadyRecorded still expose DateTime<Utc> primitives directly. Move these to a dedicated occurrence timestamp value object and convert at boundaries once, so invariants are guaranteed at construction.

Refactor sketch
-pub struct RecordScheduleOccurrence {
-    pub id: ScheduleId,
-    pub occurrence_at: DateTime<Utc>,
-}
+pub struct RecordScheduleOccurrence {
+    pub id: ScheduleId,
+    pub occurrence_at: ScheduleOccurrenceAt,
+}

 pub enum RecordScheduleOccurrenceError {
   OccurrenceAlreadyRecorded {
       id: ScheduleId,
-      occurrence_at: DateTime<Utc>,
-      last_recorded_at: DateTime<Utc>,
+      occurrence_at: ScheduleOccurrenceAt,
+      last_recorded_at: ScheduleOccurrenceAt,
   },
 }

- pub fn new(id: ScheduleId, occurrence_at: DateTime<Utc>) -> Self
+ pub fn new(id: ScheduleId, occurrence_at: ScheduleOccurrenceAt) -> Self

As per coding guidelines, “Prefer domain-specific value objects over primitives (e.g., AcpPrefix instead of String). Each type's factory must guarantee correctness at construction—invalid instances should be unrepresentable.”

Also applies to: 23-27, 45-46

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@rsworkspace/crates/trogon-scheduler/src/commands/record_schedule_occurrence.rs`
around lines 9 - 12, Create a dedicated domain value object for occurrence
timestamps to replace raw DateTime<Utc> primitives throughout the module. Define
a new value object type (e.g., OccurrenceTimestamp) with a constructor that
validates and guarantees correctness at construction time. Update the
RecordScheduleOccurrence struct at lines 9-12 to use this value object instead
of the occurrence_at field's raw DateTime<Utc> type. Apply the same change to
the OccurrenceAlreadyRecorded error struct at lines 23-27 and its usage at lines
45-46, ensuring all command and error surfaces consistently use the domain value
object rather than primitives, so timestamp invariants are enforced once at the
boundaries.

Source: Coding guidelines

🧹 Nitpick comments (1)
rsworkspace/crates/trogon-scheduler/src/processor/execution/nats_execution_tests.rs (1)

501-514: ⚡ Quick win

Assert the final occurrence dispatch before accepting the expiry path.

This test checks the first dispatch headers, then only checks that processing the second occurrence expires the schedule. A regression that expires on the final occurrence without dispatching sequence 2 could still pass. Add a second target-message assertion for Trogon-Schedule-Occurrence-Sequence: 2 and Trogon-Schedule-Occurrence-At: second_occurrence_text before the checkpoint assertions.

✅ Suggested coverage addition
     let expired = processor
         .process(&read.events[2], Utc::now())
         .await
         .expect("process second occurrence event");
     assert_eq!(expired.outcome, ProcessedOutcome::Expired);
+
+    let final_dispatch = execution_stream
+        .get_last_raw_message_by_subject(EXECUTION_TARGET_SUBJECT)
+        .await
+        .expect("second user dispatch was published");
+    assert_eq!(
+        final_dispatch
+            .headers
+            .get("Trogon-Schedule-Occurrence-Sequence")
+            .unwrap()
+            .as_str(),
+        "2"
+    );
+    assert_eq!(
+        final_dispatch
+            .headers
+            .get("Trogon-Schedule-Occurrence-At")
+            .unwrap()
+            .as_str(),
+        second_occurrence_text.as_str()
+    );
 
     let checkpoint = checkpoints
         .load(&key)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@rsworkspace/crates/trogon-scheduler/src/processor/execution/nats_execution_tests.rs`
around lines 501 - 514, The test verifies the first occurrence's dispatch but
then only checks that processing the second occurrence expires the schedule,
without verifying that the second occurrence was actually dispatched. This
leaves a gap where a regression could expire the second occurrence without
dispatching it and the test would still pass. After the assertion that
expired.outcome equals ProcessedOutcome::Expired, add a second target-message
assertion to verify that the dispatched message for the second occurrence
contains the correct headers: Trogon-Schedule-Occurrence-Sequence: 2 and
Trogon-Schedule-Occurrence-At matching the second_occurrence_text value. This
assertion should come before the checkpoint read and assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@proto/trogonai/scheduler/schedules/v1/schedule_occurrence_fired.proto`:
- Line 12: The occurrence_sequence field in the ScheduleOccurrenceFired message
permits missing values in the protobuf schema, but the command-state evolution
code in state.rs treats a missing occurrence_sequence as an error
(MissingOccurrenceSequence). To resolve this contract mismatch, add the required
keyword before the occurrence_sequence field declaration to enforce that this
field must always be present when the protobuf message is constructed.

In
`@rsworkspace/crates/trogon-scheduler/src/processor/execution/nats_execution_tests.rs`:
- Around line 345-349: The RRULE boundary test uses insufficient timing
intervals that are vulnerable to real system operation delays. The
first_occurrence is set only 3 seconds in the future (in the
ChronoDuration::seconds(3) call), and the second_occurrence is only 2 seconds
after that (in the ChronoDuration::seconds(2) call). Real NATS operations and
event processing can easily exceed these tight windows, causing the test to skip
occurrences or expire prematurely. Increase both duration values
substantially—aim for at least 30+ seconds for the initial offset and 20+
seconds for the interval—to provide adequate headroom for the actual scheduling
behavior to be validated without timing luck affecting the test outcome.

---

Outside diff comments:
In
`@rsworkspace/crates/trogon-scheduler/src/processor/execution/nats_execution_tests.rs`:
- Around line 134-151: The execution_stream() and event_stream() functions
delete shared constant streams (EXECUTION_STREAM and EVENT_STREAM) without
synchronization, causing race conditions when multiple live tests run
concurrently. Implement a shared async mutex guard to serialize access across
all live tests that use these streams. Add the mutex guard at the start of each
live test function that calls execution_stream() or event_stream() to ensure
only one test accesses these shared streams at a time, preventing one test from
deleting a stream while another is actively using it.

---

Duplicate comments:
In
`@rsworkspace/crates/trogon-scheduler/src/commands/record_schedule_occurrence.rs`:
- Around line 9-12: Create a dedicated domain value object for occurrence
timestamps to replace raw DateTime<Utc> primitives throughout the module. Define
a new value object type (e.g., OccurrenceTimestamp) with a constructor that
validates and guarantees correctness at construction time. Update the
RecordScheduleOccurrence struct at lines 9-12 to use this value object instead
of the occurrence_at field's raw DateTime<Utc> type. Apply the same change to
the OccurrenceAlreadyRecorded error struct at lines 23-27 and its usage at lines
45-46, ensuring all command and error surfaces consistently use the domain value
object rather than primitives, so timestamp invariants are enforced once at the
boundaries.

---

Nitpick comments:
In
`@rsworkspace/crates/trogon-scheduler/src/processor/execution/nats_execution_tests.rs`:
- Around line 501-514: The test verifies the first occurrence's dispatch but
then only checks that processing the second occurrence expires the schedule,
without verifying that the second occurrence was actually dispatched. This
leaves a gap where a regression could expire the second occurrence without
dispatching it and the test would still pass. After the assertion that
expired.outcome equals ProcessedOutcome::Expired, add a second target-message
assertion to verify that the dispatched message for the second occurrence
contains the correct headers: Trogon-Schedule-Occurrence-Sequence: 2 and
Trogon-Schedule-Occurrence-At matching the second_occurrence_text value. This
assertion should come before the checkpoint read and assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a249ff8c-e53d-49dd-b458-fc3d9c630496

📥 Commits

Reviewing files that changed from the base of the PR and between 5ccf0da and bad4e3d.

⛔ Files ignored due to path filters (9)
  • rsworkspace/crates/trogonai-proto/src/gen/trogonai.scheduler.schedules.state.v1.state.__view.rs is excluded by !**/gen/**
  • rsworkspace/crates/trogonai-proto/src/gen/trogonai.scheduler.schedules.state.v1.state.rs is excluded by !**/gen/**
  • rsworkspace/crates/trogonai-proto/src/gen/trogonai.scheduler.schedules.v1.events.__oneof.rs is excluded by !**/gen/**
  • rsworkspace/crates/trogonai-proto/src/gen/trogonai.scheduler.schedules.v1.events.__view.rs is excluded by !**/gen/**
  • rsworkspace/crates/trogonai-proto/src/gen/trogonai.scheduler.schedules.v1.events.__view_oneof.rs is excluded by !**/gen/**
  • rsworkspace/crates/trogonai-proto/src/gen/trogonai.scheduler.schedules.v1.events.rs is excluded by !**/gen/**
  • rsworkspace/crates/trogonai-proto/src/gen/trogonai.scheduler.schedules.v1.mod.rs is excluded by !**/gen/**
  • rsworkspace/crates/trogonai-proto/src/gen/trogonai.scheduler.schedules.v1.schedule_occurrence_fired.__view.rs is excluded by !**/gen/**
  • rsworkspace/crates/trogonai-proto/src/gen/trogonai.scheduler.schedules.v1.schedule_occurrence_fired.rs is excluded by !**/gen/**
📒 Files selected for processing (27)
  • proto/trogonai/scheduler/schedules/state/v1/state.proto
  • proto/trogonai/scheduler/schedules/v1/events.proto
  • proto/trogonai/scheduler/schedules/v1/schedule_occurrence_fired.proto
  • rsworkspace/crates/trogon-scheduler/src/commands/create_schedule.rs
  • rsworkspace/crates/trogon-scheduler/src/commands/domain/mod.rs
  • rsworkspace/crates/trogon-scheduler/src/commands/domain/schedule_occurrence_sequence.rs
  • rsworkspace/crates/trogon-scheduler/src/commands/mod.rs
  • rsworkspace/crates/trogon-scheduler/src/commands/pause_schedule.rs
  • rsworkspace/crates/trogon-scheduler/src/commands/record_schedule_occurrence.rs
  • rsworkspace/crates/trogon-scheduler/src/commands/remove_schedule.rs
  • rsworkspace/crates/trogon-scheduler/src/commands/resume_schedule.rs
  • rsworkspace/crates/trogon-scheduler/src/commands/state.rs
  • rsworkspace/crates/trogon-scheduler/src/lib.rs
  • rsworkspace/crates/trogon-scheduler/src/processor/execution/execution_schedules/mod.rs
  • rsworkspace/crates/trogon-scheduler/src/processor/execution/mod.rs
  • rsworkspace/crates/trogon-scheduler/src/processor/execution/nats_execution_tests.rs
  • rsworkspace/crates/trogon-scheduler/src/processor/execution/reconciliation/mod.rs
  • rsworkspace/crates/trogon-scheduler/src/processor/execution/reconciliation/reconcile.rs
  • rsworkspace/crates/trogon-scheduler/src/processor/execution/reconciliation/recorded_events.rs
  • rsworkspace/crates/trogon-scheduler/src/processor/execution/reconciliation/request.rs
  • rsworkspace/crates/trogon-scheduler/src/processor/execution/reconciliation/rrule.rs
  • rsworkspace/crates/trogon-scheduler/src/processor/execution/reconciliation/rrule_wakeup_payload.rs
  • rsworkspace/crates/trogon-scheduler/src/processor/execution/reconciliation/schedule_subject.rs
  • rsworkspace/crates/trogon-scheduler/src/processor/execution/wakeup.rs
  • rsworkspace/crates/trogon-scheduler/src/processor/execution/worker/processor.rs
  • rsworkspace/crates/trogon-scheduler/src/processor/mod.rs
  • rsworkspace/crates/trogonai-proto/src/scheduler/schedules/codec.rs
✅ Files skipped from review due to trivial changes (2)
  • rsworkspace/crates/trogon-scheduler/src/processor/mod.rs
  • rsworkspace/crates/trogon-scheduler/src/lib.rs
🚧 Files skipped from review as they are similar to previous changes (16)
  • rsworkspace/crates/trogon-scheduler/src/processor/execution/mod.rs
  • rsworkspace/crates/trogon-scheduler/src/commands/pause_schedule.rs
  • rsworkspace/crates/trogon-scheduler/src/commands/resume_schedule.rs
  • rsworkspace/crates/trogon-scheduler/src/processor/execution/reconciliation/rrule_wakeup_payload.rs
  • rsworkspace/crates/trogonai-proto/src/scheduler/schedules/codec.rs
  • rsworkspace/crates/trogon-scheduler/src/processor/execution/execution_schedules/mod.rs
  • rsworkspace/crates/trogon-scheduler/src/processor/execution/reconciliation/mod.rs
  • rsworkspace/crates/trogon-scheduler/src/processor/execution/reconciliation/schedule_subject.rs
  • proto/trogonai/scheduler/schedules/v1/events.proto
  • rsworkspace/crates/trogon-scheduler/src/commands/mod.rs
  • rsworkspace/crates/trogon-scheduler/src/processor/execution/reconciliation/rrule.rs
  • rsworkspace/crates/trogon-scheduler/src/processor/execution/reconciliation/reconcile.rs
  • rsworkspace/crates/trogon-scheduler/src/processor/execution/reconciliation/recorded_events.rs
  • rsworkspace/crates/trogon-scheduler/src/processor/execution/worker/processor.rs
  • rsworkspace/crates/trogon-scheduler/src/commands/remove_schedule.rs
  • rsworkspace/crates/trogon-scheduler/src/processor/execution/reconciliation/request.rs

// Stable NATS token used as the schedule stream id and schedule key.
string schedule_id = 1 [features.field_presence = LEGACY_REQUIRED];
// Gapless sequence among accepted occurrences for this schedule.
uint64 occurrence_sequence = 2;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify schema/runtime presence mismatch for occurrence_sequence.

rg -n 'occurrence_sequence\s*=\s*2' proto/trogonai/scheduler/schedules/v1/schedule_occurrence_fired.proto
rg -n 'MissingOccurrenceSequence|occurrence_sequence.*ok_or' rsworkspace/crates/trogon-scheduler/src/commands/state.rs
rg -n 'occurrence_sequence|opt_uint64|Option<u64>' rsworkspace/crates/trogonai-proto/src/gen/trogonai.scheduler.schedules.v1.schedule_occurrence_fired.rs

Repository: TrogonStack/trogonai

Length of output: 1008


Require presence for occurrence_sequence to match runtime expectations.

The field at line 12 currently permits missing occurrence_sequence, but command-state evolution treats a missing value as an error (MissingOccurrenceSequence at line 85 of state.rs). This creates a contract mismatch where valid protobuf messages can cause runtime failures. Tighten the schema to enforce the field's presence.

Proposed fix
-  uint64 occurrence_sequence = 2;
+  uint64 occurrence_sequence = 2 [features.field_presence = LEGACY_REQUIRED];
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
uint64 occurrence_sequence = 2;
uint64 occurrence_sequence = 2 [features.field_presence = LEGACY_REQUIRED];
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@proto/trogonai/scheduler/schedules/v1/schedule_occurrence_fired.proto` at
line 12, The occurrence_sequence field in the ScheduleOccurrenceFired message
permits missing values in the protobuf schema, but the command-state evolution
code in state.rs treats a missing occurrence_sequence as an error
(MissingOccurrenceSequence). To resolve this contract mismatch, add the required
keyword before the occurrence_sequence field declaration to enforce that this
field must always be present when the protobuf message is constructed.

Comment on lines +345 to +349
let first_occurrence = Utc::now() + ChronoDuration::seconds(3);
let first_occurrence = first_occurrence
.with_nanosecond(0)
.expect("valid nanosecond normalization");
let second_occurrence = first_occurrence + ChronoDuration::seconds(2);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Give the RRULE boundary test more scheduling headroom.

The first wakeup is only 3 seconds out, and the second is 2 seconds later. Live NATS setup, event-store append/read, and processor work can easily cross those recurrence boundaries, making the test skip the first/second occurrence or expire early. Increase the offsets/interval so the test validates recurrence behavior instead of scheduler timing luck.

⏱️ Suggested timing adjustment
-    let first_occurrence = Utc::now() + ChronoDuration::seconds(3);
+    let first_occurrence = Utc::now() + ChronoDuration::seconds(10);
@@
-    let second_occurrence = first_occurrence + ChronoDuration::seconds(2);
+    let second_occurrence = first_occurrence + ChronoDuration::seconds(5);
@@
-    let command = create_rrule_schedule(id.clone(), &first_occurrence_text, "FREQ=SECONDLY;INTERVAL=2;COUNT=2");
+    let command = create_rrule_schedule(id.clone(), &first_occurrence_text, "FREQ=SECONDLY;INTERVAL=5;COUNT=2");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@rsworkspace/crates/trogon-scheduler/src/processor/execution/nats_execution_tests.rs`
around lines 345 - 349, The RRULE boundary test uses insufficient timing
intervals that are vulnerable to real system operation delays. The
first_occurrence is set only 3 seconds in the future (in the
ChronoDuration::seconds(3) call), and the second_occurrence is only 2 seconds
after that (in the ChronoDuration::seconds(2) call). Real NATS operations and
event processing can easily exceed these tight windows, causing the test to skip
occurrences or expire prematurely. Increase both duration values
substantially—aim for at least 30+ seconds for the initial offset and 20+
seconds for the interval—to provide adequate headroom for the actual scheduling
behavior to be validated without timing luck affecting the test outcome.

Recurrence progress must be durable, replayable, and deterministic;
deciding the next occurrence outside the event-sourced aggregate left
it able to drift or stall and unrecoverable from the stream alone.

Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
state_v1::StateValue::STATE_VALUE_PRESENT_ENABLED => {
if state.pending_occurrence_at.is_set() {
return Err(ScheduleNextOccurrenceError::AlreadyArmed { id: command.id.clone() });
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exhausted arm lacks idempotency

Medium Severity

ScheduleNextOccurrence only rejects when pending_occurrence_at is set. After a ScheduleCompleted event, pending is cleared while the schedule stays enabled, so a retried ArmNext can append another ScheduleCompleted instead of being treated as already finished.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 2894dbe. Configure here.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@rsworkspace/crates/trogon-scheduler/src/commands/rrule.rs`:
- Around line 103-108: The function schedule_or_complete_event currently accepts
raw u64 primitives for next_sequence and last_sequence parameters, which allows
invalid or unchecked sequence transitions to bypass domain validation and
persist into events. Replace the u64 parameter types with
ScheduleOccurrenceSequence value objects for both next_sequence and
last_sequence to ensure sequence values are validated according to domain
invariants before being used in the helper function. The conversion from
validated domain sequences to wire-format primitives should only occur at the
point of emitting the v1::ScheduleEvent, maintaining separation between
validated domain logic and serialized output.
- Around line 45-47: The Expansion variant in the error enum is currently
storing the error as a flattened String, which discards the source error context
and chain. Replace the message: String field in the Expansion variant with a
#[source] annotated field named source that holds the concrete rrule::RRuleError
type directly. Then update the error conversion at the location where
RRuleExpansionError::Expansion is constructed (around lines 77-79) to pass the
rrule::RRuleError as the source field instead of converting it to a string via
.to_string(). This preserves the full error context and allows error reporting
tools to traverse the error chain.

In
`@rsworkspace/crates/trogon-scheduler/src/processor/execution/reconciliation/reconcile.rs`:
- Around line 276-293: The reconcile_completed function purges and expires
checkpoints unconditionally without verifying that the checkpoint is
specifically a scheduled RRULE checkpoint. Add a guard check after the
checkpoint_for call to validate the checkpoint status and kind, similar to how
OccurrenceRecorded and OccurrenceScheduled handle validation. Only proceed with
the Purge action and expiration if the checkpoint is confirmed to be a scheduled
RRULE checkpoint; otherwise, return an appropriate error to prevent incorrect
expiration of non-RRULE schedules from out-of-contract Completed events.

In
`@rsworkspace/crates/trogon-scheduler/src/processor/execution/reconciliation/recorded_events.rs`:
- Around line 257-269: The ScheduleOccurrenceScheduled decoding in the match arm
starting at the ScheduleEventCase::ScheduleOccurrenceScheduled pattern does not
extract or validate the occurrence_sequence field, which represents recurrence
identity data that should be persisted as a validated domain type. Follow the
same extraction and validation pattern used for schedule_id and occurrence_at:
extract occurrence_sequence from the scheduled object, validate it (handling
missing or invalid values with an appropriate ScheduleEventDecodeError), and
include the validated value in the returned ScheduleChange::OccurrenceScheduled
struct alongside schedule_id and occurrence_at.

In
`@rsworkspace/crates/trogon-scheduler/src/processor/execution/worker/processor.rs`:
- Around line 495-500: The match statement on the rejection in the
CommandError::Decide error handler currently treats ScheduleNotFound and
ScheduleDeleted as success by returning Ok(()), which silently ignores these
critical errors and can cause recurrence stalls. Remove ScheduleNotFound and
ScheduleDeleted from the match arm that returns Ok(()) (which currently also
contains AlreadyArmed and SchedulePaused), allowing these variants to fall
through to the other branch that returns a proper RetrySignal error instead.
This ensures that when a schedule is not found or has been deleted, the error is
properly propagated rather than silently acknowledged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 819a18a4-6079-48b8-b1d0-41ae1018984a

📥 Commits

Reviewing files that changed from the base of the PR and between bad4e3d and 2894dbe.

⛔ Files ignored due to path filters (13)
  • rsworkspace/crates/trogonai-proto/src/gen/trogonai.scheduler.schedules.state.v1.state.__view.rs is excluded by !**/gen/**
  • rsworkspace/crates/trogonai-proto/src/gen/trogonai.scheduler.schedules.state.v1.state.rs is excluded by !**/gen/**
  • rsworkspace/crates/trogonai-proto/src/gen/trogonai.scheduler.schedules.v1.events.__oneof.rs is excluded by !**/gen/**
  • rsworkspace/crates/trogonai-proto/src/gen/trogonai.scheduler.schedules.v1.events.__view.rs is excluded by !**/gen/**
  • rsworkspace/crates/trogonai-proto/src/gen/trogonai.scheduler.schedules.v1.events.__view_oneof.rs is excluded by !**/gen/**
  • rsworkspace/crates/trogonai-proto/src/gen/trogonai.scheduler.schedules.v1.events.rs is excluded by !**/gen/**
  • rsworkspace/crates/trogonai-proto/src/gen/trogonai.scheduler.schedules.v1.mod.rs is excluded by !**/gen/**
  • rsworkspace/crates/trogonai-proto/src/gen/trogonai.scheduler.schedules.v1.schedule_completed.__view.rs is excluded by !**/gen/**
  • rsworkspace/crates/trogonai-proto/src/gen/trogonai.scheduler.schedules.v1.schedule_completed.rs is excluded by !**/gen/**
  • rsworkspace/crates/trogonai-proto/src/gen/trogonai.scheduler.schedules.v1.schedule_occurrence_recorded.__view.rs is excluded by !**/gen/**
  • rsworkspace/crates/trogonai-proto/src/gen/trogonai.scheduler.schedules.v1.schedule_occurrence_recorded.rs is excluded by !**/gen/**
  • rsworkspace/crates/trogonai-proto/src/gen/trogonai.scheduler.schedules.v1.schedule_occurrence_scheduled.__view.rs is excluded by !**/gen/**
  • rsworkspace/crates/trogonai-proto/src/gen/trogonai.scheduler.schedules.v1.schedule_occurrence_scheduled.rs is excluded by !**/gen/**
📒 Files selected for processing (25)
  • proto/trogonai/scheduler/schedules/state/v1/state.proto
  • proto/trogonai/scheduler/schedules/v1/events.proto
  • proto/trogonai/scheduler/schedules/v1/schedule_completed.proto
  • proto/trogonai/scheduler/schedules/v1/schedule_occurrence_recorded.proto
  • proto/trogonai/scheduler/schedules/v1/schedule_occurrence_scheduled.proto
  • rsworkspace/crates/trogon-scheduler/src/commands/create_schedule.rs
  • rsworkspace/crates/trogon-scheduler/src/commands/mod.rs
  • rsworkspace/crates/trogon-scheduler/src/commands/pause_schedule.rs
  • rsworkspace/crates/trogon-scheduler/src/commands/record_schedule_occurrence.rs
  • rsworkspace/crates/trogon-scheduler/src/commands/remove_schedule.rs
  • rsworkspace/crates/trogon-scheduler/src/commands/resume_schedule.rs
  • rsworkspace/crates/trogon-scheduler/src/commands/rrule.rs
  • rsworkspace/crates/trogon-scheduler/src/commands/schedule_next_occurrence.rs
  • rsworkspace/crates/trogon-scheduler/src/commands/state.rs
  • rsworkspace/crates/trogon-scheduler/src/lib.rs
  • rsworkspace/crates/trogon-scheduler/src/processor/execution/nats_execution_tests.rs
  • rsworkspace/crates/trogon-scheduler/src/processor/execution/reconciliation/mod.rs
  • rsworkspace/crates/trogon-scheduler/src/processor/execution/reconciliation/reconcile.rs
  • rsworkspace/crates/trogon-scheduler/src/processor/execution/reconciliation/recorded_events.rs
  • rsworkspace/crates/trogon-scheduler/src/processor/execution/reconciliation/request.rs
  • rsworkspace/crates/trogon-scheduler/src/processor/execution/wakeup.rs
  • rsworkspace/crates/trogon-scheduler/src/processor/execution/worker/dispatcher.rs
  • rsworkspace/crates/trogon-scheduler/src/processor/execution/worker/processor.rs
  • rsworkspace/crates/trogon-scheduler/src/processor/execution/worker/testkit.rs
  • rsworkspace/crates/trogonai-proto/src/scheduler/schedules/codec.rs
🚧 Files skipped from review as they are similar to previous changes (8)
  • proto/trogonai/scheduler/schedules/state/v1/state.proto
  • rsworkspace/crates/trogon-scheduler/src/commands/remove_schedule.rs
  • rsworkspace/crates/trogon-scheduler/src/commands/resume_schedule.rs
  • rsworkspace/crates/trogon-scheduler/src/commands/create_schedule.rs
  • rsworkspace/crates/trogon-scheduler/src/commands/pause_schedule.rs
  • rsworkspace/crates/trogon-scheduler/src/processor/execution/nats_execution_tests.rs
  • rsworkspace/crates/trogon-scheduler/src/processor/execution/wakeup.rs
  • rsworkspace/crates/trogon-scheduler/src/processor/execution/reconciliation/request.rs

Comment on lines +45 to +47
#[error("RRULE set could not be expanded: {message}")]
Expansion { message: String },
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In the Rust rrulecrate version used by this repository, what concrete error type doesRRuleSet::set_from_stringreturn, and does that type implementstd::error::Error?

💡 Result:

In the rrule crate, the RRuleSet::set_from_string method returns a Result where the error type is RRuleError [1]. RRuleError is an enum defined within the crate [2][3]. It implements the std::error::Error trait [2]. The implementation is facilitated by the thiserror crate, as shown in the source code [3], which derives the Error trait for the enum [3].

Citations:


🏁 Script executed:

cat -n rsworkspace/crates/trogon-scheduler/src/commands/rrule.rs | head -90

Repository: TrogonStack/trogonai

Length of output: 3690


Preserve the RRULE expansion source error instead of flattening to String.

The Expansion variant (lines 45–47) stores message: String, and line 77–79 converts the typed RRuleError via .to_string(), discarding the error context and source chain. Change the variant to store the concrete rrule::RRuleError with #[source] instead:

Suggested fix
#[error("RRULE set could not be expanded")]
Expansion {
    #[source]
    source: rrule::RRuleError,
},

And update the conversion:

.map_err(|source| RRuleExpansionError::Expansion { source })?;

Per the coding guidelines: "Never discard error context by converting a typed error into a string; wrap the source error as a field or variant instead."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rsworkspace/crates/trogon-scheduler/src/commands/rrule.rs` around lines 45 -
47, The Expansion variant in the error enum is currently storing the error as a
flattened String, which discards the source error context and chain. Replace the
message: String field in the Expansion variant with a #[source] annotated field
named source that holds the concrete rrule::RRuleError type directly. Then
update the error conversion at the location where RRuleExpansionError::Expansion
is constructed (around lines 77-79) to pass the rrule::RRuleError as the source
field instead of converting it to a string via .to_string(). This preserves the
full error context and allows error reporting tools to traverse the error chain.

Source: Coding guidelines

Comment on lines +103 to +108
pub(crate) fn schedule_or_complete_event(
schedule_id: &str,
next_occurrence: Option<DateTime<Utc>>,
next_sequence: u64,
last_sequence: u64,
) -> v1::ScheduleEvent {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Use ScheduleOccurrenceSequence in this helper contract, not raw u64.

Line 106 and Line 107 accept primitive sequence values in domain logic, which bypasses the sequence value-object invariants and allows saturated/invalid progression inputs to leak into persisted events. This helper should take validated domain sequence types and convert to wire primitives only at emit time.

[additional context] rsworkspace/crates/trogon-scheduler/src/commands/record_schedule_occurrence.rs:159-166 already passes a saturating_add(1) value into this API, which is precisely the kind of unchecked sequence transition this signature permits.

As per coding guidelines, "Prefer domain-specific value objects over primitives ... invalid instances should be unrepresentable."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rsworkspace/crates/trogon-scheduler/src/commands/rrule.rs` around lines 103 -
108, The function schedule_or_complete_event currently accepts raw u64
primitives for next_sequence and last_sequence parameters, which allows invalid
or unchecked sequence transitions to bypass domain validation and persist into
events. Replace the u64 parameter types with ScheduleOccurrenceSequence value
objects for both next_sequence and last_sequence to ensure sequence values are
validated according to domain invariants before being used in the helper
function. The conversion from validated domain sequences to wire-format
primitives should only occur at the point of emitting the v1::ScheduleEvent,
maintaining separation between validated domain logic and serialized output.

Source: Coding guidelines

Comment on lines +276 to +293
fn reconcile_completed(
current: Option<&ScheduleCheckpointRecord>,
schedule_id: &ScheduleId,
stream_position: StreamPosition,
event_id: Option<&str>,
) -> Result<Reconciliation, ReconcileError> {
let current = checkpoint_for(current, schedule_id)?;

Ok(Reconciliation {
action: ReconcileAction::Purge(current.subject()),
next_checkpoint: advanced(
current,
ScheduleStatus::Expired,
ReconcileOutcome::Expired,
stream_position,
event_id,
),
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard Completed reconciliation to scheduled RRULE checkpoints only.

On Line 285, reconcile_completed purges/expires unconditionally. Unlike OccurrenceRecorded/OccurrenceScheduled, this path doesn’t verify checkpoint status/kind, so an out-of-contract Completed event can incorrectly expire a non-RRULE schedule.

Suggested fix
 fn reconcile_completed(
     current: Option<&ScheduleCheckpointRecord>,
     schedule_id: &ScheduleId,
     stream_position: StreamPosition,
     event_id: Option<&str>,
 ) -> Result<Reconciliation, ReconcileError> {
     let current = checkpoint_for(current, schedule_id)?;
+    if current.status != ScheduleStatus::Scheduled || !matches!(current.schedule, Schedule::RRule { .. }) {
+        return Ok(Reconciliation {
+            action: ReconcileAction::CheckpointOnly,
+            next_checkpoint: advanced(
+                current,
+                current.status,
+                ReconcileOutcome::DuplicateStale,
+                stream_position,
+                event_id,
+            ),
+        });
+    }
 
     Ok(Reconciliation {
         action: ReconcileAction::Purge(current.subject()),
         next_checkpoint: advanced(
             current,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@rsworkspace/crates/trogon-scheduler/src/processor/execution/reconciliation/reconcile.rs`
around lines 276 - 293, The reconcile_completed function purges and expires
checkpoints unconditionally without verifying that the checkpoint is
specifically a scheduled RRULE checkpoint. Add a guard check after the
checkpoint_for call to validate the checkpoint status and kind, similar to how
OccurrenceRecorded and OccurrenceScheduled handle validation. Only proceed with
the Purge action and expiration if the checkpoint is confirmed to be a scheduled
RRULE checkpoint; otherwise, return an appropriate error to prevent incorrect
expiration of non-RRULE schedules from out-of-contract Completed events.

Comment on lines +257 to +269
ScheduleEventCase::ScheduleOccurrenceScheduled(scheduled) => {
let schedule_id = schedule_id_from(&scheduled.schedule_id)?;
let occurrence_at = timestamp_to_datetime(
scheduled
.occurrence_at
.as_option()
.ok_or(ScheduleEventDecodeError::MissingField { field: "occurrence_at" })?,
"occurrence_at",
)?;
Ok(ScheduleChange::OccurrenceScheduled {
schedule_id,
occurrence_at,
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate occurrence_sequence for ScheduleOccurrenceScheduled during decode.

Line 257 decodes ScheduleOccurrenceScheduled without validating occurrence_sequence. That allows missing/invalid sequence data to pass boundary decoding even though this event shape carries recurrence identity fields.

As per coding guidelines, “Prefer domain-specific value objects over primitives … invalid instances should be unrepresentable” and “Persist only validated domain types to storage, events, and runtime state.”

Suggested fix
         ScheduleEventCase::ScheduleOccurrenceScheduled(scheduled) => {
             let schedule_id = schedule_id_from(&scheduled.schedule_id)?;
+            let _occurrence_sequence = ScheduleOccurrenceSequence::try_new(
+                scheduled.occurrence_sequence.ok_or(ScheduleEventDecodeError::MissingField {
+                    field: "occurrence_sequence",
+                })?,
+            )
+            .map_err(|source| ScheduleEventDecodeError::OccurrenceSequence { source })?;
             let occurrence_at = timestamp_to_datetime(
                 scheduled
                     .occurrence_at
                     .as_option()
                     .ok_or(ScheduleEventDecodeError::MissingField { field: "occurrence_at" })?,
                 "occurrence_at",
             )?;
             Ok(ScheduleChange::OccurrenceScheduled {
                 schedule_id,
                 occurrence_at,
             })
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ScheduleEventCase::ScheduleOccurrenceScheduled(scheduled) => {
let schedule_id = schedule_id_from(&scheduled.schedule_id)?;
let occurrence_at = timestamp_to_datetime(
scheduled
.occurrence_at
.as_option()
.ok_or(ScheduleEventDecodeError::MissingField { field: "occurrence_at" })?,
"occurrence_at",
)?;
Ok(ScheduleChange::OccurrenceScheduled {
schedule_id,
occurrence_at,
})
ScheduleEventCase::ScheduleOccurrenceScheduled(scheduled) => {
let schedule_id = schedule_id_from(&scheduled.schedule_id)?;
let _occurrence_sequence = ScheduleOccurrenceSequence::try_new(
scheduled.occurrence_sequence.ok_or(ScheduleEventDecodeError::MissingField {
field: "occurrence_sequence",
})?,
)
.map_err(|source| ScheduleEventDecodeError::OccurrenceSequence { source })?;
let occurrence_at = timestamp_to_datetime(
scheduled
.occurrence_at
.as_option()
.ok_or(ScheduleEventDecodeError::MissingField { field: "occurrence_at" })?,
"occurrence_at",
)?;
Ok(ScheduleChange::OccurrenceScheduled {
schedule_id,
occurrence_at,
})
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@rsworkspace/crates/trogon-scheduler/src/processor/execution/reconciliation/recorded_events.rs`
around lines 257 - 269, The ScheduleOccurrenceScheduled decoding in the match
arm starting at the ScheduleEventCase::ScheduleOccurrenceScheduled pattern does
not extract or validate the occurrence_sequence field, which represents
recurrence identity data that should be persisted as a validated domain type.
Follow the same extraction and validation pattern used for schedule_id and
occurrence_at: extract occurrence_sequence from the scheduled object, validate
it (handling missing or invalid values with an appropriate
ScheduleEventDecodeError), and include the validated value in the returned
ScheduleChange::OccurrenceScheduled struct alongside schedule_id and
occurrence_at.

Source: Coding guidelines

Comment on lines +495 to +500
Err(CommandError::Decide(rejection)) => match rejection {
ScheduleNextOccurrenceError::AlreadyArmed { .. }
| ScheduleNextOccurrenceError::SchedulePaused { .. }
| ScheduleNextOccurrenceError::ScheduleNotFound { .. }
| ScheduleNextOccurrenceError::ScheduleDeleted { .. } => Ok(()),
other => Err(RetrySignal::ArmSchedule {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not silently no-op ScheduleNotFound/ScheduleDeleted in ArmNext.

On Lines 498-499, these rejections are treated as success. In the RRULE create/resume arming path, that can acknowledge the record without planning the next occurrence, causing silent recurrence stalls.

Suggested fix
         match CommandExecution::new(&self.event_store, &command).execute().await {
             Ok(_) => Ok(()),
             Err(CommandError::Decide(rejection)) => match rejection {
                 ScheduleNextOccurrenceError::AlreadyArmed { .. }
-                | ScheduleNextOccurrenceError::SchedulePaused { .. }
-                | ScheduleNextOccurrenceError::ScheduleNotFound { .. }
-                | ScheduleNextOccurrenceError::ScheduleDeleted { .. } => Ok(()),
+                | ScheduleNextOccurrenceError::SchedulePaused { .. } => Ok(()),
                 other => Err(RetrySignal::ArmSchedule {
                     source: Box::new(other),
                 }),
             },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@rsworkspace/crates/trogon-scheduler/src/processor/execution/worker/processor.rs`
around lines 495 - 500, The match statement on the rejection in the
CommandError::Decide error handler currently treats ScheduleNotFound and
ScheduleDeleted as success by returning Ok(()), which silently ignores these
critical errors and can cause recurrence stalls. Remove ScheduleNotFound and
ScheduleDeleted from the match arm that returns Ok(()) (which currently also
contains AlreadyArmed and SchedulePaused), allowing these variants to fall
through to the other branch that returns a proper RetrySignal error instead.
This ensures that when a schedule is not found or has been deleted, the error is
properly propagated rather than silently acknowledged.

Operators need to see when an occurrence was actually recorded and
planned (and the lateness between), distinct from the due instant.
These wall-clock fields are audit-only and stay out of state and
identity so the recurrence decision remains deterministic.

Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

There are 5 total unresolved issues (including 4 from previous reviews).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 1c1248b. Configure here.

}
if source.is_some() {
return Err(ScheduleRequestError::UnsupportedDispatchSource);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RRULE sampling breaks dispatch

High Severity

DispatchRequest::build_with_occurrence rejects any delivery with a sampling source, but RRULE recurrence still accepts those deliveries at create and persists them on the checkpoint. After a wakeup is recorded, reconciliation builds a dispatch for the user message and fails with UnsupportedDispatchSource, which the execution processor treats as a durable poison while the aggregate has already advanced.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 1c1248b. Configure here.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
rsworkspace/crates/trogon-scheduler/src/commands/record_schedule_occurrence.rs (1)

148-150: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid lossy sequence defaults and saturation when advancing occurrence identity.

unwrap_or(0) can turn malformed persisted progress into sequence 1, and saturating_add(1) can schedule a follow-up with the same sequence at the u64::MAX boundary. Reject invalid progress and advance the follow-up sequence through ScheduleOccurrenceSequence::next_after(...) instead.

Suggested direction
+                let prior_sequence = match (state.last_occurrence_at.as_option().is_some(), state.last_occurrence_sequence) {
+                    (false, None) => 0,
+                    (true, Some(sequence)) => sequence,
+                    _ => {
+                        return Err(RecordScheduleOccurrenceError::MalformedOccurrenceProgress {
+                            id: command.id.clone(),
+                        });
+                    }
+                };
+
                 let occurrence_sequence =
-                    ScheduleOccurrenceSequence::next_after(state.last_occurrence_sequence.unwrap_or(0))
+                    ScheduleOccurrenceSequence::next_after(prior_sequence)
                         .map_err(|source| RecordScheduleOccurrenceError::OccurrenceSequence { source })?;
 
                 let next_occurrence = next_rrule_occurrence(schedule, RRuleCursor::after(command.occurrence_at))
                     .map_err(|source| RecordScheduleOccurrenceError::NextOccurrence { source })?;
+                let next_occurrence_sequence = if next_occurrence.is_some() {
+                    ScheduleOccurrenceSequence::next_after(occurrence_sequence.as_u64())
+                        .map_err(|source| RecordScheduleOccurrenceError::OccurrenceSequence { source })?
+                        .as_u64()
+                } else {
+                    occurrence_sequence.as_u64()
+                };
 
                 let follow_up = schedule_or_complete_event(
                     command.id.as_str(),
                     next_occurrence,
-                    occurrence_sequence.as_u64().saturating_add(1),
+                    next_occurrence_sequence,
                     occurrence_sequence.as_u64(),
                     command.recorded_at,
                 );

Also applies to: 170-176

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@rsworkspace/crates/trogon-scheduler/src/commands/record_schedule_occurrence.rs`
around lines 148 - 150, The code uses unwrap_or(0) which can mask malformed
persisted progress and turn it into sequence 1, and saturating_add(1) which can
fail to advance the sequence at the u64::MAX boundary. In both locations where
occurrence_sequence is being calculated (around lines 148-150 and 170-176),
replace the pattern of unwrap_or(0) combined with saturating_add(1) with a call
to ScheduleOccurrenceSequence::next_after(...) that properly rejects invalid
progress states. This ensures that invalid progress is rejected rather than
silently defaulted, and that sequence advancement correctly handles boundary
conditions instead of saturating.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In
`@rsworkspace/crates/trogon-scheduler/src/commands/record_schedule_occurrence.rs`:
- Around line 148-150: The code uses unwrap_or(0) which can mask malformed
persisted progress and turn it into sequence 1, and saturating_add(1) which can
fail to advance the sequence at the u64::MAX boundary. In both locations where
occurrence_sequence is being calculated (around lines 148-150 and 170-176),
replace the pattern of unwrap_or(0) combined with saturating_add(1) with a call
to ScheduleOccurrenceSequence::next_after(...) that properly rejects invalid
progress states. This ensures that invalid progress is rejected rather than
silently defaulted, and that sequence advancement correctly handles boundary
conditions instead of saturating.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9da5939d-b333-4522-8e32-6b5f90564459

📥 Commits

Reviewing files that changed from the base of the PR and between 2894dbe and 1c1248b.

⛔ Files ignored due to path filters (4)
  • rsworkspace/crates/trogonai-proto/src/gen/trogonai.scheduler.schedules.v1.schedule_occurrence_recorded.__view.rs is excluded by !**/gen/**
  • rsworkspace/crates/trogonai-proto/src/gen/trogonai.scheduler.schedules.v1.schedule_occurrence_recorded.rs is excluded by !**/gen/**
  • rsworkspace/crates/trogonai-proto/src/gen/trogonai.scheduler.schedules.v1.schedule_occurrence_scheduled.__view.rs is excluded by !**/gen/**
  • rsworkspace/crates/trogonai-proto/src/gen/trogonai.scheduler.schedules.v1.schedule_occurrence_scheduled.rs is excluded by !**/gen/**
📒 Files selected for processing (9)
  • proto/trogonai/scheduler/schedules/v1/schedule_occurrence_recorded.proto
  • proto/trogonai/scheduler/schedules/v1/schedule_occurrence_scheduled.proto
  • rsworkspace/crates/trogon-scheduler/src/commands/record_schedule_occurrence.rs
  • rsworkspace/crates/trogon-scheduler/src/commands/rrule.rs
  • rsworkspace/crates/trogon-scheduler/src/commands/schedule_next_occurrence.rs
  • rsworkspace/crates/trogon-scheduler/src/commands/state.rs
  • rsworkspace/crates/trogon-scheduler/src/processor/execution/reconciliation/recorded_events.rs
  • rsworkspace/crates/trogon-scheduler/src/processor/execution/wakeup.rs
  • rsworkspace/crates/trogon-scheduler/src/processor/execution/worker/processor.rs
🚧 Files skipped from review as they are similar to previous changes (5)
  • rsworkspace/crates/trogon-scheduler/src/processor/execution/reconciliation/recorded_events.rs
  • rsworkspace/crates/trogon-scheduler/src/commands/rrule.rs
  • rsworkspace/crates/trogon-scheduler/src/commands/schedule_next_occurrence.rs
  • rsworkspace/crates/trogon-scheduler/src/commands/state.rs
  • rsworkspace/crates/trogon-scheduler/src/processor/execution/worker/processor.rs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

rust:coverage-baseline-reset Relax Rust coverage gate to establish a new baseline

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant