acpmux: agents offer a parallel chat (session chips, protocol)
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_sessiontool. A new MCP stdio server,cmux-sessions(acpmux offer-mcp), registered per session byagent_tools.rswhenACPMUX_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 recordssession_offerwith anofferIdand 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).acceptednames 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 itsparentIdwith noforkSeq, so it never shows up as a fork version. The answer is recorded assession_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
parentIdreaches 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), focusedoffer:session_offers::an_offered_chat_is_a_chip_only_the_person_starts_and_it_reports_its_parentandan_offer_needs_a_title_and_a_prompt_and_a_chat_holds_a_fewboth FAILED (no such method). - Green,
78db715d3b96, full mode:- both offer tests pass, as do
local_sessions_get_the_browser_and_cua_tools_and_skills(thecmux-sessionsserver with its session env) andsecrets_the_agent_gets_a_one_use_project_ref_never_the_value; - lint (linux) and macOS lint and tests are green.
- both offer tests pass, as do
- Not mine; they fail on this head from base:
cmux-tui-coreuntrusted_mint_tests(landed red on purpose in a65f632635bc);- Windows
cmux-tui-platformcompile errors, after the crate move in f8e3500ec0df; - valgrind not finding a
cmux_tuistartup 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
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 tohub/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
parentIdreaches 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.