Skip to content

fix(cron): a failed Bot Chat delivery records the exit code and both streams (stdout no longer dropped when stderr is present) (#104056, salvage #104068) - #113435

Merged
teknium1 merged 2 commits into
mainfrom
fix/bots-oneshot-cron-delivery-botchat-streams
Sep 16, 2026
Merged

teknium1 merged 2 commits into
mainfrom
fix/bots-oneshot-cron-delivery-botchat-streams

Conversation

@teknium1

@teknium1 teknium1 commented Sep 16, 2026 •

Copy link
Copy Markdown
Collaborator

When a cron job's bot-chat delivery turn fails, last_delivery_error (and the scheduler log line) now records the exit code plus BOTH the child's stderr and stdout tails, labeled and scrubbed — stdout is no longer dropped when stderr is present, and the banner-only shape says exit code N | stdout was only the resume banner instead of echoing the banner as if it were a reason.

  • cron/scheduler_delivery.py::_format_failure_streams (salvaged from fix(cron): report both streams on bot-chat delivery failure #104068, resolved onto the current plain-language failure message): exit code N | stderr: … | stdout: … replaces (stderr or stdout)[-500:] and the extra [-200:] cut. stderr keeps 500 chars; the stdout tail (the model's answer) is capped at 200 chars with the Resumed session / session_id: banner lines dropped; the whole detail goes through agent.redact.redact_sensitive_text(force=True, redact_url_credentials=True) before it is persisted, as cron.incidents and cron.delivery_queue already do for their stored errors.
  • Tests: three invariant tests (both streams labeled; the reported banner-only shape names the exit code and does not echo the banner; the persisted stdout tail is ≤200 chars and scrubbed), each red on the base it fixes; the salvaged PR's empty-streams suffix test dropped as a change-detector.

Validation

before: with stderr empty and stdout = Resumed session … "Bot Chat" (1 user message, 1 total messages), the recorded error was … Details: Resumed session … (banner only, no reason; the reporter's case), and when both streams carried text only stderr's last 200 chars survived.
after: … Details: exit code 1 | stderr: <reason> | stdout was only the resume banner; banner-only stdout with empty stderr → exit code 1 | stdout was only the resume banner; a 50 KB stdout tail is persisted as ≤200 chars, secrets scrubbed.

Live repro: real cron.scheduler_delivery._deliver_to_bot_chat against a real hermes chat --in ~ -c "Bot Chat" --create-if-missing -Q --query-file child in a scratch HERMES_HOME whose provider is invalid (probe_104056.py). Base: Details: Unknown provider 'openai'. …; head: Details: stderr: Unknown provider 'openai'. … — same child, stream now named; the banner-only shape is pinned by the unit test. Backend-only surface (cron scheduler → hermes cron runs / the job's last_delivery_error); no renderer change, so no Electron leg.

Tests: scripts/run_tests.sh tests/cron/test_cron_bot_chat_delivery.py → 19 passed; scripts/run_tests.sh tests/cron → 1381 passed, 0 failed.

Root cause: -Q splits the resume banner/session_id: and the real reason across the two streams, and the delivery lane kept only one of them.

Part of #104056 — this is the diagnostics atom (atom 2). Atom 3 is already on main: a Bot Chat held by a Desktop/CLI owner no longer hard-fails the leg (deferred to the owner mailbox / until the CLI owner releases — #111273, 5d8390d1a4b33, 3b0fe0cc2b5f9, 002ee41cfcc0c), and refusals ride a hermes-refusal-reason: marker on stderr (549e6aab38c49), which this change makes visible in the recorded error. Atom 1 is still open and not touched here: chat -c "Bot Chat" --create-if-missing resolving a stale title→session mapping whose row is gone/unusable still exits 1 instead of falling back to creating a fresh titled session — on origin/main, hermes_cli/main.py::_resolve_continue_arg only creates when the title lookup misses, and hermes_cli/oneshot.py::_load_resume_target raises session not found for a resolved id without a row (deliberately, per #105892 for explicit ids). With this PR that failure at least records exit code 1 | stderr: session not found: … instead of the bare resume banner; the fail-over-to-create itself belongs to a follow-up on the -c <title> --create-if-missing path, not the cron delivery code.
Salvages #104068 (@Halldrix) — commit authored by the contributor.

Review follow-up

  1. Banner-only case overclaimed (head recorded the same banner as main, only labeled) → fixed in 20d0858061a: _format_failure_streams always leads with exit code N, strips the resume banner lines from the stdout tail and writes stdout was only the resume banner when nothing else was printed. Title/body softened accordingly.

  2. Persisted last_delivery_error grew to ~1.2 KB with 500 chars of unredacted model output → fixed in 20d0858061a: stdout tail capped at 200 chars (stderr keeps 500) and the detail is scrubbed with agent.redact.redact_sensitive_text(force=True, redact_url_credentials=True). No redaction helper existed on the last_delivery_error path (cron/jobs.py stores it verbatim; grep -rn redact cron/ shows scrubbing only in incidents.py, delivery_queue.py, scheduler_script.py, scheduler_diagnostics.py), so the same helper those siblings use was applied — none invented.

  3. Body said the 'other two halves' were already on main → fixed (body edit): atom 1 (stale title→session mapping → exit 1 instead of the --create-if-missing fallback) is NOT on main; the Part of #104056 paragraph now names it as still open with the main-side evidence (hermes_cli/main.py::_resolve_continue_arg, hermes_cli/oneshot.py::_load_resume_target).

Live repro: real cron.scheduler_delivery._deliver_to_bot_chat with subprocess.run returning real CompletedProcess shapes (probe over the previous head vs this head). Banner-only stdout, empty stderr — before: Details: stdout: Resumed session 20260905_121420_8084c7 "Bot Chat" … session_id: … (304 chars, banner only); after: Details: exit code 1 | stdout was only the resume banner (223 chars). 50 KB stdout + stderr — before: 1195-char record carrying 500 chars of the answer tail; after: 909 chars with a 200-char stdout tail. Backend-only surface; no Electron leg.

Infographic

cron-bot-chat-both-streams

`-Q` writes the resume banner and `session_id:` to one stream while the real reason may sit in
the other; `(stderr or stdout)[-500:]` (and the `[-200:]` cut of the recorded detail) discarded
half the signal, so a banner-only failure left `last_delivery_error` with no diagnostic at all
(#104056). Keep both tails, labeled `stderr:` / `stdout:`, in the log line and the recorded
error. Resolved onto the current wording of the delivery failure message; the third test from
the PR (empty-streams suffix) was dropped as a change-detector.

Part of #104056
Salvages #104068
@github-actions

github-actions Bot commented Sep 16, 2026 •

Copy link
Copy Markdown

૮ >ﻌ< ა ci review

ran on 20d0858 — fix(cron): banner-only bot-chat failure names the exit code;

debug info

CI timings

CI timings · View report · View job

Wall time 6m18s vs 6m27s (-2.3%). 4 job(s) slower, 6 faster, 2 unchanged.

  • OS-specific tests / Windows-only tests: -17.0s
  • Python tests / e2e: +7.0s
  • Python lints / Windows footguns (blocking): +5.0s
  • Python lints / ruff enforcement (blocking): +3.0s
  • OS-specific tests / macOS-only tests: -3.0s

@alt-glitch alt-glitch added type/bug Something isn't working P3 Low — cosmetic, nice to have comp/cron Cron scheduler and job management labels Sep 16, 2026
…d stdout tail capped and scrubbed

Review follow-up for the both-streams change.

- `_format_failure_streams` now always leads with `exit code N`, drops the
  `Resumed session` / `session_id:` banner lines from the stdout tail, and
  says `stdout was only the resume banner` when nothing else was printed —
  so the issue's exact shape (empty stderr, banner-only stdout) records a
  reason instead of echoing the banner, which is what main already did.
- The persisted stdout tail is the model's answer: cap it at 200 chars
  (stderr keeps 500) and run the whole detail through
  `agent.redact.redact_sensitive_text(force=True, redact_url_credentials=True)`,
  the same scrub `cron.incidents` and `cron.delivery_queue` apply to their
  persisted errors — `last_delivery_error` had none.
- Tests: the banner-only test now pins the exit-code/banner-only wording;
  one new test pins the 200-char cap and the scrub. Both red on the
  previous head.
@teknium1 teknium1 changed the title fix(cron): a failed Bot Chat delivery records both streams so the reason is visible (#104056, salvage #104068) fix(cron): a failed Bot Chat delivery records the exit code and both streams (stdout no longer dropped when stderr is present) (#104056, salvage #104068) Sep 16, 2026
@teknium1
teknium1 merged commit 3719e04 into main Sep 16, 2026
34 checks passed
@teknium1
teknium1 deleted the fix/bots-oneshot-cron-delivery-botchat-streams branch September 16, 2026 23:58
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 P3 Low — cosmetic, nice to have type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants