agent_sessions 0.3.0 → 0.4.0

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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +16 -0
  3. data/README.md +87 -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 +122 -0
  33. data/doc/Agent/Sessions/Readers/Claude.md +80 -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/RoundTrip.md +57 -0
  43. data/doc/Agent/Sessions/Session.md +73 -0
  44. data/doc/Agent/Sessions/Sqlite.md +43 -0
  45. data/doc/Agent/Sessions/Store.md +57 -0
  46. data/doc/Agent/Sessions/UnknownAgent.md +6 -0
  47. data/doc/Agent/Sessions/UnreadableStore.md +6 -0
  48. data/doc/Agent/Sessions/UnsupportedFormat.md +6 -0
  49. data/doc/Agent/Sessions/Usage.md +56 -0
  50. data/doc/Agent/Sessions.md +178 -0
  51. data/doc/Agent.md +5 -0
  52. data/doc/CHANGELOG.md +77 -0
  53. data/doc/README.md +182 -0
  54. data/doc/index.csv +330 -0
  55. data/exe/agent-sessions +0 -2
  56. data/lib/agent/sessions/adapters/cursor.rb +3 -3
  57. data/lib/agent/sessions/cli.rb +2 -2
  58. data/lib/agent/sessions/message.rb +1 -2
  59. data/lib/agent/sessions/part.rb +1 -2
  60. data/lib/agent/sessions/readers/base.rb +95 -0
  61. data/lib/agent/sessions/readers/claude.rb +10 -0
  62. data/lib/agent/sessions/round_trip.rb +39 -0
  63. data/lib/agent/sessions/version.rb +1 -1
  64. data/llm.txt +178 -0
  65. metadata +56 -2
@@ -0,0 +1,178 @@
1
+ # Module Agent::Sessions <a id="module-Agent-Sessions"></a>
2
+
3
+ | | |
4
+ | --- | --- |
5
+ | **Defined in** | lib/agent/sessions.rb, lib/agent/sessions/cli.rb, lib/agent/sessions/node.rb, lib/agent/sessions/part.rb, lib/agent/sessions/audit.rb, lib/agent/sessions/check.rb, lib/agent/sessions/error.rb, lib/agent/sessions/store.rb, lib/agent/sessions/usage.rb, lib/agent/sessions/sqlite.rb, lib/agent/sessions/message.rb, lib/agent/sessions/session.rb, lib/agent/sessions/version.rb, lib/agent/sessions/location.rb, lib/agent/sessions/compaction.rb, lib/agent/sessions/readers/pi.rb, lib/agent/sessions/round_trip.rb, lib/agent/sessions/adapters/pi.rb, lib/agent/sessions/readers/amp.rb, lib/agent/sessions/adapters/amp.rb, lib/agent/sessions/env_override.rb, lib/agent/sessions/readers/base.rb, lib/agent/sessions/readers/grok.rb, lib/agent/sessions/readers/qwen.rb, lib/agent/sessions/adapters/base.rb, lib/agent/sessions/adapters/grok.rb, lib/agent/sessions/adapters/qwen.rb, lib/agent/sessions/readers/codex.rb, lib/agent/sessions/unknown_agent.rb, lib/agent/sessions/adapters/codex.rb, lib/agent/sessions/home_expansion.rb, lib/agent/sessions/readers/claude.rb, lib/agent/sessions/readers/gemini.rb, lib/agent/sessions/adapters/claude.rb, lib/agent/sessions/adapters/cursor.rb, lib/agent/sessions/adapters/gemini.rb, lib/agent/sessions/readers/copilot.rb, lib/agent/sessions/adapters/copilot.rb, lib/agent/sessions/readers/opencode.rb, lib/agent/sessions/unreadable_store.rb, lib/agent/sessions/adapters/opencode.rb, lib/agent/sessions/missing_dependency.rb, lib/agent/sessions/unsupported_format.rb, lib/agent/sessions/adapters/cursor_ide.rb, lib/agent/sessions/adapters/enumeration.rb |
6
+
7
+ ## Constants
8
+ ### `LOADER` <a id="constant-LOADER"></a> <a id="LOADER-constant"></a>
9
+ Not documented.
10
+
11
+ ### `STALE_AFTER_DAYS` <a id="constant-STALE_AFTER_DAYS"></a> <a id="STALE_AFTER_DAYS-constant"></a>
12
+ Not documented.
13
+
14
+ ### `VERIFIED_ON` <a id="constant-VERIFIED_ON"></a> <a id="VERIFIED_ON-constant"></a>
15
+ The oldest verified_on among the built-in adapters. A claim about somebody
16
+ else's software is only as current as its weakest link, so this is the honest
17
+ answer to "when was this last known to be true".
18
+
19
+ ### `VERSION` <a id="constant-VERSION"></a> <a id="VERSION-constant"></a>
20
+ Not documented.
21
+
22
+ ## Public Class Methods
23
+ ### `agents()` <a id="method-c-agents"></a> <a id="agents-class_method"></a>
24
+ Not documented.
25
+
26
+ ### `all(env: ENV)` <a id="method-c-all"></a> <a id="all-class_method"></a>
27
+ Not documented.
28
+
29
+ ### `audit(env: ENV)` <a id="method-c-audit"></a> <a id="audit-class_method"></a>
30
+ Not documented.
31
+
32
+ ### `doctor(agent = nil, env: ENV, today: Date.today)` <a id="method-c-doctor"></a> <a id="doctor-class_method"></a>
33
+ Not documented.
34
+
35
+ ### `for_project(dir, env: ENV, agents: nil)` <a id="method-c-for_project"></a> <a id="for_project-class_method"></a>
36
+ One project across every agent (or the agents: subset), lazily: adapters
37
+ earlier in the sweep satisfy `first(n)` without the later ones ever being
38
+ asked. Within one adapter, though, laziness cannot skip non-matching sessions
39
+ — sessions_for_project must still stat and check each candidate to know it
40
+ does not match, so an adapter with zero matches costs a full scan of its store
41
+ before the sweep moves on. True of the six Base-driven adapters; opencode
42
+ pushes the filter into SQL (WHERE directory = ?) and stats nothing.
43
+
44
+ Deliberately does NOT rescue MissingDependency or UnreadableStore: opencode
45
+ without the sqlite3 gem, or with a corrupt/locked database, raises. Since
46
+ `flat_map` is lazy, that raise surfaces only once enumeration reaches the
47
+ failing adapter — possibly after other agents' sessions have already been
48
+ yielded to the caller mid-iteration, and possibly not at all if `first(n)` is
49
+ satisfied first. Silently omitting an agent's sessions is this gem's worst
50
+ failure mode (design doc decision 11), so this method never trades a raised,
51
+ attributable error for a quietly incomplete list. A caller that wants the
52
+ sweep to survive one bad agent should rescue per call, e.g. by driving
53
+ <code>agents:</code> itself and catching around each adapter; a caller who
54
+ just wants to route around a known-bad agent can pass <code>agents:</code>
55
+ naming every registered agent except it. The CLI (Task 10) does the former,
56
+ turning the same exceptions into per-agent "skipped" lines instead of one
57
+ failed sweep. Note a rescue cannot resume this enumerator: re-calling each
58
+ after a raise re-raises from the same adapter. The only recovery is a fresh
59
+ call with a narrower <code>agents:</code>. Adapters are resolved eagerly, so
60
+ an unknown name in <code>agents:</code> raises here rather than mid-sweep. The
61
+ deferral above is about DATA conditions, where the raise carries information
62
+ about a store; a typo'd agent symbol is a programmer error knowable before any
63
+ I/O, and `agents: [:claude, :nope]` otherwise hands back Claude's sessions and
64
+ then crashes. Base#initialize only stores @env, so constructing all seven up
65
+ front costs nothing, and the sweep stays lazy — first(n) still stops at the
66
+ first matching adapter.
67
+
68
+ ### `installed(env: ENV)` <a id="method-c-installed"></a> <a id="installed-class_method"></a>
69
+ Not documented.
70
+
71
+ ### `locate(agent, env: ENV)` <a id="method-c-locate"></a> <a id="locate-class_method"></a>
72
+ Not documented.
73
+
74
+ ### `projects(agent, env: ENV)` <a id="method-c-projects"></a> <a id="projects-class_method"></a>
75
+ Eager, unlike sessions/for_project: project_paths already reads every session
76
+ to answer (design doc section 7 — the on-disk encodings are lossy, so the
77
+ recorded cwd is the only reliable source), sorts, and dedupes, so a lazy
78
+ return type here would promise a laziness the work underneath cannot honor.
79
+ Returns a plain, already-sorted Array. Raises MissingDependency or
80
+ UnreadableStore for opencode, as sessions does.
81
+
82
+ ### `read(session, **options)` <a id="method-c-read"></a> <a id="read-class_method"></a>
83
+ Layer 3. Takes a Session (from `sessions`, `for_project`), not an agent name,
84
+ because reading is per-session — the adapter comes from the session itself.
85
+ Raises UnsupportedFormat for an agent with no reader yet, rather than
86
+ returning a reader that yields nothing: "this gem cannot read that format" and
87
+ "that session has no messages" must never look alike.
88
+
89
+ include_events: true adds the agent's UI-level records to the stream where an
90
+ adapter has them. They are excluded by default because they are bookkeeping,
91
+ not conversation, and they outnumber real messages.
92
+
93
+ ### `register(adapter_class)` <a id="method-c-register"></a> <a id="register-class_method"></a>
94
+ Re-registering a name deliberately replaces it, so a consumer can ship a
95
+ corrected adapter for an agent whose layout moved before the gem catches up.
96
+ - **@raise** [Error]
97
+
98
+ ### `registry()` <a id="method-c-registry"></a> <a id="registry-class_method"></a>
99
+ Not documented.
100
+
101
+ ### `sessions(agent, env: ENV, since: nil)` <a id="method-c-sessions"></a> <a id="sessions-class_method"></a>
102
+ Lazy: consuming N sessions stats N files, never more. `since`, when given,
103
+ must be a Time (or anything Time#>= accepts) — comparing updated_at (always a
104
+ Time; every adapter populates it, from mtime or store metadata) against a
105
+ Date, Integer, or String raises ArgumentError("comparison of Time with ...
106
+ failed"), which already names the mistake, so no extra guard is added here.
107
+ That raise happens on enumeration, not on this call, because the filter itself
108
+ is lazy — `sessions(:x, since: bad).first(1)` can raise from inside `first`,
109
+ not from this line. Raises MissingDependency or UnreadableStore for opencode
110
+ under the same conditions described on for_project below.
111
+
112
+ ### `unresolved_project_count(agent, env: ENV)` <a id="method-c-unresolved_project_count"></a> <a id="unresolved_project_count-class_method"></a>
113
+ Companion to `projects`/`project_paths`, which both exclude a session whose
114
+ project could not be resolved rather than counting it (design doc section 7) —
115
+ so "this agent genuinely records no projects" and "this agent's project
116
+ resolution is broken" read identically from the outside. Three of seven
117
+ adapters can legitimately return a nil project_path (Amp threads with no
118
+ `trees`, cursor_ide by design, pi whenever its unverified header assumption is
119
+ wrong); this counts it for any of them, uniformly, using only the public
120
+ `sessions` enumerator —no adapter needs to know this exists. Eager and a
121
+ second full sweep of the store, same cost class as `projects` itself, so it is
122
+ opt-in (called by the CLI only under `list --project`, never under plain
123
+ `list`) rather than folded into `projects`' own return value, which is a
124
+ documented, tested plain Array and would otherwise need a shape change to
125
+ carry both numbers. Raises MissingDependency or UnreadableStore for opencode,
126
+ as sessions does.
127
+
128
+ ### `verify(agent = nil, env: ENV)` <a id="method-c-verify"></a> <a id="verify-class_method"></a>
129
+ Not documented.
130
+
131
+ # Documentation
132
+
133
+ - [Sessions/Adapters.md](Sessions/Adapters.md)
134
+ - [Sessions/Adapters/Amp.md](Sessions/Adapters/Amp.md)
135
+ - [Sessions/Adapters/Base.md](Sessions/Adapters/Base.md)
136
+ - [Sessions/Adapters/Claude.md](Sessions/Adapters/Claude.md)
137
+ - [Sessions/Adapters/Codex.md](Sessions/Adapters/Codex.md)
138
+ - [Sessions/Adapters/Copilot.md](Sessions/Adapters/Copilot.md)
139
+ - [Sessions/Adapters/Cursor.md](Sessions/Adapters/Cursor.md)
140
+ - [Sessions/Adapters/CursorIde.md](Sessions/Adapters/CursorIde.md)
141
+ - [Sessions/Adapters/Enumeration.md](Sessions/Adapters/Enumeration.md)
142
+ - [Sessions/Adapters/Gemini.md](Sessions/Adapters/Gemini.md)
143
+ - [Sessions/Adapters/Grok.md](Sessions/Adapters/Grok.md)
144
+ - [Sessions/Adapters/Opencode.md](Sessions/Adapters/Opencode.md)
145
+ - [Sessions/Adapters/Pi.md](Sessions/Adapters/Pi.md)
146
+ - [Sessions/Adapters/Qwen.md](Sessions/Adapters/Qwen.md)
147
+ - [Sessions/Audit.md](Sessions/Audit.md)
148
+ - [Sessions/Audit/Finding.md](Sessions/Audit/Finding.md)
149
+ - [Sessions/CLI.md](Sessions/CLI.md)
150
+ - [Sessions/Check.md](Sessions/Check.md)
151
+ - [Sessions/Compaction.md](Sessions/Compaction.md)
152
+ - [Sessions/EnvOverride.md](Sessions/EnvOverride.md)
153
+ - [Sessions/Error.md](Sessions/Error.md)
154
+ - [Sessions/HomeExpansion.md](Sessions/HomeExpansion.md)
155
+ - [Sessions/Location.md](Sessions/Location.md)
156
+ - [Sessions/Message.md](Sessions/Message.md)
157
+ - [Sessions/MissingDependency.md](Sessions/MissingDependency.md)
158
+ - [Sessions/Node.md](Sessions/Node.md)
159
+ - [Sessions/Part.md](Sessions/Part.md)
160
+ - [Sessions/Readers.md](Sessions/Readers.md)
161
+ - [Sessions/Readers/Amp.md](Sessions/Readers/Amp.md)
162
+ - [Sessions/Readers/Base.md](Sessions/Readers/Base.md)
163
+ - [Sessions/Readers/Claude.md](Sessions/Readers/Claude.md)
164
+ - [Sessions/Readers/Codex.md](Sessions/Readers/Codex.md)
165
+ - [Sessions/Readers/Copilot.md](Sessions/Readers/Copilot.md)
166
+ - [Sessions/Readers/Gemini.md](Sessions/Readers/Gemini.md)
167
+ - [Sessions/Readers/Grok.md](Sessions/Readers/Grok.md)
168
+ - [Sessions/Readers/Opencode.md](Sessions/Readers/Opencode.md)
169
+ - [Sessions/Readers/Pi.md](Sessions/Readers/Pi.md)
170
+ - [Sessions/Readers/Qwen.md](Sessions/Readers/Qwen.md)
171
+ - [Sessions/RoundTrip.md](Sessions/RoundTrip.md)
172
+ - [Sessions/Session.md](Sessions/Session.md)
173
+ - [Sessions/Sqlite.md](Sessions/Sqlite.md)
174
+ - [Sessions/Store.md](Sessions/Store.md)
175
+ - [Sessions/UnknownAgent.md](Sessions/UnknownAgent.md)
176
+ - [Sessions/UnreadableStore.md](Sessions/UnreadableStore.md)
177
+ - [Sessions/UnsupportedFormat.md](Sessions/UnsupportedFormat.md)
178
+ - [Sessions/Usage.md](Sessions/Usage.md)
data/doc/Agent.md ADDED
@@ -0,0 +1,5 @@
1
+ # Module Agent <a id="module-Agent"></a>
2
+
3
+ | | |
4
+ | --- | --- |
5
+ | **Defined in** | lib/agent/sessions.rb, lib/agent/sessions/cli.rb, lib/agent/sessions/node.rb, lib/agent/sessions/part.rb, lib/agent/sessions/audit.rb, lib/agent/sessions/check.rb, lib/agent/sessions/error.rb, lib/agent/sessions/store.rb, lib/agent/sessions/usage.rb, lib/agent/sessions/sqlite.rb, lib/agent/sessions/message.rb, lib/agent/sessions/session.rb, lib/agent/sessions/version.rb, lib/agent/sessions/location.rb, lib/agent/sessions/compaction.rb, lib/agent/sessions/readers/pi.rb, lib/agent/sessions/round_trip.rb, lib/agent/sessions/adapters/pi.rb, lib/agent/sessions/readers/amp.rb, lib/agent/sessions/adapters/amp.rb, lib/agent/sessions/env_override.rb, lib/agent/sessions/readers/base.rb, lib/agent/sessions/readers/grok.rb, lib/agent/sessions/readers/qwen.rb, lib/agent/sessions/adapters/base.rb, lib/agent/sessions/adapters/grok.rb, lib/agent/sessions/adapters/qwen.rb, lib/agent/sessions/readers/codex.rb, lib/agent/sessions/unknown_agent.rb, lib/agent/sessions/adapters/codex.rb, lib/agent/sessions/home_expansion.rb, lib/agent/sessions/readers/claude.rb, lib/agent/sessions/readers/gemini.rb, lib/agent/sessions/adapters/claude.rb, lib/agent/sessions/adapters/cursor.rb, lib/agent/sessions/adapters/gemini.rb, lib/agent/sessions/readers/copilot.rb, lib/agent/sessions/adapters/copilot.rb, lib/agent/sessions/readers/opencode.rb, lib/agent/sessions/unreadable_store.rb, lib/agent/sessions/adapters/opencode.rb, lib/agent/sessions/missing_dependency.rb, lib/agent/sessions/unsupported_format.rb, lib/agent/sessions/adapters/cursor_ide.rb, lib/agent/sessions/adapters/enumeration.rb |
data/doc/CHANGELOG.md ADDED
@@ -0,0 +1,77 @@
1
+ ## 0.4.0 (2026-09-10)
2
+
3
+ - Round-trip grouping in every reader: `RoundTrip` (`index`, `messages`, `usage`, `recorded`, plus `parts`, `calls`, `roles`), `reader.each_round_trip` (streams), `reader.round_trips` (eager), `reader.round_trips_recorded?`
4
+ - Claude names its round-trip id — `message.id`, the id Anthropic's API gives one response — and is the only reader today where `round_trips_recorded?` answers true; every other reader falls back to one round trip per message
5
+ - The reappearing-id warning fires only when nothing explains the split. Measured over the 60 most recent real Claude transcripts (2026-09-10): 3,315 distinct message ids, 261 of them (7.9%) split across more than one run in 33 of the 60 files; 759 of those splits are a tool_result record, 66 a last-prompt, 12 a file-history-delta — all benign, so warning on them would put four or five lines under every real session
6
+ - Conformance suite case C9: `round_trips` must never lose or duplicate a message, across every reader
7
+ - The loop view (`Loop`, `ToolCall`, `LoopView`) and its CLI command (`show`) moved to the `agent_session_context` gem before release, so they never shipped here
8
+
9
+ ## 0.3.1 (2026-08-26)
10
+
11
+ - Add tracked Markdown YARD documentation and a consolidated `llm.txt`, package both with the gem, and validate every local documentation link
12
+ - Add a deterministic, warning-free documentation generator and a fail-fast `bin/prepare_release` workflow that tests, rebuilds documentation, refreshes `llm.txt`, and builds without publishing
13
+ - Verify the supported Ruby floor and current release in CI on Ruby 3.2 and 4.0.1, and harden gem builds with a Git-independent manifest plus isolated installation and CLI checks
14
+ - Strengthen adapter conformance coverage and remove order dependence from the affected tests and fixtures
15
+ - Keep YARD tooling development-only; runtime dependencies remain `agent_homedir` and `zeitwerk`
16
+
17
+ ## 0.3.0 (2026-08-25)
18
+
19
+ - Breaking: the Ruby namespace is now `Agent::Sessions`; `AgentSessions` is removed with no alias. `require "agent_sessions"` still works through the shim and defines the new namespace
20
+ - Base-dir resolution now delegates to `agent_homedir` and runs through a per-instance resolver instead of this gem's own expansion rules
21
+ - Relative base-dir environment overrides now join `HOME` instead of the current working directory, `~user` no longer stays literal, and whitespace-only overrides fall back to adapter defaults
22
+ - `cursor_ide` now honors `XDG_CONFIG_HOME` on Linux and `APPDATA` on Windows
23
+ - Blank or non-absolute XDG overrides now fall back to defaults for Amp and opencode, and `OPENCODE_DATA_DIR` now appears in `env_overrides`
24
+ - Adapters whose base-dir rules do not define the current host OS now raise instead of silently assuming Linux
25
+
26
+ - `Usage` (`input`, `output`, `cache_read`, `cache_creation`, `reasoning`, `cost`): token counts normalized to disjoint buckets across agents. `nil` means "this format does not record that dimension", never zero; `cost` is only ever agent-reported, never derived from a pricing table
27
+ - `Message#usage` and `Message#model`, populated where the format puts them on the message (Claude); `reader.usage` returns session totals or nil
28
+ - Claude usage dedups by `message.id` before summing: one API response streams into one record per content block repeating identical usage (94 of 124 message ids in one real transcript), so a naive sum roughly doubles what was billed
29
+ - Codex usage reads the last `token_count` record's running total and subtracts `cached_input_tokens` from `input_tokens` (Codex counts them inclusively; Claude disjointly — verified against real stores on both sides, 2026-08-24)
30
+ - Claude reader recognizes `atis-latch` and `bridge-session` (session state postdating the 2026-08-12 corpus, observed live 2026-08-24) instead of warning about them
31
+ - opencode reader — the first over SQLite: 13,804 messages from 365 real sessions with zero warnings, and its summed usage equals the store's own per-session rollup columns on all 365. One `tool` part becomes a `:tool_use` and a `:tool_result`; a `subtask` spawn becomes a `:tool_use` named after its agent; step markers, patches and file attachments stay in `raw`. Needs the optional `sqlite3` gem
32
+ - pi reader, explicitly provisional: written against tokentelemetry's working parser of the format, since no pi session files exist on the machine it was written on — every mapping degrades to `:unknown` + warning rather than crashing if real pi output disagrees
33
+ - **Four new agents: Gemini CLI, GitHub Copilot CLI, Qwen Code and Grok Build**, taking the gem from 7 adapters to 11
34
+ - Gemini CLI adapter + reader, verified against a real store (12 sessions, 121 records): `~/.gemini/tmp/<projectHash>/chats/session-*.json`, one JSON document per chat. Its `cached` count sits INSIDE `input` — established arithmetically across all 97 real token records, where `total` equals `input + output + thoughts + tool` and never adds `cached` — so the reader subtracts it, as it does for Codex. `thoughts` become `:thinking` parts carrying subject and description; `info` records are opt-in events. Filenames are UTC, unlike Codex's and pi's local-clock ones, and their trailing hex is NOT a session id (two real files share one), so the id is the whole basename
35
+ - GitHub Copilot CLI adapter + reader, verified against a real store: **the format has moved** to `~/.copilot/session-store.db` (SQLite, schema_version 3) from the `session-state/<id>/events.jsonl` layout the reference tooling still reads — an adapter following the older spec reports nothing on a current install. One `turns` row is a whole exchange and becomes two messages. No token or cost column exists anywhere in that schema, and the adapter says so rather than letting nil read as zero
36
+ - Qwen Code adapter + reader, and Grok Build adapter + reader — both PROVISIONAL and declared as such at runtime: neither store exists on the machine they were written on, so they follow tokentelemetry's parsers rather than observation, and every mapping degrades to `:unknown` + a warning rather than crashing if real output disagrees
37
+ - Grok's session is a directory, not a file: Layer 2 enumerates `summary.json`, the reader streams `chat_history.jsonl` beside it, and billed usage comes from a third file — `~/.grok/logs/unified.jsonl`, shared by every session and keyed by session id. `Readers::Base#record_path` is a new hook for exactly that split
38
+ - **Cursor IDE is repointed at the store it actually uses** — `~/Library/Application Support/Cursor/User/globalStorage/state.vscdb`, table `cursorDiskKV`, keys `composerData:<uuid>` — confirmed by opening it (6 real sessions on the machine this was written on, where the 0.2 declaration `~/.cursor/projects/*/agent-transcripts/*` did not exist at all). Fidelity rises from `:unsupported` to `:metadata`; content is still unread, because every record seen carried an empty `conversation`, and the adapter says so rather than guessing the turn format
39
+ - opencode store discovery now tries `OPENCODE_DATA_DIR`, `XDG_DATA_HOME`, `~/.local/share/opencode`, macOS `~/Library/Application Support/opencode` and the Windows app-data dirs, and matches `opencode*.db` rather than the plain name — a macOS user, or one on a release channel that renames the database, previously got an empty result from an agent they had used. A candidate must actually hold a database to win, so an empty directory cannot shadow a real store; two databases holding the same session report it once
40
+ - `base_dir default:` accepts a Hash keyed by platform (`:macos`, `:linux`, `:windows`) for IDE-hosted agents whose store genuinely moves between operating systems; a Hash missing this machine's platform raises rather than falling back to another platform's path
41
+ - `AgentSessions::Sqlite` extracts the one safe way this gem opens a SQLite store (read-only URI, escaped path, 5s busy timeout), now shared by the opencode adapter and reader
42
+ - Layer 3 begins: `AgentSessions.read(session)` returns a streaming reader — `each_message`, `messages`, `compactions`, `warnings`, `fidelity`, `partial?`
43
+ - `Message` (`role`, `at`, `parts`, `text`, `raw`) and `Part` (`:text`, `:thinking`, `:tool_use`, `:tool_result`, `:image`, `:unknown`); `raw` is never dropped
44
+ - Codex reader, the first: reads 73,946 messages from 415 real sessions with zero warnings and zero exceptions, at 4.5 ms per session
45
+ - `compacted` records become boundaries rather than messages — replaying their `replacement_history` would report the same turns twice
46
+ - `event_msg` records (38% of the corpus) are excluded unless `include_events: true`
47
+ - Readers stream at an 8 MB per-record cap, not Layer 2's 1 MB: 14 real records exceed 1 MB and the largest is 2.41 MB, so the smaller cap would have dropped real messages. A record past the cap is reported, never silently skipped
48
+ - `AgentSessions.read` raises `UnsupportedFormat` for an agent with no reader, so "cannot read this format" never looks like "this session is empty"
49
+ - Claude reader: 18,111 messages from 142 real transcripts, zero exceptions, 5.6 ms per session
50
+ - Claude's spilled tool output is resolved from the sidecar file, so a `:tool_result` carries content instead of a pointer — bounded to the session's own sidecar tree, because the path is read out of tool output and must never become a file-read primitive. `resolve_spills: false` turns it off
51
+ - `reader.subagents` returns readers for the transcripts a session spawned, never merged into its own messages — 124 of them on the machine this was written against
52
+ - Claude's nine session-state record types and its `system`/`attachment` context records are separated from turns; the latter two arrive with `include_events: true`
53
+ - Amp reader: `partial?` is true there, since the server holds the canonical copy. A thread is one JSON document rather than JSONL, so it is read whole under a 32 MB cap — the bound the gem's one unbounded read never had
54
+ - `reader.tree` returns the conversation as roots and continuations for an agent that records parent links, with `reader.branching?` to ask first. Claude branches at 374 points across 83 of 151 real transcripts — a turn edited and re-run leaves two children under one parent, which reading in file order shows as two histories interleaved
55
+ - Readers that record no parent links raise `UnsupportedFormat` from `tree` rather than returning an empty list, so "this format does not record that" never reads as "this session has none"
56
+ - Agents other than Codex, Claude and Amp have no reader yet; `Session#fidelity` already says what each one will support
57
+
58
+ ## 0.2.0 (2026-08-10)
59
+
60
+ First public release. 0.1.0 was never tagged or published, so its work is
61
+ folded into this entry rather than shipping a changelog with two consecutive
62
+ "(unreleased)" headings under two different version numbers.
63
+
64
+ - Layer 1: resolve session store paths for Claude Code, Codex CLI, Cursor CLI, Cursor IDE, Amp CLI, opencode, and pi
65
+ - Layer 2: `Session`, lazy `sessions`, `for_project`, `projects` across all seven adapters
66
+ - `AgentSessions.sessions(agent, since:)`, `.for_project(dir, agents:)`, `.projects(agent)`
67
+ - `Session` is a plain, lazily-resolved value object: `project_path` is computed on first access and memoized
68
+ - `Location#files` replaces `matches`, aware of single-file layers (`single_file`, `enumerable?`)
69
+ - opencode session enumeration via a deferred read-only SQLite query, behind an **optional** `sqlite3` gem — the gemspec stays runtime-dependency-free
70
+ - CLI: `where`, `doctor`, `audit`, `list` (`--agent`, `--project`, `--since`), and `du` (`--by agent|project`), all with `--json` output
71
+ - Amp's `secrets.json` is now optional: a missing file reports drift, not failure
72
+ - `verify`'s skip gate now keys on store existence, not base-dir existence
73
+ - `doctor` takes its agent positionally, matching `where`
74
+ - `list`/`du` now exit non-zero when any agent's store had to be skipped, instead of exiting 0 with only a stderr notice
75
+ - Codex declares `archived_sessions/` (flat, optional) and enumerates it: an archived rollout file is a session, and `sessions`, `list`, `du`, and `audit` all report it now
76
+ - Claude's `Session#bytes` counts the sidecar directory each transcript gets — `<id>/subagents/`, `<id>/tool-results/` — so `du` and `audit` no longer disagree by 29% about the same store
77
+ - `Adapters::Base#bytes_for(path, stat)` is a new overridable hook, defaulting to the transcript's own size
data/doc/README.md ADDED
@@ -0,0 +1,182 @@
1
+ # agent_sessions
2
+
3
+ Where do AI coding agents store their session logs? This gem knows.
4
+
5
+ <img width="1200" height="620" alt="demo" src="https://github.com/user-attachments/assets/d13e7c68-0cb4-404c-bb31-d2c3b24d3522" />
6
+
7
+ Resolves session store paths for Claude Code, Codex CLI, Cursor (CLI and IDE), Amp CLI, opencode, pi, Gemini CLI, GitHub Copilot CLI, Qwen Code, and Grok Build. Verifies those paths against disk. Audits whether plaintext transcripts sit inside anything that syncs.
8
+
9
+ Read-only by design. Runtime dependencies: `agent_homedir` and `zeitwerk`.
10
+
11
+ Most of these eleven layouts are undocumented, or only partly documented, by their vendors and can move in any release — Copilot CLI's moved from JSONL files to SQLite, and Cursor IDE's was found in a different directory than the one previously believed. Each adapter carries the date its claims were last checked against a real install, and `agent-sessions doctor` reports both that date and what disk says now.
12
+
13
+ Two adapters, Qwen Code and Grok Build, are marked provisional: no such store existed on the machine they were written against, so they follow another tool's working parser of the same format rather than direct observation. They say so at runtime.
14
+
15
+ ## Installation
16
+
17
+ Use Ruby 3.2 or newer.
18
+
19
+ Install the CLI directly:
20
+
21
+ ```sh
22
+ gem install agent_sessions
23
+ ```
24
+
25
+ Or add to your application's Gemfile:
26
+
27
+ ```ruby
28
+ gem "agent_sessions"
29
+ ```
30
+
31
+ Enumerating the SQLite-backed agents — opencode, Cursor IDE, and GitHub Copilot CLI — needs the optional `sqlite3` gem. Every other adapter works without it.
32
+
33
+ ## Quick start
34
+
35
+ ```sh
36
+ agent-sessions where
37
+ agent-sessions doctor
38
+ agent-sessions audit
39
+ agent-sessions list --since 30d
40
+ agent-sessions du --by project
41
+ ```
42
+
43
+ Add `--json` to `where`, `doctor`, `audit`, `list`, or `du` for machine-readable output. `du --by project` is the one command in the gem that is not stat-only: resolving a project name pays one bounded read per session for the file-based agents (opencode answers from its own SQL query instead, so it pays nothing extra).
44
+
45
+ ## Supported agents
46
+
47
+ Use the bare name in CLI arguments and the symbol in Ruby calls:
48
+
49
+ | Agent | CLI | Ruby |
50
+ | --- | --- | --- |
51
+ | Claude Code | `claude` | `:claude` |
52
+ | Codex CLI | `codex` | `:codex` |
53
+ | Cursor CLI | `cursor` | `:cursor` |
54
+ | Cursor IDE | `cursor_ide` | `:cursor_ide` |
55
+ | Amp CLI | `amp` | `:amp` |
56
+ | opencode | `opencode` | `:opencode` |
57
+ | pi | `pi` | `:pi` |
58
+ | Gemini CLI | `gemini` | `:gemini` |
59
+ | GitHub Copilot CLI | `copilot` | `:copilot` |
60
+ | Qwen Code | `qwen` | `:qwen` |
61
+ | Grok Build | `grok` | `:grok` |
62
+
63
+ Ruby callers passing any other symbol get `Agent::Sessions::UnknownAgent`; its message lists the valid names. The CLI catches that error, prints its message, and exits with status 1.
64
+
65
+ ## Ruby API
66
+
67
+ <img width="1200" height="620" alt="demo-ruby" src="https://github.com/user-attachments/assets/a0dd54b6-d597-4dad-85ae-97ce07aba2a4" />
68
+
69
+ Browse the [generated API documentation](https://github.com/lucianghinda/agent_sessions/blob/main/doc/Agent/Sessions.md) or the consolidated [`llm.txt`](https://github.com/lucianghinda/agent_sessions/blob/main/llm.txt) reference.
70
+
71
+ ```ruby
72
+ require "agent_sessions" # compatibility shim for Agent::Sessions
73
+
74
+ store = Agent::Sessions.locate(:codex)
75
+ store.effective.path # => "/Users/you/.codex/sessions"
76
+ store.format # => :jsonl
77
+ store.documented? # => false
78
+ store.retention # => nil ("grows forever")
79
+ ```
80
+
81
+ List agents:
82
+
83
+ ```ruby
84
+ Agent::Sessions.all # every supported agent
85
+ Agent::Sessions.installed # only agents present on this machine
86
+ ```
87
+
88
+ Verify claims against disk:
89
+
90
+ ```ruby
91
+ Agent::Sessions.verify(:codex)
92
+ # => [#<Check status: :pass, claim: "store sessions exists", ...>]
93
+ ```
94
+
95
+ Resolve for an environment that is not your own:
96
+
97
+ ```ruby
98
+ Agent::Sessions.locate(:codex, env: { "CODEX_HOME" => "/tmp/x" })
99
+ ```
100
+
101
+ Enumerate sessions and map them to projects:
102
+
103
+ ```ruby
104
+ Agent::Sessions.sessions(:claude).first(5) # lazy; stats files, never parses them
105
+ Agent::Sessions.for_project(Dir.pwd) # every agent's sessions for one project
106
+ Agent::Sessions.projects(:codex) # distinct recorded project paths (reads headers)
107
+ ```
108
+
109
+ Among the SQLite-backed agents, readers exist for opencode and Copilot CLI; Cursor IDE remains metadata-only.
110
+
111
+ Read a session's messages and token usage (Claude, Codex, Amp, opencode, pi, Gemini CLI, Copilot CLI, Qwen and Grok):
112
+
113
+ ```ruby
114
+ reader = Agent::Sessions.read(session)
115
+ reader.each_message { |m| puts "#{m.role}: #{m.text}" } # streams; raw is never dropped
116
+ reader.usage # session token totals, or nil if not recorded
117
+ reader.usage&.input # disjoint buckets: input, output, cache_read,
118
+ # cache_creation, reasoning, cost
119
+ ```
120
+
121
+ `Usage` is normalized across agents: `input` never includes cached tokens (Codex counts them inclusively; the reader subtracts), and a `nil` dimension means the format does not record it — never zero. `cost` is only ever what the agent itself reported; this gem ships no pricing table.
122
+
123
+ `Session#bytes` is what that session occupies on disk, not just its transcript. Claude Code writes a sidecar directory beside each transcript — `<id>/subagents/`, `<id>/tool-results/` — and those bytes belong to the session that produced them, which is why `du` and `audit` agree on the same store.
124
+
125
+ ### Round trips
126
+
127
+ A round trip is one model response, however many records the store split it across:
128
+
129
+ ```ruby
130
+ reader.round_trips # eager: an Array of RoundTrip
131
+ reader.each_round_trip { |trip| ... } # streams; breaking early reads one group, not the file
132
+ reader.round_trips_recorded? # true only where the store names its own groups
133
+ ```
134
+
135
+ A `RoundTrip` carries `index`, `messages`, `usage`, and `recorded`, and derives `parts` (every part, in file order), `calls` (its `:tool_use` parts), and `roles` (the distinct roles seen, first-seen order).
136
+
137
+ Claude names its groups (`message.id`); every other reader falls back to one round trip per message, and `round_trips_recorded?` answers `false` there — "not recorded" must never read as "none".
138
+
139
+ Pairing a call with the result that answers it, and rendering a session as a loop, live in the sibling [`agent_session_context`](https://github.com/lucianghinda/agent_session_context) gem, via its `agent-session-context loop` command.
140
+
141
+ ### Errors
142
+
143
+ Every domain-specific error the gem raises descends from `Agent::Sessions::Error`:
144
+
145
+ - Catch `Agent::Sessions::UnknownAgent` for a name outside the supported-agents table; the message lists valid names.
146
+ - Catch `Agent::Sessions::MissingDependency` when a SQLite-backed agent is enumerated without the optional `sqlite3` gem installed.
147
+ - Catch `Agent::Sessions::UnsupportedFormat` when `read` is called on a session whose format has no reader (Cursor CLI and Cursor IDE).
148
+ - Catch `Agent::Sessions::UnreadableStore` when a store exists but cannot be opened.
149
+
150
+ ## Roadmap
151
+
152
+ - 0.2: enumerate sessions, map them to projects (`list`, `du`)
153
+ - **0.3 (this release):** read and normalize messages; Ruby API renamed to `Agent::Sessions` while `require "agent_sessions"` stays as the compatibility shim, and base-dir resolution delegates to `agent_homedir`
154
+ - 0.4: a reader for Cursor IDE, which is metadata-only today
155
+ - 0.5: `export` with secret redaction
156
+
157
+ ## Contributing
158
+
159
+ Activate Ruby 3.2 or newer.
160
+
161
+ Run the tests before sending a change:
162
+
163
+ ```sh
164
+ bundle install
165
+ bundle exec rake test
166
+ ```
167
+
168
+ Before a release, run:
169
+
170
+ ```sh
171
+ bin/prepare_release
172
+ ```
173
+
174
+ It runs the test suite, regenerates the API documentation and `llm.txt`, and builds the gem without publishing it.
175
+
176
+ ## License
177
+
178
+ MIT
179
+
180
+ ## History
181
+
182
+ View the [changelog](CHANGELOG.md).