acpmux: agent messages waiting for a busy chat go in together as one turn, after the person's

Merged
TE
16d ago
+1137-60

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-steer send;
  • 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 dequeued record that names its turn (joinedPromptId, joinedTurnId). Its session/prompt answers with that turn's answer, under its own prompt id.
    • The digest's user_message records 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.
  • 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_message or subsession_message provenance (cmux agent message, cmux-subsessions);
    • it comes from a connection that proved it runs in an acpmux session (acpmux send from a lane);
    • it carries _meta.acpmux.fromAgent (new acpmux send --from NAME, for senders over ssh such as report-coord). Naming yourself an agent only puts you later in the queue, so it needs no proof.

Also:

  • withdraw follows a joined message into its turn, and back if it is put back.
  • An edited message restores as the person's.
  • A queued agent_message from before this build restores as an agent's.
  • A partial queue_resumed unholds only the prompts it names on restore.
  • cmux agents chat read marks a digest origin: agent_digest with its senders.
  • 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 (fromAgent and agent_message provenance) run as ONE turn after the person's, verbatim and in order, under sender lines. Every prompt is answered (joinedPromptId, same turnId), and the log keeps the agent_digest provenance and the dequeued records;
    • 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;
    • fromAgent must be one short line.
  • chat.rs: a digest reads as origin: agent_digest with its senders.
  • incomingMessage.test.ts: the digest card.
  • Unit tests in hub/digest.rs: who counts as an agent, the fromAgent check, and the digest's order and verbatim text.

Leo-ask: ask-24fc4dc254
Leo-ask: ask-15e110414f

Agent session: cosmic-gray

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.