Skip to content

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

Description

@vybe

Summary

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 #160 context: 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:

  1. 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.)
  2. 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.
  3. 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

  1. In the Sessions tab, create a session with an agent and exchange at least one turn (a Claude UUID gets cached).
  2. 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).
  3. Send another message in the same session.
  4. 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:

# stream_parser.py — generic execution_error branch
errors = msg.get("errors", [])
metadata.error_type = "execution_error"
metadata.error_message = result_text or (errors[0] if errors else "Execution error")
# headless_executor.py — finalize, alongside the rate_limit/max_turns/auth checks
if ctx.metadata.error_type == "execution_error":
    err = sanitize_text(ctx.metadata.error_message or "Execution error")
    logger.error(f"[Headless Task] Claude Code execution error: {err[:300]}")
    raise HTTPException(status_code=502, detail=f"Execution error: {err[:300]}")

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.

Environment

  • Trinity version: dev @ 13a29857 (v0.8.0 + dev)
  • Agent base image: built from the same tree
  • OS: Ubuntu 22.04, Docker prod compose

Related

Metadata

Metadata

Assignees

Labels

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions