acpmux: Take over holds the managing agent's messages until Hand back

Merged
TE
4d ago
+472-30

Leo asked for a ChatGPT-style "X is managing this chat [Take over]" on chats that Chief or another agent runs. #30765 adds the banner; this PR makes Take over actually stop the manager, in acpmux. This is item 3 of the agent group-chats plan.

What changes

  • New method _acpmux/agent_hold {sessionId, held} (capability agent-hold-v1).

    • held: true (Take over) tags the chat acpmux.agentHold. From then on, the pair guard (hub/agent_pairs.rs) holds every message from the chat's manager as a pause with reason taken_over. Held messages are kept across restarts, as the guard already does.
    • held: false (Hand back) removes the tag and delivers the held messages in order.
  • Manager means:

    • the session that started the chat (startedBy, spawnedBy),
    • its delegator,
    • or, for a chat a Chief started (mux.parent = optchat-chief[:<home>]), any turn of that Chief (cmux.chief = <home>).

    Other agents' messages pass as before.

  • The person's own prompts into a taken-over chat run, and release nothing. agent_pair_resume_inner(by: "person") skips pairs that a hold keeps.

  • Only the person can take over or hand back. The method requires local_person and is refused from web origins (remote_guard). _acpmux/tag refuses the hold tag to anyone but the person (person.rs), so a manager cannot lift its own hold.

  • The hold applies even when the pair guard is switched off.

  • Wired through rpc.rs, protocol/mod.rs and the spec json, capabilities.rs, requests.rs, agent_messages.rs and peer_forward.rs (daemon-own).

  • Pane policy:

    • policy.json (gesture always, params sessionId, held) and its test cases.
    • The Swift mirror (AcpmuxPaneMethods*.swift).
    • The webview's generated methods.ts.
    • direct.ts agentHold(held).
  • Wiring the banner's Take over to agentHold (plus a Hand back line) is a follow-up once #30765 lands.

Tests

  • tests/hub_integration/agent_pairs.rs:
    • a_taken_over_chat_holds_its_managers_messages_until_handed_back:
      • An agent cannot take over.
      • The Chief's messages are held with trip taken_over.
      • Another agent passes.
      • The person's prompt runs and releases nothing.
      • The manager can't lift the tag.
      • Hand back delivers 3 messages in order.
      • Later messages pass.
    • another_chiefs_turn_is_not_the_manager.
  • Fleet (cmux-ci test):
    • cargo test -p acpmux --test hub_integration agent_pairs: 26 passed.
    • cargo test -p acpmux --lib: 2572 passed.
    • cargo test -p cmux-agent-pane-policy: all pass, including the_allowlist_is_what_the_page_sends.
    • cargo clippy -p acpmux --all-targets -D warnings: clean.
    • cargo fmt: clean.
    • Webview bun run check: 0 lint errors, no type errors.

Rebase

This replaces #30833 (Codex SHIP at a7a1c02, receipt https://github.com/manaflow-ai/cmux/pull/30833#issuecomment-6557530873), which conflicted with base. The same diff is re-applied onto feat-cmux-next 64f07ad. The only hand-resolved spots are the agent_hold entries in the pane method lists (policy.json, AcpmuxPaneMethods.swift) and the sessions.json cases. Follow-ups: #30849, #30850, #30852.

Risk

acpmux message delivery (state machine). It changes behaviour only for chats carrying the new tag, which only the person sets. The pair guard's paths for untagged chats are unchanged, apart from the guard-off early return moving below sender resolution.

Agent session: agent session

🤖 Generated with Claude Code