feat(cron): retry-on-failure for agent jobs (configurable, opt-in) - #5
Merged
Merged
Conversation
Agent jobs (script:null, run inside run_conversation) had no retry — a
transient LLM timeout/rate-limit/network blip failed the job and
mark_job_run re-armed next_run_at to the next cron tick (hours away).
This adds an optional per-job retry config:
retry: {max_attempts: N, delay_seconds: S}
On failure, the scheduler re-arms next_run_at to now+delay (up to N
times) before reverting to the cron schedule. The normal ticker picks up
the retried job via its existing next_run_at<=now due-check — no new
queue, no new thread, no due-check change.
Design (minimal, rides existing mechanisms):
- jobs.py: add retry config field + retry_count to create_job; reset
retry_count on success in mark_job_run. Validation rejects max_attempts<1
or delay_seconds<1.
- scheduler.py: _record_job_outcome helper wraps both mark_job_run call
sites in run_one_job. Always records the honest outcome first, then
post-overrides next_run_at when a retry is due (mark_job_run's own
next_run_at=comput_next_run would otherwise clobber it).
- The interrupted-flag consume stays OUTSIDE the helper so an
interrupted run is never retried.
Opt-in: jobs without a retry field hit the exact same path as today.
The non-regression test (test_no_retry_config_failure_preserves_behavior)
pins this. Full tests/cron/ suite: 668 passed, 9 pre-existing env failures
(croniter-missing + gateway-env tests) unchanged from clean main.
7 new tests in TestJobRetry cover: config persistence, validation rejection,
re-arm on failure, reset on success, exhaustion, and the non-regression guard.
girnarholdings
added a commit
that referenced
this pull request
Jul 21, 2026
Agent jobs (script:null, run inside run_conversation) had no retry — a
transient LLM timeout/rate-limit/network blip failed the job and
mark_job_run re-armed next_run_at to the next cron tick (hours away).
This adds an optional per-job retry config:
retry: {max_attempts: N, delay_seconds: S}
On failure, the scheduler re-arms next_run_at to now+delay (up to N
times) before reverting to the cron schedule. The normal ticker picks up
the retried job via its existing next_run_at<=now due-check — no new
queue, no new thread, no due-check change.
Design (minimal, rides existing mechanisms):
- jobs.py: add retry config field + retry_count to create_job; reset
retry_count on success in mark_job_run. Validation rejects max_attempts<1
or delay_seconds<1.
- scheduler.py: _record_job_outcome helper wraps both mark_job_run call
sites in run_one_job. Always records the honest outcome first, then
post-overrides next_run_at when a retry is due (mark_job_run's own
next_run_at=comput_next_run would otherwise clobber it).
- The interrupted-flag consume stays OUTSIDE the helper so an
interrupted run is never retried.
Opt-in: jobs without a retry field hit the exact same path as today.
The non-regression test (test_no_retry_config_failure_preserves_behavior)
pins this. Full tests/cron/ suite: 668 passed, 9 pre-existing env failures
(croniter-missing + gateway-env tests) unchanged from clean main.
7 new tests in TestJobRetry cover: config persistence, validation rejection,
re-arm on failure, reset on success, exhaustion, and the non-regression guard.
Co-authored-by: Hermes Agent <kathanc99@icloud.com>
girnarholdings
pushed a commit
that referenced
this pull request
Aug 3, 2026
…review #5) The fence-cancel poll loops (sync host wait in conversation_compression, async hygiene wait in gateway/run) spun at 1kHz while the worker held the fence through its lock-setup window — which rides SessionDB write patience and can last seconds. 25ms keeps sub-tick cancel latency without the spin.
girnarholdings
pushed a commit
that referenced
this pull request
Aug 19, 2026
fix(openai): cover nested sparse response fields
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds an optional per-job retry config so agent jobs (
script: null, run insiderun_conversation) retry on transient failures (LLM timeout, rate-limit, network blip) instead of waiting hours for the next cron tick.On failure, the scheduler re-arms
next_run_attonow + delay_seconds(up tomax_attemptstimes) before reverting to the cron schedule. The normal ticker picks up the retried job via its existingnext_run_at <= nowdue-check.Why
Agent briefing jobs (e.g. BoltNews pre/post/mid-day, earnings daily pipeline) fail transiently on DeepSeek API timeouts. Currently a single transient failure = no briefing that cycle, and
mark_job_runre-armsnext_run_atto the next cron tick (hours away). The bash retry wrapper shipped forno_agentsummarizer jobs cannot wrap an LLM session — this closes that gap at the scheduler layer.Design (minimal, rides existing mechanisms)
cron/jobs.py— schema + reset:create_jobacceptsretry: {max_attempts, delay_seconds}(validated; defaultsNone= no retry).retry+retry_count: 0in the job dict.mark_job_runresetsretry_count = 0on success.cron/scheduler.py—_record_job_outcomehelper:mark_job_runcall sites inrun_one_job(normal + exception paths).retryconfigured, run failed, attempts remain — post-overridesnext_run_at = now + delayand bumpsretry_count. The override runs aftermark_job_runbecause its ownnext_run_at = compute_next_run(...)would otherwise clobber the retry time._consume_interrupted_flagguard stays outside the helper so an interrupted run is never retried.No changes to:
get_due_jobs,compute_next_run, the ticker loop,claim_dispatch. The retry rides the existing due-check.Opt-in / non-regression
Jobs without a
retryfield hit the exact same code path as today (_record_job_outcomecallsmark_job_runand returns — the retry branch is gated onjob.get("retry")). Thetest_no_retry_config_failure_preserves_behaviortest pins this.Full
tests/cron/suite: 668 passed, 9 pre-existing env failures (croniter-missing in the base Python + gateway/fire-claim env tests) — identical to clean main. CI hascroniter==6.0.0installed (core dep in pyproject.toml), so most of those 9 pass there.Tests (7 new in
TestJobRetry)test_create_job_with_retry_config— config persisted, retry_count=0test_create_job_without_retry_defaults_none— back-compattest_invalid_retry_max_attempts_rejected/test_invalid_retry_delay_rejected— validationtest_retry_re_arms_next_run_at_on_failure— next_run_at = now+delay, retry_count=1test_retry_resets_on_success— recovery clears the countertest_no_retry_config_failure_preserves_behavior— non-regression guardTest plan
ci.ymlorchestrator + python lint/test sub-workflows passall-checks-passgate green