Skip to content

fix(scheduler): wire timer→executor bridge so scheduled jobs run (#347)#372

Open
gnanirahulnutakki wants to merge 1 commit into
devfrom
worktree-fix-347-scheduler-executor
Open

fix(scheduler): wire timer→executor bridge so scheduled jobs run (#347)#372
gnanirahulnutakki wants to merge 1 commit into
devfrom
worktree-fix-347-scheduler-executor

Conversation

@gnanirahulnutakki

Copy link
Copy Markdown
Member

Fixes #347.

Root cause (verified in-tree, not assumed)

The unattended-execution path was never wired end-to-end. A user could create a schedule and it persisted, but no runtime component ever executed it — three disjoint stores with no timer→executor bridge:

  • crates/cron/src/scheduler.rsCronScheduler spawns a real ticker that flips job status Pending→Running→Completed but never reads job.command; it's a lifecycle marker, not an executor, and is instantiated nowhere outside its own crate.
  • crates/automation/src/proactive.rsProactiveAutomationLoop::fire_due/fire_schedule is a complete, tested executor (validate → FusedRuntime::submit_with_provisioning → deliver → record fire), but had zero callers outside an in-crate test. Nothing drove it on a clock.
  • crates/cli/src/main.rsardur schedule fire was an explicit dry-run printing execution engine not yet wired, over a <state>/schedules/*.json store no executor read.

Reproduced live: ardur schedule create … "every 5 minutes" then ardur schedule fire <id>note: execution engine not yet wired; no receipt, no run.

Fix

Reconcile everything onto the automation executor, and make the CLI <state>/schedules directory the single durable store that feeds it.

  • crates/automationScheduleDriver (new driver.rs): the missing timer→executor bridge. Owns a ProactiveAutomationLoop and ticks fire_due(Utc::now()) on a fixed interval. run_bounded(Some(n)) for bounded/one-shot inline runs (CLI --max-ticks, tests); start/stop/is_running for an always-on background driver. Mirrors CronScheduler's shape but calls the real executor.
  • crates/cliCliScheduleStore (new schedule_exec.rs): adapts <state>/schedules/*.json to the ScheduleStore contract by materializing each persisted record into an executable AutomationSchedule at fire time. Per fire it mints a fresh, short-lived, attenuated cap-token and a per-fire budget top-up (a token minted at create time would have expired by the time a daily job fires days later — so per-fire minting is the only correct design for a durable scheduler), and persists last_fire_at/fire_count back to the record. Fires run the ordinary fused ten-stage pipeline (cap-token → Cedar → cost admission → provider → signed receipt → journal) and append to the same signed receipt chain a chat turn would. Runtime wiring mirrors FusedEngine (provider selection + offline stub fallback + persistent keys/policies + file-backed receipt log).
  • ardur schedule fire <id> now executes end-to-end; new ardur schedule run [--interval-secs N] [--max-ticks N] drives every due schedule via ScheduleDriver. ScheduleRecord moved to the lib with additive serde-defaulted fire-state fields, so pre-[Critical] Unattended-execution gap: scheduled jobs are persisted but never run #347 records load unchanged.

Security posture preserved: unattended fires require an attenuated cap-token (validate_schedule); Cedar still governs (a fire under a deny-all policy is denied, exactly like a chat turn); per-fire cap-tokens are 300s and tool-scoped to chat.submit + memory read/write; the CLI store id is path-traversal-checked.

CronScheduler is intentionally left as a lifecycle marker — the automation loop is the single executor. A server-boot adoption of ScheduleDriver::start (sharing the server's FusedRuntime) is the natural follow-up, deferred to keep this PR scoped to the CLI repro + the reusable bridge.

Verification

  • ardur-automation driver unit tests — a bounded tick and a background start/stop each fire a due schedule through the executor.
  • ardur-clischedule_exec unit tests + updated schedule_commands integration test asserting a fire mints exactly one receipt.
  • New E2E scenario_scheduled_job_execution — a due AutomationSchedule driven through ScheduleDriver over the real fused runtime (stub provider) delivers the response, bumps fire_count, and mints a signed receipt on the on-disk chain that verifies under the publishing JWKS. This is the regression guard: if any half regresses to "persist but never run", it fails.
  • Live smoke (offline stub, permissive policy): reproduced the issue's exact repro — schedule fire now mints a receipt and bumps fire_count/last_fire_at instead of printing "not yet wired"; schedule run --max-ticks 1 fires an always-due schedule and mints a receipt, and correctly does not fire a not-yet-due minute (dedup honored).
  • cargo test -p ardur-automation -p ardur-cli green; cargo test -p ardur-e2e-tests all binaries green, 0 failures; cargo fmt --check clean; cargo clippy -p ardur-automation -p ardur-cli --all-targets --all-features -- -D warnings clean.

Do not merge — for peer review.

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 467f9205-10e8-4db3-87d9-a4735f612808

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree-fix-347-scheduler-executor

Comment @coderabbitai help to get the list of available commands.

Unattended scheduling was inert end-to-end: `ardur schedule create`
persisted a job, but nothing ran it. Three disjoint schedule stores
existed with no timer→executor bridge — `ardur schedule fire` printed
"execution engine not yet wired", and `ProactiveAutomationLoop` (the
real, tested executor) had zero callers.

Root cause (verified in-tree):
- crates/cron `CronScheduler` marks job lifecycle but never reads
  `job.command` — a lifecycle marker, not an executor.
- crates/automation `ProactiveAutomationLoop::fire_due` executes fully
  (cap-token → FusedRuntime::submit_with_provisioning → deliver → record)
  but nothing drove it on a clock.
- crates/cli `ardur schedule fire` was a dry-run over a store no executor
  read.

Fix (reconcile onto the automation executor; the CLI `<state>/schedules`
directory becomes the single durable store that feeds it):
- ardur-automation: add `ScheduleDriver` — the missing timer→executor
  bridge. Ticks `fire_due(Utc::now())` on an interval; `run_bounded` for
  one-shot/bounded runs, `start`/`stop` for a background driver.
- ardur-cli: add `CliScheduleStore`, adapting `<state>/schedules/*.json`
  to the `ScheduleStore` contract by materializing each record into an
  executable `AutomationSchedule` at fire time — per-fire it mints a
  fresh, short-lived, attenuated cap-token and budget top-up (a token
  minted at create time would expire before a daily job fires) and
  persists `last_fire_at`/`fire_count` back. Fires run the ordinary fused
  pipeline and append to the same signed receipt chain a chat turn would.
- ardur-cli: `ardur schedule fire <id>` now executes end-to-end; new
  `ardur schedule run [--interval-secs N] [--max-ticks N]` drives due
  schedules via `ScheduleDriver`. `ScheduleRecord` moved to the lib with
  additive serde-defaulted fire-state fields (pre-#347 records load
  unchanged).

Verification:
- ardur-automation driver unit tests: a bounded tick and a background
  start/stop each fire a due schedule.
- ardur-cli: schedule_exec unit tests + updated schedule_commands
  integration test asserting a fire mints exactly one receipt.
- New e2e scenario_scheduled_job_execution: a due schedule driven through
  ScheduleDriver over the real fused runtime mints a signed receipt that
  verifies under the publishing JWKS.
- Live smoke reproduced the issue's exact repro: fire now mints a receipt
  and bumps fire_count instead of printing "not yet wired".
- Affected crates + `cargo test -p ardur-e2e-tests` + fmt + clippy
  (-D warnings) all green.

Checkpoint: architect/sessions/issue-347-scheduler-executor/journal.md
Signed-off-by: GR <gnanirn@gmail.com>
@gnanirahulnutakki
gnanirahulnutakki force-pushed the worktree-fix-347-scheduler-executor branch from 7f12240 to f0c79aa Compare July 23, 2026 18:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant