Skip to content

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

Merged
gnanirahulnutakki merged 1 commit into
devfrom
worktree-fix-347-scheduler-executor
Aug 11, 2026
Merged

fix(scheduler): wire timer→executor bridge so scheduled jobs run (#347)#372
gnanirahulnutakki merged 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: 40cf8e88-fad3-4c02-93b8-71b3452f8d09

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

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@gnanirahulnutakki
gnanirahulnutakki force-pushed the worktree-fix-347-scheduler-executor branch from 7f12240 to f0c79aa Compare July 23, 2026 18:29
@gnanirahulnutakki
gnanirahulnutakki force-pushed the worktree-fix-347-scheduler-executor branch from f0c79aa to d109a54 Compare August 11, 2026 19:06
gnanirahulnutakki added a commit that referenced this pull request Aug 11, 2026
…uinn-proto, resync Cargo.lock (#387)

Three coupled fixes that together restore a mergeable `dev`.

1. cargo-deny (required check) is failing on every open PR and on `dev`
   itself with a single finding:

       error[unmaintained]: bitmaps is unmaintained
         ID: RUSTSEC-2026-0247
         Solution: No safe upgrade is available!

   `bitmaps` is transitive only, via matrix-sdk -> imbl ->
   imbl-sized-chunks. It is an unmaintained notice, not a vulnerability,
   and cargo-deny itself reports that no safe upgrade exists. That is
exactly the class `[advisories].ignore` already documents (bincode via
   syntect, paste via fastembed, proc-macro-error2 via Matrix/Teloxide,
rustls-pemfile via qdrant-client), so it is ignored on the same terms.
   Vulnerability advisories are still upgraded rather than ignored — see
   (2).

2. Patch the two crates behind all five open Dependabot alerts. Both are
   reachable from the workspace and both have released fixes:

     - russh 0.62.2 -> 0.62.5 (direct dep of crates/terminal), closing
       GHSA-m65r-rprj-r5rg (CVE-2026-68930, channel-scoped server
       callbacks reachable without an open channel), GHSA-g9hv-x236-4qp3
       (pre-auth X25519 wrong-length panic), GHSA-cqjc-rmpq-xprq
       (post-auth pty-req panic) and GHSA-5xvq-cp9x-6p6r (pre-auth
       all-zero Curve25519 encode_mpint OOB).
     - quinn-proto 0.11.14 -> 0.11.16 (transitive), closing the one HIGH
       alert, GHSA-4w2j-m93h-cj5j (remote memory exhaustion from
       unbounded out-of-order stream reassembly).

Dependabot did raise these as #385 and #382, but against `main`, where
   they fail DCO; `dev` never received them.

3. Resync Cargo.lock, which had drifted from the manifests: it was
   missing `ardur-delegate-tool` (#332) and `ardur-durability` (#327)
   along with tar/filetime/xattr, and still carried stale entries.
   `.github/workflows/release.yml` builds `--locked`, so a release cut
from `dev` would have failed to resolve. `cargo metadata --locked` now
   succeeds.

Verified locally: `cargo deny check` reports advisories/bans/licenses/
sources all ok; `cargo fmt --check`, `cargo build --workspace` and
`cargo clippy --workspace --all-targets -D warnings` are clean.

---

**Why this lands first:** `cargo-deny` is a required check and is
currently red on *every* open PR (#368, #369, #370, #372, #373, #374,
#375 all show exactly one failing required check, and it is this one).
Nothing can merge into `dev` until the advisory is resolved.

Signed-off-by: GR <gnanirn@gmail.com>
@gnanirahulnutakki
gnanirahulnutakki force-pushed the worktree-fix-347-scheduler-executor branch from d109a54 to 78c1111 Compare August 11, 2026 19:57
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 78c1111 to e6aa641 Compare August 11, 2026 21:01
@gnanirahulnutakki
gnanirahulnutakki merged commit 493b7e7 into dev Aug 11, 2026
13 checks passed
@gnanirahulnutakki
gnanirahulnutakki deleted the worktree-fix-347-scheduler-executor branch August 11, 2026 21:23
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