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,177 @@
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/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/Session.md](Sessions/Session.md)
172
+ - [Sessions/Sqlite.md](Sessions/Sqlite.md)
173
+ - [Sessions/Store.md](Sessions/Store.md)
174
+ - [Sessions/UnknownAgent.md](Sessions/UnknownAgent.md)
175
+ - [Sessions/UnreadableStore.md](Sessions/UnreadableStore.md)
176
+ - [Sessions/UnsupportedFormat.md](Sessions/UnsupportedFormat.md)
177
+ - [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/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,69 @@
1
+ ## 0.3.1 (2026-08-26)
2
+
3
+ - Add tracked Markdown YARD documentation and a consolidated `llm.txt`, package both with the gem, and validate every local documentation link
4
+ - 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
5
+ - 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
6
+ - Strengthen adapter conformance coverage and remove order dependence from the affected tests and fixtures
7
+ - Keep YARD tooling development-only; runtime dependencies remain `agent_homedir` and `zeitwerk`
8
+
9
+ ## 0.3.0 (2026-08-25)
10
+
11
+ - 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
12
+ - Base-dir resolution now delegates to `agent_homedir` and runs through a per-instance resolver instead of this gem's own expansion rules
13
+ - 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
14
+ - `cursor_ide` now honors `XDG_CONFIG_HOME` on Linux and `APPDATA` on Windows
15
+ - Blank or non-absolute XDG overrides now fall back to defaults for Amp and opencode, and `OPENCODE_DATA_DIR` now appears in `env_overrides`
16
+ - Adapters whose base-dir rules do not define the current host OS now raise instead of silently assuming Linux
17
+
18
+ - `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
19
+ - `Message#usage` and `Message#model`, populated where the format puts them on the message (Claude); `reader.usage` returns session totals or nil
20
+ - 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
21
+ - 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)
22
+ - 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
23
+ - 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
24
+ - 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
25
+ - **Four new agents: Gemini CLI, GitHub Copilot CLI, Qwen Code and Grok Build**, taking the gem from 7 adapters to 11
26
+ - 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
27
+ - 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
28
+ - 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
29
+ - 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
30
+ - **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
31
+ - 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
32
+ - `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
33
+ - `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
34
+ - Layer 3 begins: `AgentSessions.read(session)` returns a streaming reader — `each_message`, `messages`, `compactions`, `warnings`, `fidelity`, `partial?`
35
+ - `Message` (`role`, `at`, `parts`, `text`, `raw`) and `Part` (`:text`, `:thinking`, `:tool_use`, `:tool_result`, `:image`, `:unknown`); `raw` is never dropped
36
+ - Codex reader, the first: reads 73,946 messages from 415 real sessions with zero warnings and zero exceptions, at 4.5 ms per session
37
+ - `compacted` records become boundaries rather than messages — replaying their `replacement_history` would report the same turns twice
38
+ - `event_msg` records (38% of the corpus) are excluded unless `include_events: true`
39
+ - 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
40
+ - `AgentSessions.read` raises `UnsupportedFormat` for an agent with no reader, so "cannot read this format" never looks like "this session is empty"
41
+ - Claude reader: 18,111 messages from 142 real transcripts, zero exceptions, 5.6 ms per session
42
+ - 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
43
+ - `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
44
+ - 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`
45
+ - 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
46
+ - `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
47
+ - 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"
48
+ - Agents other than Codex, Claude and Amp have no reader yet; `Session#fidelity` already says what each one will support
49
+
50
+ ## 0.2.0 (2026-08-10)
51
+
52
+ First public release. 0.1.0 was never tagged or published, so its work is
53
+ folded into this entry rather than shipping a changelog with two consecutive
54
+ "(unreleased)" headings under two different version numbers.
55
+
56
+ - Layer 1: resolve session store paths for Claude Code, Codex CLI, Cursor CLI, Cursor IDE, Amp CLI, opencode, and pi
57
+ - Layer 2: `Session`, lazy `sessions`, `for_project`, `projects` across all seven adapters
58
+ - `AgentSessions.sessions(agent, since:)`, `.for_project(dir, agents:)`, `.projects(agent)`
59
+ - `Session` is a plain, lazily-resolved value object: `project_path` is computed on first access and memoized
60
+ - `Location#files` replaces `matches`, aware of single-file layers (`single_file`, `enumerable?`)
61
+ - opencode session enumeration via a deferred read-only SQLite query, behind an **optional** `sqlite3` gem — the gemspec stays runtime-dependency-free
62
+ - CLI: `where`, `doctor`, `audit`, `list` (`--agent`, `--project`, `--since`), and `du` (`--by agent|project`), all with `--json` output
63
+ - Amp's `secrets.json` is now optional: a missing file reports drift, not failure
64
+ - `verify`'s skip gate now keys on store existence, not base-dir existence
65
+ - `doctor` takes its agent positionally, matching `where`
66
+ - `list`/`du` now exit non-zero when any agent's store had to be skipped, instead of exiting 0 with only a stderr notice
67
+ - 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
68
+ - 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
69
+ - `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,162 @@
1
+ # agent_sessions
2
+
3
+ Where do AI coding agents store their session logs? This gem knows.
4
+
5
+ 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.
6
+
7
+ Read-only by design. Runtime dependencies: `agent_homedir` and `zeitwerk`.
8
+
9
+ 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.
10
+
11
+ 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.
12
+
13
+ ## Installation
14
+
15
+ Use Ruby 3.2 or newer.
16
+
17
+ Install the CLI directly:
18
+
19
+ ```sh
20
+ gem install agent_sessions
21
+ ```
22
+
23
+ Or add to your application's Gemfile:
24
+
25
+ ```ruby
26
+ gem "agent_sessions"
27
+ ```
28
+
29
+ Enumerating the SQLite-backed agents — opencode, Cursor IDE, and GitHub Copilot CLI — needs the optional `sqlite3` gem. Every other adapter works without it.
30
+
31
+ ## Quick start
32
+
33
+ ```sh
34
+ agent-sessions where
35
+ agent-sessions doctor
36
+ agent-sessions audit
37
+ agent-sessions list --since 30d
38
+ agent-sessions du --by project
39
+ ```
40
+
41
+ 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).
42
+
43
+ ## Supported agents
44
+
45
+ Use the bare name in CLI arguments and the symbol in Ruby calls:
46
+
47
+ | Agent | CLI | Ruby |
48
+ | --- | --- | --- |
49
+ | Claude Code | `claude` | `:claude` |
50
+ | Codex CLI | `codex` | `:codex` |
51
+ | Cursor CLI | `cursor` | `:cursor` |
52
+ | Cursor IDE | `cursor_ide` | `:cursor_ide` |
53
+ | Amp CLI | `amp` | `:amp` |
54
+ | opencode | `opencode` | `:opencode` |
55
+ | pi | `pi` | `:pi` |
56
+ | Gemini CLI | `gemini` | `:gemini` |
57
+ | GitHub Copilot CLI | `copilot` | `:copilot` |
58
+ | Qwen Code | `qwen` | `:qwen` |
59
+ | Grok Build | `grok` | `:grok` |
60
+
61
+ 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.
62
+
63
+ ## Ruby API
64
+
65
+ 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.
66
+
67
+ ```ruby
68
+ require "agent_sessions" # compatibility shim for Agent::Sessions
69
+
70
+ store = Agent::Sessions.locate(:codex)
71
+ store.effective.path # => "/Users/you/.codex/sessions"
72
+ store.format # => :jsonl
73
+ store.documented? # => false
74
+ store.retention # => nil ("grows forever")
75
+ ```
76
+
77
+ List agents:
78
+
79
+ ```ruby
80
+ Agent::Sessions.all # every supported agent
81
+ Agent::Sessions.installed # only agents present on this machine
82
+ ```
83
+
84
+ Verify claims against disk:
85
+
86
+ ```ruby
87
+ Agent::Sessions.verify(:codex)
88
+ # => [#<Check status: :pass, claim: "store sessions exists", ...>]
89
+ ```
90
+
91
+ Resolve for an environment that is not your own:
92
+
93
+ ```ruby
94
+ Agent::Sessions.locate(:codex, env: { "CODEX_HOME" => "/tmp/x" })
95
+ ```
96
+
97
+ Enumerate sessions and map them to projects:
98
+
99
+ ```ruby
100
+ Agent::Sessions.sessions(:claude).first(5) # lazy; stats files, never parses them
101
+ Agent::Sessions.for_project(Dir.pwd) # every agent's sessions for one project
102
+ Agent::Sessions.projects(:codex) # distinct recorded project paths (reads headers)
103
+ ```
104
+
105
+ Among the SQLite-backed agents, readers exist for opencode and Copilot CLI; Cursor IDE remains metadata-only.
106
+
107
+ Read a session's messages and token usage (Claude, Codex, Amp, opencode, pi, Gemini CLI, Copilot CLI, Qwen and Grok):
108
+
109
+ ```ruby
110
+ reader = Agent::Sessions.read(session)
111
+ reader.each_message { |m| puts "#{m.role}: #{m.text}" } # streams; raw is never dropped
112
+ reader.usage # session token totals, or nil if not recorded
113
+ reader.usage&.input # disjoint buckets: input, output, cache_read,
114
+ # cache_creation, reasoning, cost
115
+ ```
116
+
117
+ `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.
118
+
119
+ `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.
120
+
121
+ ### Errors
122
+
123
+ Every domain-specific error the gem raises descends from `Agent::Sessions::Error`:
124
+
125
+ - Catch `Agent::Sessions::UnknownAgent` for a name outside the supported-agents table; the message lists valid names.
126
+ - Catch `Agent::Sessions::MissingDependency` when a SQLite-backed agent is enumerated without the optional `sqlite3` gem installed.
127
+ - Catch `Agent::Sessions::UnsupportedFormat` when `read` is called on a session whose format has no reader (Cursor CLI and Cursor IDE).
128
+ - Catch `Agent::Sessions::UnreadableStore` when a store exists but cannot be opened.
129
+
130
+ ## Roadmap
131
+
132
+ - 0.2: enumerate sessions, map them to projects (`list`, `du`)
133
+ - **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`
134
+ - 0.4: a reader for Cursor IDE, which is metadata-only today
135
+ - 0.5: `export` with secret redaction
136
+
137
+ ## Contributing
138
+
139
+ Activate Ruby 3.2 or newer.
140
+
141
+ Run the tests before sending a change:
142
+
143
+ ```sh
144
+ bundle install
145
+ bundle exec rake test
146
+ ```
147
+
148
+ Before a release, run:
149
+
150
+ ```sh
151
+ bin/prepare_release
152
+ ```
153
+
154
+ It runs the test suite, regenerates the API documentation and `llm.txt`, and builds the gem without publishing it.
155
+
156
+ ## License
157
+
158
+ MIT
159
+
160
+ ## History
161
+
162
+ View the [changelog](CHANGELOG.md).