You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
bug: agent-server masks error_during_execution as a successful context: fork placeholder — failed session resumes recorded as success, Sessions-tab fallback never fires #1673
The agent-server records Claude Code error_during_execution results as successful executions. When a headless task fails with an error result whose result text is empty (e.g. --resume against a Claude session whose JSONL no longer exists: "No conversation found with session ID"), the finalize path falls through to the #160context: fork placeholder branch and returns HTTP 200 with "(Task completed with no direct output — skill may use context: fork.)". The execution is stored as status=success, error=null.
Downstream this defeats the Sessions-tab self-healing designed in EXEC-023 Phase 2.2: the router's resume-not-found fallback only fires on result.status != "success", so the stale cached Claude UUID is never cleared — worse, step 6 re-caches the same stale UUID (the result event echoes the resume target as session_id). The session is permanently broken: every turn "succeeds" in ~2s at $0 with the placeholder text, consecutive_resume_failures stays 0, and nothing surfaces to the user or operator.
Observed in production on a dev-tracking instance (2026-07-17): a Sessions-tab session created 2026-05-14 whose cached Claude JSONL no longer exists on disk (consistent with Claude Code's default 30-day cleanupPeriodDays transcript cleanup — guaranteed to happen to any session idle >30 days). Two consecutive user turns each failed the resume and were both recorded as green successes.
Component
Agent Runtime (agent-server: headless_executor, stream_parser) — consequence in Backend (routers/sessions.py)
Priority
P1
Error
What Claude Code actually emitted (captured in the stored execution_log, while the execution row says success):
{
"type": "result",
"subtype": "error_during_execution",
"is_error": true,
"num_turns": 0,
"total_cost_usd": 0,
"errors": ["No conversation found with session ID: <claude-session-uuid>"]
}
Agent-server log sequence for the failing turn:
[Task] Resuming session <claude-session-uuid>: ...
[Headless Task] Resuming session: <claude-session-uuid>
[Headless Task] Starting task <execution-id>: claude --print --output-format stream-json --verbose...
WARNING stream_parser: Claude Code result is_error=true: type=execution_error, message=
WARNING [Headless Task] Task <execution-id> produced a result but did not exit — finalizing early, terminating teardown
INFO [Headless Task] Task ... exited cleanly with no assistant text in the parent stream (cost=$0, duration=0ms) — likely a `context: fork` skill. Returning placeholder response.
POST /api/task HTTP/1.1" 200 OK
Note message= is empty in the warning — the parser reads msg["result"], but for error_during_execution the text lives in msg["errors"].
Location
Three cooperating defects:
docker/base-image/agent_server/services/stream_parser.py (~L281–296) — the generic execution_error branch sets metadata.error_message = result_text (from msg.get("result", "")), which is empty for error_during_execution; the actual message is in msg.get("errors", []). (The max_turns branch two lines up already reads errors — this branch doesn't.)
docker/base-image/agent_server/services/headless_executor.py (~L907–935, finalize) — the error dispatch handles error_type values rate_limit, max_turns, and authentication_failed, but execution_error is never checked. With return_code == 0 (finalize-early path) and cost_usd == 0 (present, so is not None), control falls into the bug: Headless task returns empty response for skills with context: fork #160 empty-response placeholder branch (~L1055–1090) and returns 200 success.
src/backend/routers/sessions.py (~L645–680, ~L703) — by design the resume-not-found fallback (_is_resume_not_found markers: "no conversation found" / "session not found") clears the cached UUID and retries cold — but only when result.status != "success". Because of (2) it never fires, and step 6 re-caches the stale UUID from result.session_id, locking the session into the broken state.
Root Cause
Claude Code error results with empty result text are indistinguishable from the legitimate context: fork "clean exit, no parent-stream text" case in the finalize path, because metadata.error_type == "execution_error" is never consulted before the placeholder branch. The placeholder branch was added in #160 specifically for clean exits; an is_error=true result should never reach it.
Reproduction Steps
In the Sessions tab, create a session with an agent and exchange at least one turn (a Claude UUID gets cached).
Remove the corresponding Claude Code JSONL transcript inside the agent container (equivalently: let the session idle past Claude Code's cleanupPeriodDays, default 30 days, or recreate the container without that transcript).
Send another message in the same session.
Observe: HTTP 200, assistant reply is the context: fork placeholder text, execution recorded status=success / error=null / num_turns=0 / cost $0; the session_resume_fallback log line never appears; consecutive_resume_failures stays 0; every subsequent turn repeats the failure.
Suggested Fix
Surface execution_error in finalize before the empty-response handling, and make the parser fall back to errors[] for the message:
With that in place, the existing sessions-router fallback works as designed: the 502 detail contains "No conversation found", _is_resume_not_found matches, the stale UUID is cleared, and the turn retries cold — the session self-heals with no user-visible failure. Agent containers need a base-image rebuild to pick up the agent-server fix.
Summary
The agent-server records Claude Code
error_during_executionresults as successful executions. When a headless task fails with an error result whoseresulttext is empty (e.g.--resumeagainst a Claude session whose JSONL no longer exists: "No conversation found with session ID"), the finalize path falls through to the #160context: forkplaceholder branch and returns HTTP 200 with "(Task completed with no direct output — skill may usecontext: fork.)". The execution is stored asstatus=success,error=null.Downstream this defeats the Sessions-tab self-healing designed in EXEC-023 Phase 2.2: the router's resume-not-found fallback only fires on
result.status != "success", so the stale cached Claude UUID is never cleared — worse, step 6 re-caches the same stale UUID (the result event echoes the resume target assession_id). The session is permanently broken: every turn "succeeds" in ~2s at $0 with the placeholder text,consecutive_resume_failuresstays 0, and nothing surfaces to the user or operator.Observed in production on a dev-tracking instance (2026-07-17): a Sessions-tab session created 2026-05-14 whose cached Claude JSONL no longer exists on disk (consistent with Claude Code's default 30-day
cleanupPeriodDaystranscript cleanup — guaranteed to happen to any session idle >30 days). Two consecutive user turns each failed the resume and were both recorded as green successes.Component
Agent Runtime (agent-server:
headless_executor,stream_parser) — consequence in Backend (routers/sessions.py)Priority
P1
Error
What Claude Code actually emitted (captured in the stored
execution_log, while the execution row sayssuccess):{ "type": "result", "subtype": "error_during_execution", "is_error": true, "num_turns": 0, "total_cost_usd": 0, "errors": ["No conversation found with session ID: <claude-session-uuid>"] }Agent-server log sequence for the failing turn:
Note
message=is empty in the warning — the parser readsmsg["result"], but forerror_during_executionthe text lives inmsg["errors"].Location
Three cooperating defects:
docker/base-image/agent_server/services/stream_parser.py(~L281–296) — the genericexecution_errorbranch setsmetadata.error_message = result_text(frommsg.get("result", "")), which is empty forerror_during_execution; the actual message is inmsg.get("errors", []). (Themax_turnsbranch two lines up already readserrors— this branch doesn't.)docker/base-image/agent_server/services/headless_executor.py(~L907–935, finalize) — the error dispatch handleserror_typevaluesrate_limit,max_turns, andauthentication_failed, butexecution_erroris never checked. Withreturn_code == 0(finalize-early path) andcost_usd == 0(present, sois not None), control falls into the bug: Headless task returns empty response for skills with context: fork #160 empty-response placeholder branch (~L1055–1090) and returns 200 success.src/backend/routers/sessions.py(~L645–680, ~L703) — by design the resume-not-found fallback (_is_resume_not_foundmarkers: "no conversation found" / "session not found") clears the cached UUID and retries cold — but only whenresult.status != "success". Because of (2) it never fires, and step 6 re-caches the stale UUID fromresult.session_id, locking the session into the broken state.Root Cause
Claude Code error results with empty
resulttext are indistinguishable from the legitimatecontext: fork"clean exit, no parent-stream text" case in the finalize path, becausemetadata.error_type == "execution_error"is never consulted before the placeholder branch. The placeholder branch was added in #160 specifically for clean exits; anis_error=trueresult should never reach it.Reproduction Steps
cleanupPeriodDays, default 30 days, or recreate the container without that transcript).context: forkplaceholder text, execution recordedstatus=success/error=null/num_turns=0/ cost $0; thesession_resume_fallbacklog line never appears;consecutive_resume_failuresstays 0; every subsequent turn repeats the failure.Suggested Fix
Surface
execution_errorin finalize before the empty-response handling, and make the parser fall back toerrors[]for the message:With that in place, the existing sessions-router fallback works as designed: the 502 detail contains "No conversation found",
_is_resume_not_foundmatches, the stale UUID is cleared, and the turn retries cold — the session self-heals with no user-visible failure. Agent containers need a base-image rebuild to pick up the agent-server fix.Environment
13a29857(v0.8.0 + dev)Related
context: forkplaceholder branch (this bug is that branch swallowing genuine error results)