acpmux: agents offer a parallel chat (session chips, protocol)

Merged
TE
23d ago
+678-135

Summary

Piece 1 of session chips (cx-807da, cmuxterm-hq#1957). This is the acpmux protocol by which an agent offers the person a parallel chat. The chip UI and the accept flow come in later PRs.

  • offer_session tool. A new MCP stdio server, cmux-sessions (acpmux offer-mcp), registered per session by agent_tools.rs when ACPMUX_OFFER_TOOLS=1. The app sets that switch with the chip flag in a later PR. Claude gets the tool through --mcp-config, and Codex through its MCP config (the same path the secret tools use), so neither needs a harness-specific path. The tool returns at once, and the agent's turn keeps going.
  • _acpmux/offer_session {sessionId, title, prompt, worktree?, cwd?, harness?, model?}. Local unix socket only. It checks the fields: title 1โ€“80 characters, prompt 1โ€“20 000, cwd an existing absolute folder, defaulting to the chat's. It records session_offer with an offerId and leaves the chat's status alone. A chat holds at most 8 open offers, and a chat a remote device started offers none, matching the secret rule.
  • _acpmux/offer_respond {sessionId, offerId, outcome: accepted|dismissed, childSessionId?}. Only the person's connection may call it (is_person, the same check secrets use). accepted names the chat the pane started for the offer. It must be new since the offer and have no parent, and it takes the offering chat as its parentId with no forkSeq, so it never shows up as a fork version. The answer is recorded as session_offer_decision. An offer takes one answer.
  • History. Both records are transcript mux kinds, so a reloaded chat shows its chips and their outcomes. The records are the state: the daemon reads a chat's open offers from them once, then keeps them in memory.
  • Refactor. The secret tools' MCP stdio loop is now secret_mcp::serve, shared by both servers. Secrets behave the same; their test passes.

Known limits, for follow-ups:

  • Offers on chats a peer daemon hosts are refused there (the forwarded call is not local or not the person).
  • The child's new parentId reaches session lists on its next change.
  • The first offer call in a chat reads its record log once.

Testing

Red then green, hosted CI:

  • Red, da87865e5851 (tests only), focused offer: session_offers::an_offered_chat_is_a_chip_only_the_person_starts_and_it_reports_its_parent and an_offer_needs_a_title_and_a_prompt_and_a_chat_holds_a_few both FAILED (no such method).
  • Green, 78db715d3b96, full mode:
    • both offer tests pass, as do local_sessions_get_the_browser_and_cua_tools_and_skills (the cmux-sessions server with its session env) and secrets_the_agent_gets_a_one_use_project_ref_never_the_value;
    • lint (linux) and macOS lint and tests are green.
  • Not mine; they fail on this head from base:
    • cmux-tui-core untrusted_mint_tests (landed red on purpose in a65f632635bc);
    • Windows cmux-tui-platform compile errors, after the crate move in f8e3500ec0df;
    • valgrind not finding a cmux_tui startup test.

Reviewed by a review subagent. Its findings are fixed: a 'static bound on the spawned tool future, a wrong status assertion in the test, fmt module order, refusing remote-origin chats, and accepting only a chat started after the offer, checked and set under the child's lock.

Changelog

none

๐Ÿค– Generated with Claude Code


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


Summary by cubic

Agents can now offer the person a parallel chat through a new protocol; the chip UI and accept flow come in later PRs.

The offer_session MCP tool runs in a new cmux-sessions stdio server (acpmux offer-mcp) registered per session when ACPMUX_OFFER_TOOLS=1. It calls _acpmux/offer_session over the local unix socket with title, prompt, cwd, worktree, harness, and model, records session_offer, and returns at once. Answers go through _acpmux/offer_respond, which only the person's connection may call: accepted names a new session that takes the offering chat as its parentId with no forkSeq, and dismissed leaves nothing behind. Both records are transcript mux kinds, so reloaded chats keep their chips and outcomes.

  • Title (1โ€“80 characters), prompt (1โ€“20 000), and cwd (existing absolute folder) are validated; a chat holds at most 8 open offers, and a chat a remote device started offers none.
  • The secret tools' MCP stdio loop is now shared secret_mcp::serve, used by both servers; orchestrator tag handling moved to hub/tags.rs, and the tags/rules/unread hub test to its own module to stay under the god-file line caps.

Known limits, for follow-ups:

  • Offers on chats a peer daemon hosts are refused there: only the app of the daemon that hosts a chat answers its offers.
  • The child's new parentId reaches session lists on its next change.
  • The first offer call in a chat streams its records once and keeps open offers in memory; a chat moved away and back drops the cache and re-reads on the next use.