fix(execution): telemetry + transcript on a failing error_during_execution/504 row (#1853) - #2224
Conversation
|
Resolve by running |
…ing error_during_execution/504 row (#1853) Trinity Rule #1 (requirements before implementation). Documents Approach B: the FAILED applier mirrors the SUCCESS branch — persists sanitized execution_log + tool_calls summary + UUID-validated claude_session_id + salvaged cost/context on the error_during_execution (502) and timeout (504) FAILED row. Names the residuals (_write_terminal_and_gate + standalone scheduler still land bare). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…cution 502 and timeout 504 bodies (#1853) The execution_error 502 raised a bare detail="Execution error: <msg>", discarding ctx.metadata (session_id/cost/context) and ctx.raw_messages (the full stream-json transcript) that were in scope. New _execution_error_502_detail mirrors _timeout_504_detail: {message, metadata, execution_log}. The message text is byte-identical (preserves #1938 + the backend resume-not-found self-heal). _timeout_504_detail gains the same validated session_id fallback + execution_log, so the 504 path persists them for real too. _valid_session_id UUID-shape-validates the session_id fallback before it is persisted — ctx.claude_session_uuid can be an untrusted resume_session_id (log-forging vector; sanitize_dict does not strip newlines) (FI-1). Also corrects the stale "reaps after 24h" comment (real: 6h sweep / 1h age guard = 1-7h effective). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ow (#1853) _extract_agent_error now returns the agent execution_log transcript too; the httpx handler threads it + the validated session_id onto the existing TerminalEnvelope fields. apply_result FAILED branch mirrors the SUCCESS branch: sanitize_execution_log(json.dumps(transcript)) + the #1741 tool_calls summary + claude_session_id, passed into the EXISTING db.update_execution_status call. cost/context already salvaged from metadata. Single terminal applier (#1483) preserved — payload widened, no new CAS writer, _write_terminal_and_gate untouched. All side-effects stay gated on the existing won bool (#1578/#1804) — the new columns are added above the gate, no predicate widened. Bare-string old-image bodies leave the columns null (graceful mixed-fleet degrade). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
_SESSION_ID_UUID_RE / _valid_session_id use re.compile; headless_executor did not import re. Completes the previous agent-side commit. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…#1853) New tests/unit/test_1853_error_telemetry_salvage.py (21 tests): - _valid_session_id UUID guard incl. the FI-1 newline-injection case - _execution_error_502_detail / _timeout_504_detail carry transcript + validated session_id; the structured body does NOT trip _is_reader_race_signature (ENG#2) - _finalize_headless_result error branch raises a 502 dict with an UNCHANGED message (#1938) + transcript + session id (ENG#10) - _extract_agent_error 3-tuple; resume-not-found message preserved (#1673/#1849) - apply_result FAILED branch persists the transcript with the embedded secret REDACTED, a #1741 tool_calls summary, the validated session id, salvaged cost; won-gated close/emit unchanged on a lost CAS (#1578/#1804) - real-sqlite readback proving the column names (ENG#11) Refines the now-misleading test_1083 comment (a FAILED write CAN carry the transcript when the envelope has one). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
) task_execution_service bullet: the FAILED branch of apply_result mirrors the SUCCESS telemetry (execution_log/tool_calls/claude_session_id) above the won gate; _extract_agent_error 3-tuple + the _timeout_504_detail transcript/session extension. Names the residual bare-terminal writers. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
) - task-execution-service.md: FAILED applier mirrors SUCCESS telemetry; _extract_agent_error 3-tuple - parallel-headless-execution.md: structured 502/504 body + _execution_error_502_detail + validated session_id fallback; stale 24h->1-7h comment note - feature-flows.md: Recent Updates index row Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
) Extends the #1853 suite with the #1944 §4.4 live-capture pattern: a REAL error tail written into the agent's ~/.claude/projects/-home-developer/ drives the REAL _finalize_headless_result (genuine #1870 recovery decline, not monkeypatched), so the structured 502 is proven to carry the transcript + validated session id on the exact failure path the fleet takes — and a #1870-recoverable tail is NOT re-failed by #1853. Adds the end-to-end chain (real error tail -> real finalize 502 body -> backend _extract_agent_error -> apply_result -> a persisted, redacted, session-tagged FAILED row in real sqlite). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tured shape (#1853) #1853 deliberately changed the error_during_execution 502 body from a bare string to a structured {message, metadata, execution_log} dict. Two #1673 contract tests asserted the old string shape and began failing: - test_502_detail_carries_the_resume_marker_backend_matches_on: the resume- not-found marker the backend substring-matches now lives in detail["message"] (carried verbatim, preserving #1938 + the self-heal). Assert the dict shape. - test_execution_error_not_a_reader_race_dict_body -> _does_not_trip_reader_race: "not a dict" is no longer the discriminator (the body IS a dict now). Assert the real contract: _is_reader_race_signature(detail) is False (it keys on recovery_attempted, which the #1853 body omits). Intent preserved on both; behavior unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…uctured shape (#1853) `_detail_for` asserted the error_during_execution 502 `detail` was a plain string, but #1853 made it a structured body (message + metadata + execution_log). Mirror the real consumer (`_extract_agent_error` derives `result.error` from `detail["message"]`) — the same migration already applied to the sibling test_1673. Fixes the 3 regression-diff failures in test_1849 (test_resume_fallback_fires_through_the_502_detail, test_resume_fallback_survives_a_403_bearing_uuid, test_multi_error_truncation_limit_is_known). Also fixes a pre-existing test-isolation leak surfaced by this PR's new test file reordering: test_audit_chain_edges::test_enabling_the_hash_chain_survives_a_restart (added by #2026) writes `audit_hash_chain_enabled='true'` to the real global system_settings and never restores it, so a later `platform_audit_service.log()` silently takes the chained writer — flaking test_1966's test_real_audit_service_swallows_its_own_failures. Add an autouse snapshot/restore fixture (mirrors the #762 restore in tests/test_audit_log_unit.py). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
4baa8f9 to
a75ac6b
Compare
CodeQL check — 10 pre-existing false positives dismissedThe
Every line logs pure telemetry ( They surfaced as "new" only because All other checks — regression diff, 6 pytest seeds, prod-image-smoke, both Analyze jobs, container-security, guards — are green. |
vybe
left a comment
There was a problem hiding this comment.
Validated via /validate-pr. The FAILED branch mirrors the SUCCESS applier and stays inside the existing db.update_execution_status CAS call — no new terminal writer, _write_terminal_and_gate untouched, and every side-effect remains gated on won (#1483/#1578/#1804 preserved). Transcript is sanitized with the same sanitize_execution_log the SUCCESS branch uses, and the 502 message text is byte-identical so #1938 and the reader-race classifier are unaffected. Old-image bodies degrade to null columns, so a mixed fleet is safe both directions. Residuals (_write_terminal_and_gate terminals, scheduler RETRY-001 writes) are named in the description rather than implied. All checks green.
Fixes #1853
Problem
An execution that ends
error_during_execution(Claude Code reportsis_errorwith a non-terminal last message — commonly a fan-out whose<task-notification>lands afterstop_reason=end_turnand interrupts the follow-on turn) was written toschedule_executionswith status + error string only — noclaude_session_id, nocost, noexecution_log. The Claude JSONL that would carry the evidence is auto-persisted (#678) but reaped on a 6h sweep / 1h age-guard (1–7h effective), and the row carries no session id to locate it with, so the whole failure class was undiagnosable after the fact.Measured on an ops instance: 26
successrows all carried log + session_id + cost; 6failedrows carried none. This is the observability substrate for the Aug-14 "system agent identifies errors" initiative (reliability lane; TOWARD the #1401 recovery trace).Fix (Approach B — inline the sanitized transcript)
Mirror the SUCCESS applier onto the FAILED path, so an
error_during_execution(502) / timeout (504) row is diagnosable via the same API as a SUCCESS row.Agent (
headless_executor.py)_execution_error_502_detail(ctx, message)mirrors_timeout_504_detail:{message, metadata, execution_log}, whereexecution_log = ctx.raw_messages(the full stream-json transcript, in-memory so present for short≤600sruns too — no JSONL dependency).session_idfallback (metadata.session_id or ctx.claude_session_uuid) is UUID-shape-validated via new_valid_session_id(FI-1: the fallback can be an untrustedresume_session_id— a log-forging vector, sincesanitize_dictdoesn't strip newlines).messagetext is byte-identical to before, so fix(agent-server): never surface Claude Code's [ede_diagnostic] header as the error cause (#1849) #1938's error-string fix and the backend's resume-not-found self-heal (which readsdetail["message"]) are preserved, and the structured body does not trip_is_reader_race_signature(no false Async chat_with_agent: long execution silently fails with null response (reader-thread) #678 auto-retry)._timeout_504_detailgains the same validatedsession_idfallback +execution_log, so the 504 path persists them for real too.Backend (
task_execution_service.py)_extract_agent_errorreturns the transcript (now a 3-tuple); theexcept httpx.HTTPErrorhandler threads it +session_idonto the existingTerminalEnvelope.execution_log/.session_idfields.apply_result's FAILED branch mirrors the SUCCESS branch — identicalsanitize_execution_logredaction + bug: schedule_executions.tool_calls stores a verbatim copy of execution_log — breaks the tool-call metric and survives log retention #1741tool_callssummary +claude_session_id— passing the columns into the existingdb.update_execution_statuscall, added to the SET clause above theif wongate.Invariants preserved: single terminal applier (#1483) — no new CAS writer,
_write_terminal_and_gateuntouched; CAS-won close/emit (#1578/#1804) — every side-effect stays gated onwon. No schema change (execution_log/claude_session_id/tool_callsalready exist).Named residuals:
_write_terminal_and_gateterminals (backend timeout/budget/crash) and standalone-scheduler RETRY-001 FAILED writes still land bare — the Aug-14 initiative must not assume every FAILED row carries telemetry.The agent half lives in
trinity-agent-base. A running fleet keeps writing bare FAILED rows until./scripts/deploy/build-base-image.shruns AND each agent is COLD-recreated — a plain restart may not adopt it (#1809). The backend half is live on backend deploy. Old-image bodies degrade gracefully (bare string →execution_log/session_idstay null = today's behaviour), so a mixed fleet is safe both directions. Same rollout note #1944 (#1870) and #1938 (#1849) carried.Docs updated
docs/memory/requirements/scheduling.md— new §10.4.4docs/memory/architecture.md—task_execution_service.pyapply_resultFAILED-branch notedocs/memory/feature-flows.md(index) +feature-flows/task-execution-service.md+feature-flows/parallel-headless-execution.mdTests
tests/unit/test_1853_error_telemetry_salvage.py(26) — agent helpers incl. the FI-1 log-forging guard, the unchanged-message-text (#1938) + no-reader-race-collision regressions, backend FAILED-branch salvage with credential redaction proven in real sqlite, the #1944 §4.4 live-capture pattern (real on-disk error tail → real_finalize_headless_result), and the full real error tail → finalize →_extract_agent_error→apply_result→ persisted redacted row chain.Also migrated two
test_1673_execution_error_not_success.pycontract tests whose assertions targeted the old bare-string 502 body (now the structured dict) — intent preserved (resume-marker reachability; no reader-race collision).All touched suites green locally (
test_1853,test_1673,test_1083_apply_result,test_1201_timeout_telemetry_salvage,test_792_subscription_retry), including underpytest-randomly.🤖 Generated with Claude Code