Class: Agent::Sessions::Readers::Base

Inherits:
Object
  • Object
show all
Defined in:
lib/agent/sessions/readers/base.rb

Overview

Layer 3: turning one session file into messages. Subclasses supply the mapping; everything about how the file is read lives here, because the three rules that make this layer survivable (design doc §5) are properties of the reading, not of any one agent's format:

1. raw is never dropped.
2. Unknown records become :unknown parts and warnings, never exceptions.
3. Reading streams. No code path may assume a file fits in memory.

Rule 3 is why this does not use File.foreach without a chunk size. A truncated log can hold no newline at all, and "read one line" would then mean "read 2.6 GB into a String" — the file the article that started this gem found on a real machine.

Direct Known Subclasses

Amp, Claude, Codex, Copilot, Gemini, Grok, Opencode, Pi, Qwen

Constant Summary collapse

MAX_RECORD_BYTES =

Chunk size, and so the largest record that can be read whole. Measured against 415 real Codex rollout files (128,987 records, 2026-08-12): 14 records exceed 1 MB and the largest is 2.41 MB, so Layer 2's MAX_LINE_BYTES of 1 MB would silently drop real messages. 8 MB is ~3.3x the observed maximum. A record beyond it is reported, never dropped in silence, because a missing message is this gem's worst failure mode.

8_000_000

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(session, include_events: false) ⇒ Base

Returns a new instance of Base.



30
31
32
33
34
# File 'lib/agent/sessions/readers/base.rb', line 30

def initialize(session, include_events: false)
  @session = session
  @include_events = include_events
  @warnings = []
end

Instance Attribute Details

#sessionObject (readonly)

Returns the value of attribute session.



28
29
30
# File 'lib/agent/sessions/readers/base.rb', line 28

def session
  @session
end

Instance Method Details

#branching?Boolean

Whether this agent records which turn each turn followed. False here: most stores are an append-only list and a tree would have to be invented.

Returns:

  • (Boolean)


65
# File 'lib/agent/sessions/readers/base.rb', line 65

def branching? = false

#compactionsObject

Boundaries where the agent replaced earlier turns with a summary. Its own pass: a caller asking only for compactions should not have to materialize every message to get them.



99
100
101
102
103
# File 'lib/agent/sessions/readers/base.rb', line 99

def compactions
  found = []
  each_record { |record, _line| (boundary = compaction_for(record)) && found << boundary }
  found
end

#each_messageObject

Streams. Yields each message as it is parsed; a caller that breaks after one has read one record, not the file.



49
50
51
52
53
54
55
56
# File 'lib/agent/sessions/readers/base.rb', line 49

def each_message
  return enum_for(:each_message) unless block_given?

  each_record do |record, line_number|
    message = message_for(record, line_number)
    yield message if message
  end
end

#fidelityObject



36
# File 'lib/agent/sessions/readers/base.rb', line 36

def fidelity = session.fidelity

#messagesObject

Eager, for sessions small enough to hold. The design doc offers both and names this the convenience: messages is what a script wants, and each_message is what a 2.6 GB file requires.



61
# File 'lib/agent/sessions/readers/base.rb', line 61

def messages = each_message.to_a

#partial?Boolean

True where the local file is not the whole story — Amp, whose server holds the canonical copy. Overridden there, false everywhere else.

Returns:

  • (Boolean)


40
# File 'lib/agent/sessions/readers/base.rb', line 40

def partial? = false

#treeObject

The conversation as roots and their continuations, for an agent that records parent links. Unlike every other method here this cannot stream — a tree is not knowable until the last record is read — so it holds one session's messages at once and says so rather than pretending otherwise.

Raises rather than returning an empty list or nil for a store with no parent links, for the reason Agent::Sessions.read raises: "this format does not record that" must never read as "this session has none".



75
76
77
78
79
80
81
82
# File 'lib/agent/sessions/readers/base.rb', line 75

def tree
  unless branching?
    raise UnsupportedFormat,
          "#{session.agent} does not record parent links; its messages are a flat list"
  end

  build_tree
end

#usageObject

This session's token totals as a Usage, or nil where the format does not record them (Amp) or this reader has not learned where they live. nil, not an empty Usage: "this store does not say" must never read as "this session cost nothing" — the same rule tree() enforces by raising.

Each reader that overrides this also decides its own summation rule, because that rule is format knowledge: Claude repeats one API response's usage across several records (94 of 124 message ids in one real transcript), Codex writes a running total where only the last record counts. A base-class sum would get both wrong.



94
# File 'lib/agent/sessions/readers/base.rb', line 94

def usage = nil

#warningsObject

Populated as records are read, so this answers for whatever has been consumed so far. uniq because a second pass over the same file would otherwise repeat every warning it already reported.



45
# File 'lib/agent/sessions/readers/base.rb', line 45

def warnings = @warnings.uniq