acpmux: wait --print shows a failed turn's error and its retry, never the reply before it
The gui-push coordinator (2026-10-20-gui-push-6f, remote acpmux ergonomics), word for word:
"(1) acpmux wait --print after a turn that failed (capacity) prints the previous turn's reply, not the error, so a failed turn reads as done"
(Part (2), send --prompt-file, is #25362. It was split out to keep each PR near 800 lines; this one is 822, about 470 of them tests.)
Cause
wait --print picked "the last reply" with last_replies(…, 1), the newest assistant item in the transcript. A turn that failed before it said anything has none, so the reply came from the turn before it. The daemon's _acpmux/wait could also answer in the moment between a failed turn's end and its automatic retry being scheduled, so even a careful client saw retry: null.
What changed
CLI (cli/orchestrate.rs): when the resolved session's last turn failed and no turn runs:
--printprints that turn's error. If the turn streamed output of its own (output::turn_replyforlastTurn.turnId), that is printed too, never the turn before it.- It prints the automatic retry that waits (#25232's
retry): which attempt, what it waits out, when the next attempt runs, andacpmux retry NAMEto run it now. If none waits, it says that. - The state line reads
ready: turn failed[, retry waits], and--notifysaysNAME's turn failed. It exits 1 as before.--jsonrows gainerrorandretry.
Daemon (hub/auto_resume.rs, hub/views.rs, server/wait.rs, protocol/session.rs):
- The session row gains
retryDeciding, read together withretryfrom one read of the chat's auto-resume mark (Hub::retry_row). It is true while the last turn failed on an error a retry answers and whether, and when, it is retried is not settled yet: the turn's end is being settled, the retry's time is not set yet, or the retry is starting or running. If the retry the row read is gone by the time it is checked (it fired meanwhile), the mark is read again, up to 3 times (settled_retry_row), so a row never shows neither a retry nor its decision. The field is absent when false and from older peers. The protocol IR and the webviews types are regenerated. - A decision for no retry on such a turn records
auto_resume_settled {turnId, why}(stopped,off,not_retried), forwarded to watchers and peers, so a peer's copy of the row clears too. Stop records it for a mark it takes (settled_by_stop). _acpmux/waitholds its answer for a row that saysretryDeciding(and whose turn is null and is not closed, missing or stale), for at most 5 s from when that wait first saw it. That only bounds a row nothing refreshes, such as a record its peer link lost. It adds no timer: the wait's existing record wake and 1 s re-read end the hold.
Before (base, from the red run):
$ acpmux wait lane --print # last turn: "API Error: 503 no non-exhausted claude accounts …"
lane ready
echo: the answer before
After (a CI run of the built binary, https://github.com/manaflow-ai/cmux/actions/runs/40276666964):
$ acpmux wait lane --print
lane ready: turn failed, retry waits
error: API Error: 503 no non-exhausted claude accounts available. This is a server-side issue, usually temporary — try again in a moment. If it persists, check your inference gateway (127.0.0.1:4016). (error type server_error, HTTP 503)
retry 1 of 1 waits out capacity: the provider or the gateway is full; the next attempt runs in 1m12s (`acpmux retry lane` runs it now)
$ echo $?
1
$ acpmux --json wait lane # its row, abridged
{"name": "lane", "status": "ready", "error": "API Error: 503 no non-exhausted claude accounts …",
"retry": {"attempt": 1, "of": 1, "reason": "capacity", "nextAt": 1792994132107, "promptId": "…", "turnId": "…"},
"reply": ""}
Takes back #25232: its row line retry: self.waiting_retry(session) (hub/views.rs) becomes Hub::retry_row, which reads the same retry together with the new retryDeciding from one read of the mark. The retry a row shows is unchanged, and waiting_retry is folded into retry_row.
Tests (red, then green)
tests/cli_ergonomics.rs(new; a real daemon and the fake agent): aftercap-fail-once,wait --printprints the error, the retry andacpmux retry lane, and not the earlier reply. It exits 1, and--jsoncarrieserrorandretry. Red at a5fabce8 (https://github.com/manaflow-ai/cmux/actions/runs/40273595532).tests/hub_integration/auto_resume_capacity.rs:a_wait_that_ends_with_a_cut_turn_answers_with_its_retry: red at a5fabce8.no_row_of_a_cut_turn_reads_settled_before_its_retry_is_set(multi-thread, a reader samples rows while the turn ends): red at 3dcd9aac (https://github.com/manaflow-ai/cmux/actions/runs/40278912811).a_cut_turn_with_no_retry_records_the_decision_for_watchers: red at 1d525288.stop_records_the_decision_for_a_retry_that_waited: red at 25feee0b (https://github.com/manaflow-ai/cmux/actions/runs/40281874017).
tests/wait_peer_retry.rs(two hubs over a real peer link): a wait on a peer's cut turn answers with the peer's retry.- Unit:
cli/orchestrate/wait_tests.rs,server/wait.rsretry_deciding_tests, andhub/auto_resume.rs(a_row_reads_the_mark_again_when_its_retry_moved_on,stop_settles_an_ending_or_waiting_turn). - Green at acefb690 (9f17e1e4 adds only rustfmt): fmt and clippy clean, hub_integration 449/449 twice, lib 902/902, cli_ergonomics, wait_peer_retry, routes_rpc, cmux-tui remote_cli_docs (https://github.com/manaflow-ai/cmux/actions/runs/40281873972).
Review
Codex gpt-6-astra (xhigh), reading the bare mirror only, four rounds: BLOCK at af47d2c (a row read before its retry was set; peers), BLOCK at d0b375f (a retry starting between the mark read and its check; a peer row left deciding), BLOCK at eff6645 (Stop taking a waiting mark with retries off), each fixed with a red test first. SHIP at 9f17e1e46b493094fe91186ab23b0499061aa17b ("No blocking findings. stop_auto_resume now captures Ending and Waiting marks under the lock and emits exactly one settlement record, while Firing/running paths retain their existing handling.").
No capture: CLI text and a row field only. The before and after output is above.
Stop conditions: no new timer, observer or loop. The wait's hold ends at its existing 1 s re-read or record wake, after at most 5 s per chat, or at the wait's own timeout. settled_retry_row reads the mark at most 3 times per row.
Agent session: big-red
🤖 Generated with Claude Code