Class: Agent::Sessions::Readers::Claude
- Defined in:
- lib/agent/sessions/readers/claude.rb
Overview
Claude Code transcripts. Written against 142 real transcripts, 29,688 records, inventoried 2026-08-12.
The content vocabulary is a straight match for this gem's: text, thinking, tool_use, tool_result and image are exactly the five part types the design doc names, so nothing here has to invent a mapping. What Claude adds is everything around the conversation — a third of all records are session state, and two more kinds carry context the model saw without being a turn anyone took.
Constant Summary collapse
- NON_MESSAGE_TYPES =
State, not conversation, and together 11,000+ of the records written. Skipped in silence: warning about a record deliberately classified would teach a caller that warnings are noise.
atis-latch and bridge-session postdate the corpus above — found by running this reader over a live 2026-08-24 transcript and reading its own warnings (23 and 17 records), the same way Codex's tool list grew. Both are session plumbing: a latch marker, and the record tying a local transcript to its cloud session id (bridgeSessionId, owner uuids). Neither is a turn anyone took.
%w[ai-title mode permission-mode agent-name last-prompt file-history-snapshot file-history-delta queue-operation pr-link summary atis-latch bridge-session].freeze
- EVENT_TYPES =
Context the model saw, but not a turn:
systemis turn_duration, stop_hook_summary, away_summary, local_command;attachmentis hook output, skill listings, task reminders, pasted files. Same judgement Codex's event_msg gets — available on request, never on by default. %w[system attachment].freeze
- CONTENT_PARTS =
{ "text" => :text, "thinking" => :thinking, "tool_use" => :tool_use, "tool_result" => :tool_result, "image" => :image }.freeze
- SPILL =
How Claude Code points at output too large to inline. It is prose, not a structured field — 24 real tool_result parts and 149 attachments carry this sentence — so the path has to be matched out of the text.
/Full output saved to:\s*(\S+)/- MAX_SPILL_BYTES =
A spilled file is read whole. The largest observed is well under this; the cap exists because the pointer says nothing about the size.
4_000_000
Constants inherited from Base
Instance Attribute Summary
Attributes inherited from Base
Instance Method Summary collapse
-
#branching? ⇒ Boolean
Every uuid-bearing record names the record it followed, and 380 branch points sit across 85 of 151 real transcripts — a turn edited and re-run leaves two children under one parent.
-
#initialize(session, resolve_spills: true, **rest) ⇒ Claude
constructor
A new instance of Claude.
-
#subagents ⇒ Object
The transcripts of agents this session spawned, as readers of their own.
-
#usage ⇒ Object
Session totals, summed over assistant records but deduplicated by message.id first — and the dedup is most of the number.
Methods inherited from Base
#compactions, #each_message, #fidelity, #messages, #partial?, #tree, #warnings
Constructor Details
#initialize(session, resolve_spills: true, **rest) ⇒ Claude
Returns a new instance of Claude.
55 56 57 58 |
# File 'lib/agent/sessions/readers/claude.rb', line 55 def initialize(session, resolve_spills: true, **rest) super(session, **rest) @resolve_spills = resolve_spills end |
Instance Method Details
#branching? ⇒ Boolean
Every uuid-bearing record names the record it followed, and 380 branch points sit across 85 of 151 real transcripts — a turn edited and re-run leaves two children under one parent. Exactly one root per file and no orphaned parent link was found in that corpus, so the links are trustworthy enough to build a tree from.
53 |
# File 'lib/agent/sessions/readers/claude.rb', line 53 def branching? = true |
#subagents ⇒ Object
The transcripts of agents this session spawned, as readers of their own. Exposed rather than inlined, per design doc 8.1: a subagent's turns are not the parent's turns, and merging them would break every count taken from this reader. 124 of these sit beside real sessions on this machine.
isSidechain is false on all 22,072 records in the main transcripts, so there is nothing to filter out there — the separation is already how Claude Code writes them.
68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
# File 'lib/agent/sessions/readers/claude.rb', line 68 def subagents entries = begin Dir.children(File.join(sidecar_root, "subagents")) rescue SystemCallError return [] end entries.sort.filter_map do |name| next unless File.extname(name) == ".jsonl" child = child_session(File.join(sidecar_root, "subagents", name)) child && self.class.new(child, resolve_spills: @resolve_spills, include_events: include_events) end end |
#usage ⇒ Object
Session totals, summed over assistant records but deduplicated by message.id first — and the dedup is most of the number. One API response streams into one record PER CONTENT BLOCK, each carrying the same message.id and the same usage: in one real transcript on this machine (2026-08-24), 260 assistant records share 124 message ids, 94 of which repeat with byte-identical usage. A naive sum reports roughly double what Anthropic billed. An id-less record (not observed, but rule 2 says formats drift) is counted rather than dropped: overcounting a novelty beats silently ignoring it.
92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 |
# File 'lib/agent/sessions/readers/claude.rb', line 92 def usage seen = {} total = nil each_record do |record, _line_number| usage = usage_from(record) next unless usage id = record.dig("message", "id") next if id && seen[id] seen[id] = true if id total = total ? total + usage : usage end total end |