cmux-tui: unsend to a deleted session, pump mark failure, unsend spec (agent messaging 4c)
Follow-up to agent messaging 4/6 (#20744) and 4b (#21067), bead cx-55d27.1.
A queued message whose hand-over went out unaccepted, to an acpmux session that's since been deleted, could never be unsent. acpmux answers the withdraw with no session matches ⦠(its Hub::resolve error, which carries no typed data.reason). The daemon read that the same as "this session's log holds no such prompt", which 4b turns into a retryable failed / acpmux_unavailable, while the pump kept looking for the session on every backoff pass.
Now that answer is its own case, Withdrawn::NoSession, but only once the daemon has confirmed the session is really gone:
- acpmux answers a withdraw on its own daemon only (
MUX_PROMPT_WITHDRAWis inSESSION_SCOPED_EXCLUDEDinpeer_forward.rs). It does forward a peer session's_acpmux/infoand prompts, so a session on a peer daemon also readsno session matcheson withdraw yet can run the prompt. - So on that answer the daemon asks
_acpmux/info {sessionId}on the same link:- If acpmux still names the session, the answer counts as "no such prompt", and a tried queued delivery stays
failed/acpmux_unavailable. - Only a session acpmux can't name is
NoSession. A queued delivery that gets it becomesunsentand leaves the pump's queue, so the retries stop. - A delivered delivery that gets it still reads
too_late.
- If acpmux still names the session, the answer counts as "no such prompt", and a tried queued delivery stays
- acpmux keeps a killed or closed session in
hub.sessions, and that answerswithdraw.not_found. Sono session matchesmeans a deleted (purged) or never-known session.
Also:
- Pump mark failure. When recording a hand-over that went out fails, the pump returns
Waitsinstead ofDone, so later messages to that recipient don't overtake it in the pass and the backoff timer re-arms. There's no boundary test for this: makingagent.message.markfail inside a running daemon would need a fault hook the daemon doesn't have. - Blank acpmux reason. A whitespace-only
data.reasonnow trims to empty and readswithdraw.refused. - The pump uses unsend's
clipinstead of its duplicatetruncate. - Spec. The
agent.message.unsendsection ofcmux-tui/spec/resource-api-v2.mdnow says that a queued delivery a hand-over went out for isfailed/acpmux_unavailablewhen acpmux holds no such prompt, andunsentwhen acpmux no longer knows the session (deleted, and no peer runs it). It also says that acpmux's answers commit even when other state changed while it answered, becauseexpected_revisionis checked before acpmux is asked. The JSON catalog is unchanged.
The fake acpmux in the test harness now answers a withdraw for a session it doesn't run, or that a peer runs, the way acpmux does (no session matches "<id>", code -32002, no data). It records which session each _acpmux/info names and gains delete_session and place_peer_session.
Known gap, not fixed here. When a session transfers back in, acpmux purges the old copy (transfer_in.rs:244) before the import is inserted (transfer.rs:191). An unsend in that window gets NoSession and stores unsent, and the restored queue may then run the prompt. The real fix belongs in acpmux: reserve the id across the replace, or answer transfer.* meanwhile.
Verification
Fleet scripts/ci/cmux-tui-rust-check.sh test agent_message_, isolated class:
- Deleted session and blank reason:
- Red on test commit 67a8fed72d6e (step
57f3d15c9484f04c34c676fa): 19 passed, 2 failed on the asserted behavior. One gotfailedwhere it expectedunsent, the other" "where it expectedwithdraw.refused. - Green on fix commit 0f90612d40dc (step
82af6531301ebecc7bd08697): 21 passed.
- Red on test commit 67a8fed72d6e (step
- Peer session (
an_unsettled_delivery_to_a_peer_session_is_never_called_unsent):- Red on test commit ff3ea4330f2b (step
19d11e20133d064f351397a2): 21 passed, 1 failed. It gotunsentwhere it expectedfailed. - Green on fix commit d7ae6b9542f9 (step
f777e266e73621095d0c9ec2): 22 passed, 0 failed.
- Red on test commit ff3ea4330f2b (step
- Two infrastructure failures were rerun. Step
1aa3e0eebd107d5466967295couldn't compile because the worker ran out of disk. In step29fc81856ff004dba3d1b09d,workspace createtimed out on a loaded worker before any assertion ran. cmux-tui-rust-check.sh allon 0f90612d40dc (step821c260f93244b5037638c1d):cargo fmt --all --checkandcargo clippy --workspace --all-targets -- -D warningspassed.- The workspace tests went red on 16 tests outside this PR's paths: the acpmux protocol registry/IR, hermetic guard and remote_sandbox tests, agent-pane-policy swift/typescript, cmux-git hostile_repository, pty, action_session_arg_app, cli apps_store/kitty, detached_owner and terminal_host_recovery. All
agent_message_pumptests passed in that run. - Clippy hasn't run on d7ae6b9542f9. It changes only
agent_message_acpmux.rsplus comments and the spec.
- Locally:
rustfmt --edition 2024on the touched files,check-resource-api-boundary.pyandcrash_ratchet.pyall pass.
Changelog
- Fixed:
agent message unsendtakes back a queued message to an acpmux session that was deleted, instead of failing as retryable forever.
š¤ Generated with Claude Code