acpmux MCP tools: agents message lanes with acpmux_send, drawn as a message card
Agents message other lanes by piping text into a shell, printf ⦠| timeout 60 acpmux send --no-wait NAME -. The chat then draws that as a command block. This PR gives them real tools for it.
- New tools. Every local agent session gets an
acpmuxMCP server (acpmux acpmux-mcp,acpmux_mcp.rs). Behind thecmuxserver it offers:acpmux_send {to, text, steer?}messages a session by name, id orhost/name. Likeacpmux send --no-wait, it hands the message to this daemon's outbox (_acpmux/send_queue), which delivers it as this session and retries a slow target or a peer that is down. It returns thepromptIdand delivery id once the delivery settles (steered in, queued, or a new turn), or after 5 s with the message on its way. It never waits for the reply.steerdefaults to true.acpmux_ls {status?},acpmux_last {name, count?}andacpmux_info {name}read the sessions.- Each tool description says it messages or reads another lane or session, and to use it instead of
acpmux sendin a shell.
- How the pane draws it. The agent pane draws an
acpmux_sendcall as the collapsed "Sent to NAME" message row (AgentMessageRow), never as an MCP row. This works however the harness names the call:mcp__cmux__acpmux_send, Codex's{server: "cmux", tool: "acpmux_send", arguments},mcp__acpmux__send, or the bare or dotted name. (A shellacpmux sendis drawn by base'sacpmuxSends.ts, #25045.) - Gallery. New
lanesvariant onagent-pane.agent-message-row. - Docs. The README documents the tools.
Risk: risk:normal. This adds no new socket method and no policy change. The authorization analysis follows.
Authorization
- Same power as the CLI. Each tool makes the same request (
session/promptwith_acpmux/steer,_acpmux/sessions,_acpmux/info, the event read behindlast) that the agent's ownacpmuxCLI already makes. It goes over this daemon's unix socket with the session's caller token (ACPMUX_SESSION_ID/_TOKEN/_TOKEN_SOCKETfromhub::person::caller_env). The subsession and forum servers get their token the same way. - Stamped as the agent's message. The token makes the daemon stamp a message as
session_messagefrom this session (prompt_provenance.rs). It can never pass as the person's words. - Remote and Web sessions get nothing new. A remote-origin session gets none of cmux's agent tools (
agent_tools::left_out), so this server is left out as well. A Web client can't reach this stdio server at all. - One difference: sandboxed agents. An agent whose shell sandbox can't reach the daemon socket can now message other sessions through MCP. Its harness asks permission for an MCP tool the same way it does for a shell command. That is the point of the change.
Changelog
Added: agents message other lanes and sessions with the acpmux_send tool, and the chat shows each message as a "Sent to" line instead of a shell command.
Tests
- Rust:
cargo test -p acpmux --lib acpmux_mcp(4 tests, against a fake agent over a real socket): send delivers and returns its promptId; ls, last and info read the sessions; an unknown session or a missing argument is an error the agent reads; the descriptions are checked.--test agent_tools: every local session gets theacpmuxupstream with the caller token.chief_builtin_presetsandtool_inventorylist the new server.
- Web:
agentMessages.test.ts: every MCP naming, Codex's nested call, the shell forms, and the negatives (another server's send, no recipient, a waiting send).test/transcript-row-message.test.tsx: anacpmux_sendcall renders as the collapsed.cv-agentmsg.
Red/green, hosted ./scripts/verify-cmux-tui-hosted.sh --filter local_sessions_get_the_browser_and_cua_tools:
- Red on the test commit ed72ddee2d17ca33333be84194b4990f34df2bd4 (scratch branch): RED_RESULT
- Green on the head 7c28ce950be6c8c4f45da14c1e7e890059593644: GREEN_RESULT
Locally on huge-blue, the 3 new webview assertions failed on the test commit and pass on the head.
Observed / Not run
- Observed on huge-blue.
- Rust:
cargo test -p acpmux --lib acpmux_mcpand theagent_tools,agent_displays,chief_builtin_presets,tool_inventoryandforumintegration tests pass withumask 022. With this host's default umask,chief_builtin_presetsstops on "/tmp/acb-tools-* is writable by another user". That is environmental, and it happens on base too. - Checks:
cargo clippy -p acpmux --all-targets -D warningsandprepush-gate.pyare clean. - Web: the two webview test files pass.
vp checkfinds no format or lint errors in the touched files.
- Rust:
- Not run locally.
- The full
cargo test -p acpmuxfilled the disk, so it was stopped; PR CI runs it. bun run check's generated-copy checks andgallery-coverage.test.tsneed the Swift sources, which this sparse worktree doesn't have; CI runs them.scripts/gallery-compare.shcan't reach the gallery host (cmux-lawrence) from huge-blue. Someone off huge-blue can run it to see the newlanesvariant.- No app build: there is no Swift change.
- The full
- Known limits.
- A Codex session in code mode calls tools from a
cmux_execscript, so its sends there stay a script row. - When the async
send --no-waitdaemon request lands (cmux/acpmux-send-async, not pushed yet),acpmux_sendshould move onto it. For now it uses the samequeue_promptthatsend --no-waituses today.
- A Codex session in code mode calls tools from a
Adopted and rebased (2026-10-25)
The stale-pr-reconciler, word for word: "These PRs in your area have no live owner (Leo, 2026-10-25: "these should never have been this stale for this long"). Adopt each: pr-owner claim N (in that repo), rebase onto its base, an independent review at the new head, then land it".
- Not superseded.
cmux-subsessions.send_messagereaches only one's own sub-sessions or parent.acpmux_sendreaches any session by name orhost/name. - Re-applied onto feat-cmux-next (it was 4,101 commits behind). Base's #25045 (
acpmuxSends.ts) now draws a shellacpmux send, so this PR's shell parser (withoutTimeoutandacpmuxMessage) and its tests were dropped. Only the MCP call's row is left. - Codex review (gpt-6-astra xhigh), BLOCK: 1.
sendcalledqueue_promptdirectly and bypassed base's durable outbox (_acpmux/send_queue), so a peer that was down or a slow target got no retry and no restart survival. 2. The 10-minute wrapper dropped a JoinHandle and left the nestedsession/promptattached. Fixed:sendnow submits_acpmux/send_queue(unix socket only, delivered as this session with its caller token, retried for about 34 minutes) and reads_acpmux/send_statusfor at most 5 s. Nothing in the MCP server outlives the call. Test first:send_goes_through_the_outbox_and_names_its_delivery(red: "no delivery id"). - Rebased again past
CollectOpts.steer. The gallery and tests now saybuild-box, not a fleet host (prepush gate). The re-review said SHIP at ba3998d5b32 ("Outbox delivery remains unix-local, caller-stamped, restart/retry durable, and attachment-checked").
Tests on the rebased branch (fast-checks on Blacksmith)
- Red at the test commits:
agent_tools(noacpmuxupstream), the two new webview tests, then the outbox test. - Green at ba3998d5b32:
--lib acpmux_mcp5 pass;--test agent_tools,chief_builtin_presetsandtool_inventorypass; clippy-D warnings(acpmux, cmux-tui) and fmt are clean. WebviewsagentMessages.test.tsandtranscript-row-message.test.tsx: 22 pass, 1 fail ("opening it grows the message in place"), which fails the same way on base 5e6cb133c60. - PR CI reds that aren't from this PR:
Script tests(base), Swift test reds in tooltip and split-focus code (no Swift change here), generated files (base'sopen-remote), gallery health, and GitHub API rate limits in the status, publish and same-tree jobs.
No timers or observers are left: the old SEND_MAX task is gone. No capture: the new gallery variant lanes shows the row.
Agent session: big-red
š¤ Generated with Claude Code