Skip to main content
Claude Code and Codex can delegate work to sub-agents. Since Agent Journal 1.11.0, each sub-agent is recorded as a run of its own, linked to the run of the agent that started it. The parent’s files hold the parent’s own activity only, so a reader can analyse the main loop, one sub-agent, or the whole execution tree, and no token or dollar is counted twice. This guide walks through the capture path for each provider with the fixtures shipped in the repository, then explains the accounting, the status semantics and the limits. The other four providers (Gemini CLI, Grok, Antigravity, Junie) are unchanged from 1.10.1 and record no sub-agents.

What the two providers expose

Requirements

Dependencies. claude-code-capture brings claude-code-sdk 1.7.0 and needs a Java 21 runtime. codex-cli-capture depends on journal-core only and runs on Java 17.
Fixture execution (what this page runs) needs only the two dependencies and the fixture files from the repository at tag v1.11.0:
  • claude-code-capture/src/test/resources/fixtures/claude-subagents-synthetic.jsonl, a hand-written Claude Code session with four sub-agents and one refused spawn; its README.md next to it has the token arithmetic.
  • codex-cli-capture/src/test/resources/fixtures/codex-subagents-synthetic/{root,child-a,child-b,child-c,orphan}.jsonl, a hand-written Codex execution with three children; fixtures/README.md has the arithmetic.
Live execution needs the CLI and an account. For Claude Code, start the CLI (or the SDK’s CLIOptions) with --forward-subagent-text; claude-code-sdk 1.6.0 and later expose it as CLIOptions.forwardSubagentText. For Codex, run codex exec and locate the rollout files Codex writes under its sessions directory: the root file is named with the root thread id, and each child file with the child’s thread id, which the root’s SubAgentActivity{kind:"started"} records as agent_thread_id. agent-client. The released agent-client (0.31.0 on Maven Central) does not set --forward-subagent-text, harvests the root rollout only, calls the single-file CodexSessionParser.parse, and does not apply withSessionBaseline. Those four pieces exist on pushed proof branches of markpollack/agent-client (proof/journal-1.11.0-subagent-capture, proof/codex-rollout-children, proof/codex-subagent-capture-journal-1.11.0), none of them merged, and 0.31.0 pins Journal 1.10.0. Until an agent-client release adopts them, use the Journal APIs directly as shown here.

Claude Code: capture and record

The parser takes the SDK’s Iterator<ParsedMessage>. Live, that iterator comes from the SDK’s query; from a saved stream file, build it with the SDK’s MessageParser, one line at a time, which is also how the repository’s tests read the fixture.
Running this against the fixture prints:
A fifth spawn, toolu_spawn_refused, never started; it is not in subagents() but in capture.subagentsWithoutTrack(). recordOnce is the simplest path. The alternative is to open the run yourself, which is what callers did before 1.11.0; it records the same runs but is not guarded against being called twice:
Capture versus recording. The capture holds more than the journal stores. A sub-agent’s forwarded text and thinking are in SubagentCapture.textOutput() and thinkingBlocks(), and in the portable trace file when parse is given one. No journal event stores text, for the main loop or for a sub-agent.

Codex: parse the root with its children

Codex writes one rollout file per thread. Journal does not search the filesystem: you read each file and tag it with its thread id and, for a child, its parent’s thread id. The parent id is the parent_thread_id of the child’s first session_meta record.
Output for the fixture:
The single-file CodexSessionParser.parse(Path, …) and parse(BufferedReader, …) are unchanged; a capture parsed from the root file alone has subagentTracksAvailable() == false and no sub-agent runs.

Read the runs back and check the linkage

Open a fresh storage instance on the same directory. Each sub-agent run carries the tag track=subagent and the join keys in its run.json:
For the Claude fixture this lists five runs: Every row but the first has tags.track = subagent; “parent” is parentRunId and “spawning tool call” is config["subagent.spawnToolUseId"]. The join: a child’s subagent.spawnToolUseId equals the id of a tool_call event in its parent’s events.jsonl. Sub-agent c was started by sub-agent b, so its parentRunId is b’s run and its subagent.depth is 2. The parent’s llm_call metadata lists the children under subagents (spawn id, run id, depth, status) and the spawns that produced no record under subagentsWithoutTrack. The keys in a sub-agent’s run.json:

Accounting

Claude Code

Claude Code reports one cost and one modelUsage for the whole call, sub-agents included, and per-message usage for each agent’s own turns. Journal records:
  • the call’s cost on the main loop’s llm_call, marked costIncludesSubagents=true, and split over the main loop’s steps in analysis.jsonl;
  • each sub-agent’s llm_call with cost 0, costAvailable=false and costSource=included_in_parent;
  • each agent’s own tokens on its own run. Input tokens exclude cache reads; cache reads and cache creation are separate fields.
Worked example, the fixture (every message counted once): The last row equals the result line’s modelUsage, and the main-loop row equals its usage. Live, with --forward-subagent-text, the same reconciliation held exactly (12 / 41,544 / 151,728 against modelUsage on Claude Code 2.1.292). Two figures are not exact:
  • A sub-agent’s output tokens are a lower bound, because the stream reports no final output figure per sub-agent (536 recorded against 672 reported in the live run).
  • Without --forward-subagent-text, a sub-agent’s text-only turns are not on the stream, so its recorded usage is short by those turns (about 58,000 cache-read tokens across two sub-agents in the live check). The main loop’s own figures and the call’s cost are complete either way.
Adding cost over a run and its sub-agent runs gives the reported total once; adding costAvailable=false calls as zero is correct for Claude sub-agents because their cost is inside the parent’s figure.

Codex

Codex reports no dollar cost for any thread, so every llm_call has cost 0 with costAvailable=false and costSource=unreported. Treat that as unknown, not free. Each run’s tokens are its own file’s last token_count; Codex’s root total excludes its children, so each thread is counted once and the root’s llm_call is unchanged by its children. inputTokens excludes cached input (cachedInputTokens is separate), and thinkingTokens holds the reasoning count, which is a subset of outputTokens. Worked example, the fixture: An execution total is partial when a spawn produced no record (subagentsWithoutTrack non-empty) or a child’s end was not observed; the fixture’s fourth spawn failed, so its row is missing from the sum.

Missing evidence and status

What is missing is recorded as missing. A spawn that produced no sub-agent record is listed in the parent’s subagentsWithoutTrack: for Claude Code a refused or never-started spawn, next to Claude Code’s own counts in subagentStats; for Codex a spawn_failed spawn or a started child whose file you did not supply (not_collected). If Claude Code’s messages arrive without their wire lines, the agents cannot be told apart: the metadata has subagentTracksAvailable=false, no sub-agent run is written, and the activity is merged into the parent’s run as before 1.11.0. Claude Code status is what the CLI reported, never inferred; subagent.statusSource names where it was read (task_notification, tool_use_result, or none). Codex status is the last observed turn outcome, because Codex writes no thread-terminal record. Journal takes the parent’s last completed or interrupted activity for the child; failing that, the child’s last own task_complete or turn_aborted; failing that, unknown. completed maps to FINISHED, interrupted to FAILED, anything else to CRASHED, and every Codex child carries subagent.statusMeaning=last_observed_turn so a reader does not mistake FINISHED for a terminal event. A forked child (subagent.forked=true) replays its parent’s history at the start of its file; those records are skipped and counted in subagent.replayedRecordsSkipped, so the child’s text, duration and tokens are its own. Run end for the recorder itself. A Claude Code RunRecorder closed without finish() records CRASHED; recordOnce calls finish() for you, or ends the run FAILED when the capture reports an error.

Record an execution once

Recording the same capture twice writes a second parent run with a second set of sub-agent runs. RunRecorder.recordOnce and CodexRunRecorder.recordOnce guard against that within one experiment and one storage root:
  • The parent run carries the execution’s key in its config, capture.sourceKind and capture.sourceKey: for Claude Code the session id plus the final assistant message id, for Codex the root thread id.
  • Before writing, the recorder looks for that key in the stored run records, so the check holds across a restart.
  • The same source again writes nothing and returns the earlier run with recordedNow() == false. A different execution, including a later prompt of the same Claude Code session, records normally.
  • An earlier recording that has not ended, or that ended FAILED with an error recorded, is neither resumed nor duplicated: the call throws IllegalStateException naming the run. Remove that run, then record again.
Two limits. The check is not a lock: two processes recording the same source at the same time can both miss the other’s run. And recordPhase on a run you created yourself is unchanged and unguarded. A resumed Codex thread keeps its id, so codex exec resume turns are found as already recorded and are not captured; resume is not supported for sub-agent capture.

Consecutive prompts in one Claude Code session

Claude Code reports total_cost_usd, modelUsage and duration_api_ms as the session’s running totals, while usage, duration_ms and num_turns cover the query alone. A caller that records one capture per prompt in one session passes the previous capture to SessionLogParser.withSessionBaseline(current, previous):
The recorder writes costBasis=session_delta and sessionCumulativeCostUsd in the llm_call metadata of a rebased capture. The method never subtracts across session ids and returns current unchanged when the counters restarted. parse itself is unchanged: a record without costBasis from a later prompt carries the running total. You apply the rebase; agent-client 0.31.0 does not.

Capabilities and limits

Troubleshooting