acpmux: cross-harness handoff RPCs and the continue verb

Merged
TE
1mo ago
+2111-8

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):

MethodWhat it does
_acpmux/handoff_prepareBuilds 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_getReads the persisted record by handoffId or sessionId. An ordinary session returns null.
_acpmux/handoff_draftSaves the user's edits under a revision.
_acpmux/handoff_startSends the capsule to the target exactly once. Needs an attested checkpoint ref.
_acpmux/handoff_discardCloses the unprompted target, with no confirmation. This is the undo. The source session stays as it was.
  • Advertising: initialize lists the methods in _meta.acpmux.operations, and _meta.acpmux.handoff.maxCapsuleBytes is 65536.
  • Enforcement: every session summary carries enforcement (policy, label: "native_policy", isolation: "unverified", detail).
  • CLI: the new verb continue covers prepare, show, draft, start and discard. Since cmux acp runs the acpmux CLI in-process, it works as both acpmux continue and cmux acp continue.
  • Schema: docs/acpmux-schema.json documents the methods, types and reasons.

Delivery and recovery

  • Exactly once: start records starting with its promptId before sending. The promptId defaults to the handoffId. The send goes through Hub::prompt_with with {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 answers already_started with 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 handoffKey returns 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 is key_conflict.
  • Draft replay: a replayed draftKey answers 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=1 drops 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.policy is always the acpmux permission policy id, so source and target can be compared. The harness's own mode is reported in detail. 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 --full at 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-tui CLI 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


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 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

  • prepare builds a capsule from the source's recent context (up to 64 KiB, oldest cut first) and creates an unprompted target session; get reads the record, draft saves edits under a revision, start delivers the capsule once, and discard closes the unprompted target and keeps the source.
  • acpmux continue (also cmux acp continue) drives prepare, show, draft, start and discard.

Delivery and recovery

  • start records 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 to draft.
  • prepare is idempotent per handoffKey; a replayed draftKey answers with its own write, locks are per key, and records persist atomically under handoffs/.
  • initialize now advertises the methods and maxCapsuleBytes; session summaries carry enforcement (policy is the acpmux permission policy so source and target compare, harness mode in detail); the schema documents the shapes.
  • 14 new integration tests cover prepare, get, draft, start, discard, retry and replay paths.