acpmux: _acpmux/queue_text, and the sender session and harness on queued agent messages

Merged
TE
5d ago
+536-11

Leo, 2026-10-29: "What are we gonna do with these 200 fing messages? You never end a fing turn. As soon as it comes in, it fing, like, occupies you for another fing turn." and "you can't you just deal with this? Like ACPmux can't be this stuff. How do swarms work?"

For the queued-messages UI, as quoted by its lane: "If I expand it, I expect to see the whole thing" and "why are some of these icons still only placeholders?"

This PR adds the two daemon fields that UI needs.

1. A queued prompt's whole text: _acpmux/queue_text

Each queue row (the session summary's queue, and _acpmux/composers) carries only a 200-character prompt preview. So a queued agent message expanded after a reload showed cut text.

No existing read returns one queued prompt by id. _acpmux/events has the queued record with the full text, but only by kind (kinds: ["queued"]), so a client would have to page the whole log to find one prompt.

New method: _acpmux/queue_text {sessionId, promptId} returns {sessionId, promptId, turnId, text, prompt, fromAgent, fromSessionId, fromHarness}.

  • It reads from the live queue only.
  • Once the prompt has left the queue it answers queue.not_found (-32002, data.reason). After that the text is in the prompt's user_message record (it ran) or its queued record (it left another way).
  • Authorization is the same as _acpmux/events: it is a read for anyone who may read the session's log, which already holds the same text. It is on the read-only pairing allow list. A peer's session is forwarded as usual.
  • Capability queue-text-v1.
  • Full text is NOT added to every row. A coordinator's queue may hold hundreds of messages of up to 64 KB each.

How this relates to draft #26855 (_acpmux/queue_get): that PR is the owner's approval read for a guest's held prompt. It is owner-only, behind the Web-control check, returns revision, and is stacked on the guest-approval work. This PR is a display read with log-read authorization. Neither replaces the other. If both land, queue_get could reuse Hub::queued_text.

2. Sender session and harness: fromSessionId, fromHarness

An agent message's provenance now carries fromSessionId and fromHarness (the sending session's harness family: claude, codex, ...). The daemon stamps them only when it bound them itself:

  • A local proven caller (its caller token): acpmux send from inside a session, including --no-wait through the outbox, which proves the caller again.
  • A lane's report to its parent.
  • A peer link (Origin::Peer): the forwarding daemon sets them from its own proven caller, in peer_forward.rs name_sender.
  • The mesh relay: the hub message's from.sessionId, which the sending daemon set from its proven caller. Session id only: the hub's MeshSender schema (cmux-platform openapi.json) has no harness field and additionalProperties: false, so sending one would get the message refused. Adding a harness to the hub is a follow-up there.

Unproven values are dropped, never passed through:

  • From any connection other than a peer link, a client's fromSessionId or fromHarness is ignored.
  • A forwarding daemon removes the client's values before it sets its own.
  • A peer prompt marked via (forwarded for that peer's Web, app or paired-device client) names no sender.
  • Malformed values are dropped one by one, never refused.
  • A value bound from a caller token replaces any claimed value.

Where they show up:

  • _meta.provenance on the queued and user_message records.
  • Queue rows in the session summary and in _acpmux/composers.
  • _acpmux/queue_text.

They survive a restart (the provenance is restored with the queue) and an edit drops them along with the provenance.

These are display fields only. Nothing makes an authorization decision from them.

acpmux send --no-wait (the outbox) now gets a proper sender too:

  • The outbox already records the caller its connection proved when it took the message. It is saved in outbox.json, so a retry after a restart keeps it, and it proves that caller again on delivery. Its prompts therefore get the proven session's session_message provenance with fromSessionId and fromHarness. tests/send_async.rs a_no_wait_send_from_a_session_names_its_proven_sender now holds this, including that claimed values are ignored.
  • hub/prompt_line.rs counts acpmux-outbox#… as a shell send. A --no-wait message that has no proven sender but starts with a [name] words line therefore reads as that name, for display only, the same as acpmux-cli# and acpmux-peer#. (A sub-session's note to its parent was showing as a plain box of raw text.)

Not covered: sub-session messages and task briefs. Their hub-set provenance already names the sender's session in from.

Docs: docs/acpmux-schema.json, spec/acpmux-protocol.json, README, and the regenerated web protocol types.

The spec also gains _acpmux/mesh_control. The registry already lists it, but the committed IR did not, so protocol_ir_is_current was red on base.

Overlap

  • #27038 (persist session message events) adds a separate peer-carried messageSender {name, sessionId, host} for the durable _acpmux/message record and edits the same name_sender function. The two changes are additive. Whichever lands second merges base.
  • hub/queue.rs and digest.rs are untouched.
  • lane_reports.rs gets 6 additive lines.

Stack size

durable_agents was already flaky on base because a debug daemon overflowed tokio's 2 MiB worker stack. Adding to the session/prompt arm made it worse, so the sender stamping and the queue_text arm now live in their own functions outside dispatch_request. #27643 fixes the stack itself and lands before this PR.

Risk

risk:low. This adds a new read-only method and new display-only fields on existing records. Nothing changes who may do what. The remaining risk is that a peer daemon older than this one passes a local unix client's claimed values through unchanged. That is the same trust fromAgent already has, and it is still display only.

Tests

Red first: https://github.com/manaflow-ai/cmux/actions/runs/40938145206 (at 7db3d020691). The tests-only commit fails 3 hub_integration tests and 1 mesh_relay test on assertions. The fix is green: https://github.com/manaflow-ai/cmux/actions/runs/40940033703 (at 6d92b119988: the lib tests including protocol_ir_is_current, hub_integration prompt_provenance::, mesh_relay, lane_reports, and clippy -D warnings).

  • prompt_provenance::a_queued_agent_message_reads_whole_and_names_its_senders_session_and_harness
  • prompt_provenance::a_senders_session_and_harness_come_from_a_proven_caller_or_a_peer_link_only
  • prompt_provenance::a_prompt_sent_from_inside_another_session_records_that_session_as_its_sender (updated)
  • mesh_relay::a_relayed_message_names_its_sending_session_for_display
  • unit tests prompt_provenance::sender_tests

Capture

No capture: daemon protocol only. The only webviews files touched are the generated protocol types, so nothing in the UI changes; the UI lane shows these fields.

Changelog

none

Agent session: agent session

šŸ¤– Generated with Claude Code