Skip to content

fix(cron): stop one-shot CLI cron run from orphaning the job; reap dead-owner claims on tick - #86853

Merged
teknium1 merged 1 commit into
mainfrom
fix/86721-cron-run-orphan
Aug 15, 2026
Merged

fix(cron): stop one-shot CLI cron run from orphaning the job; reap dead-owner claims on tick#86853
teknium1 merged 1 commit into
mainfrom
fix/86721-cron-run-orphan

Conversation

@teknium1

Copy link
Copy Markdown
Contributor

Summary

Fixes #86721hermes cron run <job_id> from a one-shot CLI invocation orphaned the job: the run was background-dispatched onto a daemon thread of the calling process (when the CLI inherited a gateway/desktop session env and resolved a session key), the process exited immediately after printing Triggered job: ..., the runner died mid-LLM-call, the async delegation ended state='unknown', and the job's cron/executions.db row stayed status='claimed' forever — blocking every subsequent run of that job.

Fix

Two parts, matching the issue's expected behaviors (2) and (3):

  1. One-shot CLI cron run executes synchronously to completionhermes_cli/cron.py::_job_action("run", ...) now declares the delivery channel stateless (scoped _SESSION_ASYNC_DELIVERY set/reset around the call) before invoking the cron API. async_delivery_supported() then gates off _try_dispatch_background_run, so the run takes the existing synchronous path and the CLI blocks until the job finishes — the same treatment hermes -z already gets via declare_stateless_channel(). The declaration is token-scoped so in-process callers (tests, embedding apps) aren't tainted.

  2. Dead-owner claim reclaim on the scheduler tick — execution rows already carry their owner pid + process start time, but recover_interrupted_executions() previously ran only at scheduler startup, so a claim orphaned while the gateway ticker was already running was never reaped. cron/scheduler.py::tick() now runs the recovery periodically (throttled to once per 300s so idle 60s ticks don't pay a ledger connection each cycle — same concern as fix(cron): skip idle config loads in scheduler tick #33612). Only rows whose exact owner process is provably dead (_owner_is_live: pid liveness + process start time match) are transitioned to unknown; live runs in other processes are never rewritten.

Tests

tests/cron/test_dead_owner_claim_reclaim.py (8 behavioral tests):

  • the exact cron run from one-shot CLI orphans the job: async delegation dies with the calling process, execution stuck 'claimed' forever #86721 wedge: a claimed row owned by a real dead pid (finished subprocess) is cleared to unknown by a tick
  • a running row from a dead owner is also reclaimed
  • a claim owned by a live process survives the reap untouched
  • the reap is throttled between back-to-back ticks and re-fires after the window expires
  • a reap failure never breaks the tick
  • CLI run action declares the channel stateless during the call and restores it after; non-run actions leave it alone
  • end-to-end: _try_dispatch_background_run refuses background dispatch under a stateless channel even with an inherited HERMES_SESSION_KEY

Sabotage-verified: with the two fix files reverted to origin/main, all 8 tests fail; re-applying the fix greens them. Full tests/cron/ + tests/hermes_cli/test_cron.py + tests/tools/test_cronjob_run_background.py: 733 passed, 1 skipped.

Infographic

cron run orphan fix

…dead-owner claims on tick

`hermes cron run <job_id>` from a one-shot CLI invocation could
background-dispatch the run onto a daemon thread of the calling process
(when the CLI inherited a gateway/desktop session env and resolved a
session key). The CLI printed "Triggered job: ..." and exited instantly,
killing the runner mid-LLM-call: the async delegation died with
state='unknown' and the job's row in cron/executions.db stayed
status='claimed' forever, blocking every subsequent run of that job.

Two-part fix:

1. hermes_cli/cron.py: `_job_action("run", ...)` declares the delivery
   channel stateless (scoped ContextVar set/reset around the call) before
   invoking the cron API, so `async_delivery_supported()` gates off
   `_try_dispatch_background_run` and the run executes synchronously to
   completion in the CLI process — the same behavior `hermes -z` already
   gets via declare_stateless_channel().

2. cron/scheduler.py: tick() now periodically invokes
   recover_interrupted_executions() (previously only run at scheduler
   startup), so execution rows whose exact owner process is provably dead
   (pid + process start time check in _owner_is_live) are reaped to
   'unknown' by the long-lived gateway ticker without a restart.
   Throttled to once per 300s so idle 60s ticks don't pay a ledger
   connection every cycle.

Tests: tests/cron/test_dead_owner_claim_reclaim.py covers the dead-owner
reap (real dead pid via a finished subprocess), live-owner rows surviving
the reap, throttle behavior, reap-failure isolation, the CLI stateless
gate (including restoration after the call), and the end-to-end refusal
of background dispatch under a stateless channel.

Fixes #86721
@github-actions

github-actions Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

૮ >ﻌ< ა ci review

ran on e415c8b — fix(cron): stop one-shot CLI cron run from orphaning the j

⚠️ Warnings

CI timings · View report · View job

Wall time 5m47s vs 3m43s (+55.6%). 11 job(s) slower, 13 faster,

  • Python tests / Run tests slice 4/12: -44.0s
  • Python tests / Run tests slice 12/12: -41.0s
  • Python tests / Run tests slice 11/12: +38.0s
  • Python tests / Run tests slice 7/12: -31.0s
  • Python tests / Run tests slice 9/12: +21.0s

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.

@alt-glitch alt-glitch added type/bug Something isn't working P1 High — major feature broken, no workaround comp/cron Cron scheduler and job management comp/cli CLI entry point, hermes_cli/, setup wizard labels Aug 15, 2026
@teknium1
teknium1 merged commit 0fc2a10 into main Aug 15, 2026
47 checks passed
@teknium1
teknium1 deleted the fix/86721-cron-run-orphan branch August 15, 2026 09:44
teknium1 added a commit that referenced this pull request Aug 15, 2026
…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cli CLI entry point, hermes_cli/, setup wizard 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.

cron run from one-shot CLI orphans the job: async delegation dies with the calling process, execution stuck 'claimed' forever

2 participants