acpmux: agent messages waiting for a busy chat go in together as one turn, after the person's
In progress: fleet tests and an independent review are running; review fixes are pushed.
Why
coord-deputy's chat showed "121 more queued". Each queued lane message ran as its own turn, and Leo's own message waited behind all of them. Then one turn failed at 05:27Z ("agent process closed"), and pause_queue held all 121. Newer messages ran ahead of them, so they waited until someone pressed resume.
Before that pause: Steering already covers delivery inside a turn: an agent that steers reads a message at its next tool boundary (#22729, #22797). This PR covers the messages that still end up in the queue:
- a message to an agent that doesn't steer;
- a steer that the turn ended before reading;
- a
--no-steersend; - a send that waits for its reply.
What
- The person goes first. A prompt of the person's joins the queue ahead of every agent message that's waiting. It goes behind the person's own earlier prompts and behind a prompt sent now. Prompts held after a failed turn stay where they are.
- Agent messages go in together. When an agent message's turn starts, the agent messages waiting right behind it under the same rules go into the same turn.
- Each message goes in as it was sent, oldest first, under a line
[agent message i of N] from <sender>. Nothing is dropped, summarized, superseded or rewritten. - A turn takes up to 128 KiB of text; the rest goes in together as the next turn.
- A joined prompt leaves the queue with a
dequeuedrecord that names its turn (joinedPromptId,joinedTurnId). Itssession/promptanswers with that turn's answer, under its own prompt id. - The digest's
user_messagerecords the provenance{kind: "agent_digest", messages: [{promptId, from, provenance?}]}, so who sent what is kept. - Joining happens only after the turn has started, so no message leaves the queue for a turn that never runs.
- Each message goes in as it was sent, oldest first, under a line
- A failed turn doesn't eat agent messages.
- The messages that joined a failed turn go back to the head of the queue, held and in order, and their waiting tasks wait again. This covers a turn that never reached the agent (spawn failure, dispatch refusal) as well as one that failed at the agent.
- A message is put back once only, so a message that fails every turn can't keep pausing the person's queue.
- Held agent messages, from this path or from
pause_queue, go in after the chat's next completed turn. The person's held prompts still wait for the person. - The head message keeps today's semantics: its sender gets the failure.
- Who counts as another agent. It takes positive evidence; everything else stays the person's, as before (the GUI pane, web, the TUI, a plain CLI send). A prompt counts as an agent's when:
- it has an
agent_messageorsubsession_messageprovenance (cmux agent message, cmux-subsessions); - it comes from a connection that proved it runs in an acpmux session (
acpmux sendfrom a lane); - it carries
_meta.acpmux.fromAgent(newacpmux send --from NAME, for senders over ssh such asreport-coord). Naming yourself an agent only puts you later in the queue, so it needs no proof.
- it has an
Also:
withdrawfollows a joined message into its turn, and back if it is put back.- An edited message restores as the person's.
- A queued
agent_messagefrom before this build restores as an agent's. - A partial
queue_resumedunholds only the prompts it names on restore. cmux agents chat readmarks a digestorigin: agent_digestwith itssenders.- The agent pane draws a digest as one card from its senders, so the agents' words aren't shown as the person's.
This is the delivery slice of the agent forum design (manaflow-ai/cmuxterm-hq#2570). Broadcast and FYI traffic that should go to a pull feed instead of the turn queue is a later slice. Code: cmux-tui/crates/acpmux/src/hub/digest.rs, with hooks in queue.rs (enqueue order, the joined wait) and turns.rs (run_prompt).
Tests
tests/hub_integration/agent_digest.rs:- the person's prompts go ahead of queued agent messages, and keep their own order;
- three queued agent messages (
fromAgentandagent_messageprovenance) run as ONE turn after the person's, verbatim and in order, under sender lines. Every prompt is answered (joinedPromptId, sameturnId), and the log keeps theagent_digestprovenance and thedequeuedrecords; - a single agent message runs exactly as sent;
- a person's prompt the person moved between agent messages ends their run;
- a failed turn holds the agent messages behind it, and the next completed turn releases them while the person's held prompt still waits;
- a failed digest puts its joined messages back, and they go in together after the next completed turn, each answered;
fromAgentmust be one short line.
chat.rs: a digest reads asorigin: agent_digestwith its senders.incomingMessage.test.ts: the digest card.- Unit tests in
hub/digest.rs: who counts as an agent, thefromAgentcheck, and the digest's order and verbatim text.
Leo-ask: ask-24fc4dc254
Leo-ask: ask-15e110414f
Agent session: cosmic-gray
🤖 Generated with Claude Code
Need help on this PR? Tag @codesmith-bot with what you need.