Support Codex subagent transcripts

This commit is contained in:
iris committed 2026-09-13 00:41:59 -04:00
1 parent 7d9df5d572
commit 83b113ef0f
6 files changed
+557 -38

No files matched your search

+40 -14
View File
@@ -1,9 +1,10 @@
# Subagents
A session's subagents -- the helpers a Claude Code session starts through its
Task tool -- each get a transcript of their own, listed under the session's
card and readable in the same transcript view the session has. Designed
2026-09-05; the decisions Bryan has not yet reviewed are in `DECISIONS.md`.
A session's subagents -- helpers started by Claude Code's Task tool or Codex's
collaboration tools -- each get a transcript of their own, listed under the
session's card and readable in the same transcript view the session has.
Designed 2026-09-05; extended to Codex's multiplexed app-server threads on
2026-09-13. The decisions Bryan has not yet reviewed are in `DECISIONS.md`.
## What a subagent is here
@@ -14,24 +15,33 @@ own. Everything it shares with a session -- the transcript file format, the
paging routes, the SSE stream, the phone's cache and rendering -- is reused
by addressing, not by copying.
The CLI reports a subagent's messages on the parent's own stream-json
output, each carrying `parent_tool_use_id` = the id of the Task `tool_use`
that started it. Before this the translator dropped those lines
Claude reports a subagent's messages on the parent's own stream-json output,
each carrying `parent_tool_use_id` = the id of the Task `tool_use` that started
it. Before this the translator dropped those lines
(`subagent_events_are_not_duplicated_into_the_transcript`); now it routes
them to that subagent's own translator and transcript. The parent's
transcript still shows only the Task call itself.
Codex app-server multiplexes every thread in the session tree onto the root
process's stdout. Its notifications carry `threadId`; `subAgentActivity`
items name the child thread and its lifecycle, and `collabAgentToolCall`
items carry the spawn prompt. The Codex translator routes a non-root
`threadId` exactly as Claude routes a `parent_tool_use_id`. The child thread
id is the subagent id on disk. An asynchronously delivered `agentMessage` is
a `PeerMessage`, not assistant text from the recipient.
## Storage
Under the session directory:
```
<session>/subagents/<tool_use_id>/meta.json {title, created}
<session>/subagents/<tool_use_id>/transcript.jsonl same SeqEvent lines as the session's
<session>/subagents/<subagent_id>/meta.json {title, created}
<session>/subagents/<subagent_id>/transcript.jsonl same SeqEvent lines as the session's
```
The id is the Task tool_use id (`toolu_…`), which is unique, stable across a
backend restart, and already the key everything on the parent side uses.
The id is Claude's Task tool_use id (`toolu_…`) or Codex's child thread id.
Both are unique, stable across a backend restart, and already the key their
parent-side lifecycle uses.
Only ids matching `[A-Za-z0-9_-]+` are ever created or looked up, since the
id becomes a path.
@@ -119,6 +129,16 @@ transcript is still being written to and its process is the session's to stop.
precisely the one nothing in this process has touched -- and it would
otherwise read `running` again every time its session was started.
For Codex the same lifecycle is expressed by app-server rather than Claude's
task notices: `subAgentActivity.started` creates the child,
`subAgentActivity.interacted` reopens it, and `completed` or `interrupted`
finishes it. A child's own `turn/completed` is not its end; it remains running
until that activity edge. The root's `turn/completed` reports `waiting` while
the registry contains an open child, and the last activity completion reports
`idle` if the root is between turns. Because the child thread id is also the
on-disk id, an adopted driver can route and finish a child whose spawn record
is already behind the durable stdout offset.
A subagent that was mid-flight when the backend restarted keeps working:
the registry reopens the existing transcript on the next child line, and
the file continues its sequence -- the same reopening #4 describes, whether
@@ -127,9 +147,12 @@ the backend was down nothing recorded that until the next line arrives, so
its last status stays `Running`, which the list reports as **unknown**
rather than as running (see the wire shape) until then.
Title: the Task call's `description` input, then ` (<subagent_type>)` when
one is given; falling back to the tool's name when the child arrives before
(or without) the parent call being seen.
Title: for Claude, the Task call's `description` input, then
` (<subagent_type>)` when one is given; falling back to the tool's name when
the child arrives before (or without) the parent call being seen. For Codex,
the first lifecycle record uses the spawned thread's name or the last segment
of `agentPath`, with underscores shown as spaces, then falls back to
`subagent`.
## Server layout
@@ -142,6 +165,9 @@ one is given; falling back to the tool's name when the child arrives before
- `session/claude/translate.rs` -- routes child lines by parent id, holds
one child `Translator` per subagent, remembers pending Task calls'
description/prompt/subagent_type.
- `session/codex/translate.rs` -- routes multiplexed app-server notifications
by thread id, remembers collaboration prompts, and translates activity
edges into the same registry lifecycle.
- `session/echo.rs` -- `/subagent [n]`: the test rig. Starts *n* (default 1)
subagents at once, each named "helper k". Each writes the prompt as its
user message, streams a few words of text, runs one `Bash` tool call, then