acpmux: session move SESSION... --host PEER|auto (move chats to fleet hosts)
Leo (2026-10-29): "i thought acpmux had a move... or rather i did some stuff in cmux classic to do it tho it was a bit hacky maybe". He wants idle sessions moved off an overloaded interactive Mac to fleet hosts, not just stopped.
What
acpmux session move SESSION... --host PEER|auto [--all-idle] [--interrupt] [--leave-dirty] [--dry-run] moves chats from this daemon to an ssh peer's daemon, with their history, queue and draft. It builds on the existing _acpmux/transfer_* protocol (session transfer, which only reaches a socket on the same machine):
- Refusals (
cli/session_move_plan.rs): a running turn without--interrupt, a chat that is being moved, one that already moved away, a folder that no longer exists here, a folder outside the home (unless--cwd PATHnames the folder on the peer), a detached HEAD, a checkout with noorigin, or uncommitted changes without--leave-dirty. - Target: the peer named, or
auto, which takes the placement's pick (_acpmux/place) among the peers. It never picks this machine or a host the fleet registry holds (placement::held, the one reader ofholdandreservations, from #27731). Chats in one checkout go to one host, and a batch spreads by the chats it has already placed. - Freeze first: the chat is frozen (
transfer_begin) before anything else is done, the checkout is read again, and any later failure aborts the move. - Folder: a checkout's branch is pushed to
origin(never forced) and becomes a fresh worktree of the same branch on the peer (wt newwhere the peer has it, else a freshmktempclone of its own under~/Projects/checkouts), with the subfolder kept (added to a sparse checkout). A plain folder under the home maps to the same path under the peer's home. - Move: the peer daemon's own local socket is forwarded over
ssh -L(ssh_forward.rs), so the peer accepts the move as a local client. The source's watch knows the target as{socket, ssh, host}, so a move whose client goes away is still ended by asking the target through a forward, and records the peer it went to. - One writer: the commit records
SessionMeta.moved_to {host, sessionId, cwd, branch, commit}. A moved chat is restored frozen after a restart (moving), so every prompt, queue, draft and policy change is refused.reopen_to_startrefuses it withtransfer.moved, and a second move is refused. A stopped (closed) chat can now move, because commit, abort and the watch key onmoved_toinstead of onClosed; the mark is saved at commit, so a restart keeps it moved, and a stopped chat whose move only began is never replaced by a same-id receive. A restart in the middle of moving a stopped chat freezes it again from its bundle undertransfers/. - Links follow it: summaries carry
movedTo.resolve(agent links, PR footers) ranks the copy the chat moved to first and adds it when only the old copy matched. The CLI card printsmoved to HOST/ID.
Placement (placement.rs): host rows now carry load (refused only for room, load or swap), which the move's auto choice uses. Holds and reservations come from the base's reader; this PR adds no second one.
Tests
session_move_plan_tests: cwd remap (worktree, including subfolder; plain folder under home), refusals (outside home, detached, dirty, no origin, unsafe branch), repo slugs, turn/moving/moved refusals.session_move_tests: theautochoice (never local, spreads a batch, overloaded fallback, never reserved), the forward argv, checks on the peer's reply, a named peer must be an ssh peer.placement_tests: a reserved host is skipped while the reservation is active, also when every other host is full, and lapses afteruntil; an unreadableuntilholds the host.locatetests: a link to a moved chat leads to the new copy, and the old card says where it went.hub_integration/transfer: a stopped chat moves; the source refuses a prompt and a second move, never reopens, and reportsmovedTo; after a source restart it is still moved; a stopped chat whose move only began refuses a same-id receive.session_move_plan_tests:--cwdnames the folder outright and refuses a relative or..path; the home maps to the peer's home with no trailing slash.
Ran on Linux CI (cmux-ci): acpmux lib tests (session_move, placement, locate, protocol, transfer), hub_integration transfer, transfer_cli, clippy, fmt.
Reviewed by Codex gpt-6-astra (high, fast): two NO-SHIP rounds, every finding fixed here.
No capture: no native or web UI change; the only web change is the regenerated protocol type for the new optional movedTo field.
Risk
Medium. This touches the move protocol's commit and abort conditions (now keyed on moved_to, not Closed) and adds a SessionSummary.movedTo field (optional, skipped when absent). It does not change how a non-stopped chat moves.
š¤ Generated with Claude Code
Agent session: agent session