acpmux: session inbox NAME --digest drains agent messages into one prompt
Leo, 2026-10-29: "What are we gonna do with these 200 fucking messages? You never end a fucking turn. As soon as it comes in, it fucking, like, occupies you for another fucking turn." and "you can't you just deal with this? Like ACPmux can't be this shit. How do swarms work?"
What
A coordinator session that 30+ lanes report to had 285 agent messages queued, and each one it read cost it a turn. This adds a one-shot drain:
acpmux session inbox NAME --digest (_acpmux/inbox_digest {sessionId, dryRun?}, hub/inbox_digest.rs)
- Takes the other agents' text messages waiting in NAME's queue (the ones
digest.rscounts as another agent's) out of it and puts ONE digest prompt in their place. - The person's own prompts are never touched: they stay where they are, ahead of the digest (held ones too).
- Each original leaves the queue with a
dequeuedrecord carryingdigestPromptIdanddigestTurnId. Its full text stays in the log under its own promptId (itsqueuedrecord), and its waitingsession/promptanswers with the digest's answer (joinedPromptId: the digest). - When NAME's turn is running, below its turn cap (#27520), and its agent steers, the digest goes in as ONE steer. Otherwise it waits at the head of the agent messages in the queue, behind the person's prompts, and runs as its own turn: no later agent message joins it (
take_joinedskips digests both ways). --dry-run(andinbox NAMEwithout--digest) prints the digest and changes nothing.- Allowed only over the local unix socket, from the person or from NAME's own agent (its caller token). Refused from Web, LocalApp and Peer (
inbox_digest.origin_refused), and from another session's caller token (inbox_digest.other_agent). Never forwarded to a peer. - Left in the queue: messages with anything but text (images, uploads, hidden context), slash commands, sub-session family messages, ones sent or steering now, Web ones, and earlier digests.
The renderer (shared with P2)
render(digest_id, &[Part {prompt_id, from, from_name, class, received_at, text}]) -> Rendered {text, provenance}:
- Blocks and questions first.
- Results grouped by sender (senders in order of their oldest result), oldest first.
- FYIs as one line each (first line, up to 160 characters).
- Exact-duplicate texts from one sender fold into the newest copy, marked
(×N). A sender is its address (from), not its display name.
Each part is headed by its sender and promptId. Blocks, questions and results keep their whole text up to DIGEST_MAX_BYTES in total; past that a part shows its first 400 characters and [full text: promptId …].
The digest's provenance is hub-set, never taken from a client: {kind: "inbox_digest", digestId, parts: [{promptId, from, fromName, class, receivedAt, bytes, duplicateOf?}], counts: {block, question, result, fyi}}.
Class
A message's class comes from its queue entry (PromptRun.class, set by send --class, #27512), and a message with none counts as a result. a_digest_puts_blocks_and_questions_first_and_fyis_last_by_the_class_sent checks the order end to end.
Also
- Restart safety: the digest's
queuedrecord is logged before the originals'dequeuedrecords and names every original (provenance.parts). A resend of an original after a restart answers from the digest's turn once that turn started (prompt_ledger.rs). - A prompt a restart restored whose task has not claimed it yet stays queued, as
take_joinedleaves it. The drain takes no turn lock, so a prompt whose turn came just as the drain took it answers from the digest (queue_slot); if that digest's turn fails and puts it back, it waits again. - A joined answer's
turnIdis the turn it went into: a steered digest's originals name the turn that ran then. - A digest refused or failed puts the messages it took back before its answer (
requeue_joined). Its turn takes them over as it starts (take_joined), so a failed turn puts them back exactly as it puts back messages that joined it: held only after its prompt reached the agent. They are loggedqueuedwithrequeued: true, so their senders keep waiting and a restart restores them. A digest refused before its turn puts them back as they were (held when a failed turn held them); one whose turn failed puts them back held. - A restart before a digest's turn started (its
turn_started, or its steer'suser_message; still queued, refused, stopped between its prompt and its turn, or a steer cut before it was written) drops the digest and gives its messages back in its place, each once (queue_waiting.rsDigests). After the digest's turn started, its turn is theirs and none runs again on its own. The digest'squeuedrecord names its messages and holds the queue's new order, so this holds whichever later record a stop cut off. A resend follows the digest only once its turn started. A digest the person edited is their prompt and stays; one they withdrew answered its messagescancelledand a resend answerswithdrawn: a restart gives back none of their messages. Messages given back keep their order (a partial requeue included), and drained again they follow the newest digest. - A part's text is a cmux agent message's own words, without its envelope.
- Only a proven caller token counts as the chat's own agent. A token this daemon run did not issue is refused with
inbox_digest.caller_not_proven. A unix connection that names no session is trusted as the socket already trusts it for every queue change: it mayqueue_remove,queue_edit, cancel or kill any chat. The acpmux client names its session by itself when it runs in one. prompt_line.rs: a digest's sidebar line reads as its senders plus "inbox digest of N messages".- A digest whose turn ends before its steer lands goes back to the head of the agent messages without stopping any turn (
steer_queued). - The protocol IR (
spec/acpmux-protocol.json) and the generated web method list gain_acpmux/inbox_digest.docs/acpmux-schema.jsondocuments the method and the newdequeuedfields.
No capture: no visible change. The only webviews file is the generated protocol method list (methods.ts, regenerated from the registry), and no UI calls the new method.
Size
About 2,090 changed lines: roughly 500 for inbox_digest.rs, 1,100 for tests (most of them the restart, refusal and turn-cap cases five review rounds asked for), and about 300 for wiring and restart handling. It isn't split because the drain, its renderer, its restart safety and their tests are one state change on the queue. The renderer alone has no caller, and a drain without its restart handling loses messages.
Tests (red first)
- Red, tests only (742f62d): run 40937377967, all 5 of its
inbox_digestintegration tests fail (no such method). The class-order test came after #27512 landed. - Green at e09437e2a91: run 40981198206.
cargo fmt --checkpasses; lib 1981/1981; hub_integration 572 pass, including all 17inbox_digesttests (the 2 failing are the base reds below);clippy -D warningsis clean. Final head 28e2e3d8d36: run 40982413576, the same. Earlier heads were green the same way: run 40967913993 at a588e9ffdb1, run 40959842140 at ef9f349f935, run 40956862813 at a0b96e01b1a, run 40954965470 at 245fe71aa07, run 40947068400 at 0e1f29b9591 and run 40939715498 at 085901bbc38. - Review round 1 (red then green): the tests for its findings failed in run 40953443333 at 81f95970d21. The forged caller drained the chat. A stop that cut the
dequeuedrecords restored the originals. A resend answeredinterrupted. A steered digest's answers named the digest's queue turn instead of the running one. - Review round 2 (red then green): its tests failed against a0b96e01b1a in run 40960683158. A steered digest the stop cut before its steer was written was lost. A cut
queue_orderput a slash command the digest left ahead of it. A resend whosedequeuedrecord was cut answeredinterrupted. The FYI-of-an-envelope unit test failed in run 40958523333. - A sibling review found a message-loss bug here. A digest refused before its turn (
session.folder_missing) never put back the messages it took. Its test failed in run 40959738138 at d4152dcc77c ("no requeued queued record"). - Review round 3 (red then green): its tests failed at b270265935 in run 40967931881. A digest that never ran stayed in the restored queue, which a refusal of its resumed run would lose. A steered digest cut before it was written did not give its messages back. A stop between a refusal and the
requeuedrecords lost them. - Review round 4 (red then green): its tests failed at 8b738a004ab/b33c83ec086 (tests only) in run 40975687732, and pass with the fix in run 40975734444. A stop between a digest's prompt and its turn kept the digest without its messages. A person's edit of a waiting digest was lost at a restart. A withdrawn digest's messages came back. A partial requeue reversed their order. Messages drained twice did not follow their newest digest.
- With #27520's turn cap on base: a digest no longer steers into a turn past its cap; it waits for the next turn, as the messages it took would have. Its test failed at 8e073dc6f8e (tests only) in run 40976789992 ("steer", not "queued").
- Review round 5 (red then green): its tests failed at 5c9af96ed14 (tests only) in runs 40979730098 and 40981169826. A digest whose agent could not start put its messages back held, so they waited out the 10 s retry. A digest drained behind a held prompt of the person's went ahead of it. A digest marked to steer that found the turn past its cap when its task steered it steered in anyway. A sender who changed its display name between two identical messages was not folded.
- The 2 hub_integration reds are base reds. They fail the same way on base a653ee370c9 in run 40939611174, 3 of 3 times each:
auto_resume::retry::stop_before_a_retrys_turn_starts_prevents_it(auto_resume_retry.rs:222, "turn was stopped before this prompt reached the agent") andtranscript_events::orchestration_fields_reach_subagent_events_and_session_summary(transcript_events.rs:721,tokensNull != 1234).prompt_supersede::a_prompt_queued_before_a_restart_is_still_replacedis flaky on base too: it failed 1 of 4 runs there.
Integration (tests/hub_integration/inbox_digest.rs): the person's prompts stay ahead and untouched; originals leave with dequeued.digestPromptId while their text stays in the log; one steer into a steering (Claude-style) agent's turn, otherwise queued at the head of the agent messages and run as one turn that nothing joins; grouping and folding; the budget pointer; --dry-run changes nothing; refusal from Web, LocalApp, Peer and another agent's caller token; the chat's own agent may drain. Unit (hub/inbox_digest_tests.rs): class order, folding, budget, the FYI line, class read from the queue entry with none meaning result, and which prompts are digestible.
Risk
Medium. This changes the queue only when someone runs the drain. The rest is two narrow exclusions in fan-in (take_joined never joins a digest), digest placement in enqueue, and steer_queued not interrupting for a digest. The worst case is a digest turn that fails: its originals go back to the queue held, as a failed agent message's turn holds it, and their texts stay in the log under their promptIds. The restart rules (when a digest counts as started, an edited or withdrawn digest) are the subtle part; each has a test that cuts the log where a stop would.
The UI half (grouped rendering, full text on expand) is another lane's and builds against the wire contract above.
Agent session: agent session
🤖 Generated with Claude Code