Skip to content

fix(cron): reap stale execution claims at the manual-run dispatch point (#86721) - #86899

Merged
teknium1 merged 2 commits into
mainfrom
salv/86862-cron-run-claim-reap
Aug 15, 2026
Merged

fix(cron): reap stale execution claims at the manual-run dispatch point (#86721)#86899
teknium1 merged 2 commits into
mainfrom
salv/86862-cron-run-claim-reap

Conversation

@teknium1

Copy link
Copy Markdown
Contributor

Summary

Manual cron runs now self-heal stale execution claims at the dispatch point: _try_dispatch_background_run calls recover_interrupted_executions() before any claim attempt, so a claimed/running row stranded by a dead owner process no longer blocks a retry until the scheduler tick's 5-minute reap window (#86853) catches it.

Salvages #86862 by @ygd58 onto current main (fixes the immediate-retry half of #86721).

Changes

  • tools/cronjob_tools.py: dead-owner claim reap at the top of _try_dispatch_background_run, before routing capture and the claim; reclaim counts logged at warning level, recovery failure at debug (follow-up commit — mirrors the scheduler tick's handling instead of the original bare except: pass)
  • tests/cron/test_cron_run_stale_claim_reap_86721.py: 3 regression tests using the real-subprocess dead-PID pattern (genuinely dead owner, not a mock), including a negative control reproducing the stranded-claimed symptom

Validation

Result
tests/cron/test_cron_run_stale_claim_reap_86721.py + tests/tools/test_cronjob_run_background.py 16/16 passed
Base gate vs origin/main 0 behind, diff = 2 files, +171

Complementary to #86853: that PR reaps on the long-lived scheduler tick (throttled to 5 min); this one reaps at the manual-run dispatch point, which is exactly when a user is watching a retry fail.

Infographic

Cron: reap stale claims before dispatch

ygd58 and others added 2 commits August 15, 2026 03:21
… run` dispatch

Fixes #86721.

`hermes cron run <job_id>` (a one-shot CLI invocation) dispatches
manual runs via the same background-delegation path as an agent's
`cronjob(action='run')` tool call (tools/cronjob_tools.py's
_try_dispatch_background_run -> dispatch_async_delegation(role=
"cron_run", runner=_runner, ...)). The runner thread lives in the
calling process's shared daemon executor. When the one-shot process
exits right after printing "Triggered job: ...", the in-flight runner
dies mid-execution, leaving its cron/executions.db row permanently
stuck at status='claimed' -- every subsequent `hermes cron run` on the
same job then reports "Ran now: failed" because of the still-claimed
row.

cron/executions.py already has the exact self-heal this needs:
recover_interrupted_executions() correctly identifies and reclassifies
'claimed'/'running' rows whose owner process has provably exited
(_owner_is_live checks PID existence AND matches process start-time,
so a reused PID isn't mistaken for the original live owner) to
'unknown', unblocking the job for a fresh claim. But it was only ever
called once, at the long-lived scheduler ticker's own startup
(cron/scheduler.py:379's self.recover_interrupted()) -- a one-shot CLI
invocation has no equivalent "startup" moment of its own, so this
self-heal never ran for it.

Added a call to recover_interrupted_executions() at the top of
_try_dispatch_background_run, right after the async-delivery-supported
gate and before any claim attempt for the current job -- mirroring
exactly what the long-lived scheduler already does at its own
startup, just triggered per one-shot invocation instead of once at
daemon startup. Wrapped in try/except: pass (best-effort; a failure
here must not block the actual dispatch this function exists for).

Traced (but did not attempt to fix) the deeper "why does the runner
die with the process at all" question -- that's the harder problem
options 1/2 in the issue describe (route to the persistent scheduler,
or block the one-shot process until completion). This fix addresses
the more urgent, more clearly-scoped symptom: a stranded stale claim
permanently blocking ALL future manual runs of the affected job, which
is option 3 from the issue and the one with an existing, already-
correct implementation just needing to be wired into this call site.

Added 3 regression tests to a new file, following the established
real-subprocess dead-owner pattern already used in
tests/cron/test_execution_ledger.py (a genuinely-dead PID, not a
mock, matching the real-world failure mode exactly): a sanity test
confirming the stale claim sits unrecovered without the fix; a direct
test of recover_interrupted_executions() reaping such a claim; and a
unit test on _try_dispatch_background_run itself confirming recovery
is called before any claim attempt. Verified as a genuine regression
by reverting the fix and confirming the unit test fails with recovery
never having been called.

35/35 pass across the new test file plus tests/cron/test_execution_ledger.py
and tests/tools/test_cronjob_run_background.py (no regression).
…g it

Follow-up to the salvaged #86862: surface reclaim counts at warning level
(mirrors the scheduler tick's reap handling from #86853) and keep a debug
trace when the best-effort recovery itself fails, instead of a bare pass.
@github-actions

github-actions Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

૮ >ﻌ< ა ci review

ran on 8a5851b — fix(cron): log the pre-dispatch stale-claim reap instead of

⚠️ Warnings

OSV vulnerability scan · View job

5 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 3m31s vs 4m18s (-18.2%). 13 job(s) slower, 8 faster, 3 unchanged.

  • Python tests / Run tests slice 7/12: -58.0s
  • Python tests / Run tests slice 8/12: -51.0s
  • Python tests / Run tests slice 11/12: +35.0s
  • Python tests / Run tests slice 1/12: +25.0s
  • Python tests / Run tests slice 2/12: +23.0s

@alt-glitch alt-glitch added type/bug Something isn't working comp/cron Cron scheduler and job management P1 High — major feature broken, no workaround labels Aug 15, 2026
@teknium1
teknium1 merged commit bd3a966 into main Aug 15, 2026
48 checks passed
@teknium1
teknium1 deleted the salv/86862-cron-run-claim-reap branch August 15, 2026 10:53
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 P1 High — major feature broken, no workaround type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants