Agent pane: a failed turn's Retry asks acpmux to retry the turn, not a second copy of the prompt
Second sustained-chat error-recovery dead end (turn retry/continue UX). Stacked on #25942. This PR's own commits:
fcf4181d17e: testf99d39a83b0: fixdc98a35e8f3: review fixes
Review
Review (codex gpt-6-astra xhigh): SHIP at dc98a35e8f3d0f2373474073f63a43f451d6f584
The first round, at f99d39a83b0, returned CHANGES:
retry.nothing_failedre-sent work acpmux had already finished.- Retry used the client's selected chat, so during a switch it could retry the next one.
- A failed hidden auto-resume attempt drew no Retry.
All three are fixed in dc98a35e8f3.
Known gap: _acpmux/retry is not on the pane allowlist yet
_acpmux/retry is in neither Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AcpmuxPaneMethods.swift nor cmux-tui/crates/cmux-agent-pane-policy/policy.json. So the Mac host refuses it before acpmux, with -32601 "Refused by the cmux host" (AcpmuxPaneMethods.swift:253). The subagent Retry merged in #25733 is in the same position.
- What that means today:
retryFailedTurntreats-32601as a daemon without the method and re-sends the prompt. On a Mac pane, Retry therefore behaves as it does on base: no regression. - What doesn't work yet: the acpmux turn-retry benefits below.
- The fix: add
_acpmux/retryto both lists. That's a separaterisk:highchange (agent pane allowlist,docs/ci/risk-tiers.md), with its own relay analysis and policy tests. Its params name the pane's own session; asubagentIdaddresses one of that chat's subagents. It's tracked as a follow-up from this lane.
The dead end
The failed turn's Retry, on its error card and in its footer, re-sent the whole prompt as a new message (resendPrompt + chat.send). acpmux serves its own turn retry, _acpmux/retry with turn-retry-v1, which does three things the re-send does not:
- Continues instead of restarting: the continuation, not the whole prompt.
- Takes over a waiting automatic retry instead of racing it.
- Releases the held queue once its turn completes (
held_by).
Change
conversation/turnRetry.tsretryFailedTurn:- It asks acpmux for the turn retry of the chat the row was drawn for, and re-sends only when the method is missing or refused (
-32601). retry.nothing_failed, busy, stopped, still running and no answer send nothing; each says why, using the existingretry.*notes.
- It asks acpmux for the turn retry of the chat the row was drawn for, and re-sends only when the method is missing or refused (
TurnRows.tsx: the card's and footer's Retry go throughretryFailed. Retry is disabled while one is in flight, and a refusal shows its note on the card.App.tsx:retryFaileduses Continue and Restart's reach checks (restartOwner/restartReaches). They are checked before asking and again before a fallback send.turns.ts/model.ts: the last turn's summary carriesretryable, so a failed hidden attempt still offers Retry.retryState.ts:refusalNoteis exported.
Tests
webviews/test/turn-retry.test.tsx (new):
- Retry asks acpmux and sends nothing itself.
- An old daemon, or
-32601, re-sends. nothing_failed, busy, stopped, running or no answer never re-sends.- The card's and footer's Retry use
retryFailed. - A refusal's note shows on the card, and a double click asks once.
- A failed hidden attempt still offers Retry.
Red at fcf4181d17e. Red at f99d39a83b0 with the review-fix tests (2 fail). Green at dc98a35e8f3: bun test test/turn-retry.test.tsx test/turn-error-card.test.tsx, 29 pass.
Observed / Not run
- Observed: the red and green runs above, plus
vp lintexit 0, a clean type check and no newprepush-gate.pyviolations. - Not run:
- A live failed turn against a real acpmux. On a Mac pane it would take the
-32601fallback until_acpmux/retryis allowlisted (above). - The chat-switch reach check has no unit test of its own.
- A live failed turn against a real acpmux. On a Mac pane it would take the
Risk
Normal. Until the allowlist lands, behavior matches base on a Mac pane.
Changelog
Fixed: Retry on a failed agent turn no longer sends a second copy when another retry already ran it, and says why when acpmux refuses.
Agent session: agent session
š¤ Generated with Claude Code