Skip to content

fix: dispatch cronjob(action='run') to the background like delegate_task - #80807

Merged
teknium1 merged 2 commits into
mainfrom
fix/cron-run-background-dispatch
Aug 7, 2026
Merged

fix: dispatch cronjob(action='run') to the background like delegate_task#80807
teknium1 merged 2 commits into
mainfrom
fix/cron-run-background-dispatch

Conversation

@teknium1

@teknium1 teknium1 commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Summary

cronjob(action='run') now dispatches the job to the background like delegate_task: the tool call returns immediately with a handle, and the job's outcome re-enters the conversation as a completion event when it finishes. Manual runs also gain in-flight dedupe shared with the scheduler ticker (salvaged from #53395 by @izumi0uu).

Root cause of the incident this fixes: a manual run executed the full cron job (a complete agent run, minutes to hours) synchronously inside ONE tool call on the calling agent's thread. The parent turn was uninterruptible for the whole window (the interrupt flag is only checked between loop iterations) and a batch of manual runs executed strictly serially — a Telegram session that kicked off dozens of new jobs "right now" was wedged for hours ignoring every interrupt. Reported earlier as #52705 (first fix attempt by @Tranquil-Flow in #52720).

Changes

  • tools/cronjob_tools.py:
    • _execute_job_now() split into claim + _run_claimed_job() halves (behavior unchanged).
    • New _try_dispatch_background_run(): takes the at-most-once claim synchronously (paused / missing / already-firing / already-running still report immediately in the tool response), then fires the run through tools.async_delegation.dispatch_async_delegation on the shared daemon executor. Completion rides the existing process_registry.completion_queue rail (CLI drain + gateway _async_delegation_watcher) — role alternation and prompt cache preserved, zero new drain loops.
    • Completion block reports ok/failure, delivery target, next scheduled run, and an excerpt of the job's saved output.
    • Sync fallbacks preserved: no routable session (direct Python callers / hermes cron run), async_delivery_supported() false (one-shot runners, cron child sessions, Kanban workers, stateless HTTP), and dispatch pool at capacity (claim already taken — runs inline rather than stranding it).
  • cron/scheduler.py (salvaged from fix(cron): run manual jobs without blocking the caller #53395, credited via co-author):
    • Running-set dedupe extracted into shared try_register_running_job() / release_running_job(); the ticker's _submit_with_guard and manual runs now use ONE owner. The fire claim's 300s TTL is routinely outlived by real jobs, so the claim alone couldn't stop a manual run from double-firing a mid-run job.
    • Manual runs become visible to get_running_job_ids() (gateway shutdown drain, /update bypasses drain and interrupts in-flight cron tool work #60432) and mark_running_jobs_interrupted(), which previously couldn't see them.
  • tests/tools/test_cronjob_run_background.py: 13 tests — immediate return with handle, completion event on the shared queue (ok + failure), claim-lost immediate report, all three sync fallbacks, in-flight dedupe (both directions + release), tool-level integration for both modes.

Validation

Before After
Tool call duration (job takes 0.5s) 0.5s+ (blocks) 0.04s (returns handle)
Parent turn during job uninterruptible free; interrupts work
Batch of N manual runs serial parallel (async pool)
Job outcome inline tool result completion event, new turn
Manual run of a mid-run job double-fires after claim TTL (300s) refused: "already running"
Shutdown drain sees manual runs no yes (shared running set)
hermes cron run / one-shot runtimes sync sync (unchanged)

E2E: real job store in temp HERMES_HOME, real run_one_job body (agent-run boundary stubbed with the real 4-tuple signature) — tool returns in 0.04s, completion event lands with correct session_key/status/output excerpt, last_status persisted; second manual run while the first is mid-flight is refused immediately and the running set is empty after completion. Targeted suites: 501/501 passing (tests/cron/ + all cronjob tool tests + shutdown-interrupt/execution-ledger).

Infographic

cron run background dispatch

A manual cronjob run executed the job synchronously on the calling
agent's tool thread. A cron job is a full agent run that routinely
takes minutes to hours, so the parent turn sat inside ONE tool call
the whole time: uninterruptible (the interrupt flag is only checked
between loop iterations) and serial (a batch of manual runs executed
one by one). A Telegram session that kicked off dozens of new jobs
'right now' was wedged for hours ignoring every interrupt.

action='run' now rides the async-delegation rail delegate_task
background mode uses: the at-most-once claim is taken synchronously
(so paused/missing/already-firing jobs still report immediately),
the run executes on the shared daemon executor, the tool returns at
once with a delegation handle, and the job's outcome re-enters the
conversation as a type='async_delegation' completion event through
the existing completion-queue drains (CLI + gateway) — preserving
message-role alternation and the prompt cache.

Sync fallbacks preserved:
- no routable session (direct Python callers, hermes cron run)
- async delivery unsupported (hermes -z, cron child sessions,
  Kanban workers, stateless HTTP)
- dispatch pool at capacity (claim already taken — runs inline
  rather than stranding it)

The completion block reports ok/failure, delivery target, next
scheduled run, and an excerpt of the job's saved output.
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

૮ >ﻌ< ა ci review

ran on acec730

⚠️ Warnings

OSV vulnerability scan · View job

50 known vulnerabilities found in pinned dependencies.

How to fix:

Review the findings in the Security tab. Update the affected dependencies if a patched version is available.


debug info

CI timings

CI timings · View report · View job

Wall time 3m30s vs 6m42s (-47.8%). 11 job(s) slower, 11 faster, 1 unchanged.

  • Python tests / Run tests slice 1/12: -42.0s
  • Python tests / Run tests slice 2/12: +20.0s
  • Python tests / Run tests slice 5/12: -17.0s
  • Python tests / Run tests slice 9/12: -16.0s
  • Python tests / Run tests slice 8/12: +15.0s

@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/cron Cron scheduler and job management labels Aug 7, 2026
Salvaged from PR #53395 by @izumi0uu: the fire claim's 300s TTL is
routinely outlived by real cron jobs, so claim_job_for_fire alone
cannot stop a manual cronjob(action='run') from double-firing a job
the ticker (or another manual run) is still executing.

Extract the ticker's _submit_with_guard running-set check into shared
module-level helpers (try_register_running_job / release_running_job)
and register manual runs through the same set — one dedupe owner, no
drift. Manual runs also become visible to get_running_job_ids (the
gateway shutdown drain, #60432) and mark_running_jobs_interrupted,
which previously could not see them.

The background dispatch path pre-checks the running set so a mid-run
job reports 'already running' in the tool response immediately
instead of as a delayed error completion event; the authoritative
atomic check remains in _run_claimed_job on the worker.

Co-authored-by: izumi0uu <izumi0uu@gmail.com>
@teknium1
teknium1 merged commit 3671c9f into main Aug 7, 2026
43 checks passed
@teknium1
teknium1 deleted the fix/cron-run-background-dispatch branch August 7, 2026 05:19
teknium1 added a commit that referenced this pull request Aug 7, 2026
Covers the behavior shipped in #80807 (background dispatch for
cronjob action='run') and #80838 (per-run '## Run Context' prompt,
gateway-loop delivery): immediate return with handle, completion
re-entering the conversation, in-flight dedupe, transient context
injection with prompt scanning, and the sync fallbacks.
POWERFULMOVES pushed a commit to POWERFULMOVES/PMOVES-hermes-agent that referenced this pull request Aug 10, 2026
Covers the behavior shipped in NousResearch#80807 (background dispatch for
cronjob action='run') and NousResearch#80838 (per-run '## Run Context' prompt,
gateway-loop delivery): immediate return with handle, completion
re-entering the conversation, in-flight dedupe, transient context
injection with prompt scanning, and the sync fallbacks.
randlee pushed a commit to randlee/hermes-agent that referenced this pull request Aug 11, 2026
Covers the behavior shipped in NousResearch#80807 (background dispatch for
cronjob action='run') and NousResearch#80838 (per-run '## Run Context' prompt,
gateway-loop delivery): immediate return with handle, completion
re-entering the conversation, in-flight dedupe, transient context
injection with prompt scanning, and the sync fallbacks.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cron Cron scheduler and job management P2 Medium — degraded but workaround exists type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants