Summary
Agent turn failures are being collapsed to a generic, transient "Turn error" indicator in the UI that disappears before the user can act on it. The system already has enough information to distinguish failure classes, but that information is discarded at two seams:
- Harness boundary —
emit_turn_error in crates/buzz-relay/src/lib.rs serializes turn outcomes to JSON but does not include an error_class field, so all failure types arrive at the UI as undifferentiated errors.
- UI store —
activeAgentTurnsStore.ts handles turn_error / agent_panic by immediately ending the turn with no persistent error state, so the failure badge vanishes before the user can read it.
Root Cause
Part 1 — Harness (crates/buzz-relay/src/lib.rs)
The emit_turn_error closure (~line 2785) captures result.outcome but emits only outcome: "error". The PromptOutcome variants already carry enough information to classify the failure:
| Variant |
Suggested error_class |
PromptOutcome::AgentExited |
"exited" |
PromptOutcome::Timeout |
"timeout" |
PromptOutcome::Error(AcpError::IdleTimeout) |
"idle_timeout" |
PromptOutcome::Error(AcpError::HardTimeout) |
"hard_timeout" |
PromptOutcome::Error(AcpError::AgentError) |
"agent_error" ← Codex auth-expiry lands here (crates/buzz-relay/src/acp.rs:866) |
PromptOutcome::Error(AcpError::Json) |
"protocol" |
transport errors (via existing is_transport_error check ~line 2867) |
"transport" |
Fix: add a string error_class field to the existing serde_json::json! payload at ~lines 2791–2794. No new types needed.
Part 2 — UI Store (desktop/src/activeAgentTurnsStore.ts)
// current (~lines 342–352)
case "turn_error":
case "agent_panic":
endTurn(...) // deletes immediately, no error trace left
Fix: add an errorClass field to ActiveTurn and populate it from the event payload on turn_error before (or instead of immediately) calling endTurn. Keep the turn alive with an isError: true flag for a grace window (or until dismissed), so the badge can render "Auth error" / "Transport error" / "Timed out" instead of vanishing.
Reproduction
- Trigger a Codex agent turn that fails due to auth expiry (or any network/timeout failure).
- Observe the "Turn error" badge in the session UI.
- Badge disappears immediately — no actionable error class is shown.
Affected Code
crates/buzz-relay/src/lib.rs ~line 2785 — emit_turn_error closure, JSON payload
crates/buzz-relay/src/acp.rs ~line 866 — AcpError::AgentError construction (Codex auth path)
desktop/src/activeAgentTurnsStore.ts ~lines 342–352 — turn_error / agent_panic handling
Suggested Fix
One PR, two commits:
- Harness commit: expand
emit_turn_error to include error_class in the JSON payload (~10 lines, no new types).
- Frontend commit: add
errorClass/isError to ActiveTurn, read error_class from the event in the store, and render a classified error badge.
Open product question: Should the error badge stay until dismissed, auto-clear after N seconds, or clear on the next successful turn? The code for any option is trivial once decided.
Test Gaps
- No tests for the
AgentError / auth-expiry path through emit_turn_error
- No tests for
turn_error tombstone behavior in activeAgentTurnsStore
- Tests needed for: Codex/auth
AgentError, transport error, timeout paths
Out of Scope
- New triage agent
- Channel posting / notification policy
- Dashboard / activity-feed redesign
References
Internal investigation by Ferret & Lazy Joe in channel #bugfix-codex-logged-out.
Summary
Agent turn failures are being collapsed to a generic, transient "Turn error" indicator in the UI that disappears before the user can act on it. The system already has enough information to distinguish failure classes, but that information is discarded at two seams:
emit_turn_errorincrates/buzz-relay/src/lib.rsserializes turn outcomes to JSON but does not include anerror_classfield, so all failure types arrive at the UI as undifferentiated errors.activeAgentTurnsStore.tshandlesturn_error/agent_panicby immediately ending the turn with no persistent error state, so the failure badge vanishes before the user can read it.Root Cause
Part 1 — Harness (
crates/buzz-relay/src/lib.rs)The
emit_turn_errorclosure (~line 2785) capturesresult.outcomebut emits onlyoutcome: "error". ThePromptOutcomevariants already carry enough information to classify the failure:error_classPromptOutcome::AgentExited"exited"PromptOutcome::Timeout"timeout"PromptOutcome::Error(AcpError::IdleTimeout)"idle_timeout"PromptOutcome::Error(AcpError::HardTimeout)"hard_timeout"PromptOutcome::Error(AcpError::AgentError)"agent_error"← Codex auth-expiry lands here (crates/buzz-relay/src/acp.rs:866)PromptOutcome::Error(AcpError::Json)"protocol"is_transport_errorcheck ~line 2867)"transport"Fix: add a string
error_classfield to the existingserde_json::json!payload at ~lines 2791–2794. No new types needed.Part 2 — UI Store (
desktop/src/activeAgentTurnsStore.ts)Fix: add an
errorClassfield toActiveTurnand populate it from the event payload onturn_errorbefore (or instead of immediately) callingendTurn. Keep the turn alive with anisError: trueflag for a grace window (or until dismissed), so the badge can render "Auth error" / "Transport error" / "Timed out" instead of vanishing.Reproduction
Affected Code
crates/buzz-relay/src/lib.rs~line 2785 —emit_turn_errorclosure, JSON payloadcrates/buzz-relay/src/acp.rs~line 866 —AcpError::AgentErrorconstruction (Codex auth path)desktop/src/activeAgentTurnsStore.ts~lines 342–352 —turn_error/agent_panichandlingSuggested Fix
One PR, two commits:
emit_turn_errorto includeerror_classin the JSON payload (~10 lines, no new types).errorClass/isErrortoActiveTurn, readerror_classfrom the event in the store, and render a classified error badge.Open product question: Should the error badge stay until dismissed, auto-clear after N seconds, or clear on the next successful turn? The code for any option is trivial once decided.
Test Gaps
AgentError/ auth-expiry path throughemit_turn_errorturn_errortombstone behavior inactiveAgentTurnsStoreAgentError, transport error, timeout pathsOut of Scope
References
Internal investigation by Ferret & Lazy Joe in channel
#bugfix-codex-logged-out.