acpmux: move a chat to another daemon with its history, queue and draft (acpmux.transfer.v1, cx-xtg0.6)

Merged
TE
25d ago
+973-91

Summary

An agent chat could not move between daemons. _acpmux/export / import only wrote and read bundle directories on one disk. Import never made the queue live, and nothing retired the source, so a queued prompt could run on both daemons or on neither.

This adds acpmux.transfer.v1: any client moves a chat from one acpmux daemon to another, carrying its history, queue (held state included), draft and agent store. The other daemon can be a sibling build's (tagged builds each run their own daemon) or a remote machine's. The client pumps the bundle, so neither daemon needs to reach the other. This is the base for the Swift session.transfer chat slice (cx-xtg0.7) and for Move to Cloud.

Source

  • _acpmux/transfer_begin {sessionId, interrupt?}:
    • Freezes the chat: no prompt, steer, queue change, draft or policy change, and nothing queued starts.
    • A running turn refuses the move (transfer.busy) unless interrupt stops it.
    • Answers the manifest: {schema, transferId, sessionId, files:[{path,size}], session}.
  • _acpmux/transfer_read {transferId, path, offset, limit≤4 MiB}: returns {data (base64), next, eof}.
  • _acpmux/transfer_commit {sessionId, transferId, to?}: withdraws the queue (it runs on the target), clears the draft and closes the chat.
  • _acpmux/transfer_abort {sessionId, transferId}: gives the chat back as it was.

Target

  • _acpmux/transfer_receive {manifest}: returns {receiveId}. Refused with transfer.exists (that chat is here) or transfer.harness (agent not configured here). A chat that moved away from here can come back.
  • _acpmux/transfer_write {receiveId, path, offset, data}: pages, in order.
  • _acpmux/transfer_finish {receiveId, cwd?, name?, from?}:
    • imports the chat, in cwd if its folder has another path here;
    • its queue is back before any client sees it, and runs at once;
    • logs transferred_in {transferId} and answers the session summary.

Exactly once: the move is in the log (transfer_out, transfer_aborted, transferred). A daemon that restarts mid-move keeps the chat frozen until commit or abort. A client aborts only when the target lacks the move. If a finish answer is lost, the client commits only when the target's log has transferred_in with this transfer id.

Reference client: acpmux transfer SESSION --to SOCKET [--cwd] [--name] [--interrupt].

Limits:

  • Local socket only. A bundle holds the whole chat, and its queue runs on the target under the rules it was sent with. A remote daemon needs its own guarded receive; that is follow-up work with Move to Cloud, which the continuity lane is building on this.
  • Size: at most 4 GiB in 10,000 files per move. Moves left a day without a finish, commit or abort are swept.
  • Summary: the session summary gains moving, and initialize features gain sessionTransfer.

closes cx-xtg0.6

Testing

tests/hub_integration/transfer.rs runs two in-process daemons with the fake agent; the test is the client that moves the chat.

  • The moved chat keeps its history, its queue and its draft. Each queued prompt runs once, on the target. The source runs nothing after begin and is closed after commit.
  • A chat being moved refuses a busy move without interrupt, and refuses a prompt, a queue edit and a draft change. An aborted move gives it back: its queue runs and it takes prompts again.
CommitCommandResult
Red, tests onlycmux-tui-rust-check.sh test transfer:: (fleet)2 fail (no transfer methods)
Greencmux-tui-rust-check.sh all transfer (fleet: fmt, clippy, tests)both pass; fmt and clippy clean. The only failure is cmux-remote interactive_echo_stays_responsive_during_bidirectional_bulk_transfer, which the filter also matches; it is unrelated and failed the same way on another worker.
Regressiontest draft (fleet)13 pass
Regressiontest queue (fleet)17 acpmux queue tests pass, including the durable restart ones (aws worker; on the cmux11s-13s minis those durable tests fail at daemon start, also without this change). Only failure: chatmux-relay writer_queue_preserves_complete_fifo_lines, unrelated and seen on base runs before

Two review subagents read this (correctness and security); their findings are fixed above. Notable ones:

  • a prompt waiting on the turn lock as the move began;
  • peers bypassing remote_guard, now local socket only;
  • edits and steers mid-move;
  • a lost finish answer leading to an abort and a double run;
  • disk caps;
  • return moves after a restart.

God-file check: no file over budget grew. requests.rs has two one-line changes; hub_integration.rs is unchanged in length.

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.


Summary by cubic

Adds acpmux.transfer.v1, letting any client move a chat from one acpmux daemon to another—carrying its history, queue (including held prompts), draft, and agent store. The client pumps the bundle between the two daemons, so neither needs to reach the other.

Closes cx-xtg0.6.

Key points

  • transfer_begin freezes the chat; a running turn refuses the move unless interrupt is set.
  • transfer_read/transfer_write page the bundle; transfer_finish imports it with its queue live.
  • transfer_commit withdraws the queue and closes the source; transfer_abort gives the chat back.
  • The move is logged, so a daemon restart keeps the chat frozen until commit or abort; each queued prompt runs exactly once.
  • A move is capped at 4 GiB in 10,000 files; stale moves are swept after a day.
  • Local socket only; acpmux transfer SESSION --to SOCKET is the reference client.