agent_sessions 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +8 -0
  3. data/README.md +71 -3
  4. data/doc/Agent/Sessions/Adapters/Amp.md +91 -0
  5. data/doc/Agent/Sessions/Adapters/Base.md +198 -0
  6. data/doc/Agent/Sessions/Adapters/Claude.md +63 -0
  7. data/doc/Agent/Sessions/Adapters/Codex.md +79 -0
  8. data/doc/Agent/Sessions/Adapters/Copilot.md +38 -0
  9. data/doc/Agent/Sessions/Adapters/Cursor.md +54 -0
  10. data/doc/Agent/Sessions/Adapters/CursorIde.md +49 -0
  11. data/doc/Agent/Sessions/Adapters/Enumeration.md +135 -0
  12. data/doc/Agent/Sessions/Adapters/Gemini.md +67 -0
  13. data/doc/Agent/Sessions/Adapters/Grok.md +64 -0
  14. data/doc/Agent/Sessions/Adapters/Opencode.md +93 -0
  15. data/doc/Agent/Sessions/Adapters/Pi.md +137 -0
  16. data/doc/Agent/Sessions/Adapters/Qwen.md +32 -0
  17. data/doc/Agent/Sessions/Adapters.md +5 -0
  18. data/doc/Agent/Sessions/Audit/Finding.md +27 -0
  19. data/doc/Agent/Sessions/Audit.md +21 -0
  20. data/doc/Agent/Sessions/CLI.md +36 -0
  21. data/doc/Agent/Sessions/Check.md +27 -0
  22. data/doc/Agent/Sessions/Compaction.md +23 -0
  23. data/doc/Agent/Sessions/EnvOverride.md +19 -0
  24. data/doc/Agent/Sessions/Error.md +6 -0
  25. data/doc/Agent/Sessions/HomeExpansion.md +11 -0
  26. data/doc/Agent/Sessions/Location.md +60 -0
  27. data/doc/Agent/Sessions/Message.md +59 -0
  28. data/doc/Agent/Sessions/MissingDependency.md +6 -0
  29. data/doc/Agent/Sessions/Node.md +24 -0
  30. data/doc/Agent/Sessions/Part.md +41 -0
  31. data/doc/Agent/Sessions/Readers/Amp.md +41 -0
  32. data/doc/Agent/Sessions/Readers/Base.md +91 -0
  33. data/doc/Agent/Sessions/Readers/Claude.md +77 -0
  34. data/doc/Agent/Sessions/Readers/Codex.md +81 -0
  35. data/doc/Agent/Sessions/Readers/Copilot.md +31 -0
  36. data/doc/Agent/Sessions/Readers/Gemini.md +38 -0
  37. data/doc/Agent/Sessions/Readers/Grok.md +45 -0
  38. data/doc/Agent/Sessions/Readers/Opencode.md +37 -0
  39. data/doc/Agent/Sessions/Readers/Pi.md +38 -0
  40. data/doc/Agent/Sessions/Readers/Qwen.md +30 -0
  41. data/doc/Agent/Sessions/Readers.md +5 -0
  42. data/doc/Agent/Sessions/Session.md +73 -0
  43. data/doc/Agent/Sessions/Sqlite.md +43 -0
  44. data/doc/Agent/Sessions/Store.md +57 -0
  45. data/doc/Agent/Sessions/UnknownAgent.md +6 -0
  46. data/doc/Agent/Sessions/UnreadableStore.md +6 -0
  47. data/doc/Agent/Sessions/UnsupportedFormat.md +6 -0
  48. data/doc/Agent/Sessions/Usage.md +56 -0
  49. data/doc/Agent/Sessions.md +177 -0
  50. data/doc/Agent.md +5 -0
  51. data/doc/CHANGELOG.md +69 -0
  52. data/doc/README.md +162 -0
  53. data/doc/index.csv +316 -0
  54. data/exe/agent-sessions +0 -2
  55. data/lib/agent/sessions/adapters/cursor.rb +3 -3
  56. data/lib/agent/sessions/cli.rb +2 -2
  57. data/lib/agent/sessions/message.rb +1 -2
  58. data/lib/agent/sessions/part.rb +1 -2
  59. data/lib/agent/sessions/version.rb +1 -1
  60. data/llm.txt +177 -0
  61. metadata +54 -2
@@ -0,0 +1,81 @@
1
+ # Class Agent::Sessions::Readers::Codex <a id="class-Agent-Sessions-Readers-Codex"></a>
2
+
3
+ | | |
4
+ | --- | --- |
5
+ | **Inherits** | [Agent::Sessions::Readers::Base](Base.md) |
6
+ | **Defined in** | lib/agent/sessions/readers/codex.rb |
7
+
8
+ Codex rollout files. Every mapping here was written against a real corpus
9
+ rather than from the format notes: 415 files, 128,987 records, inventoried
10
+ 2026-08-12. The distribution is the reason for several decisions below
11
+ —response_item 57%, event_msg 38%, turn_context 4%, session_meta 416,
12
+ world_state 182, inter_agent_communication_metadata 129, compacted 18.
13
+
14
+ Codex was chosen as the first reader for exactly this reason. pi was the
15
+ planned reference implementation, but its store held no session files at all
16
+ on the machine available, so every claim about its content would have been
17
+ inference. A reference implementation has to be falsifiable.
18
+
19
+ ## Constants
20
+ ### `CALL_INPUTS` <a id="constant-CALL_INPUTS"></a> <a id="CALL_INPUTS-constant"></a>
21
+ Where a tool call keeps what it was called with. custom_tool_call uses input,
22
+ function_call uses arguments, web_search_call uses action, and
23
+ tool_search_call uses arguments as a Hash rather than a String.
24
+
25
+ ### `CALL_OUTPUTS` <a id="constant-CALL_OUTPUTS"></a> <a id="CALL_OUTPUTS-constant"></a>
26
+ And where an output keeps its result.
27
+
28
+ ### `CONTENT_PARTS` <a id="constant-CONTENT_PARTS"></a> <a id="CONTENT_PARTS-constant"></a>
29
+ encrypted_content maps to :unknown deliberately, not for want of a better
30
+ bucket: 80 real content items are encrypted by the model and this gem will
31
+ never read them. Recognized-and-unreadable is a different thing from
32
+ unrecognized, and only the second deserves a warning — a warning that fires on
33
+ a permanent, understood condition is noise on every read.
34
+
35
+ ### `NON_MESSAGE_ITEMS` <a id="constant-NON_MESSAGE_ITEMS"></a> <a id="NON_MESSAGE_ITEMS-constant"></a>
36
+ Internal state that happens to travel as a response_item. Skipped in silence
37
+ for the same reason turn_context is: it is not conversation, and a warning a
38
+ caller must learn to ignore is worse than no warning.
39
+
40
+ ### `NON_MESSAGE_TYPES` <a id="constant-NON_MESSAGE_TYPES"></a> <a id="NON_MESSAGE_TYPES-constant"></a>
41
+ Known, and deliberately not messages: the session header, per-turn
42
+ configuration, and two state records Codex added in July 2026. Silence here is
43
+ a judgement, not an oversight — these are not conversation, and warning about
44
+ them would train a caller to ignore warnings.
45
+
46
+ ### `ROLES` <a id="constant-ROLES"></a> <a id="ROLES-constant"></a>
47
+ "developer" is what Codex writes where the normalized vocabulary says :system.
48
+ It is 101 of 292 role-bearing records in the sample, so this is the common
49
+ path, not an edge case.
50
+
51
+ ### `TOOL_CALLS` <a id="constant-TOOL_CALLS"></a> <a id="TOOL_CALLS-constant"></a>
52
+ Every entry past the first three in each list came from running this reader
53
+ over all 415 files and reading its own warnings: a 25-file sample showed none
54
+ of them. Counts in that corpus: web_search_call 288, ghost_snapshot 197,
55
+ agent_message 129, tool_search_call and tool_search_output 26 each,
56
+ image_generation_call 1.
57
+
58
+ ### `TOOL_OUTPUTS` <a id="constant-TOOL_OUTPUTS"></a> <a id="TOOL_OUTPUTS-constant"></a>
59
+ Not documented.
60
+
61
+ ## Public Instance Methods
62
+ ### `usage()` <a id="method-i-usage"></a> <a id="usage-instance_method"></a>
63
+ Session totals. Codex writes no usage on its messages; it writes token_count
64
+ event records whose info.total_token_usage is a RUNNING TOTAL — verified
65
+ against a real rollout on this machine (2026-08-24): consecutive records
66
+ report total 33,751 then 69,135 while their last_token_usage differ, so the
67
+ last record is the session and summing would multiply-count every earlier
68
+ turn.
69
+
70
+ Two normalizations, both from that same file:
71
+
72
+ input_tokens INCLUDES cached_input_tokens (33,431 including 19,200
73
+ in the sample) — the opposite of Claude's disjoint spelling — so the
74
+ cached share is subtracted to make Usage#input mean one thing across
75
+ agents. Clamped at zero: a count that went negative would mean the
76
+ two fields disagree, and a wrong zero beats a negative token count.
77
+
78
+ cache_write_input_tokens maps to cache_creation. total_tokens is
79
+ deliberately not mapped anywhere: it restates the other fields, and
80
+ any bucket it landed in would be double-counted by a caller summing
81
+ buckets.
@@ -0,0 +1,31 @@
1
+ # Class Agent::Sessions::Readers::Copilot <a id="class-Agent-Sessions-Readers-Copilot"></a>
2
+
3
+ | | |
4
+ | --- | --- |
5
+ | **Inherits** | [Agent::Sessions::Readers::Base](Base.md) |
6
+ | **Defined in** | lib/agent/sessions/readers/copilot.rb |
7
+
8
+ GitHub Copilot CLI turns, from the SQLite store the adapter enumerates.
9
+
10
+ The SCHEMA is verified (schema_version 3 on this machine, 2026-08-24): turns
11
+ holds id, session_id, turn_index, user_message, assistant_response, timestamp.
12
+ The CONTENT is not — the one real session here has zero turn rows, so no turn
13
+ has ever been read. The column names are unambiguous enough to map without
14
+ guessing at structure, which is why this reader exists at all rather than
15
+ waiting; what it cannot promise is that a real turn holds plain text in those
16
+ columns rather than, say, JSON.
17
+
18
+ fidelity is :messages, not :full — one row is a whole exchange, so the tool
19
+ calls and reasoning that happened inside it are not recoverable from this
20
+ table. What a caller gets is what was said, not how.
21
+
22
+ ## Public Instance Methods
23
+ ### `each_message()` <a id="method-i-each_message"></a> <a id="each_message-instance_method"></a>
24
+ One row is a user turn AND the assistant's reply, so each row yields two
25
+ messages. They share a raw record: rule 1 keeps the row intact, and splitting
26
+ it into two half-rows would misreport what was stored.
27
+
28
+ ### `usage()` <a id="method-i-usage"></a> <a id="usage-instance_method"></a>
29
+ No token or cost column exists anywhere in this schema — not on sessions, not
30
+ on turns. nil is the format speaking, and must not be mistaken for a session
31
+ that cost nothing.
@@ -0,0 +1,38 @@
1
+ # Class Agent::Sessions::Readers::Gemini <a id="class-Agent-Sessions-Readers-Gemini"></a>
2
+
3
+ | | |
4
+ | --- | --- |
5
+ | **Inherits** | [Agent::Sessions::Readers::Base](Base.md) |
6
+ | **Defined in** | lib/agent/sessions/readers/gemini.rb |
7
+
8
+ Gemini CLI chat files. Written against a real store on this machine
9
+ (2026-08-24): 12 sessions, 121 records — user 20, gemini 97, info 4.
10
+
11
+ A chat is one JSON document, not JSONL, so it is read whole under a cap the
12
+ way Amp's thread is. The cap is the reason this does not simply JSON.parse the
13
+ file: the largest real chat here is 103 KB, but nothing in the format bounds
14
+ it, and an unbounded read is the failure the base reader's chunked streaming
15
+ exists to prevent.
16
+
17
+ ## Constants
18
+ ### `MAX_DOCUMENT_BYTES` <a id="constant-MAX_DOCUMENT_BYTES"></a> <a id="MAX_DOCUMENT_BYTES-constant"></a>
19
+ Amp's bound, for the same reason: a whole document must fit in memory to be
20
+ parsed at all, so the only protection available is refusing to read one that
21
+ is absurdly large.
22
+
23
+ ### `ROLES` <a id="constant-ROLES"></a> <a id="ROLES-constant"></a>
24
+ "gemini" is the assistant. "info" is the CLI talking to the user ("Update
25
+ successful! The new version will be used on your next run."), which is neither
26
+ turn — context the operator saw, the same judgement Claude's system records
27
+ get, so it arrives with include_events.
28
+
29
+ ## Public Instance Methods
30
+ ### `header()` <a id="method-i-header"></a> <a id="header-instance_method"></a>
31
+ The document's own header, exposed because Layer 2's session id is the
32
+ filename (the trailing hex in it is shared between sessions) while the agent's
33
+ own sessionId lives in here.
34
+
35
+ ### `usage()` <a id="method-i-usage"></a> <a id="usage-instance_method"></a>
36
+ Session totals, summed per message — the counts are per API call, not a
37
+ running total (verified: the real series falls as well as rises, 64138 then
38
+ 8069 then 8265, which no cumulative counter does).
@@ -0,0 +1,45 @@
1
+ # Class Agent::Sessions::Readers::Grok <a id="class-Agent-Sessions-Readers-Grok"></a>
2
+
3
+ | | |
4
+ | --- | --- |
5
+ | **Inherits** | [Agent::Sessions::Readers::Base](Base.md) |
6
+ | **Defined in** | lib/agent/sessions/readers/grok.rb |
7
+
8
+ Grok Build sessions. PROVISIONAL, like the adapter: written against
9
+ tokentelemetry's parser of this format, not against real Grok output.
10
+
11
+ Two things make this reader unlike every other one here. The session it is
12
+ handed points at summary.json, while the conversation is in chat_history.jsonl
13
+ beside it — so the streaming base reads a SIBLING file. And billed usage is
14
+ not in the session directory at all: it lives in ~/.grok/logs/unified.jsonl,
15
+ one row per request across every session, keyed by session id. A rotated log
16
+ means no usage, which is why `usage` answers nil rather than zero when the log
17
+ is gone.
18
+
19
+ ## Constants
20
+ ### `INFERENCE` <a id="constant-INFERENCE"></a> <a id="INFERENCE-constant"></a>
21
+ The row that records one completed request, per the reference parser.
22
+
23
+ ### `ROLES` <a id="constant-ROLES"></a> <a id="ROLES-constant"></a>
24
+ Not documented.
25
+
26
+ ### `TRANSCRIPT` <a id="constant-TRANSCRIPT"></a> <a id="TRANSCRIPT-constant"></a>
27
+ Not documented.
28
+
29
+ ### `UNIFIED_LOG` <a id="constant-UNIFIED_LOG"></a> <a id="UNIFIED_LOG-constant"></a>
30
+ Not documented.
31
+
32
+ ## Public Instance Methods
33
+ ### `summary()` <a id="method-i-summary"></a> <a id="summary-instance_method"></a>
34
+ The session's summary.json, exposed because it holds what Layer 2 does not
35
+ surface: generated_title, session_summary, current_model_id, the git branch
36
+ and commit the work happened on.
37
+
38
+ ### `usage()` <a id="method-i-usage"></a> <a id="usage-instance_method"></a>
39
+ Summed across this session's rows in the shared inference log.
40
+
41
+ prompt_tokens INCLUDES cached_prompt_tokens (the reference parser subtracts
42
+ one from the other, as this gem does for Codex and Gemini), so `input` is the
43
+ difference and `cache_read` the cached share. The cached count is clamped to
44
+ the prompt first: a log row claiming more cached than prompt would otherwise
45
+ produce a negative input.
@@ -0,0 +1,37 @@
1
+ # Class Agent::Sessions::Readers::Opencode <a id="class-Agent-Sessions-Readers-Opencode"></a>
2
+
3
+ | | |
4
+ | --- | --- |
5
+ | **Inherits** | [Agent::Sessions::Readers::Base](Base.md) |
6
+ | **Defined in** | lib/agent/sessions/readers/opencode.rb |
7
+
8
+ opencode sessions, read from the shared SQLite database the adapter already
9
+ enumerates. Written against a real store on this machine (2026-08-24): 365
10
+ sessions, whose message and part rows settled every mapping below — this is
11
+ the first reader whose "corpus" is a database rather than files.
12
+
13
+ A "record" here is synthetic: one message row's parsed `data` plus every part
14
+ row belonging to it, as {"message" => ..., "parts" => [...]}. That composite
15
+ IS the raw a Message carries — rule 1 needs the parts included, because the
16
+ content lives in them, not in the message row.
17
+
18
+ ## Constants
19
+ ### `CONTENT_PARTS` <a id="constant-CONTENT_PARTS"></a> <a id="CONTENT_PARTS-constant"></a>
20
+ Conversation content. text and reasoning map 1:1; a `tool` part holds BOTH the
21
+ call and its result in one row (state.input / state.output), so it becomes two
22
+ Parts — the assistant's act and the tool answering —rather than flattening one
23
+ of them away.
24
+
25
+ ### `STATE_PARTS` <a id="constant-STATE_PARTS"></a> <a id="STATE_PARTS-constant"></a>
26
+ State, not conversation, skipped in silence — the same judgement Claude's
27
+ session-state records get. Observed counts in the real store: step-start
28
+ 4,391, step-finish 4,380 (consumed for usage below), patch 569 (files a step
29
+ touched), file 25 (attachments), agent 1, compaction 1 (surfaced through
30
+ `compactions`, not as a message).
31
+
32
+ ## Public Instance Methods
33
+ ### `usage()` <a id="method-i-usage"></a> <a id="usage-instance_method"></a>
34
+ Session totals, summed per message. No dedup is needed: one row is one API
35
+ response, and the sum was verified against the store's own per-session rollup
36
+ columns — 9,727,437 input / 94,266 output / 22,184,157 cache-read, exactly
37
+ equal both ways on the real store.
@@ -0,0 +1,38 @@
1
+ # Class Agent::Sessions::Readers::Pi <a id="class-Agent-Sessions-Readers-Pi"></a>
2
+
3
+ | | |
4
+ | --- | --- |
5
+ | **Inherits** | [Agent::Sessions::Readers::Base](Base.md) |
6
+ | **Defined in** | lib/agent/sessions/readers/pi.rb |
7
+
8
+ pi session files. PROVISIONAL in a way no other reader is: this machine holds
9
+ nine real pi project directories and zero session files inside them
10
+ (2026-08-24), so every mapping below is written against tokentelemetry's
11
+ working parser of the same format (resources/tokentelemetry, backend/main.py,
12
+ _scan_pi_sessions) rather than a corpus of pi's own output. That is
13
+ observation of running code, not of data — one step better than the design
14
+ doc's prose, one step short of every other reader's evidence. Where the two
15
+ could disagree, rule 2 already decides the outcome: a shape this reader has
16
+ not seen becomes an :unknown part and a warning, never an exception, and raw
17
+ carries what really happened.
18
+
19
+ The format per that parser: a header record {"type":"session", id, cwd,
20
+ timestamp}, then typed records — "model_change" (provider, modelId) and
21
+ "message" ({role, model, content[], usage}). usage spells its keys camelCase
22
+ (cacheRead, cacheWrite) and carries agent-computed cost.
23
+
24
+ ## Constants
25
+ ### `NON_MESSAGE_TYPES` <a id="constant-NON_MESSAGE_TYPES"></a> <a id="NON_MESSAGE_TYPES-constant"></a>
26
+ The header and settings records are session state, not conversation —the same
27
+ judgement Codex's session_meta gets. model_change is state too: the model a
28
+ LATER message used is on that message.
29
+
30
+ ### `ROLES` <a id="constant-ROLES"></a> <a id="ROLES-constant"></a>
31
+ Not documented.
32
+
33
+ ## Public Instance Methods
34
+ ### `usage()` <a id="method-i-usage"></a> <a id="usage-instance_method"></a>
35
+ Session totals, summed per message record. No dedup: nothing observed or
36
+ reported suggests pi repeats one response across records the way Claude does —
37
+ but nothing proves it either, so if pi totals ever read roughly double a
38
+ provider's bill, this is where to look.
@@ -0,0 +1,30 @@
1
+ # Class Agent::Sessions::Readers::Qwen <a id="class-Agent-Sessions-Readers-Qwen"></a>
2
+
3
+ | | |
4
+ | --- | --- |
5
+ | **Inherits** | [Agent::Sessions::Readers::Base](Base.md) |
6
+ | **Defined in** | lib/agent/sessions/readers/qwen.rb |
7
+
8
+ Qwen Code chat files. PROVISIONAL, like the adapter: written against
9
+ tokentelemetry's parser of this format, not against real Qwen output.
10
+
11
+ The record shape is Anthropic's — type user/assistant, message.content as an
12
+ array of typed parts, message.usage with the same five spellings Claude uses.
13
+ This is deliberately NOT a subclass of Readers::Claude despite that overlap:
14
+ Claude's reader also carries Claude Code's sidecar machinery (spilled tool
15
+ output, subagent transcripts, uuid/parentUuid branching), none of which is
16
+ known to exist here, and inheriting would mean disabling each one and then
17
+ re-checking every future Claude change against an agent nobody can test. Two
18
+ readers with two evidence bases will drift honestly; one reader pretending to
19
+ serve both will drift silently.
20
+
21
+ ## Constants
22
+ ### `CONTENT_PARTS` <a id="constant-CONTENT_PARTS"></a> <a id="CONTENT_PARTS-constant"></a>
23
+ Not documented.
24
+
25
+ ## Public Instance Methods
26
+ ### `usage()` <a id="method-i-usage"></a> <a id="usage-instance_method"></a>
27
+ Session totals, deduplicated by message.id the way Claude's are: the same API
28
+ response can stream into one record per content block, and both agents speak
29
+ the same wire format. Unverified for Qwen — if its writer does not repeat ids,
30
+ this dedup is simply a no-op.
@@ -0,0 +1,5 @@
1
+ # Module Agent::Sessions::Readers <a id="module-Agent-Sessions-Readers"></a>
2
+
3
+ | | |
4
+ | --- | --- |
5
+ | **Defined in** | lib/agent/sessions/readers/pi.rb, lib/agent/sessions/readers/amp.rb, lib/agent/sessions/readers/base.rb, lib/agent/sessions/readers/grok.rb, lib/agent/sessions/readers/qwen.rb, lib/agent/sessions/readers/codex.rb, lib/agent/sessions/readers/claude.rb, lib/agent/sessions/readers/gemini.rb, lib/agent/sessions/readers/copilot.rb, lib/agent/sessions/readers/opencode.rb |
@@ -0,0 +1,73 @@
1
+ # Class Agent::Sessions::Session <a id="class-Agent-Sessions-Session"></a>
2
+
3
+ | | |
4
+ | --- | --- |
5
+ | **Inherits** | Object |
6
+ | **Defined in** | lib/agent/sessions/session.rb |
7
+
8
+ One recorded conversation. Everything here comes from a stat or the store's
9
+ own metadata — except project_path, which may need to read inside the session
10
+ file (design doc section 7: the on-disk directory encodings are lossy, so the
11
+ recorded cwd inside the file is the only reliable source). It is computed on
12
+ first access and memoized, which is why this is a plain class rather than a
13
+ frozen Data: an instance is immutable except for that one memo.
14
+
15
+ Equality is identity, not value — unlike every sibling value object here,
16
+ which gets value equality for free from Data. A value comparison would have to
17
+ force project_path on both operands, turning a `uniq` over thousands of
18
+ sessions into the full content sweep the design works to avoid. A caller
19
+ keying a mixed-agent collection should use `uid`, which exists for exactly
20
+ that. Note `to_h` includes `uid`, so its output does not round-trip back
21
+ through `new`.
22
+
23
+ ## Constants
24
+ ### `UNRESOLVED` <a id="constant-UNRESOLVED"></a> <a id="UNRESOLVED-constant"></a>
25
+ Not documented.
26
+
27
+ ## Attributes
28
+ ### `agent` [R] <a id="attribute-i-agent"></a> <a id="agent-instance_method"></a>
29
+ Returns the value of attribute agent.
30
+
31
+ ### `bytes` [R] <a id="attribute-i-bytes"></a> <a id="bytes-instance_method"></a>
32
+ Returns the value of attribute bytes.
33
+
34
+ ### `fidelity` [R] <a id="attribute-i-fidelity"></a> <a id="fidelity-instance_method"></a>
35
+ Returns the value of attribute fidelity.
36
+
37
+ ### `format` [R] <a id="attribute-i-format"></a> <a id="format-instance_method"></a>
38
+ Returns the value of attribute format.
39
+
40
+ ### `id` [R] <a id="attribute-i-id"></a> <a id="id-instance_method"></a>
41
+ Returns the value of attribute id.
42
+
43
+ ### `path` [R] <a id="attribute-i-path"></a> <a id="path-instance_method"></a>
44
+ Returns the value of attribute path.
45
+
46
+ ### `started_at` [R] <a id="attribute-i-started_at"></a> <a id="started_at-instance_method"></a>
47
+ Returns the value of attribute started_at.
48
+
49
+ ### `updated_at` [R] <a id="attribute-i-updated_at"></a> <a id="updated_at-instance_method"></a>
50
+ Returns the value of attribute updated_at.
51
+
52
+ ## Public Instance Methods
53
+ ### `initialize(agent:, id:, path:, started_at:, updated_at:, bytes:, format:, fidelity:, project_path: UNRESOLVED, &project_path_resolver)` <a id="method-i-initialize"></a> <a id="initialize-instance_method"></a>
54
+ - **@return** [Session] a new instance of Session
55
+
56
+ ### `inspect()` <a id="method-i-inspect"></a> <a id="inspect-instance_method"></a>
57
+ Never calls project_path: inspecting a session in a debugger must not trigger
58
+ the read that enumeration deliberately deferred.
59
+
60
+ ### `project_path()` <a id="method-i-project_path"></a> <a id="project_path-instance_method"></a>
61
+ A resolver that raises is deliberately not memoized: a failed read is not an
62
+ answer, so the next call retries rather than freezing the failure in place.
63
+
64
+ Not thread-safe by design: concurrent first access can run the resolver more
65
+ than once, but every run yields the same value and the assignment is atomic on
66
+ MRI, so there is no torn read to guard against. Do not add a mutex.
67
+
68
+ ### `to_h()` <a id="method-i-to_h"></a> <a id="to_h-instance_method"></a>
69
+ The honest full dump — includes project_path, so it forces that read. Callers
70
+ listing thousands of sessions should build their own slimmer rows.
71
+
72
+ ### `uid()` <a id="method-i-uid"></a> <a id="uid-instance_method"></a>
73
+ Collision-free across a mixed-agent collection, where bare ids may repeat.
@@ -0,0 +1,43 @@
1
+ # Module Agent::Sessions::Sqlite <a id="module-Agent-Sessions-Sqlite"></a>
2
+
3
+ | | |
4
+ | --- | --- |
5
+ | **Defined in** | lib/agent/sessions/sqlite.rb |
6
+
7
+ The one way this gem opens a SQLite store: read-only, URI-escaped, with a
8
+ bounded retry against a live writer's lock. Extracted from the opencode
9
+ adapter when the opencode reader became its second caller — two copies of
10
+ escape_uri_path would drift, and the bug it guards is subtle enough that a
11
+ drifted copy would look correct in review.
12
+
13
+ What differs between callers stays with them: the adapter raises
14
+ UnreadableStore on a query failure because a vanished database means no
15
+ sessions at all, while a reader warns and yields nothing because one
16
+ unreadable session must not take down a sweep (Layer 3 rule 2). This module
17
+ only opens; it never decides what a failure means.
18
+
19
+ ## Public Class Methods
20
+ ### `escape_uri_path(path)` <a id="method-c-escape_uri_path"></a> <a id="escape_uri_path-class_method"></a>
21
+ IMPORTANT, caught in review: SQLite's URI parser gives `%`, `#` and `?`
22
+ syntactic meaning, and the path was being interpolated raw. `#` starts a
23
+ fragment (silently truncating the path there); `?` starts the query string,
24
+ colliding with the `?mode=ro` open_readonly appends. The worst case, confirmed
25
+ directly: a path segment that merely CONTAINS a valid-looking percent-escape —
26
+ a directory literally named "a%23b" —gets that escape DECODED by the URI
27
+ parser into a different path ("a#b"), so a second, unrelated database sitting
28
+ at THAT path is read instead, silently, with no exception at all. One pass,
29
+ not two sequential gsubs: escaping # to %23 and THEN escaping the % that
30
+ produced would double-encode it to %2523.
31
+
32
+ ### `open_readonly(path)` <a id="method-c-open_readonly"></a> <a id="open_readonly-class_method"></a>
33
+ No immutable=1: it tells SQLite to trust that the file will never change and
34
+ skip locking AND the WAL entirely — against a live, WAL-mode opencode.db that
35
+ means silently missing every committed-but-not-yet- checkpointed session.
36
+ Opening a WAL db even read-only touches its -shm and -wal sidecars (SQLite's
37
+ own reader bookkeeping, confirmed directly); the recorded sessions themselves
38
+ are never written.
39
+
40
+ busy_timeout gives SQLite up to 5s to retry internally against a lock held by
41
+ the agent's own live writer. A WAL writer's lock is normally held only for the
42
+ instant of a commit, so a lock that has not cleared within 5s is a stuck
43
+ process, not ordinary contention.
@@ -0,0 +1,57 @@
1
+ # Class Agent::Sessions::Store <a id="class-Agent-Sessions-Store"></a>
2
+
3
+ | | |
4
+ | --- | --- |
5
+ | **Inherits** | Data |
6
+ | **Defined in** | lib/agent/sessions/store.rb |
7
+
8
+ ## Attributes
9
+ ### `agent` [R] <a id="attribute-i-agent"></a> <a id="agent-instance_method"></a>
10
+ Returns the value of attribute agent
11
+ - **@return** [Object] the current value of agent
12
+
13
+ ### `documented` [R] <a id="attribute-i-documented"></a> <a id="documented-instance_method"></a>
14
+ Returns the value of attribute documented
15
+ - **@return** [Object] the current value of documented
16
+
17
+ ### `effective` [R] <a id="attribute-i-effective"></a> <a id="effective-instance_method"></a>
18
+ Returns the value of attribute effective
19
+ - **@return** [Object] the current value of effective
20
+
21
+ ### `env_overrides` [R] <a id="attribute-i-env_overrides"></a> <a id="env_overrides-instance_method"></a>
22
+ Returns the value of attribute env_overrides
23
+ - **@return** [Object] the current value of env_overrides
24
+
25
+ ### `label` [R] <a id="attribute-i-label"></a> <a id="label-instance_method"></a>
26
+ Returns the value of attribute label
27
+ - **@return** [Object] the current value of label
28
+
29
+ ### `layers` [R] <a id="attribute-i-layers"></a> <a id="layers-instance_method"></a>
30
+ Returns the value of attribute layers
31
+ - **@return** [Object] the current value of layers
32
+
33
+ ### `retention` [R] <a id="attribute-i-retention"></a> <a id="retention-instance_method"></a>
34
+ Returns the value of attribute retention
35
+ - **@return** [Object] the current value of retention
36
+
37
+ ### `retention_source` [R] <a id="attribute-i-retention_source"></a> <a id="retention_source-instance_method"></a>
38
+ Returns the value of attribute retention_source
39
+ - **@return** [Object] the current value of retention_source
40
+
41
+ ### `verified_on` [R] <a id="attribute-i-verified_on"></a> <a id="verified_on-instance_method"></a>
42
+ Returns the value of attribute verified_on
43
+ - **@return** [Object] the current value of verified_on
44
+
45
+ ### `warnings` [R] <a id="attribute-i-warnings"></a> <a id="warnings-instance_method"></a>
46
+ Returns the value of attribute warnings
47
+ - **@return** [Object] the current value of warnings
48
+
49
+ ## Public Instance Methods
50
+ ### `documented?()` <a id="method-i-documented-3F"></a> <a id="documented?-instance_method"></a>
51
+ - **@return** [Boolean]
52
+
53
+ ### `format()` <a id="method-i-format"></a> <a id="format-instance_method"></a>
54
+ Not documented.
55
+
56
+ ### `installed?()` <a id="method-i-installed-3F"></a> <a id="installed?-instance_method"></a>
57
+ - **@return** [Boolean]
@@ -0,0 +1,6 @@
1
+ # Class Agent::Sessions::UnknownAgent <a id="class-Agent-Sessions-UnknownAgent"></a>
2
+
3
+ | | |
4
+ | --- | --- |
5
+ | **Inherits** | [Agent::Sessions::Error](Error.md) |
6
+ | **Defined in** | lib/agent/sessions/unknown_agent.rb |
@@ -0,0 +1,6 @@
1
+ # Class Agent::Sessions::UnreadableStore <a id="class-Agent-Sessions-UnreadableStore"></a>
2
+
3
+ | | |
4
+ | --- | --- |
5
+ | **Inherits** | [Agent::Sessions::Error](Error.md) |
6
+ | **Defined in** | lib/agent/sessions/unreadable_store.rb |
@@ -0,0 +1,6 @@
1
+ # Class Agent::Sessions::UnsupportedFormat <a id="class-Agent-Sessions-UnsupportedFormat"></a>
2
+
3
+ | | |
4
+ | --- | --- |
5
+ | **Inherits** | [Agent::Sessions::Error](Error.md) |
6
+ | **Defined in** | lib/agent/sessions/unsupported_format.rb |
@@ -0,0 +1,56 @@
1
+ # Class Agent::Sessions::Usage <a id="class-Agent-Sessions-Usage"></a>
2
+
3
+ | | |
4
+ | --- | --- |
5
+ | **Inherits** | Data |
6
+ | **Defined in** | lib/agent/sessions/usage.rb |
7
+
8
+ Token counts an agent reported, for one message or one whole session,
9
+ normalized to five DISJOINT buckets: `input` never includes what was read from
10
+ or written to cache, and `output` never includes `reasoning`. Agents disagree
11
+ here — Codex's input_tokens includes its cached_input_tokens, Claude's does
12
+ not (both verified against real stores on this machine, 2026-08-24) — and a
13
+ caller summing across agents needs one rule, not one per agent. Readers do the
14
+ subtraction; this object only holds the result.
15
+
16
+ nil means "this format does not record that dimension", and it is load-
17
+ bearing: absence must never read as zero, for the same reason
18
+ Agent::Sessions.read raises on a format with no reader. `cost` is reported by
19
+ the agent or absent — never derived from a pricing table, which would go stale
20
+ in a gem and is a consumer's decision anyway.
21
+
22
+ ## Attributes
23
+ ### `cache_creation` [R] <a id="attribute-i-cache_creation"></a> <a id="cache_creation-instance_method"></a>
24
+ Returns the value of attribute cache_creation
25
+ - **@return** [Object] the current value of cache_creation
26
+
27
+ ### `cache_read` [R] <a id="attribute-i-cache_read"></a> <a id="cache_read-instance_method"></a>
28
+ Returns the value of attribute cache_read
29
+ - **@return** [Object] the current value of cache_read
30
+
31
+ ### `cost` [R] <a id="attribute-i-cost"></a> <a id="cost-instance_method"></a>
32
+ Returns the value of attribute cost
33
+ - **@return** [Object] the current value of cost
34
+
35
+ ### `input` [R] <a id="attribute-i-input"></a> <a id="input-instance_method"></a>
36
+ Returns the value of attribute input
37
+ - **@return** [Object] the current value of input
38
+
39
+ ### `output` [R] <a id="attribute-i-output"></a> <a id="output-instance_method"></a>
40
+ Returns the value of attribute output
41
+ - **@return** [Object] the current value of output
42
+
43
+ ### `reasoning` [R] <a id="attribute-i-reasoning"></a> <a id="reasoning-instance_method"></a>
44
+ Returns the value of attribute reasoning
45
+ - **@return** [Object] the current value of reasoning
46
+
47
+ ## Public Instance Methods
48
+ ### `+(other)` <a id="method-i--2B"></a> <a id="+-instance_method"></a>
49
+ Sums dimension-wise, keeping the nil/zero distinction: nil + nil stays nil
50
+ ("neither side records this"), nil + n is n — one recorded value is a real
51
+ value, not a value plus an unknown, because per-message absence under a format
52
+ that does record the dimension means "none reported for this message", the one
53
+ place absence and zero do coincide.
54
+
55
+ ### `initialize(input: nil, output: nil, cache_read: nil, cache_creation: nil, reasoning: nil, cost: nil)` <a id="method-i-initialize"></a> <a id="initialize-instance_method"></a>
56
+ - **@return** [Usage] a new instance of Usage