Skip to content

fix(cron): delivery failures record last_status=delivery_failed and surface in list/doctor/manual run (#83993, salvage #100163 #86622) - #100916

Merged
teknium1 merged 6 commits into
mainfrom
p1/cron-delivery-2
Sep 2, 2026
Merged

fix(cron): delivery failures record last_status=delivery_failed and surface in list/doctor/manual run (#83993, salvage #100163 #86622)#100916
teknium1 merged 6 commits into
mainfrom
p1/cron-delivery-2

Conversation

@teknium1

@teknium1 teknium1 commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Summary

A cron run whose agent succeeded but whose output never reached the user is no longer recorded or shown as ok: it gets a distinct last_status: delivery_failed, hermes cron list renders it yellow with the reason, cron doctor reports it as a delivery issue, and a manual cronjob run reports success: false with the delivery error instead of a green result.

Changes

  • cron/jobs.py::mark_job_run (salvage fix(cron): surface delivery_failed instead of last_status ok #100163 @jwilson411) — derives last_status as error > delivery_failed > ok (an explicit status= override such as blocked_config still wins). failure_streak is untouched by delivery failures (the agent did its job); last_error stays None. This is the storage-level fix the issue asked for: everything that keys off "ok" now sees the failure.
  • hermes_cli/cron.py (fix(cron): surface delivery_failed instead of last_status ok #100163) — cron list renders delivery_failed: <last_delivery_error> in yellow (the "⚠ Delivery failed" detail line stays); cron doctor no longer double-reports a delivery failure as "last run failed: unknown error".
  • tools/cronjob_tools.py::_run_claimed_job (re-applies the fix(cron): surface delivery failures instead of hiding under last_status ok #84006 @webtecnica direction) — a manual run returns success: false and surfaces last_delivery_error as the error when the record is delivery_failed; previously it derived success from == "ok" and read only last_error, so a delivery-failed run came back as an unexplained failure.
  • tools/cronjob_tools.py::_manual_run_delivery_note (salvage fix(cron): stop manual-run notice from asserting delivery that never happened #86622 @strzhao) — the manual-run completion summary no longer appends "output was delivered there by the job itself" over a failed delivery; it follows the refreshed record (⚠ delivery FAILED: <reason>), treats a falsy deliver as local, and reads the target from the claimed snapshot. Pinned so the headline reads Result: FAILED rather than Result: ok for such runs.
  • Docswebsite/docs/user-guide/features/cron.md gains a "Delivery failures are a distinct status" section.

Not taken: #84006's "ok (delivery failed)" string status (a compound value every == "ok" reader would have to parse; delivery_failed is the cleaner enum) and #89132's delivery_state == "pending" && delivery_attempts == 0 heuristic (those fields are not written anywhere on main; the storage-level status makes the CLI heuristic unnecessary).

Validation

Check Result
tests/cron/test_jobs.py, tests/hermes_cli/test_cron.py (new: delivery_failed status, override precedence, streak, doctor) passed
tests/tools/test_cronjob_run_delivery_notice.py (new, 15 tests incl. real background-dispatch summary) passed
tests/tools/test_cronjob_run_immediate.py (+2: manual run reports delivery_failed) passed
Neighbourhood: test_cronjob_run_background.py, test_cronjob_tools.py, test_run_one_job.py, test_preflight_config.py, test_scheduler.py, test_cron_incidents.py, test_execution_ledger.py 400 passed total
Sabotage: stash the _run_claimed_job change → test_delivery_failed_status_is_not_success_and_surfaces_reason 1 failed; restored → passes
ruff check on touched files clean

Live repro: real cron.jobs store under an isolated HERMES_HOME, real mark_job_run(job, True, None, delivery_error="live adapter send failed: 502 ...") (the exact call run_job makes), then the real hermes cron list renderer — before (origin/main): record last_status='ok', list shows Last run: 2026-09-01T21:22:33 ok with the failure only on a secondary ⚠ Delivery failed: line; _format_joblast_status='ok'. After: record last_status='delivery_failed', list shows Last run: ... delivery_failed: live adapter send failed: 502 Bad Gateway (target telegram:5160665427); _format_joblast_status='delivery_failed'.

Closes #83993

Salvages #100163 (@jwilson411 — storage + CLI status; cherry-picked with authorship) and #86622 (@strzhao, issue author — manual-run notice honesty; 3 commits cherry-picked with authorship). Supersedes #84006 (@webtecnica — earliest submission for this issue; its tool-result direction is re-applied here with co-author credit) and #89132 (@LeonardoLGDS — CLI-only rendering, superseded by the status-level fix).

Infographic

cron-delivery-failed-status

De-risking (commit dcd9d294415)

Audit of every last_status consumer outside the scheduler (rg -n last_status across web/, apps/desktop/, hermes_cli/, tui_gateway/, tools/, scripts/, website/; the auth.py/web_server.py:14288/video-gen hits are unrelated credential-pool / polling fields):

Consumer Before After
web dashboard CronPage.tsx last_status never rendered — a delivery_failed job showed a green scheduled badge + a small red delivery: line new pure cronLastResult() (web/src/lib/cron-job.ts) maps the closed literal set to tones (ok→success, delivery_failed/blocked_config→warning, error/unknown→destructive); card shows an amber delivery_failed badge (title = last_delivery_error)
Desktop hermes-bots routine inspector (cron.tsx) Last result printed the raw literal routineLastResult() spells out each literal (Ran, but delivery failed, Blocked by configuration (not run), …); unknown passes through
in-chat /cron list (cli_commands_mixin.py) Last run: <ts> (delivery_failed) with no reason (last_error is None) appends the delivery reason: (delivery_failed: telegram: 502 Bad Gateway)
hermes cron list / cron doctor / cronjob tool already handled on this branch unchanged
== "ok" for success only tools/cronjob_tools.py manual-run path — already fixed by this branch no other consumer compares == "ok"
docs developer-guide/cron-internals.md: table of last_status literals + which field carries the detail
  • Live repro (real hermes dashboard on a temp HERMES_HOME seeded with a delivery_failed job, real /api/cron/jobs, CronPage rendered against it): before — card badges [scheduled, default, telegram:123]; after — [scheduled, delivery_failed (border-warning/30 bg-warning/15 text-warning, title "telegram: 502 Bad Gateway", data-testid cron-last-result), default, telegram:123].
  • Tests: web/src/lib/cron-job.test.ts +5 (cronLastResult), apps/desktop/src/plugins/hermes-bots/cron-detail.test.tsx +1 (routineLastResult for every literal), tests/hermes_cli/test_cron.py +2 (TestSlashCronListLastStatus). Runs: web vitest 15 passed, desktop vitest 10 passed, tsc --noEmit + eslint clean in both, Python 160 passed (test_cron.py, test_cronjob_run_delivery_notice.py, test_cronjob_run_immediate.py, test_jobs.py).

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

૮ >ﻌ< ა ci review

ran on dcd9d29 — fix(cron): every last_status consumer renders delivery_faile

⚠️ Warnings

OSV vulnerability scan · View job

13 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 5m28s vs 5m18s (+3.1%). 8 job(s) slower, 8 faster,

  • OS-specific tests / macOS-only tests: +70.0s
  • Check no committed infographics / check-no-committed-infographics: -44.0s
  • Python lints / Windows footguns (blocking): +42.0s
  • OSV scan / Emit review status: +34.0s
  • OS-specific tests / Windows-only tests: -32.0s

@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 sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages labels Sep 2, 2026
@webtecnica

Copy link
Copy Markdown
Contributor

@/tmp/comment-100916.md

jwilson411 and others added 6 commits September 1, 2026 23:47
A successful agent run whose delivery failed used to persist
last_status=ok and bury the failure in last_delivery_error. CLI list
painted that as green and the run looked identical to a quiet success.

Record last_status=delivery_failed instead, keep last_delivery_error,
do not increment failure_streak, and teach cron list/doctor not to
treat it as ok.

Fixes #83993
…happened

The _execute_job_now completion notice unconditionally claimed
"(output was delivered there by the job itself)" for non-local
delivery targets, even when the job record's last_delivery_error
showed the delivery failed (#83993). Derive the note from the
refreshed job record so a failed delivery is reported honestly to
the calling agent.
Review follow-up on the #83993 fix: a stored falsy deliver ("", JSON
null) fell through the local check and produced 'output was delivered
there by the job itself' for a target that does not exist — the exact
false-delivery-claim class the PR removes. Fire time already normalizes
falsy deliver to local (no delivery, output persisted in last_output,
no delivery error), so the summary now canonicalizes with the
scheduler's own _normalize_deliver_value and reads saved-locally.

Whitespace-only deliver is deliberately not folded in: fire time
records 'no delivery target resolved' for it, and the error-driven
FAILED wording must stay visible.
Main grew claim_job_for_fire(job_id, return_job=True) — a claimed
snapshot dict instead of a bool — while this branch sat on an older
base. The merge-ref CI ran the hybrid: the wiring tests still mocked
return_value=True, which fails isinstance(claimed_job, dict) and fell
into the 'already being fired' branch, so every dispatch assert failed.

Mock the claim to return the job snapshot (the API's success shape),
read the summary's deliver from the claimed snapshot the run actually
executes, and keep the dispatch-result failure renderer. Rebased onto
current main; cron suite 710 passed.
…or the distinct status

A manual cronjob(action='run') derived success from last_status == 'ok'
and read the error from last_error — so a run that now records
delivery_failed came back as success=False with error=None, an unexplained
failure. Surface last_delivery_error as the error in that case (the
#84006 direction, re-applied on the delivery_failed status), and pin the
manual-run completion summary to say 'Result: FAILED' over an undelivered
run. Document the status in the cron user guide.

Co-authored-by: webtecnica <webtecnica@gmail.com>
…tly (dashboard badge, Desktop inspector, /cron list, docs)

Audit of every last_status reader outside the scheduler (rg last_status across
web/, apps/desktop/, hermes_cli/, tui_gateway/, tools/, scripts/, website/):

- web dashboard CronPage: last_status was never rendered at all — a
  delivery_failed job showed a green 'scheduled' badge and only a small red
  'delivery: ...' line. New pure cronLastResult() helper maps the closed
  literal set to tones (ok=success, delivery_failed/blocked_config=warning,
  error/unknown=destructive) and the card now shows an amber
  'delivery_failed' badge (title = last_delivery_error).
- Desktop hermes-bots routine inspector: 'Last result' printed the raw
  literal; routineLastResult() spells out each one ('Ran, but delivery
  failed', 'Blocked by configuration (not run)', ...), unknown passes through.
- /cron list (cli_commands_mixin): 'Last run: <ts> (delivery_failed)' now
  appends the delivery reason, since last_error is None for those runs.
- hermes cron list/doctor and the cronjob tool already handled the literal
  on this branch; no consumer compared == 'ok' for success apart from the
  cronjob manual-run path, which the branch already fixed.
- developer-guide/cron-internals.md: table of last_status literals + which
  detail field carries the reason.

Live repro (real 'hermes dashboard' on a temp HERMES_HOME with a
delivery_failed job, CronPage rendered against the live /api/cron/jobs):
before — badges [scheduled, default, telegram:123]; after — badges
[scheduled, delivery_failed (warning tone, title 'telegram: 502 Bad
Gateway'), default, telegram:123].
@strzhao

strzhao commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Thanks @teknium1 for the salvage and the clean landing — authorship on the three cherry-picked commits confirmed on my side, and the delivery_failed enum (over a compound "ok (delivery failed)" string) was the right call: every == "ok" reader now sees the failure for free.

Happy to help if direction-3 work (cached-output redelivery, #16645) ever picks up. Nice working together on this one.

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 sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cron delivery failures are silently swallowed — last_status:ok hides last_delivery_error, user never knows

5 participants