Skip to content

feat: surface agent errors to the TUI + progress-aware resume backoff#53

Open
mkonopelski-gd wants to merge 6 commits into
mainfrom
claude/agent-error-tui-interface-a777b2
Open

feat: surface agent errors to the TUI + progress-aware resume backoff#53
mkonopelski-gd wants to merge 6 commits into
mainfrom
claude/agent-error-tui-interface-a777b2

Conversation

@mkonopelski-gd

Copy link
Copy Markdown
Contributor

What

Agent/phase-level failures (SDK crashes, resume give-ups, model fallbacks, workspace aborts) previously existed only in per-phase log files — nothing durable, nothing in /status, nothing in the TUI. This PR gives them the same treatment app-level errors already get (red badge + Error panel), and reworks the resume counter so only consecutive zero-progress failures exhaust it.

Motivating incident (est-e2dfad7b42d7/ws-03-1-phase12, 2026-07-17): three transient socket errors ~2h apart exhausted the fixed 2-resume budget even though every attempt saved real work (commits, 136 tool uses). The workspace hard-aborted, stalled for 3 days, and a manual retry then finished the "failed" phase in 4 minutes for $0.41. A log-corpus sweep (63 errors, 32 files, 6 generations) showed 81% of crash-affected phases rode transient errors to cap exhaustion.

1. Durable agent-error event channel

  • New bounded agent_error_events (last 200) on the session doc + per-workspace agent_state badge, written only via new GenerationSessionStateMachine methods (Commandment VII; grep-guard clean).
  • Best-effort recorder wired through TelemetryContext (same pattern as the totals handler) — deep agent code records events with zero signature changes and can never raise into the agent loop.
  • /status ships the newest 50 events + agent_state passthrough; key omitted for old docs (no shape change).

2. TUI surfacing

  • Yellow Agent warnings panel on the dashboard (mirror of the Error panel, last 5, press e for all).
  • Bare RETRYING / ABORTED badges + markers on workspace rows — details deliberately live in the drill-in: a per-workspace warnings block (model switches pinned first) between Live messages and Stats, plus crash/retry narrative lines published into the live feed itself (new "error" stream kind; forward-compatible for old TUIs).
  • New scrollable Events screen (e).
  • check_status chat message gains a one-line warnings clause while running.
  • Fixes the Recent-activity panel tailing macOS .DS_Store as binary garbage (logs live nested; discovery is now recursive *.log-only).

3. Unified resume loop with progress-aware backoff

agent_query_with_resume rewritten as one attempt loop (coding + validator per attempt) with a single classify_error-driven except handler; all loop state lives in one ResumeGeneration object (single source for the loop condition, log lines, events, and the TUI badge — they can't disagree).

  • Every crash waits on one schedule: 1 / 3 / 5 / 10 / 30 / 60 min.
  • Saved progress resets the schedule (progress = new git commit during the attempt OR ≥20 tool uses, harvested from stream metrics that survive mid-stream crashes + rev-parse HEAD before/after).
  • Six consecutive zero-progress crashes give up (~1h49m of waits rides out a real outage — also de-fangs the correlated ConnectionRefused bursts seen in the corpus). No total cap for productive crashes.
  • Validator-driven budget unchanged at 2 (the anti-stuck contract).
  • Model-routing crashes keep the existing fallback and now record a model_fallback event (old → new model + reason) — a silently swapped model is no longer invisible.

4. Minimum-survivors gate (un-drops what #47 deferred, strengthened)

A total codegen wipeout previously ended COMPLETED/green with an empty estimate (aborts are absorbed per-workspace; P10Y deliberately doesn't fail on empty aggregates). PR #47 built _raise_if_all_workspaces_failed and dropped it as out-of-scope (3e35205). This PR reinstates it, strengthened: codegen fails when survivors < min(2, total) — variance-reduced estimates need ≥2 samples (single-workspace runs still pass). Deploy keeps the all-failed rule. The aggregated message names per-workspace causes and the retry path; workspaces stay ALLOCATED and checkpoints untouched, so retry_generation resumes only the failed workspaces.

Behavior changes to review

  1. Productive-crash phases are no longer capped at 2 attempts (bounded instead by the zero-progress schedule).
  2. Every crash now waits ≥1 min before retrying (previously immediate).
  3. A dead/timed-out validator retries under the crash budget instead of silently marking the phase complete.
  4. The "Reached maximum resume attempts" warning can no longer fire on a successful final attempt (previous check/message mismatch).
  5. < min(2, total) codegen survivors → generation FAILS (previously green).
  6. Give-up with validator-unsatisfied still checkpoints the phase complete (unchanged), but now leaves a phase_incomplete event.

Testing

  • Backend 2102 passed / 36 skipped (+62 new: test_resume_generation.py encodes the budget table verbatim; test_agent_query_with_resume.py includes an incident-regression test; survivors-gate, state-machine, recorder, executor-abort, and /status tests). Also fixes a pre-existing broken test (test_execute_all_phases_connection_error passed a non-awaitable mock to the newer cancellation check).
  • mcp_server 697 passed (+26 new: render rows/badges, dashboard panel, drill-in warnings block, Events screen via Textual pilot, nested-log activity fix, chat clause).
  • Complexity improved: whole-tree CC avg 3.63 → 3.60; agent_query_with_resume 19 → 18; MI avg +0.05.
  • Rendered dashboard smoke with an incident-shaped payload (badges, markers, warnings panel verified visually).
  • Pending manual check (needs a live run): induce a connection drop mid-codegen → RETRYING badge + schedule reset on recovery; kill all-but-one workspace → red FAILED with the insufficient-survivors message + r retry resumes from checkpoints.

Follow-ups (agreed, not in this PR)

  1. Retry-with-a-different-model chooser + optional llm_overrides on the retry endpoint (design agreed).
  2. Cross-workspace cascade fix: the stuck detector can fail a generation mid-run (720-min window vs 36h phases), after which healthy siblings' checkpoint writes are rejected and they abort — 4/10 aborts in the log corpus.
  3. Structured-signal error classification (SDK labels transport drops as subtype='success' + is_error=True; turn-budget exhaustion is not transient).

Agent/phase-level failures (SDK crashes, resume give-ups, model
fallbacks, workspace aborts) existed only in per-phase log files —
nothing durable, nothing in /status, nothing in the TUI. Incident
est-e2dfad7b42d7/ws-03-1-phase12: three transient socket errors ~2h
apart exhausted the fixed 2-resume budget despite each attempt saving
real work; the workspace stalled 3 days and a later retry finished the
"failed" phase in 4 minutes.

Durable event channel:
- agent_error_events (bounded, 200) on the session doc, written only
  via new state-machine methods (Commandment VII); best-effort recorder
  wired through TelemetryContext like the totals handler, so deep agent
  code records events with no signature changes and never raises.
- /status ships the newest 50 events + per-workspace agent_state badge;
  TUI renders a yellow "Agent warnings" panel (mirror of the Error
  panel), bare RETRYING/ABORTED badges + warning markers on workspace
  rows, a per-workspace warnings block on the drill-in (model switches
  pinned first), crash/retry lines in the live feed (new "error" stream
  kind), a full Events screen (e), and a check_status warnings clause.

Unified resume loop (retries are the main logic, not an appendix):
- one attempt loop (coding + validator per attempt) with a single
  classify_error-driven except handler; ResumeGeneration is the sole
  source for loop condition, log lines, events, and the TUI badge.
- every crash waits on one schedule (1/3/5/10/30/60 min); saved
  progress (new git commit OR >=20 tool uses) resets the schedule; six
  consecutive zero-progress crashes give up; no total cap. Validator
  budget stays 2. A dead validator now retries instead of silently
  marking the phase complete, and the give-up warning can no longer
  fire on success.

Minimum-survivors gate (un-drops what #47 deferred, strengthened):
- codegen fails the run when survivors < min(2, total) — variance
  estimates need two samples; a total wipeout previously completed
  GREEN with an empty estimate. Deploy keeps the all-failed rule.
  Aggregated message names per-workspace causes and the retry path;
  model-caused failures recommend changing the model in Settings.

Also fixes the Recent-activity panel tailing macOS .DS_Store garbage
(logs live nested; now rglob("*.log") only).
The earlier `.DS_Store`/nested-log discovery fix made this panel actually
locate the agent log — which exposed that it renders a raw tail of
DEBUG-level records (full `UserMessage(content=[ToolResultBlock(...)])`
dumps with escaped newlines): a wall of text that isn't useful at a
glance. Hide it from the dashboard for now.

`_activity_panel` and `tui/activity.py` are kept intact (and still point
at the correct nested log dir), so a proper filtered/prettified view can
be revived by re-appending the panel in build_dashboard.
The session dashboard should show only the run's outcome — the estimate
when complete, or the fatal Error — not a running list of per-workspace
warnings. The "Agent warnings" panel violated that: it grew a scrolling
log right under Workspaces (and, with Recent activity now hidden, became
the prominent panel there).

Remove it from build_dashboard. Nothing is lost: the Workspaces panel
already carries bare RETRYING/ABORTED/⚠ markers at a glance, and the full
per-event detail lives in the workspace drill-in warnings block and the
Events screen (`e`). `_event_row_text` stays (shared by both of those).
Manual/auto retry (reset_for_retry) reset retry_count/error but left the
transient per-workspace agent_state (RETRYING/ABORTED) untouched. Because
that badge is cleared only as a side-effect of the resume loop's
crash→backoff path, a retried run left stale ABORTED/RETRYING markers on
any workspace that didn't happen to re-enter that path — so after pressing
"r", one workspace's badge cleared while its siblings kept a phantom
ABORTED.

reset_for_retry now strips agent_state from every workspace_phases entry
(preserving last_completed_phase / planning_data so the retry still
resumes from the right phase), giving all workspaces a clean slate.
agent_query_with_resume derived its final AgentResult from last_returned
regardless of stop_reason. Because the crash branch only populates
last_returned when it is still None, a single clean-but-incomplete attempt
before a sustained outage left last_returned holding that stale success
(is_error=False). On a NO_PROGRESS_BUDGET (or TOOL_CALL_FAILURE) give-up,
final returned it verbatim, so execute_all_phases took the non-error branch:
it checkpointed the incomplete phase as complete and never hit the
CONNECTION_ERROR abort path (#47). On retry the phase was silently skipped.

Return an is_error=True result carrying last_error for crash-based give-ups;
COMPLETED and VALIDATOR_BUDGET (the anti-stuck contract) still return the
last coding result as-is. Adds two regression tests covering the mixed
clean-incomplete -> outage and clean-incomplete -> tool-call sequences.
The helper runs a coding query then a validator query; the new name says what it does (coding pass + completion check) rather than the generic 'attempt'.
@mkonopelski-gd
mkonopelski-gd marked this pull request as ready for review July 24, 2026 08:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant