Agent pane: retry state for failed subagents and sub-sessions (part 1 of 2)

Merged
TE
9d ago
+766-26

Leo, 2026-10-26: "this needs to retry :("

Part 1 of 2. This PR holds the state, the adapter and the session rows. Part 2 (stacked on this one, opened next) adds the subagent end line's Retry and ⋯ menu, the running tray, the gallery and their strings. #25226 was the two combined, and it was split to keep each PR under 800 lines.

Two Claude Task subagents ended with "Subagent failed: …" after a transient gateway 503. acpmux now retries them itself and lets a person retry them (#25232, #25264, #25234, #25296, #25381). This is the webview half.

Can our side retry a failed Claude Task subagent?

Not through ACP or the Claude Code SDK directly. Only the parent agent can, and acpmux asks it to. claude-agent-acp (0.94) exposes only Query.stopTask / _session/async_task/stop for a task, and nothing that re-runs one. The SDK's Query has no retry or restart for a task either. A failed Agent-tool call is the parent's tool result, so a new run has to come from the parent: another Agent call, or a SendMessage to the subagent's agentId, which resumes it from its transcript. acpmux's _acpmux/retry {sessionId, subagentId} does the second one: it prompts the parent, in the same session, to resume that subagent by its agent id. So a Task subagent that acpmux marks retryable gets a real Retry. "Ask parent to retry" (one line naming the task and its description, sent through the composer) is the fallback when retryable is false or absent, or the daemon doesn't serve the capability.

What changes

  • One owner for a row's retry (subagents/retryState.ts). subagentRetry(agent, capabilities, child?) and sessionRetry(session) give each row one state:
    • retrying: a retry waits.
    • settling: acpmux is deciding, starting or running a sub-session's retry (retryDeciding).
    • failed: carries the Retry the row offers and the retries acpmux gave up on.
    • none: the row isn't failed, is nested, or its retry is stale.
      retryKey is the one cache-key part for it. Part 2's surfaces read the same functions.
  • Adapter. readRetry reads retry: {attempt, of, reason, nextAt}|null from _meta.acpmux.subagents entries and from the session row, along with retryDeciding and lane.error. The fold keeps retryable and error. "Gave up" comes only from a subagent_retry step with outcome exhausted. A cleared retry never means "gave up", since acpmux also clears it when a retry fires.
  • runRetry runs the one Retry a row offers:
    • A Task subagent with retryable gets _acpmux/retry {sessionId: chat, subagentId} when acpmux names subagent-retry-v1. Otherwise it gets the parent ask, and so does a daemon that answers -32601 or retry.subagent_unsupported.
    • A sub-session gets {sessionId: child} under turn-retry-v1. It never falls back to asking the parent.
    • Refusals map to short notes: retry.busy; retry.nothing_failed by its why; retry.stopped; retry.unknown_subagent.
    • runRetry keeps no state, and acpmux's retry.busy answers a second Retry.
  • Session rows show "Retrying (2/4) after a capacity error" with <Spinner loading /> (never a dot) while acpmux waits to retry. This covers the sub-session card, the summary list and its counts, and the Inspector's started-chat rows.
  • Retry continuations (provenance auto_resume or subagent_retry) don't draw as person bubbles. direct.ts reads acpmux's capabilities and adds retry() for the RPC.
  • 11 strings in Localizable.xcstrings, in every shipped language (non-English marked needs_review).

No UI action ships in this part: nothing calls runRetry until part 2. The visible change is the retrying label on session rows. It has no timers, observers or loops, and no countdown to nextAt.

Risk

Low. The new reads are of fields acpmux already sends, and the only visible change is the retrying label on session rows. Part 2 carries the before and after captures (the Retry affordance).

Tests

  • subagents/retryState.test.ts (new), 23 tests:
    • the adapter on the daemon's shapes
    • each state from subagentRetry and sessionRetry
    • the fold's exhausted step
    • which rows get which action under each capability set
    • RPC params, refusals and their why, and the fallbacks
    • the started-chat rows and counts
    • the strings
  • On huge-blue: tsc clean, vp fmt no diff, vp lint 0 errors. retryState.test.ts passes 23/23 run alone. A whole src/agent-session/acpmux/ run fails base's 67 tests (other files mock i18n) and 5 of these for the same reason.

Agent session: agent session

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.