Repository navigation
Agent Journal 1.11.0
Captures Claude Code and Codex sub-agents as runs of their own, corrects the cost of a later prompt
in a multi-prompt Claude Code session, and fixes how runs end and how tokens are counted. Several stored values change meaning with this version. Records written by earlier
versions are never rewritten, and from this version the event and analysis streams say which version wrote them, so old
and new records can be told apart.
Read this first: values that change
Every boundary below is the same test: the header line of events.jsonl and analysis.jsonl
now carries producer and producerVersion. A header without producerVersion was written by
1.10.1 or earlier.
| Value | Up to 1.10.1 | From 1.11.0 |
|---|---|---|
run.json status of a Claude Code RunRecorder run that was closed without finish() |
FINISHED, success=true, even when an exception left the block |
CRASHED, success=false |
Claude Code llm_call.tokenUsage input and cache tokens |
added once per stream line, so a message with several content blocks was counted several times | once per message |
Claude Code llm_call.tokenUsage.outputTokens |
the sum of the per-message counts on the stream, which are start-of-message figures | the count on the result message, when larger |
Claude Code llm_call.metadata.turns, tool_call.turnIndex |
one turn per stream line | one turn per message |
Claude Code step_cost events |
a step for every stream line, including the text and thinking lines of a message that also called a tool | one step per tool call, plus one per message without a tool call; the shares differ accordingly |
| Claude Code runs that started sub-agents | the sub-agents' tool calls, tokens and steps were recorded as the run's own | the run holds the main loop only; each sub-agent is its own run |
Codex llm_call.tokenUsage.inputTokens |
Codex's input count, cached input included | cached input excluded, as for every other agent. An older value converts exactly: subtract cacheReadTokens |
Grok, Codex and Junie tool_call.durationMs |
0 | -1, "not measured" |
Why the schema version is still 1
The schema version says what a reader must know to parse a file: keys, types, units, enum
vocabularies and each field's documented definition. It changes when a file written by the new
version could be misread by a reader that correctly implements the previous format. It does not
change when the producer writes better values under an unchanged definition; those changes are
told apart by producerVersion.
| Change | Kind | Why |
|---|---|---|
CRASHED for a recorder closed without finish() |
producer correction | CRASHED already meant "ended abnormally"; the recorder now uses it where it used to write FINISHED wrongly |
| Claude Code input and cache tokens once per message | producer correction | the value was counted several times; the definition ("the tokens of the call") is unchanged |
| Claude Code output tokens from the result message | producer correction | the per-message figures were start-of-message counts; same definition |
One turn per message in turns and turnIndex |
producer correction | a turn was always documented as an assistant message; lines were being counted |
step_cost events and shares |
producer correction | derived from the corrected turns under the same attribution method |
| Sub-agents as their own runs | producer correction plus added keys | a run's events were never defined to include another agent's; the new config, tag, summary and metadata keys are additive, and parentRunId existed |
Codex inputTokens without cached input |
producer correction, narrowed definition | TokenUsage said whether input included cache reads "depends on what the agent reports"; it now says input excludes them. New values satisfy both readings; old Codex values convert exactly |
-1 for an unmeasured tool duration |
producer correction | -1 was already documented as "not measured" |
producer and producerVersion in the header |
added keys | every reader skips the header line |
What would have required a new schema version, and did not happen here: a renamed or removed
key; a changed type or unit; a key reused for a different quantity; a new value in a stored
enum. The deferred additions (FileChange.oldPath, more components on step_cost, a "cost not
reported" attribution value) fall under that rule.
Three computed values also change, and are not stored: TokenUsage.total(),
LLMCallEvent.totalTokens() and PhaseCapture.totalTokens() no longer add thinking tokens on top
of output tokens, which already include them; TokenUsage.cacheHitRatio() is cache reads divided
by input plus cache reads and stays between 0 and 1; TokenUsage.effectiveInputTokens() is
deprecated and returns the input tokens.
Sub-agent capture (Claude Code)
Claude Code sends a sub-agent's messages on the same stream as the main loop's, each marked with
the ID of the tool call that started the sub-agent. Earlier versions did not read that mark.
What is captured. For each sub-agent whose messages arrive: its prompt, its tool calls and
tool results with the usage of the turns that made them, and, when Claude Code is started with
--forward-subagent-text, its text and thinking together with its turns that called no tool.
They are in PhaseCapture.subagents(), one SubagentCapture each, and the recorder writes each
as a run:
parentRunIdis the run of the agent that started it: the recorder's run, or the enclosing
sub-agent's run for a nested one.- The tag
track=subagentmarks it. - The config key
subagent.spawnToolUseIdis the ID of the spawning tool call. That ID is also
theidof thetool_callevent in the parent run, which is the join. - Its files have the same kinds of events as any run. No record type gained a field and no enum
gained a value. - The parent's
llm_callmetadata lists the sub-agent runs undersubagents.
Cost. Claude Code reports one cost for the call, sub-agents included, and none per
sub-agent. That cost stays on the main loop's run, marked costIncludesSubagents=true, and is
split over the main loop's steps. A sub-agent's llm_call has a cost of 0 with
costAvailable=false and costSource=included_in_parent, and its step costs are 0 marked
EVEN_SPLIT. Adding cost over a run and its sub-agent runs gives the reported total once.
Tokens. Each run has its own. With --forward-subagent-text, input and cache tokens over a
run and its sub-agent runs add up exactly to what Claude Code reports for the whole call
(verified live through agent-client: 12 / 41,544 / 151,728 against the same modelUsage
figures). Without the flag, a sub-agent's turns that called no tool are not on the stream, so its
recorded usage is short by those turns (in the same live check, about 58,000 cache-read tokens
across two sub-agents); the call's own total is complete either way. A sub-agent's output count
is a lower bound in both cases: the stream reports no final output figure for a sub-agent (the
same check: 536 recorded against 672 reported).
How a sub-agent's run ends.
| Claude Code reported | Run status | summary |
|---|---|---|
completed |
FINISHED |
subagent.status=completed |
failed or killed |
FAILED |
the status as reported |
| another status, or none | CRASHED |
the status as reported, or unknown. Here CRASHED means that no successful end was seen, not that the sub-agent failed |
subagent.statusSource says where the status was read. A status is never inferred.
What is missing is recorded as missing.
- A spawning tool call for which no sub-agent messages arrived is listed under
subagentsWithoutTrackin the parent'sllm_callmetadata, next to Claude Code's own counts
(subagentStats: spawned, completed, failed, killed, refused). - If the messages came without their wire lines, the agents cannot be told apart. The metadata
then hassubagentTracksAvailable=false, no sub-agent run is written, and the sub-agent's
activity is merged into the run as before. - A run without sub-agents has none of these keys.
subagent.depthis 1 for a sub-agent of the main loop and 2 for a sub-agent of a sub-agent. A
sub-agent whose spawning tool call was not seen is linked to the recording run with depth -1.- A sub-agent run's
step_costevents derive again from its own stored events: a run whose
llm_callhascostAvailable=falsegets one zero-costEVEN_SPLITstep per tool call, the same
way it was recorded.
Supported, and not
| Observed with Claude Code 2.1.292 | Sub-agent tool calls, results, prompt and usage, with and without --forward-subagent-text, in --print mode and with the options claude-code-sdk 1.7.0 passes. Sub-agent text with that flag. A sub-agent started by a sub-agent. Two sub-agents started by one message |
| Covered by a hand-written test session only | Sub-agent thinking; a failed sub-agent; a refused start; a sub-agent whose end is not reported |
| Not observed, and not claimed | Other Claude Code versions; background sub-agents; nesting deeper than two; a resumed session |
| Verified through the Java path | Claude CLI 2.1.292 → claude-code-sdk 1.7.0 → agent-client (a local branch adding a forwardSubagentText option) → RunRecorder → files → fresh reader, flag on and off: linkage, separation, status, and the exact input and cache reconciliation above |
| Needs the caller | Complete sub-agent capture needs Claude Code started with --forward-subagent-text (CLIOptions.forwardSubagentText in claude-code-sdk 1.6.0 and later). Without it the sub-agents' prompts, tool calls, results and tool-turn usage are captured, but not their text, thinking, or text-only turns and that usage |
| Where sub-agent text lives | In SubagentCapture.textOutput and in a trace file when one is written. No journal event or run file stores text, for the main loop or for a sub-agent; this is unchanged |
| Not captured | Sub-agents of Antigravity, Grok and Junie. Sub-agents from Claude Code's transcript files |
If you read the files yourself
A sub-agent's run is a run directory like any other. A reader that treats every run directory as
one item of an experiment will count each sub-agent as an item, at a cost of 0 and without the
item's own configuration. Exclude run directories whose run.json has tags.track == "subagent",
and only those. Do not exclude on parentRunId alone: it is a general field, and an ordinary
nested run that sets it is a legitimate item (measured on a synthetic experiment: the tag rule
restores the item count, pass rate and total cost exactly; the parentRunId rule loses an item
and its cost).
The trace file is unchanged: it is still a flat record of every message, sub-agents' included.
Sub-agent capture (Codex)
Codex writes one rollout file per thread, and a sub-agent is its own thread. The journal does not
search the filesystem; the caller collects the root rollout and the child rollouts (agent-client's
harvester follows each SubAgentActivity{kind:"started"}.agent_thread_id to the file named with that
id) and hands them to CodexSessionParser.parse(CodexRollouts, …). The existing single-file parse
methods are unchanged.
- Identity and linkage. A child is identified by the id in its first
session_metarecord and
joined to thespawn_agentcall whosecall_idis the id of the parent'sstartedactivity for
that thread. Thatcall_idis theidof the parent'stool_callevent (the accepted
function_callrecording, with arguments kept verbatim, is unchanged) and the child run's
subagent.spawnToolUseId. The child run also recordssubagent.threadId,subagent.agentPath,
subagent.forkedandsubagent.sessionId(the root thread id). - Forked children. A forked child's file replays its parent's history; records below
subagent_history_start_ordinalare skipped and counted insubagent.replayedRecordsSkipped, so
the child's text, duration and error flag are its own. - Status. Codex writes no thread-terminal status. A child's status is the last observed turn
outcome: the parent's lastcompletedorinterruptedactivity for the child, else the child's
last owntask_completeorturn_aborted, elseunknown;subagent.statusSourcesays which and
subagent.statusMeaning=last_observed_turn.completed→FINISHED,interrupted→FAILED,
otherwiseCRASHED(no end observed). Aspawn_agentcall with nostartedactivity is a failed
spawn; a started child whose file was not supplied isnot_collected; both are listed under the
parent'ssubagentsWithoutTrack. - Accounting. Codex reports no cost for any thread: every
llm_callkeeps cost 0 with
costAvailable=false. Each run's tokens are its own file's lasttoken_count; Codex's parent total
excludes its children, so nothing is counted twice and the parent'sllm_callis unchanged by its
children. There is nocostIncludesSubagentskey for Codex. - Supported. Codex CLI 0.160.1, headless
codex exec, non-forked children at depth 1,
verified live; rollouts written by 0.147.0 and later parse. Forked children and depth 2 are parsed by
the same rules but covered by the synthetic fixture only. Not supported:codex exec resumefor
sub-agent capture,codex app-server, importing historical sessions. A child'sdurationMsis its
last turn's duration, not the thread's.
Recording an execution once
Recording the same capture twice wrote a second parent run with a second set of sub-agent runs.
RunRecorder.recordOnce(experimentId, capture, configure) and
CodexRunRecorder.recordOnce(experimentId, capture, configure) record an execution at most once
per experiment and storage: the new run carries the execution's key in its config
(capture.sourceKind, capture.sourceKey: for Claude Code the session id and the final assistant
message id, for Codex the root thread id), and the recorder looks for that key in the stored run
records before writing, so the check holds across a restart. The same source processed again
writes nothing and returns the earlier run; a different execution, including a later prompt of the
same Claude Code session, records normally. An earlier recording that has not ended is neither
resumed nor duplicated: recordOnce fails with an error naming it. The check is not a lock between
concurrent writers, and recordPhase on a run you created yourself is unchanged and unguarded.
A resumed Codex thread keeps its id and therefore counts as already recorded; Codex resume is not
supported for sub-agent capture.
Cost of a later prompt in a multi-prompt Claude Code session
Claude Code reports total_cost_usd, modelUsage and duration_api_ms on every result line as
the session's running total, also across --resume, while usage, duration_ms and num_turns
cover the query alone (verified live on 2.1.292). A caller that sends several prompts into one
session and records one phase per prompt (agent-client's ClaudeAgentSession does) therefore
recorded each phase's cost as the total so far, and adding the phases over-counted.
SessionLogParser.parse is unchanged and still records the result line verbatim. New
SessionLogParser.withSessionBaseline(current, previous) returns a copy of a later capture whose
cost, per-model costs and API time are the difference from the previous capture of the same
session id; the running totals are kept in the new PhaseCapture.sessionCost() component and
costBasis() says which figure a capture carries. It never subtracts across session ids and leaves a
capture unchanged when the counters restarted. The recorder writes costBasis=session_delta and
sessionCumulativeCostUsd in the LLM call's metadata for a rebased capture, and nothing new otherwise.
Comparison boundary: records without costBasis carry the result line's figure, which is the
query's own cost for a session's first or only prompt and the running total for a later prompt. The
caller has to apply the rebase; agent-client's adoption is tracked separately.
Run lifecycle
RunRecorder.close()cannot tell a normal exit from an exception leaving the
try-with-resources block. Onlyfinish()now records a completed run; a recorder closed
without it ends its runCRASHED. Callfinish()on the success path. A plainRunis
unchanged:close()still ends itFINISHED.- When derived events were logged to storage that does not keep them, the recorder now ends the
run before it throws, instead of leaving itRUNNINGwith no end time.
Reading files from other versions
- Unknown fields in a record are ignored, so files from a later version that only adds fields
still load. Unknown@typenames, unknown enum values and damaged lines still fail. - The schema version rule is corrected: it changes when a field is renamed, removed or given a
new meaning, and when a stored enum gains a value, since an older reader fails on one. It
is still 1. events.jsonloranalysis.jsonlwith a newer schema version than the reader knows is now
refused with an error that names both versions, instead of being half-read.- Event types registered with
Journal.registerEventTypeare kept for the whole process and
apply to storage configured later.
Also in this release
About thirty smaller fixes since 1.10.1, none of which changes a public signature. The ones a
reader of stored data may notice:
- Codex
function_calltool calls are recorded under their own names, with the arguments kept
unchanged. A Codex tool call that exited with code 0 is no longer marked failed. - A Grok tool call is marked failed from its final status. A Grok, Antigravity or Codex stream
with no final record is marked as an error. - A run that finishes as failed or crashed records
success=false. A failed run is saved when
its exception has no message. The run record is saved when a summary value is set. - Config and summary entries keep their insertion order.
- A Claude Code LLM call event carries its response ID.
- A custom
DerivedEventis rejected when written, not when the file is next read. - A phase or a tool call without a name is recorded instead of throwing, and a trace line that
cannot be written no longer stops parsing. - An ended call is never left current.
The Javadoc of every public type was rewritten, and the in-repository documentation site was
removed.
Dependencies
- Jackson 2.22.2 → 2.22.3, and the Jackson 3 line that arrives through
claude-code-sdk
3.2.2 → 3.2.3. Both clear four denial-of-service advisories published on 2026-09-22 and
2026-09-23 againstjackson-coreandjackson-databind(CVE-2026-89407, CVE-2026-89425,
CVE-2026-91776, CVE-2026-91777). 1.10.1 pins the affected versions. claude-code-sdkstays at 1.7.0 andgemini-cli-sdkat 0.30.0.
Known limits
- Cost per phase in a shared session. Claude Code's
total_cost_usdis the running total of
a session. The rebase (withSessionBaseline, above) is applied by the caller: a caller that
records several prompts of one session as separate phases without it still gets each later
phase's cost as the total so far, and such records carry nocostBasis. - Per-turn output tokens on the stream are start-of-message figures. The run total is now
right; the per-turn figures, and so the weights that split cost over steps, are not. - Codex reasoning text and approval decisions are not in Codex's session files (only
encrypted reasoning and the approval policy are), so they are not captured. - Claude Code permission denials are kept only in a trace written with
TraceRawMode.FULL.
Upgrading
- Call
RunRecorder.finish()where a run completes;close()alone now recordsCRASHED. - Replace
TokenUsage.effectiveInputTokens()withinputTokens(). - Callers that build a
PhaseCapturethemselves need no change: every earlier constructor
remains. A record pattern overPhaseCaptureneeds four more components (subagents,
subagentTracksAvailable,reportedSubagentStats,sessionCost), and one overCodexPhaseCapturethree. - If you serialise
PhaseCapture, it has four more properties:subagents,
subagentTracksAvailable,reportedSubagentStatsandsessionCost;CodexPhaseCapturehas
subagents,subagentTracksAvailableandspawnedThreadIds. - Do not compare Claude Code or Codex token counts across the 1.11.0 boundary without the table
at the top.
Maven Central
io.github.markpollack version 1.11.0: agent-journal-parent, journal-core,
claude-code-capture, gemini-cli-capture, grok-cli-capture, codex-cli-capture,
antigravity-cli-capture, junie-cli-capture.