Agent pane: a failed subagent or sub-session offers Retry, and shows acpmux's retry while it waits

TE
10d ago
+1123-49

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

Two Claude Task subagents ended with "Subagent failed: …" after a transient gateway 503 (no account with capacity). Nothing on the row could run them again. This is the webview half. The daemon half (automatic retry with backoff, the retry state, _acpmux/retry, the subagent_retry records) has landed in acpmux (#25232, #25264, #25234, #25296, #25381). This PR reads those real fields and gates on acpmux's capabilities.

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) say where a row stands: retrying (a retry waits), settling (acpmux decides, starts or runs a sub-session's retry, retryDeciding), failed with the Retry it offers and the retries acpmux gave up on, or none (not failed, nested, or a stale retry). The end line, the summary list, the running tray and the sub-session card all read that and nothing else; retryKey is the one cache-key part for it.
  • Adapter. readRetry reads retry: {attempt, of, reason, nextAt}|null from each _meta.acpmux.subagents entry and from the session row (session/list, with lane.error and retryDeciding). The fold keeps retryable and error. "Gave up after N tries" comes only from a subagent_retry record with outcome exhausted. A cleared retry is never read as giving up, since acpmux also clears it when a retry fires.
  • Retrying state. While a retry waits, the end line reads "Retrying (2/3) after a capacity error" with <Spinner loading />, never a dot. The sub-session card, the summary list and the running tray show the same; the tray lists a retrying subagent or chat with the running ones. A sub-session retry that waits on a parent run slot shows the existing "Waiting for a slot".
  • Retry action, on a failed or lost line of the chat's own, never nested and never while a retry waits or settles. It's an inline button, and the same single action sits in the row's ⋯ menu beside Restart. Menus open on the click.
    • A Task subagent with retryable gets _acpmux/retry {sessionId: chat, subagentId} when acpmux names subagent-retry-v1. Otherwise it gets "Ask parent to retry", and so does a daemon that answers -32601 or retry.subagent_unsupported.
    • A sub-session gets _acpmux/retry {sessionId: child} when acpmux names turn-retry-v1, and no Retry otherwise. Only acpmux can retry a chat's turn, so it never asks the parent.
    • A daemon that lists no capabilities gets the method tried.
    • Refusals show as a short inline note: retry.busy; retry.nothing_failed by its why; retry.stopped; retry.unknown_subagent.
  • No double fire. The client keeps no in-flight state. A Retry that went stays disabled until the row changes. acpmux refuses a second retry of its own as retry.busy, which also covers an attempt that already fired. Retry uses the same reach checks as Restart, checked again before the fallback send: never in a View Only chat or a read-only pane, and never on the next chat during a switch.
  • Retry continuations (provenance auto_resume or subagent_retry) don't draw as person bubbles.
  • 15 new strings in Localizable.xcstrings, in every shipped language (non-English marked needs_review).

Follow-up (daemon): after acpmux fires an attempt it clears retry, so the row reads failed with Retry until the subagent runs; a click meanwhile gets "Busy, try again shortly". A per-subagent "attempt in flight" field from acpmux would let the row keep its spinner. The webview won't guess it (rounds 6 to 8 of review showed each client-side guess breaking).

No new timers, observers or loops, and no countdown to nextAt. Refusal notes are React state keyed to the row's version.

Gallery: agent-pane.subagent-retry

Synthetic rows named after Leo's two (no private data), Linux Chromium, w720, at head 3a4fd56.

StateDarkLight
Before: the base line, failed, with no way to run it again at rest
After (A, picked): failed (capacity), inline Retry
Retrying (2/4) and (1/4), turning ring
Gave up after 4 tries, Retry once more
Task subagent acpmux can't retry: Ask parent to retry
The ⋯ menu (on hover), opened on the click: the row's one action
Version B: link after the time
Version C: ⋯ menu only (opened)

The failed row's Retry was built three ways: A an inline outlined button (picked), B a link after the time, C in the ⋯ menu only. A is the pick: a failure gets a visible way out at rest, and it reads apart from the sentence. B blends into the line's text. C shows nothing until hover, which is how the base already hid Restart. B and C stay in the gallery (RetryAffordanceContext).

Risk

Low to medium, contained to the subagent end line, the sub-session card, the summary list and the tray. A failed line always shows a small Retry now. A real Retry prompts the parent chat (acpmux does it), with the same reach as Restart. About 500 of the ~1,075 lines are the test file, the gallery and strings; the rest is one feature across the line, card, tray and summary, so it lands as one PR.

Tests

  • subagents/retryState.test.ts (new): the adapter on the daemon's shapes; the fold keeping retry, retryable, error and the exhausted step (fired/scheduled clear it, a cleared retry alone doesn't set it); the session list's retry; which rows get which action under each capability set; the ask's one line; each phase from subagentRetry/sessionRetry (retrying, settling, failed, none for stale or nested rows); the tray placing retrying rows with the running ones; RPC params, refusals and their why, the -32601 and subagent_unsupported fallbacks (a subagent asks, a sub-session never does); the strings in the catalog.
  • On huge-blue at the head: bunx tsc --noEmit -p . clean, bunx vp fmt no diff, bunx vp lint 0 errors, bun test src/agent-session/acpmux/subagents/ src/agent-session/acpmux/summary/ test/subagent-tray.test.tsx 116 pass. test/gallery-coverage.test.ts fails the same way on base (entries across the pane that aren't in this PR).

Agent session: agent session

🤖 Generated with Claude Code