acpmux: read one subagent's transcript, a page at a time (_acpmux/subagent_transcript)
Leo, 2026-10-24: "lawrence also sent some of this stuff with dynamic workflows claude "trajectories" thats what he calls sessions for some reason / and we can use that to help think about how cmux-next/acpmux wants to expose/reveal/render dynamic workflows". This is the drill-in to one agent's trajectory. Design: manaflow-ai/cmuxterm-hq#3720 and #3735. The request shape was agreed with the gallery lane that builds the workflow UI (#24874).
The pane gets runId and agentId from the workflowAgents records that #24789 (landed) sends and #24818 (landed) keeps.
acpmux owner notice. plans/cmux-next/acpmux-audit.md names the acpmux owner as session cmux-4f (PL-acpmux). That session can't be reached from here, so this is the notice. The change adds a hub-served read (hub/subagent_transcript.rs), a no-symlink open that reuses the media reader's walk (media_read::open_regular_below, with open_in unchanged), a peer feature check, and a Web refusal in remote_guard.
Risk: high by path (remote_guard, a new read of local files). The read is limited to the chat's own Claude store, opened by ids through the same walk the media reader trusts. It is never served to the Web, and never to a peer that doesn't list the feature. Codex reviewed it three times.
What changed
A Claude workflow's agents never stream into the chat. Claude Code keeps each agent's transcript in its store, and a run can have a thousand agents. Rather than acpmux pushing them all into the chat's log, the pane pulls the one transcript a person opens:
_acpmux/subagent_transcript {sessionId, runId?, agentId, before?, after?, limit?} ā {updates, lines, start, end, truncated}
The daemon's hub serves it (hub/subagent_transcript.rs), not the agent's adapter. Its reply goes to the caller only and is never written to the chat's log, and it doesn't touch the agent host's event loop.
- Where the file is. The chat's Claude store is the one its agent runs with: its profile's env as the spawn builds it (defaults, preset, chat store and session env,
~expanded:spawn_profile_for), soCLAUDE_CONFIG_DIRthere, else~/.claude. Its project folder comes from the store itself (adopt::find_claude). A workflow agent's file is<project>/<session>/subagents/workflows/<runId>/agent-<agentId>.jsonl; a background agent's, withoutrunId, is<project>/<session>/subagents/agent-<agentId>.jsonl. A run started before the chat's Claude session forked is looked for under the project's other sessions; a background agent is looked for only under the chat's own. - No caller paths, no links. The caller names plain ids:
runIdiswf_plus[A-Za-z0-9_-], andagentIdis plain too. Nothing the harness says becomes a path. The file is opened below the project folder one component at a time, without following a link or crossing a mount, and must be a regular file with one link. This ismedia_read::open_regular_below, the media reader's walk;open_inis unchanged. - Replay. The page's lines go through a fresh translator as one subagent's whole lines (text, thinking, tool calls and their results), and come back as the
session/updateobjects the pane already draws. Every line before the page is replayed too, with its updates dropped, so a call on an older page still pairs with its result (a Write's diff, a nested agent). The transcript's first line, the agent's prompt, comes back as the person's message. - Paging. Cursors count the transcript's lines. By default a page holds the
limitlines beforebefore(or before the end);limitdefaults to 200 and may be at most 500. Withafter, it holds the lines fromafteron, for a live view's next read.- Size. A page holds about 4 MB of updates and at least one line. A page read forward keeps its first lines and a page read back keeps its last, so
startandendnever skip a line. - Huge lines. A line whose updates alone pass that budget goes as one
omittedstub with its size and reason (as event paging does), so a reply always fits and the cursors move on. - Long transcripts. A transcript over 32 MB is read from its end (
truncated: true), and its line numbers count from there. The first line is kept when the cut falls just before it. - Parsing. The file is indexed by line (at most the last 2^20 lines, else
truncated), and only the page and its replay context are parsed, off the runtime: at most 16 MB of source. A line over the page budget is never parsed and goes as the stub.
- Size. A page holds about 4 MB of updates and at least one line. A page read forward keeps its first lines and a page read back keeps its last, so
- Cost. At most two reads run at once per daemon. The file read happens off the runtime. A page replays at most 1,000 lines before it, on a blocking thread with its own small runtime, never on the hub's.
- Who may call it. The unix socket, the app (
LocalApp), and a peer forwarding for either. A Web client is refused (transcriptRead.remote), as for the media reader. A peer that doesn't list thesubagentTranscriptinitialize feature is never sent the read; the caller getstranscriptRead.unsupportednaming the peer's build. - Capability
subagent-transcript-v1. The protocol IR and the generated web method list include the method.
Review
The first version read the transcript in the Claude adapter. An independent Codex review (xhigh, read-only) blocked it, and I found one more problem; all of the following are fixed by the rework above:
- it ran on the agent host's event loop (blocking Claude's stdout);
- it took paths from the harness;
- it lost state across pages;
- it couldn't reach past 32 MB;
- its trim could skip forward lines;
- (my own find) each page was written into the chat's log as an
Inreply.
Codex's merge reviews of the rebased version then blocked twice on the read, and every finding is fixed:
- a whole first line lost at an exact cut;
- parsing the whole tail before paging;
- one huge line breaking the reply;
- a huge line still being parsed;
- an unbounded line index;
- parsing on the runtime.
Codex's re-review of the rework confirmed all of those fixed and returned REVISE with three new P2s, all fixed in the latest commit:
- the store must come from the spawn's full env, not the profile and preset env alone;
- an end page replayed its whole prefix on the hub runtime;
- an older peer got a generic error instead of an explicit "unsupported".
No capture: a daemon change with no visible UI. Its only webview file is the generated protocol method list (protocol/generated/methods.ts), which adds one method name.
Merge review: Codex (xhigh, read-only, a different model) SHIP at d7033c7ffbf1f4bd20a3fd9e7ce84ae46a6cec09. The receipt is in the PR comments.
Testing
cargo test -p acpmux --lib -- claude_stdio hub::subagent_transcript capabilities protocolat the head: 91 passed (protocol_ir_is_currentincluded).hub::media_readrun serially: 15 passed.- New tests:
- a transcript replays as its prompt, steps and answer;
- a page keeps what earlier lines started (its tool result reads the same as with its call on the page);
- a workflow agent is read under the chat's session or one it forked from, and a background agent never from another session;
- a link in the store, to the file or to a folder, is never followed;
- ids are plain names;
- pages walk back and forward without skipping a line;
- a long transcript is read from its end;
- a cut at a line start keeps the line;
- a line too large for a reply goes as a stub;
- a page parses a bounded amount of the transcript;
- a huge line is never parsed;
- the line index is bounded.
cargo test -p acpmux --test remote_guard: 13 passed. It now checks that a Web client is refused the read and the app is not.cargo fmt --checkis clean.cargo clippy -p acpmux --lib --tests -- -D warningspasses.bun scripts/acpmux-protocol-codegen.ts --checkreports the generated code is current.- Note:
hub::media_read::tests::a_file_swapped_for_a_symlink_is_never_followed(not touched here; those tests never call the new function) fails about one run in three when the media tests run in parallel ("the file changed since it was opened"). It passed every serial run and 5 of 5 runs alone. It looks like a timing flake between the media reader's race tests.
The change is daemon-only, so there's no UI capture.
Agent session: huge-blue
š¤ Generated with Claude Code