Agent pane: a failed turn's Retry asks acpmux to retry the turn, not a second copy of the prompt

Merged
TE
7d ago
+405-11

Second sustained-chat error-recovery dead end (turn retry/continue UX). Stacked on #25942. This PR's own commits:

  • fcf4181d17e: test
  • f99d39a83b0: fix
  • dc98a35e8f3: review fixes

Review

Review (codex gpt-6-astra xhigh): SHIP at dc98a35e8f3d0f2373474073f63a43f451d6f584

The first round, at f99d39a83b0, returned CHANGES:

  1. retry.nothing_failed re-sent work acpmux had already finished.
  2. Retry used the client's selected chat, so during a switch it could retry the next one.
  3. 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: retryFailedTurn treats -32601 as 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/retry to both lists. That's a separate risk:high change (agent pane allowlist, docs/ci/risk-tiers.md), with its own relay analysis and policy tests. Its params name the pane's own session; a subagentId addresses 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.ts retryFailedTurn:
    • 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 existing retry.* notes.
  • TurnRows.tsx: the card's and footer's Retry go through retryFailed. Retry is disabled while one is in flight, and a refusal shows its note on the card.
  • App.tsx: retryFailed uses 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 carries retryable, so a failed hidden attempt still offers Retry.
  • retryState.ts: refusalNote is 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 lint exit 0, a clean type check and no new prepush-gate.py violations.
  • Not run:
    • A live failed turn against a real acpmux. On a Mac pane it would take the -32601 fallback until _acpmux/retry is allowlisted (above).
    • The chat-switch reach check has no unit test of its own.

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