acpmux MCP send: a batch, a file or a tag in one call, one result row per recipient

Merged
TE
5d ago
+1086-91

Summary

Leo (2026-10-30): "these should be properly rendered too/acpmux batch send tools given to you rendered consistently with other stuff".

Coordinators batch-send today through a shell function (s(){ acpmux send --no-wait --from X "$1" "$(cat "$2")"; }), which the transcript draws as raw shell. The acpmux MCP send tool took one message with inline text only, so there was no tool call for the transcript to draw as message cards. This is the acpmux half; the transcript lane is drawing the cards.

send now also takes:

  • messages: [{to|tag, text|file, steer?, class?, pr?, thread?, supersede?}]: up to 32, sent in order. The top-level fields are each item's defaults, so {text, messages: [{to: a}, {to: b}]} and {messages: [{to: a, text}, {to: b, file}]} both work.
  • file in place of text: a regular UTF-8 file the caller owns (euid), at most 256 KiB, read non-blocking, so a FIFO is refused at once. 256 KiB is about 64k tokens, a third of a 200k context and more than any brief. A larger or binary file is what acpmux send --attach is for, and the error says so. Another user's readable file is refused, because a message travels to other sessions and peers.
  • tag in place of to: every live session carrying it, from fanout::tag_targets (the CLI's --tag), never the sender, at most 32 per tag. A tag that matches more is that item's error and sends nothing to those sessions. One call reaches at most 64 sessions in all; past that, nothing is sent.
  • supersede, as the CLI's --supersede.

With messages or tag, the result is results: [{index, to, target, tag, ok, promptId, deliveryId, status, sessionId, accepted, delivery, error?, replaced?}] plus sent and failed. Each recipient gets its own row, and a bad one never stops the others. isError is true only when nothing was sent.

Order: the outbox runs each delivery on its own and retries it, so two messages queued to one session at once could arrive swapped, and a supersede correction could be replaced by the order it corrects. A batch therefore sends to each session in turn: the next message is queued only once the previous one settled (taken or refused), and different sessions run side by side within the same 5 s STATUS_WAIT. A message whose predecessor is still on its way at the deadline is not sent, and its row says why. "Same session" is decided by union-find over each target's aliases (its key, its name, and for a named target its id, resolved within 2 s). Names that don't resolve all share one chain, and an unresolved name on a host (a peer that is down) also joins every target on that host.

The call without messages or tag keeps its first shape: the same result fields, text and errors. required is gone from the schema because a batch names recipients and texts per item; a call that still lacks them gets "to and text are required" as before. The tool description now says to batch with messages instead of a shell loop. The CLI's send --help, the README and docs/cli-sessions.md point at the tool.

The code moves to acpmux_mcp_send.rs, so acpmux_mcp.rs gets smaller.

Rendering contract: the input and result shape above went to the transcript lane before merge; their cards PR links here when it opens.

Testing

Tests added in acpmux_mcp_tests.rs, against a real daemon with the fake agent:

  • a_batch_sends_every_good_recipient_and_names_the_bad_one: 3 items, the middle one to an unknown session, gives rows ok/false/ok; both good sessions receive their text; the call returns in under 2Ɨ STATUS_WAIT; a batch where nothing goes is isError.
  • a_message_can_be_a_file_of_yours: file delivered with the first shape's result; over 256 KiB, binary, empty, directory, missing, a FIFO and another user's file (/etc/passwd) are each refused with their own error.
  • supersede_replaces_your_queued_message: a second message under the key replaces the queued first (replaced: 1, queue_removed reason superseded).
  • a_tag_expands_to_every_live_session_carrying_it: area=acpmux reaches exactly the two tagged sessions, not the area=cloud one; an unmatched tag is an error row and the item after it still goes.
  • top_level_fields_are_each_items_defaults, and the_send_schema_keeps_its_first_shape (a snapshot of the six original properties and the item schema).
  • a_message_waits_for_the_one_before_it_to_the_same_session (a first delivery that settles late: the second isn't queued until it has; one still on its way stops the rest of its chain), two_messages_to_one_session_arrive_in_order, messages_to_one_session_form_one_chain, one_session_by_name_and_by_id_is_one_chain, an_unresolved_peer_session_stays_in_one_chain_with_its_tag_target.
  • The supersede test holds the queue with a FIFO-gated turn, not a timed one.
  • The existing send tests (first shape, outbox, class, unknown session) are unchanged.

Observed / Not run

Observed at head abcf83d840b6 on Linux CI (fast checks through cmux-ci):

  • cargo test -p acpmux --lib acpmux_mcp: 19 passed, 0 failed.
  • cargo clippy -p acpmux --lib --bins --tests -- -D warnings: clean.
  • rust_panic_ratchet.py (workspace, --lib --bins): acpmux deny counts unwrap 142 / expect 18 / indexing_slicing 682 / string_slice 94, each at or under the baseline (143/18/686/94). The ratchet is red only for cmux-rd-link and cmux-tui-core, at lines this PR doesn't touch (base). Tracked-only warnings: acpmux arithmetic 1490 and casts 221, the same as at the first push.

Not run: a live agent calling the tool in a tagged cmux build (the transcript lane's cards cover the drawing); the full acpmux suite (only the acpmux_mcp tests).

Review (codex gpt-6-astra, different model): BLOCK at 270154258dea (ordering, test race), BLOCK at 2fbb6d2a29d6 (name vs id), BLOCK at 87022a638c06 (unresolved peer), each fixed; SHIP at abcf83d840b669ec05c23be4e70144a139ccf073.

Changelog

Added: Agents can message several sessions in one send tool call, by name or by tag, with each message's text inline or from a file, and get one result per recipient.

Proof

No capture: the tool has no UI of its own; the transcript lane's PR shows the cards.

Agent session: agent session

šŸ¤– Generated with Claude Code