Skip to content

fix(cron): harden runtime ownership, supervision, and recovery - #75833

Open
cirwel wants to merge 12 commits into
NousResearch:mainfrom
cirwel:fix/cron-runtime-ownership
Open

fix(cron): harden runtime ownership, supervision, and recovery#75833
cirwel wants to merge 12 commits into
NousResearch:mainfrom
cirwel:fix/cron-runtime-ownership

Conversation

@cirwel

@cirwel cirwel commented Aug 1, 2026

Copy link
Copy Markdown

What changed

This PR hardens cron execution as one end-to-end ownership and recovery contract:

  • separates declarative cron/jobs.json definitions from mutable cron/runtime.db state;
  • migrates existing combined stores without losing schedules, counters, status, or claims;
  • gives every fire a durable, token-fenced claim across ticker, provider, and direct-run paths;
  • makes execution-ledger transitions crash-safe and terminalizes failed claim/dispatch paths;
  • renews ownership before irreversible external delivery so stale workers cannot publish;
  • runs agent work in an isolated subprocess with activity-driven inactivity heartbeats;
  • terminates complete script/worker trees on timeout or ownership loss, including POSIX descendants that create a new session and Windows taskkill /T /F fallbacks;
  • prevents shutdown-queued jobs and check-to-spawn races from beginning side effects, with worker spawn/registration atomic against shutdown;
  • makes quick snapshot/restore fail closed when the cron store lock cannot be acquired;
  • archives default and named-profile cron definition/runtime pairs from one coherent generation and deletes the whole archive if either pair member cannot be written.

Why

Cron's declarative configuration and scheduler-owned runtime fields previously shared jobs.json. Normal execution dirtied the same artifact operators would back up, review, deploy, or source-control. In parallel, in-process running guards could not prevent two scheduler processes from winning the same fire, and several early-failure, timeout, shutdown, and delivery paths could leave claims, ledgers, or descendant processes behind.

The result was an unsafe boundary: stale workers could continue after ownership changed, mutable state could race backup/restore, and process-group-only cleanup missed descendants that detached with setsid().

Compatibility and recovery

  • Existing combined jobs.json files migrate on load; declarations remain in JSON and runtime fields move to SQLite.
  • Missing-definition and interrupted-migration recovery paths reconcile from the pending definition journal.
  • Runtime tombstones prevent removed jobs from reappearing from stale state.
  • Definition digests reject restoring live claims onto changed definitions.
  • All storage and execution state remains profile-local.
  • POSIX and Windows process cleanup paths have separate regression coverage.
  • The live user cron profile was not used or mutated during testing; tests used isolated homes or the repository's hermetic test runner.

How to verify

  1. Start with a legacy cron/jobs.json containing schedules, last-run fields, counters, and a claim; load/list jobs and confirm definitions remain in JSON while runtime fields appear in cron/runtime.db.
  2. Run two independent processes against the same due job and confirm exactly one wins the fire claim.
  3. Force claim loss immediately before delivery and confirm the stale worker produces no external delivery.
  4. Run an agent/script that continues producing activity and confirm its parent pulse prevents a false inactivity timeout.
  5. Time out a worker or script that spawns both ordinary and start_new_session=True descendants; confirm no marker/descendant survives.
  6. Interrupt a queued job and the check-to-worker-spawn window during shutdown; confirm no worker side effect starts and exact claims are released.
  7. Mutate default and named-profile cron stores immediately after full-backup staging; confirm the archive contains coherent pre-mutation pairs, then fault one pair-member ZIP write and confirm no partial archive remains.

Test plan

  • Canonical hermetic runner: scripts/run_tests.sh -j 4 tests/cron tests/hermes_cli/test_backup.py tests/hermes_cli/test_web_server_cron_profiles.py tests/tools/test_cronjob_run_immediate.py -q488 passed, 0 failed across 38 per-file subprocesses.
  • ruff check over all 23 changed Python files — passed.
  • Python compileall over implementation and relevant test trees — passed.
  • git diff --check origin/main — passed.
  • Windows-footgun checker over all affected production files — passed.
  • Added real multiprocessing contention, crash-ledger, stale-delivery, dispatch-rejection, heartbeat, shutdown-queue, detached-descendant, fail-closed lock, and coherent profile-backup regressions.
  • Repository-wide suite — an earlier non-hermetic run reached 4,345 passes but encountered broad e2e/global-state/provider-environment failures and was stopped; the canonical focused suite above is the reliable local evidence, with hosted CI left as the full-suite arbiter.

Risk

Medium-high. This touches scheduler ownership, persistence, process supervision, and backup semantics. The change is deliberately token-fenced and fail-closed: stale owners cannot renew, finalize, or deliver; backup/restore aborts instead of degrading without the cross-process lock; and terminal execution rows are immutable.

Tested on macOS; Windows behavior is covered with mocked taskkill success, timeout, and failure/fallback regressions plus the repository footgun checker.

Related work and credit

Review follow-up (2026-08-01)

Addressed both hermes-sweeper findings:

  • Terminal/declaration management path: completed (runtime-tombstoned) declarations are now reachable through supported surfaces — opt-in listing (include_completed on the tool, hermes cron list --all), removal by id or name, and revive via a cadence edit (schedule/repeat/enabled) that re-arms the counter and reschedules from now. Live-only actions on a completed job return an explicit terminal error. Reconciliation no longer revives tombstones on non-cadence edits (that path could fire a revived job once past its repeat limit). 8 new end-to-end tool/CLI regressions, RED/GREEN verified.
  • Docs: cron-internals.md and the user-guide cron page now document the jobs.json definition / runtime.db runtime split, combined-store migration, digest binding, tombstone lifecycle, completed-job management, and backup-pair coherence.

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for the thorough ownership and recovery work. The current-main premise is real: cron/scheduler.py:4011 delivers before mark_job_run() at line 4030, and tick() only pre-advances recurring schedules at cron/scheduler.py:4154-4160.

Problems

  • cron/jobs.py:1751-1804 hides every runtime_tombstone from get_job, resolve_job_ref, and list_jobs. But tools/cronjob_tools.py:770-795 resolves all management actions before remove or update. A repeat-exhausted declaration retained for reproducibility therefore cannot be listed, removed, edited, or revived through supported surfaces. This contradicts the nearby recovery intent in _reconcile_runtime_state().
  • The persistence migration has no documentation update. website/docs/developer-guide/cron-internals.md:36-64 and website/docs/user-guide/features/cron.md:249-259,780-788 still describe mutable lifecycle state as living solely in jobs.json.

Suggested changes

  • Add an explicit terminal/declaration management path and end-to-end tool/CLI regressions for list, remove, and revive/edit of a completed job.
  • Document the definition/runtime split, migration, and paired-backup behavior.

Automated hermes-sweeper review.

Comment thread cron/jobs.py Outdated
@teknium1 teknium1 added sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Aug 1, 2026
spfcraze added a commit to spfcraze/hermes-agent that referenced this pull request Aug 1, 2026
The scheduler's pre-dispatch loop called advance_next_run per due job —
one full load_jobs() + one full save_jobs() of the jobs file each — so
N due jobs cost N reads + N writes of the whole file (gateway-restart
catch-up or co-scheduled bursts). advance_next_runs() does one load +
at most one save for the whole due set with identical per-job semantics;
advance_next_run() is now a thin wrapper over it.

Measured (50 due recurring jobs, real jobs file): 107.9 ms -> 2.5 ms
(45x; 50 loads + 50 saves -> 1 + 1).

Tests: batch advances recurring and skips one-shots, single load + save
I/O pin (fails pre-fix — no such function), no save when nothing
advances, and per-job wrapper semantics unchanged. Related: NousResearch#60946 and
NousResearch#75833 both restructure this loop's call site for correctness — neither
addresses the I/O cost, and this batch primitive composes with either
dispatch design; happy to rebase onto whichever lands first.
@cirwel
cirwel force-pushed the fix/cron-runtime-ownership branch from 4d38aa3 to 7034a85 Compare August 1, 2026 20:24
@cirwel

cirwel commented Aug 1, 2026

Copy link
Copy Markdown
Author

Both review points addressed in 7034a85 (rebased onto current main):

Terminal/declaration management path. get_job / resolve_job_ref / list_jobs now take an include_terminal opt-in, and the management surfaces route through it:

  • listcronjob(action="list", include_completed=True) and hermes cron list --all show completed declarations as state="completed" with completed_reason / completed_at (the CLI shows the completion badge instead of the stale next-run time).
  • remove — works on a completed declaration by id or name and drops it from jobs.json.
  • revive/edit — an update that changes cadence (schedule / repeat / enabled) revives the job: the reconciler resets the exhausted counter and the next run is recomputed from now, not the stale pre-completion next_run_at. The response carries revived: true. A non-cadence edit (e.g. prompt) persists the declaration change but keeps the job completed, and says so.
  • pause/resume/run on a completed job now return an explicit terminal error with the revive/remove options, instead of "not found".

One deliberate semantics change while closing this: _reconcile_runtime_state() no longer pops the tombstone on non-cadence definition edits. That branch could revive a repeat-exhausted job with completed >= times and a stale next_run_at, firing it once past its limit before re-tombstoning. Revival is now exactly the cadence edit that re-arms the counter, which also resolves the contradiction with the hidden-lookup surfaces flagged in the review.

End-to-end regressions: 8 new tests (tool-layer list/remove-by-id/remove-by-name/revive-by-repeat/revive-by-schedule/prompt-only-stays-completed/live-action-terminal-errors, plus a CLI list --all test asserting the completed badge and reason). RED/GREEN verified: all 8 fail without the fix, pass with it.

Docs. website/docs/developer-guide/cron-internals.md now documents the jobs.json definition / runtime.db state split, the combined-store migration, digest binding, tombstone lifecycle, and backup-pair coherence; the tick-cycle description no longer claims runtime writes go to jobs.json. The user guide's job-storage section covers the split, migration, and a new "Completed jobs" section (list --all, remove, revive via schedule/repeat edit).

Focused suites after the rebase: 159 passed across tests/cron, tests/tools/test_cronjob_tools.py, tests/tools/test_cronjob_run_immediate.py, tests/hermes_cli/test_cron.py (includes the new TestGithubExemptionAbuse from current main); ruff and git diff --check clean. Note: tests/cron/test_cron_inactivity_timeout.py process-tree kill tests are timing-sensitive under parallel local runs — they fail intermittently on unmodified main under load and pass serially with this branch; hosted CI remains the arbiter there.

@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/cron Cron scheduler and job management comp/cli CLI entry point, hermes_cli/, setup wizard area/config Config system, migrations, profiles platform/windows Native Windows-specific behavior or breakage labels Aug 1, 2026
@cirwel
cirwel force-pushed the fix/cron-runtime-ownership branch from 7034a85 to f7dce23 Compare August 1, 2026 22:02
@cirwel

cirwel commented Aug 1, 2026

Copy link
Copy Markdown
Author

Follow-up hardening in the two commits now on the branch (rebased onto current main again): after the earlier fix I ran an adversarial multi-lens review over the terminal-management delta (every finding reproduced by execution before fixing) and closed seven coherence gaps it surfaced:

  • Live-first resolution with terminal fallback everywhere (tool actions, remove_job, cron edit): a live job always wins a name tie with a retained completed namesake — previously one finished one-shot could permanently make name-based management of its live namesake ambiguous. Completed declarations stay reachable when nothing shadows them, and ambiguity payloads now carry state so completed twins are distinguishable.
  • hermes cron edit / /cron edit can reach completed jobs — the revive path the docs promise now works end-to-end (both surfaces pre-resolved with live-only lookup and returned "Job not found").
  • context_from accepts completed upstreams on tool create/update and the dashboard validator: fire-time injection reads the persisted output directory, so a finished one-shot collector is a legitimate chaining source. Previously the tool showed the completed job in listings but rejected chaining to it.
  • A final run reports the completed record: a run that exhausts the repeat limit now returns state: completed with completed_reason/completed_at instead of a fabricated schedulable {'id': ...} shape, and execution_success no longer misreports on the final run.
  • One-shot revive error is honest: repeat-only revive of a completed one-shot says a new schedule is required instead of the generic "time is in the past".
  • Listing coherence: a completed declaration lists on the completed opt-in alone (the disabled filter applies to live jobs only), and tombstone reads are isinstance-guarded against corrupted stores.
  • Docs accuracy: digest binding is scoped to the cadence digest (the stored full-definition digest has no comparer), and the architecture + cron-troubleshooting pages now describe the split store alongside the internals/user-guide updates.

12 new regressions across the tool, CLI, and jobs layers (including the gateway-PATCH-only enabled revive leg). Focused suites: 169 passed post-change, plus tests/gateway/test_api_server_jobs.py 16/16; ruff and whitespace clean.

Known limitation, deliberately out of scope: the web dashboard's job list/detail endpoints still use live-only lookup, so completed declarations aren't visible there (its context_from validator IS terminal-aware now, so chained jobs stay editable). Routing the dashboard's list/get through the same opt-in is a small follow-up I'm happy to do separately — kept out of this PR to avoid growing the diff into another subsystem.

kshitijk4poor pushed a commit that referenced this pull request Aug 2, 2026
The scheduler's pre-dispatch loop called advance_next_run per due job —
one full load_jobs() + one full save_jobs() of the jobs file each — so
N due jobs cost N reads + N writes of the whole file (gateway-restart
catch-up or co-scheduled bursts). advance_next_runs() does one load +
at most one save for the whole due set with identical per-job semantics;
advance_next_run() is now a thin wrapper over it.

Measured (50 due recurring jobs, real jobs file): 107.9 ms -> 2.5 ms
(45x; 50 loads + 50 saves -> 1 + 1).

Tests: batch advances recurring and skips one-shots, single load + save
I/O pin (fails pre-fix — no such function), no save when nothing
advances, and per-job wrapper semantics unchanged. Related: #60946 and
#75833 both restructure this loop's call site for correctness — neither
addresses the I/O cost, and this batch primitive composes with either
dispatch design; happy to rebase onto whichever lands first.
@GottZ

GottZ commented Aug 3, 2026

Copy link
Copy Markdown

This was generated by AI during triage.

Summary

One PR addresses #75607. #75833 separates declarative cron definitions in jobs.json from volatile state in runtime.db, while also adding legacy migration, reconciliation, fenced execution ownership, coherent backup/restore, documentation, and regression coverage.

Related pull requests

Suggested consolidation

Keep #75833 open with a salvage path focused on the definition/runtime separation, lossless migration and reconciliation, token-fenced ownership, coherent backup/restore, terminal-declaration management, and their regression tests. The diff is broad, so the author should keep these end-to-end storage and safety invariants coherent while narrowing unrelated changes where possible; there are no duplicate PRs to close.

Complex graph

flowchart LR
    classDef open fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
    classDef merged fill:#dcfce7,stroke:#15803d,color:#14532d
    classDef closed fill:#e5e7eb,stroke:#6b7280,color:#1f2937
    classDef unverified fill:#f3f4f6,stroke:#9ca3af,color:#374151
    classDef best stroke-width:3px,stroke:#b45309
    classDef target stroke-width:3px,stroke:#4338ca
    I75607(["issue #75607 (open)"])
    P75833["PR #75833 (open)"]
    P75833 -->|best fix| I75607
    class I75607 open
    class P75833 open
    class P75833 best
    class P75833 target
    click I75607 "https://github.com/NousResearch/hermes-agent/issues/75607"
    click P75833 "https://github.com/NousResearch/hermes-agent/pull/75833"
Loading

Graph: solid arrow = fixes / best fix, dashed arrow = partial or unverified (see edge label); boxed group = PRs duplicating each other; amber border = best fix; indigo border = target; gray node = closed (state tag in the node label).

Cross-PR triage: Reviewed 1 pull request and 1 issue in this complex. Each diff was read against this issue; Assessment working set: 305 kB of PR diffs, 9 kB of issue/PR text, 8 kB of discussion (5 comments), 2 verify verdicts. verdicts reflect diff content, not PR titles. Part of an automated triage batch.

@cirwel
cirwel force-pushed the fix/cron-runtime-ownership branch 2 times, most recently from 4425f21 to 4c67f38 Compare August 3, 2026 18:52
@cirwel

cirwel commented Aug 4, 2026

Copy link
Copy Markdown
Author

Dogfood follow-up on 3ae839011 (pushed to this branch):

  • Exercised the real CLI against isolated HERMES_HOME stores: create, immediate run by id/name, finite-repeat completion, list --all, prompt-only edit, cadence revive, re-completion, and remove-by-name. Runtime-only runs left the jobs.json SHA-256 unchanged.
  • Exercised legacy combined-store migration and compared the relevant behavior with the PR base.
  • Exercised quick and full backup capture of the cron/jobs.json + cron/runtime.db pair.

The quick-restore pass found one real WAL lifecycle defect in this branch: Python's sqlite3.Connection context manager commits/rolls back but does not close. _journal_snapshot_definitions() could therefore rename the staged runtime.db while its WAL connection was still open, leaving .runtime.db.snap_restore.*-{wal,shm} behind and producing sqlite3.OperationalError: disk I/O error on the published database. Controlled reproduction:

without explicit close: staged WAL/SHM remain; published read -> disk I/O error
with explicit close:     no staged sidecars; pending definition row is readable

3ae839011 fixes both restore-side connections with deterministic contextlib.closing(...) around the transaction context and adds a regression that forces WAL mode and retains the staged connection so garbage collection cannot hide the bug. The new test was RED before the production change and GREEN after it.

Post-fix evidence:

  • Installed-source quick snapshot/restore against a real WAL database: restore_ok=True, snapshotted prompt recovered, staged_sidecars=[].
  • Focused cron/backup/CLI/tool suite: 604 passed, 0 failed.
  • Ownership/claim/heartbeat/shutdown/timeout/recovery stress subset: 117 passed, 0 failed.
  • Independent Codex review: no blocking finding; it separately reproduced the WAL failure and ran the full affected backup test file (52 passed).
  • Ruff and git diff --check: clean.

GitHub still reports no hosted checks on the branch.

Kenny Wang added 8 commits August 10, 2026 02:07
Separate declarative jobs from volatile runtime state, fence every fire with durable ownership tokens and execution ledgers, and suppress stale delivery. Isolate cron workers with inactivity heartbeats and complete process-tree cleanup, while making cron backup and restore generation-coherent and fail-closed.
…ore split

A repeat-exhausted declaration is retained as a runtime tombstone, but every
management surface resolved jobs through live-only lookup, so a completed job
could not be listed, removed, edited, or revived through supported tools.

Give get_job/resolve_job_ref/list_jobs an include_terminal opt-in and route
management surfaces through it: completed jobs list on request (tool
include_completed, cron list --all), remove works on them, and a cadence edit
(schedule/repeat/enabled) revives them — resetting the exhausted counter and
rescheduling from now rather than firing on the stale pre-completion
occurrence. Non-cadence edits update the declaration but keep the job
completed, and reconciliation no longer revives tombstones on such edits
(that path could fire a revived job once past its repeat limit). Live-only
actions (pause/resume/run) fail with an explicit terminal error instead of
'not found'.

Document the jobs.json definition / runtime.db state split, migration, and
paired-backup coherence in cron-internals and the user guide.
Adversarial review of the terminal-management path surfaced seven coherence
gaps; each was reproduced by execution before fixing:

- Resolution is now live-first with a terminal fallback everywhere (tool
  actions, remove_job, cron edit CLI): a live job always wins a name tie
  with a retained completed namesake instead of the reference turning
  ambiguous, while completed declarations stay reachable when nothing
  shadows them. Ambiguity payloads carry state so completed twins are
  distinguishable.
- 'hermes cron edit' and the '/cron edit' console command can reach
  completed jobs — the documented revive path works end-to-end.
- context_from accepts completed upstream jobs on the tool create/update
  paths and the dashboard validator alike: fire-time injection reads the
  persisted output directory, so a finished one-shot collector is a natural
  chaining source.
- A run that exhausts the repeat limit reports the real completed record
  (state, completed_reason/completed_at) instead of a fabricated
  schedulable shape, and execution_success no longer misreports on a final
  run.
- Repeat-only revive of a completed one-shot says a new schedule is
  required instead of the generic 'time is in the past' error.
- A completed declaration is listed on the completed opt-in alone — the
  disabled filter applies to live jobs only.
- Tombstone reads are isinstance-guarded against corrupted stores; the
  internals doc scopes digest binding to the cadence digest (the stored
  full-definition digest has no comparer), and the architecture and
  troubleshooting docs now describe the split store.

12 new regressions cover the above across the tool, CLI, and jobs layers,
including the gateway-PATCH-only enabled revive leg.
…ombstone

_sweep_completed_oneshots() and its regression tests predate this branch's
runtime_tombstone mechanism (introduced in "harden runtime ownership and
recovery") — they only recognized the legacy bare-one-shot completion shape
(state == "completed" written directly to storage). A repeat-exhausted
one-shot retired via mark_job_run/claim_dispatch now carries a
runtime_tombstone instead, so the sweep never matched it and such records
would accumulate in jobs.json forever, uncounted by COMPLETED_ONESHOT_RETENTION_DAYS.

- _sweep_completed_oneshots: treat a runtime_tombstone as terminal too, not
  just state == "completed". Age falls back to the tombstone's own "at"
  timestamp when last_run_at was never written (a wedged dispatch_limit /
  stale_dispatch_limit tombstone never reaches mark_job_run, so last_run_at
  stays null forever otherwise).
- _normalize_job_record: a tombstoned job now also reads back enabled=False
  and next_run_at=None, matching the shape the legacy completion path
  already writes directly to storage, so both mechanisms present identically
  to callers.
- tests/cron/test_jobs.py: the six pre-existing tests exercising this sweep
  and completion shape now opt into include_terminal=True where they look up
  a job by id/list that has become a runtime_tombstone (get_job/list_jobs
  hide those by default as of "make completed declarations manageable").
… publishes

_create_quick_snapshot_locked wrote cron/jobs.json + cron/runtime.db straight
into snap_dir (the final, published path) while every other quick-snapshot
file staged into a hidden .partial sibling that only gets os.replace()'d onto
snap_dir at the end (introduced by upstream's "serialize and atomically
publish snapshots"). Since _snapshot_cron_pair already created snap_dir with
the pair inside it, the later os.replace(staging_dir, snap_dir) hit an
existing non-empty directory and failed with ENOTEMPTY on every quick
snapshot that has a cron store — caught by tests/hermes_cli/test_backup.py's
TestQuickSnapshot suite (15 failures) once the two commits were combined.

_snapshot_cron_pair takes snap_dir purely as a base path to join against, so
pointing it at staging_dir instead makes the cron pair land in the same
directory as everything else and publish atomically with it — preserving
both this PR's cron-pair coherence (still captured under the same
_quick_cron_store_lock, still all-or-nothing) and upstream's atomicity
guarantee for the snapshot as a whole.
@cirwel
cirwel force-pushed the fix/cron-runtime-ownership branch from 3ae8390 to 7621ee9 Compare August 10, 2026 08:39
@cirwel

cirwel commented Aug 10, 2026

Copy link
Copy Markdown
Author

Final recovery hardening is now on 7621ee917 (rebased onto current main at 56dc01d904).

Review follow-up closed the remaining safety findings:

  • owner liveness is host-scoped; foreign, legacy, or unverifiable ownership fails safe;
  • runtime schema migration is writer-serialized and retries only bounded transient SQLite locked/busy errors;
  • stale public fire/run snapshots cannot clear or replace newer durable claims;
  • backup rejects pending definition generations whose base digest does not match the authoritative definitions;
  • worker activity is recorded before descendant discovery, and known work is stopped/frozen before reparented-process scanning;
  • detached descendants receive bounded TERM/KILL/wait cleanup after timeout or claim loss.

Exact settled patch before commit: SHA-256 be85fa112721afd7d7df1fad290058b640af59ea0b13d781841527d81454682d. Three independent scoped reviews returned VERDICT: APPROVE for persistence, supervision, and backup/integration on that exact generation.

Final local evidence after the rebase:

  • tests/cron: 614 passed, 1 skipped;
  • backup/gateway/tool matrix: 165 passed;
  • concurrent first-migration stress: 10/10 passed;
  • Ruff, lockfile, diff hygiene, changed-file compilation, and Windows-footgun scan (944 files) passed;
  • website prebuild + production build passed; ASCII guard passed over all 401 docs;
  • exact patch Gitleaks scan: 0 findings; full-tree sets were identical to untouched upstream (808 / 808, zero delta).

The canonical repository-wide run is still honestly non-green: 18 failures across 12 unrelated files. Replaying those same files on untouched current upstream reproduced all 18 failures. A branch replay also saw one extra load-sensitive transcription timeout; that source is byte-for-byte unchanged from upstream and the extra timeout did not reproduce there. No cron-repair regression was demonstrated, but this is not being represented as a green full suite.

randlee pushed a commit to randlee/hermes-agent that referenced this pull request Aug 11, 2026
The scheduler's pre-dispatch loop called advance_next_run per due job —
one full load_jobs() + one full save_jobs() of the jobs file each — so
N due jobs cost N reads + N writes of the whole file (gateway-restart
catch-up or co-scheduled bursts). advance_next_runs() does one load +
at most one save for the whole due set with identical per-job semantics;
advance_next_run() is now a thin wrapper over it.

Measured (50 due recurring jobs, real jobs file): 107.9 ms -> 2.5 ms
(45x; 50 loads + 50 saves -> 1 + 1).

Tests: batch advances recurring and skips one-shots, single load + save
I/O pin (fails pre-fix — no such function), no save when nothing
advances, and per-job wrapper semantics unchanged. Related: NousResearch#60946 and
NousResearch#75833 both restructure this loop's call site for correctness — neither
addresses the I/O cost, and this batch primitive composes with either
dispatch design; happy to rebase onto whichever lands first.
cirwel added 2 commits August 21, 2026 01:25
fix(cron): fail mixed silence before delivery
feat(cron): expose durable execution identity to jobs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/config Config system, migrations, profiles comp/cli CLI entry point, hermes_cli/, setup wizard comp/cron Cron scheduler and job management P2 Medium — degraded but workaround exists platform/windows Native Windows-specific behavior or breakage sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(cron): separate declarative definitions from volatile execution state

4 participants