Skip to content

fix(cron): surface acknowledged worker failures and release stale claims - #129748

Merged
unsupportedpastels merged 4 commits into
NousResearch:mainfrom
unsupportedpastels:fix/129254-cron-worker-failure
Oct 1, 2026
Merged

unsupportedpastels merged 4 commits into
NousResearch:mainfrom
unsupportedpastels:fix/129254-cron-worker-failure

Conversation

@unsupportedpastels

Copy link
Copy Markdown

Summary

Fixes #129254

Surface post-acknowledgement worker failures: record the worker's exit cause, preserve its stderr diagnostic, retire the matching fire claim, and invoke the job's existing failure-notification lane.

The execution ledger deliberately remains unknown: an acknowledged worker may already have performed side effects. The job records an error. This does not automatically retry or claim to identify/prevent the reporter's initiating startup crash.

Root cause

After a worker adopts its execution and dies, the parent recovers its row to terminal unknown, then returns without recording the job outcome. The fire claim and empty job error survive. Separately, the payload cleanup unlinks stderr before Python prints an escaping exception, so an outer traceback wrapper cannot preserve that diagnostic.

Fix

  • Carry forward the targeted dead-owner terminalizer and observed exit status from fix(cron): record a lost manual run instead of reporting a restart that never happened #128616; never terminalize a live worker.
  • Read the existing bounded/redacted stderr tail after a lost worker exits. Keep exceptional captures until that read; successful workers still clean up their own captures.
  • Project a recovered unknown run onto the job through the existing incident/failure-delivery path. Hold its fire-owner fence through notification and bookkeeping so an already-recorded run, replay, or newer fire is not overwritten.
  • Preserve diagnostics when a concurrent recovery sweep wins the ledger transition first.
  • Keep new outcome logic in cron/scheduler_worker_failure.py; normal worker completion and unrelated waiter-error semantics remain unchanged.

Prior work and credit

  • #128616, @Finn763: cherry-picked d3f3571f8c6f4d2f188e90151d8f8393f049a573 with -x, retaining the original author. Its exit-evidence/claim fix is the base; this adds failure delivery and closes its payload-exception stderr gap.
  • #120391, @DresvyanskiyDenis: recovery projection and failure-delivery approach, hand-ported with verified Co-authored-by: Denis Dresvyanskiy <dresvyanskiydenis@gmail.com>. This version holds the fire fence over the side effect and adds worker diagnostics.
  • #129256, @dskwe, with @ehz0ah's reviews: identified the post-ack stderr unlink ordering and, at e322e76, records a worker-side traceback and releases the owner-fenced claim when run_one_job raises. This PR reaches the same claim/cause outcome from the parent, so it also covers a hard worker exit and sends the job's failure notice. No code copied. One deliberate difference: the ledger stays unknown rather than failed, because run_one_job may already have had side effects before it raised.
  • #126355, @dahux-net: managed-store relaunch fixes that are a plausible initiating cause of this crash class. Separate mechanism, not superseded here.
  • #120347, #108806, and closed #108920: broader recovery projections reviewed, not incorporated. Their periodic/restart scope remains separate; fix(cron): project recovered worker failures #120347 also failed the already-recorded-success cross-test.
  • #124847 and #124855 address bootstrap re-exec in fix(cron): adopted external worker can re-exec after deleting one-shot payload, leaving run unknown #124827; #127931 addresses dead fire-owner identity. These are not proven causes of this report and are not superseded by touching the same file.
  • Pre-ack proposal #109294 and existing merged work #113219, #124013 (carrying #123432), #116689, #126344, and #106733 informed the comparison. Existing redaction, atomic acknowledgement, dependency activation, pre-ack notices, and claim-reclaim behavior are retained.

Test plan

Canonical runner, with HERMES_PYTHON=/home/mark/.hermes/hermes-agent/venv/bin/python:

Scope and risk

No real Telegram delivery, native Windows/macOS runtime run, or hosted CI is claimed yet. Notification tests exercise the existing delivery boundary without sending messages.

This covers the surviving parent observing its worker's death, as in the report. If the gateway itself has already died, it cannot project or notify the failure here; broader periodic recovery remains separate. A failed worker may leave its stderr capture if no parent survives. No interpreter-selection, lazy-install policy, schema, or automatic retry changes. Rollback is reverting the four implementation commits.

Finn763 and others added 4 commits September 30, 2026 21:11
…at never happened (NousResearch#128509)

A restart-safe cron worker that dies AFTER adopting its attempt was reported as
"Scheduler restarted after this execution's owner exited" and treated as success.

recover_interrupted_executions() only knows that an owner is gone, so that is all it
can write -- but the waiter holding the worker's Popen knows the owner was that
worker and knows its exit status. Worse, the sweep leaves the row terminal, so the
waiter returned True, run_one_job short-circuited, and mark_job_run never ran: the
job's fire claim outlived its lost execution and refused the next manual fire with
"Job is already being fired" for the whole 300s lease, with an empty last_error.

- executions.terminalize_dead_owner(): terminalize ONE attempt with an observed
  cause. Refuses a live owner, this process, and a handoff inside the adoption
  grace, so it can never steal a running worker's attempt; returns False so the
  caller keeps the generic sweep.
- The waiter records that cause and RAISES, routing the existing post-handoff
  bookkeeping: mark_job_run records the run and retires the claim, no dispatch
  incident and no false crash notice. Also folds the worker's captured stderr into
  the error, which the pre-ack path already did (NousResearch#112729) and this path dropped.

(cherry picked from commit d3f3571)
…tics

Extend Finn763's dead-owner recovery with owner-fenced failure delivery based on PR NousResearch#120391 and retain stderr until the parent consumes an escaping worker traceback. Preserve unknown side-effect state and do not retry automatically. Cover both shared entry paths, an exception through the payload lifecycle, abrupt death, and concurrent recovery.

Co-authored-by: Denis Dresvyanskiy <dresvyanskiydenis@gmail.com>
@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 Oct 1, 2026

@arkheioncorp arkheioncorp left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Revisão: PR #129748

Verdict: APPROVE

Análise

Resolve #128509: um worker externo que morre após adoção deixava o execution como "unknown" com causa genérica "Scheduler restarted...", e o run_one_job retornava True (pulando mark_job_run), bloqueando o fire_claim para o próximo fire manual.

Mudanças principais:

  1. terminalize_dead_owner() em cron/executions.py: terminaliza um execution para "unknown" com causa real observada pelo waiter, com guards contra live workers e handoff adoption grace.
  2. cron/scheduler_worker_failure.py (new): record_unknown_worker_outcome() e external_worker_exited_reason() para visibilidade de worker failure.
  3. Modificações em _wait_for_external_cron_worker_body(): agora usa terminalize_dead_owner() antes de fallback para recover_interrupted_executions().
  4. run_one_job(): trata post-handoff failures via record_unknown_worker_outcome().
  5. Testes novos em test_restart_safe_worker_death_evidence.py cobrindo múltiplos cenários de morte de worker.

Security scan

Nenhum hardcoded secret, injection, eval/exec, ou deserialization encontrado. Código de scheduler interno.

Qualidade

  • A lógica está correta: o waiter observa a morte do worker e registra a causa real.
  • Os guards em terminalize_dead_owner() (live worker check, handoff grace) são essenciais e bem implementados.
  • Testes muito completos cobrindo hard exit, payload exception, concurrent recovery, timeout variants.

Sugestões (non-blocking)

Nenhuma.

Aprovado.

@unsupportedpastels unsupportedpastels left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Self-review (COMMENT; own PR). Independent read-only review approved exact head 166ea75 on base 7239625; exact-head CI: run 36793140949 green, including All required checks pass, Python tests, ruff/ty lints, Windows/macOS OS-specific tests and contributor attribution; also cron suite 1,422 passed / 3 skipped on a local trial merge with current main 6633626. Evidence: repro across main and 11 candidate PRs, RED checks against main and #128616 production, cron suite 1,422 passed / 3 skipped (baseline 1,411 / 3). Authorship: Finn763's commit preserved via cherry-pick -x; Denis Dresvyanskiy credited by Co-authored-by trailer.

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.

Cron agent-mode worker dies silently before agent starts (execution stuck, no error, no delivery)

3 participants