acpmux: cross-harness handoff RPCs and the continue verb
Part of the cmux-next parity work listed in #16174 (comment)
Problem
Moving a conversation from Claude Code to Codex, or the other way, meant starting over. The new agent didn't know what the first one had done, and nothing kept a retried start from sending the first message twice. #16905 (merged) built the review UI for this handoff. It stays hidden until the daemon advertises these operations.
What changes
acpmux gets five handoff methods, following the contract agreed with the frontend owner (v1, plus the v1.1 clarifications below):
| Method | What it does |
|---|---|
_acpmux/handoff_prepare | Builds a capsule from the source's recent visible context, up to 64 KiB of UTF-8, cutting the oldest context first. Creates the target session in the source's cwd with the same or a narrower acpmux permission policy. Sends no prompt. |
_acpmux/handoff_get | Reads the persisted record by handoffId or sessionId. An ordinary session returns null. |
_acpmux/handoff_draft | Saves the user's edits under a revision. |
_acpmux/handoff_start | Sends the capsule to the target exactly once. Needs an attested checkpoint ref. |
_acpmux/handoff_discard | Closes the unprompted target, with no confirmation. This is the undo. The source session stays as it was. |
- Advertising:
initializelists the methods in_meta.acpmux.operations, and_meta.acpmux.handoff.maxCapsuleBytesis 65536. - Enforcement: every session summary carries
enforcement(policy,label: "native_policy",isolation: "unverified",detail). - CLI: the new verb
continuecovers prepare, show, draft, start and discard. Sincecmux acpruns the acpmux CLI in-process, it works as bothacpmux continueandcmux acp continue. - Schema:
docs/acpmux-schema.jsondocuments the methods, types and reasons.
Delivery and recovery
- Exactly once: start records
startingwith its promptId before sending. The promptId defaults to the handoffId. The send goes throughHub::prompt_withwith{prompt_id, resend: true}, so a retry with the same promptId is deduplicated, both in memory and against the target's session log after a daemon restart. A retry answersalready_startedwith the first turn. - Failed start: a start that fails before the target records the prompt returns to
draft, keeping its promptId, so Discard still works. - Idempotent prepare: a repeated
handoffKeyreturns the same record. The target is tagged with the key, so a prepare retried after a crash adopts that target instead of creating a second one. The same key with a different harness iskey_conflict. - Draft replay: a replayed
draftKeyanswers with that write's own snapshot. A stale revision with equal content reconciles, and one with other content is refused with the current record. - Locking: locks are per key, so one slow harness launch doesn't hold up other handoffs.
- Storage: records are written atomically under
handoffs/(directory 0700, files 0600). File names come only from daemon-generated ids. - Test hook:
ACPMUX_TEST_DROP_HANDOFF_START_REPLY=1drops the first start's reply after the send, for the joint dogfood. It isn't used by tests, because environment variables are process-global; the restart test covers the same recovery path.
v1.1 clarifications (agreed with the frontend owner)
enforcement.policyis always the acpmux permission policy id, so source and target can be compared. The harness's own mode is reported indetail. Same-or-narrower is guaranteed on the acpmux policy, not on harness modes.- New reason
key_conflict.
Verification
- Red first: b4dc12f67da adds the tests. Its hosted run (37039203341) failed on an assertion: the schema test, because the methods weren't listed yet. Cargo stopped at that first failing binary, so the handoff integration tests didn't run on the red commit. 785670e7405 adds the tests for the review findings.
- Hosted
--fullat 7d550047e23 (37041153350):- All 14 handoff integration tests pass on Linux and macOS, as do the acpmux unit, daemon-lifecycle, hub and peer tests.
- Three jobs failed outside acpmux: a
cmux-tuiCLI session-shutdown test (Linux), a socket-fallback test in the Rust bindings (macOS), and the headless Chromium smoke test (macOS). The base's Linux test job also fails, on a different test. Those jobs are rerunning.
- Not verified yet: a live run with a real Claude Code and Codex. That's the joint dirty-repo dogfood with the frontend owner, once there's a combined build.
🤖 Generated with Claude Code
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.
Summary by cubic
Adds cross-harness handoff to acpmux: a continue CLI verb and five _acpmux/handoff_* RPCs that hand a session's recent transcript to a new target session on another harness. Previously, moving a conversation between harnesses meant starting over; now the target receives a reviewed first message, delivered exactly once.
RPCs and CLI
preparebuilds a capsule from the source's recent context (up to 64 KiB, oldest cut first) and creates an unprompted target session;getreads the record,draftsaves edits under a revision,startdelivers the capsule once, anddiscardcloses the unprompted target and keeps the source.acpmux continue(alsocmux acp continue) drives prepare, show, draft, start and discard.
Delivery and recovery
startrecords its promptId before sending; retries with the same promptId are deduplicated in memory and against the target log after a daemon restart, and a start that fails returns todraft.prepareis idempotent perhandoffKey; a replayeddraftKeyanswers with its own write, locks are per key, and records persist atomically underhandoffs/.initializenow advertises the methods andmaxCapsuleBytes; session summaries carryenforcement(policy is the acpmux permission policy so source and target compare, harness mode indetail); the schema documents the shapes.- 14 new integration tests cover prepare, get, draft, start, discard, retry and replay paths.