Skip to content

Preserve and display classified agent turn failures instead of collapsing all failures to transient "Turn error" #1659

Open

Description

@cameronhotchkies

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:

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

  1. Trigger a Codex agent turn that fails due to auth expiry (or any network/timeout failure).
  2. Observe the "Turn error" badge in the session UI.
  3. 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:

  1. Harness commit: expand emit_turn_error to include error_class in the JSON payload (~10 lines, no new types).
  2. 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.

Activity

  1. Orland00 commented on Jul 21, 2026

    @Orland00

    Heads-up before I open a PR for this: the cited locations have drifted, and the current state changes the shape of the fix.

    What moved / what's already there. The emit path now lives in crates/buzz-acp/src/lib.rs (emit_turn_error closure, with the is_transport_error check inside the PromptOutcome::Error arm). The backend already derives a granular outcome_label (ok / error / idle_timeout / hard_timeout / exited / cancelled / cancel_drain_timeout) and forwards the JSON-RPC code for AgentError. So most of the classification data this issue asks for is already being emitted.

    Where the collapse actually happens: the frontend. desktop/src/features/agents/activeAgentTurnsStore.ts handles turn_error and agent_panic in the same case fall-through as turn_completed — endTurn() deletes the turn state and the entire error payload (outcome, error, code) is discarded. That's why every failure renders as a transient generic "Turn error" regardless of what the backend said. The rendering infra for a persistent, classified error already exists (friendlyAgentLastError.ts → friendlyTurnErrorCopy(raw, code)).

    Proposed PR shape (one PR, backend commit + frontend commit, split-able if you prefer):

    1. Backend: additive error_class field on the turn_error and agent_panic payloads — a stable machine discriminant (timeout / transport / agent_error / protocol / panic / exited / cancelled) derived from the existing outcome_label + AcpError variants, extracted into a pure testable fn. Old frontends unaffected.
    2. Frontend: stop treating error like completed — persist a lastTurnError per agent (outcome, error, code, error_class, timestamp), render through friendlyTurnErrorCopy, clear on the next successful turn.

    Motivation from the ops side: failed turns still burn tokens; when timeouts, transport flakes, auth expiry and panics all collapse into one vanishing badge, that spend is impossible to attribute or reduce.

    Will open the PR shortly — flagging here first in case maintainers want a different field name or a backend-only/frontend-only split.

    — Orlando (Olive One)

  2. Bartok9 commented on Jul 21, 2026

    @Bartok9
    Contributor

    I'd like to take the harness-boundary half of this (seam #1).

    Scope (buzz-acp): thread a stable, machine-readable error_class onto every turn_error observer payload in handle_prompt_result, so the UI can persist a differentiated badge instead of collapsing to a transient "Turn error":

    • fatal process death → its outcome label (exited / idle_timeout / hard_timeout / cancel_drain_timeout)
    • transport/protocol errors → transport_error
    • application errors (pipe intact) → application_error
    • agent_panic → panic

    Additive only — existing outcome / error / code fields are unchanged, so the UI-store change (seam #2, activeAgentTurnsStore.ts) can consume error_class in a follow-up without coupling. Includes a focused regression test asserting error_class per outcome; cargo test -p buzz-acp green, fmt/clippy clean.

    Opening a small draft PR against this. Happy to also take the UI-store persistence in a second PR if that split works for you.

  3. trippyogi commented on Jul 25, 2026

    @trippyogi

    For coordination: my earlier full-scope implementation in #1675 was closed by the inactivity sweep while awaiting review. It overlaps the newer #2240 and #2289. I’ve asked which implementation maintainers would prefer to use as the canonical path before doing another rebase.

  4. 27 remaining items

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions