cmux-tui: unsend to a deleted session, pump mark failure, unsend spec (agent messaging 4c)

Merged
TE
19d ago
+204-20

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_WITHDRAW is in SESSION_SCOPED_EXCLUDED in peer_forward.rs). It does forward a peer session's _acpmux/info and prompts, so a session on a peer daemon also reads no session matches on 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 becomes unsent and leaves the pump's queue, so the retries stop.
    • A delivered delivery that gets it still reads too_late.
  • acpmux keeps a killed or closed session in hub.sessions, and that answers withdraw.not_found. So no session matches means a deleted (purged) or never-known session.

Also:

  • Pump mark failure. When recording a hand-over that went out fails, the pump returns Waits instead of Done, 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: making agent.message.mark fail inside a running daemon would need a fault hook the daemon doesn't have.
  • Blank acpmux reason. A whitespace-only data.reason now trims to empty and reads withdraw.refused.
  • The pump uses unsend's clip instead of its duplicate truncate.
  • Spec. The agent.message.unsend section of cmux-tui/spec/resource-api-v2.md now says that a queued delivery a hand-over went out for is failed / acpmux_unavailable when acpmux holds no such prompt, and unsent when 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, because expected_revision is 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 got failed where it expected unsent, the other " " where it expected withdraw.refused.
    • Green on fix commit 0f90612d40dc (step 82af6531301ebecc7bd08697): 21 passed.
  • 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 got unsent where it expected failed.
    • Green on fix commit d7ae6b9542f9 (step f777e266e73621095d0c9ec2): 22 passed, 0 failed.
  • Two infrastructure failures were rerun. Step 1aa3e0eebd107d5466967295 couldn't compile because the worker ran out of disk. In step 29fc81856ff004dba3d1b09d, workspace create timed out on a loaded worker before any assertion ran.
  • cmux-tui-rust-check.sh all on 0f90612d40dc (step 821c260f93244b5037638c1d):
    • cargo fmt --all --check and cargo clippy --workspace --all-targets -- -D warnings passed.
    • 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_pump tests passed in that run.
    • Clippy hasn't run on d7ae6b9542f9. It changes only agent_message_acpmux.rs plus comments and the spec.
  • Locally: rustfmt --edition 2024 on the touched files, check-resource-api-boundary.py and crash_ratchet.py all pass.

Changelog

  • Fixed: agent message unsend takes back a queued message to an acpmux session that was deleted, instead of failing as retryable forever.

šŸ¤– Generated with Claude Code