Repository navigation
Conversation
_last_delivery_error_ kept only (stderr or stdout)[-500:], so a failed -Q turn with empty stderr and a banner-only stdout recorded zero diagnostics. Keep both tails, labeled and capped. Refs NousResearch#104056
SummaryFailed bot-chat deliveries now report labeled, capped tails of both stderr and stdout ( Findings
VerdictLooks good. Tiny, precisely tested observability fix. Safe to merge. |
|
Thanks @Enough1122 —
No changes — tiny observability fix as intended, thanks! |
|
Ack — verified at 3c858e5: empty-stream guards test-pinned, diagnostic-only with delivery lane untouched. Closed, no changes needed on my side either. |
…ually writes Neither DM delivery retry (NousResearch#93091 item 5) ever fired. Both gates classified the failure with `(proc.stderr or proc.stdout)`, but a failed `hermes ... -Q` turn splits across BOTH streams: the provider prose lands on stdout ("API call failed after 3 retries: HTTP 500: ..."), while stderr carries session bookkeeping ("Session ... Starting fresh.", "session_id: ..."). stderr is non-empty on every run, so it always won, the classifier only ever saw bookkeeping, every transient failure classified as 'unknown', and the re-run was skipped. Every rate limit, 500 and context overflow failed the DM outright instead of retrying once. Two call sites, same mistake: - `tools/bot_mode_dm.py` `_run_local_turn` — same-install `message_agent` deliveries. - `tui_gateway/methods_bot_relay.py` `bot_relay.deliver` — cross-connection deliveries. Its typed `reason` was also always 'unknown', so the sending agent could not tell a rate limit from an auth failure. The existing tests for both hand the error text to stderr and leave stdout empty — a layout no real run produces — so they passed while the feature was inert. `bot_failure_reasons.transport_failure_reason` now owns the decision for both: it classifies each stream and takes the first typed answer, so provider prose on stdout is seen while the deliberate stderr contract (`active_sessions.format_refusal_stderr`, SESSION_NOT_OWNED) still outranks nothing. Both retried attempts also set `HERMES_RESUME_UNANSWERED_TURN`, because the failed attempt already persisted the user row: without it, activating these retries makes the recipient receive the DM twice. Measured against a real CLI turn at a provider that fails the first attempt — one copy delivered, reply returned. The same `(stderr or stdout)` selection in `cron/scheduler_delivery.py` is NousResearch#104068's subject (reporting, not retrying); this PR leaves it alone.
…ually writes Neither DM delivery retry (NousResearch#93091 item 5) ever fired. Both gates classified the failure with `(proc.stderr or proc.stdout)`, but a failed `hermes ... -Q` turn splits across BOTH streams: the provider prose lands on stdout ("API call failed after 3 retries: HTTP 500: ..."), while stderr carries session bookkeeping ("Session ... Starting fresh.", "session_id: ..."). stderr is non-empty on every run, so it always won, the classifier only ever saw bookkeeping, every transient failure classified as 'unknown', and the re-run was skipped. Every rate limit, 500 and context overflow failed the DM outright instead of retrying once. Two call sites, same mistake: - `tools/bot_mode_dm.py` `_run_local_turn` — same-install `message_agent` deliveries. - `tui_gateway/methods_bot_relay.py` `bot_relay.deliver` — cross-connection deliveries. Its typed `reason` was also always 'unknown', so the sending agent could not tell a rate limit from an auth failure. The existing tests for both hand the error text to stderr and leave stdout empty — a layout no real run produces — so they passed while the feature was inert. `bot_failure_reasons.transport_failure_reason` now owns the decision for both: it classifies each stream and takes the first typed answer, so provider prose on stdout is seen while the deliberate stderr contract (`active_sessions.format_refusal_stderr`, SESSION_NOT_OWNED) still outranks nothing. Both retried attempts also set `HERMES_RESUME_UNANSWERED_TURN`, because the failed attempt already persisted the user row: without it, activating these retries makes the recipient receive the DM twice. Measured against a real CLI turn at a provider that fails the first attempt — one copy delivered, reply returned. The same `(stderr or stdout)` selection in `cron/scheduler_delivery.py` is NousResearch#104068's subject (reporting, not retrying); this PR leaves it alone.
`-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
|
Thanks @Halldrix — this landed on main through #113435 (fix(cron): a failed Bot Chat delivery records the exit code and both streams (stdout no lo), merged as 3719e04. Your commits/analysis were carried in with credit (see that PR's body and Co-authored-by trailers); the salvage rebased onto current main, widened to sibling surfaces where the review found gaps, and was live-verified in the real Electron app. Closing this one as superseded — thank you for the fix. |
What does this PR do?
A failed
deliver: bot-chat:<profile>turn recorded only(stderr or stdout)[-500:]aslast_delivery_error. In-Qmode the resume banner andsession_id:go to stderr while the response rides stdout, so half the signal was always discarded — and when stderr was empty and stdout held only the resume banner (the #104056 shape), the recorded "error" carried zero diagnostics.This PR keeps both tails, labeled and each capped at 500 chars (
stderr: … | stdout: …), or no suffix when both streams are empty. No behavior change on success, timeout, or the delivery lane itself.Related Issue
Refs #104056 (partial — covers problem 2 only, stays open)
Type of Change
Changes Made
cron/scheduler_delivery.py— new_format_failure_streams()helper (None-safe, per-stream 500-char cap); the non-zero-exit branch of_deliver_to_bot_chat()uses it instead of(stderr or stdout)[-500:].tests/cron/test_cron_bot_chat_delivery.py—_completed()helper gains astdoutparam (default"", existing tests unaffected); 3 new tests: both-streams-labeled, stdout-only banner case, empty-streams no-suffix pin.How to Test
scripts/run_tests.sh tests/cron/test_cron_bot_chat_delivery.py -q # 20 passedThe two regression tests fail on pre-fix code; the empty-streams test pins the no-suffix contract.
Exclusions
Out of scope (issue stays open): problem 1 (stale-resume fallback in
--create-if-missing— it would fight the #86794 intent of creating only on initial miss) and problem 3 (queue/retry on live-owner refusal — covered by open #100319 and #102659 against #99956). HenceRefs, notFixes.Checklist
Code
fix(cron): …)scripts/run_tests.sh, 20/20) and added regression testsDocumentation
Platform
Debian GNU/Linux 13 (trixie), x86_64 — Hermes Agent v0.21.0 (v2026.8.31), upstream 245e480