feat(cron): per-job max_turns and wall-clock timeout caps for cron jobs - #45809
feat(cron): per-job max_turns and wall-clock timeout caps for cron jobs#45809fabiosiqueira wants to merge 1 commit into
Conversation
59eea0f to
fd3344f
Compare
fd3344f to
49f1d71
Compare
|
Rebased onto current Two notes on the rebase, and one on overlap with other open PRs. RebaseThe conflict was in Also: #72176 raised the default turn ceiling 90 → 500 while this PR sat. The Verified: OverlapFor maintainer triage — several open PRs cover parts of this, and I only spotted
The part of this PR that isn't covered elsewhere is the wall-clock cap and its If you'd rather land the turn-cap half from #33323, I'm happy to close this and — 🤖 Claude Opus 5 |
49f1d71 to
16bcc12
Compare
…seline de teste, lixo de origem Lições dos rebases de NousResearch#27724 e NousResearch#45809: --ours num teste superseded pode orfanar helper usado pelo commit seguinte; falha local só conta contra worktree de upstream/main puro; PR parado carrega import morto desde a origem. Consolidado em 8 entradas (cap da skill).
Routine /fork-sync catch-up. Clean merge-tree dry run (0 conflicts, 8 files auto-merged: .gitignore, agent/agent_init.py, agent/curator.py, cron/scheduler.py, run_agent.py, tools/skill_manager_tool.py, tools/skill_usage.py, tools/skills_tool.py). Open upstream PRs NousResearch#74875, NousResearch#80382, NousResearch#78819, NousResearch#45809, NousResearch#27724 re-verified mergeable clean against the new tip; carry b1e8eb1 (skip_memory_provider) has no upstream route and stays local per prior audit.
Per-job `max_turns` and `timeout` (wall-clock) caps for cron jobs, with the global `agent.max_turns` as fallback. The wall-clock cap complements the inactivity watchdog: a retry storm touches the activity tracker on every attempt, so an agent stuck retrying never trips inactivity but does trip wall-clock. Both the per-job and the global level normalize through `hermes_cli.config.resolve_turn_limit()`, the single turn-limit normalization point, so `agent.max_turns: none` / `unlimited` / an explicit `0` keep meaning "no ceiling" and an absent global stays unlimited — the per-job knob never introduces a cron-only default of its own. A per-job value that is not a real cap (non-int, <= 0, `none`) falls through to the global config instead of capping the job at a bogus value or silently uncapping it.
16bcc12 to
390780c
Compare
|
Rebased onto current The conflict was in
That keeps This implements #63267; I've linked it in the description (the PR predated that issue). Verification: — 🤖 Claude Opus 5 |
What does this PR do?
Adds optional per-job
max_turnsand wall-clocktimeoutceilings to cron jobs.Today a cron session has no per-job ceiling:
max_turnsis global-only (config.yaml agent.max_turns) and the only runtime guard — theHERMES_CRON_TIMEOUTinactivity watcher — never fires during retry storms, because every retry touches the activity tracker ("API error recovery"). An unattended agentic cron job stuck retrying can therefore keep calling the API for hours with no ceiling. These caps give each job an independent, opt-in budget.Related Issue
Implements #63267 (feat: per-job max_turns for cron jobs).
Also relevant to #82813 / #82815: the turn ceiling here now resolves through
hermes_cli.config.resolve_turn_limit()at both the per-job and the global level, somax_turns: none/unlimited/ an explicit0are honored in the cron path too rather than being swallowed by anorchain.Type of Change
Changes Made
tools/cronjob_tools.py+cron/jobs.py: optionalmax_turns(int) andtimeout(seconds) per job in the schema/tool andcreate_job/update_job;0clears the cap on update; bool/invalid values are rejected (fall back to global config rather than capping at a bogus value).cron/scheduler.py:_resolve_job_max_iterations(precedence: per-job >agent.max_turns>max_turns, each normalized byresolve_turn_limit(), so an absent global stays unlimited and the per-job knob never introduces a cron-only default) and_resolve_job_wall_clock_limit; the inactivity watcher gains a wall-clock branch that interrupts the run when total elapsed exceeds the job cap, regardless of activity (so a retry loop that keeps the inactivity watcher alive is still bounded).tests/cron/test_cron_job_caps.py: coverage.How to Test
max_turns: 5andtimeout: 30.pytest tests/cron/test_cron_job_caps.py -q→ 26 passed.Checklist
pytest tests/cron/test_cron_job_caps.py -qpasses (26 passed)