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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/README.md +71 -3
- data/doc/Agent/Sessions/Adapters/Amp.md +91 -0
- data/doc/Agent/Sessions/Adapters/Base.md +198 -0
- data/doc/Agent/Sessions/Adapters/Claude.md +63 -0
- data/doc/Agent/Sessions/Adapters/Codex.md +79 -0
- data/doc/Agent/Sessions/Adapters/Copilot.md +38 -0
- data/doc/Agent/Sessions/Adapters/Cursor.md +54 -0
- data/doc/Agent/Sessions/Adapters/CursorIde.md +49 -0
- data/doc/Agent/Sessions/Adapters/Enumeration.md +135 -0
- data/doc/Agent/Sessions/Adapters/Gemini.md +67 -0
- data/doc/Agent/Sessions/Adapters/Grok.md +64 -0
- data/doc/Agent/Sessions/Adapters/Opencode.md +93 -0
- data/doc/Agent/Sessions/Adapters/Pi.md +137 -0
- data/doc/Agent/Sessions/Adapters/Qwen.md +32 -0
- data/doc/Agent/Sessions/Adapters.md +5 -0
- data/doc/Agent/Sessions/Audit/Finding.md +27 -0
- data/doc/Agent/Sessions/Audit.md +21 -0
- data/doc/Agent/Sessions/CLI.md +36 -0
- data/doc/Agent/Sessions/Check.md +27 -0
- data/doc/Agent/Sessions/Compaction.md +23 -0
- data/doc/Agent/Sessions/EnvOverride.md +19 -0
- data/doc/Agent/Sessions/Error.md +6 -0
- data/doc/Agent/Sessions/HomeExpansion.md +11 -0
- data/doc/Agent/Sessions/Location.md +60 -0
- data/doc/Agent/Sessions/Message.md +59 -0
- data/doc/Agent/Sessions/MissingDependency.md +6 -0
- data/doc/Agent/Sessions/Node.md +24 -0
- data/doc/Agent/Sessions/Part.md +41 -0
- data/doc/Agent/Sessions/Readers/Amp.md +41 -0
- data/doc/Agent/Sessions/Readers/Base.md +91 -0
- data/doc/Agent/Sessions/Readers/Claude.md +77 -0
- data/doc/Agent/Sessions/Readers/Codex.md +81 -0
- data/doc/Agent/Sessions/Readers/Copilot.md +31 -0
- data/doc/Agent/Sessions/Readers/Gemini.md +38 -0
- data/doc/Agent/Sessions/Readers/Grok.md +45 -0
- data/doc/Agent/Sessions/Readers/Opencode.md +37 -0
- data/doc/Agent/Sessions/Readers/Pi.md +38 -0
- data/doc/Agent/Sessions/Readers/Qwen.md +30 -0
- data/doc/Agent/Sessions/Readers.md +5 -0
- data/doc/Agent/Sessions/Session.md +73 -0
- data/doc/Agent/Sessions/Sqlite.md +43 -0
- data/doc/Agent/Sessions/Store.md +57 -0
- data/doc/Agent/Sessions/UnknownAgent.md +6 -0
- data/doc/Agent/Sessions/UnreadableStore.md +6 -0
- data/doc/Agent/Sessions/UnsupportedFormat.md +6 -0
- data/doc/Agent/Sessions/Usage.md +56 -0
- data/doc/Agent/Sessions.md +177 -0
- data/doc/Agent.md +5 -0
- data/doc/CHANGELOG.md +69 -0
- data/doc/README.md +162 -0
- data/doc/index.csv +316 -0
- data/exe/agent-sessions +0 -2
- data/lib/agent/sessions/adapters/cursor.rb +3 -3
- data/lib/agent/sessions/cli.rb +2 -2
- data/lib/agent/sessions/message.rb +1 -2
- data/lib/agent/sessions/part.rb +1 -2
- data/lib/agent/sessions/version.rb +1 -1
- data/llm.txt +177 -0
- metadata +54 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 963cada8c8c93c253b1e09647a64a210b1f1e009b84e064b38e5aa39d20853af
|
|
4
|
+
data.tar.gz: 3a2c3b727bf7710e8be93f52370fe5710898a6b12abc458135826c32a0a97178
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2a947ed4f5ffe8890d0d4785048c88218857f113325656ea5be28c34761d2178f8d08a3a661adcf8d22bd096e3d53470e9234642ac734f2ee8a5a81be5030f46
|
|
7
|
+
data.tar.gz: ff29e6acb8c4bde8c8aabfa7775c9b1fc3de945419f21922ff87d7272bd5e258b8d98a5c7df102fa5564ba5aff6a50c437dd9e1ce10a2321e37d7f08ef6cd512
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
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
|
+
|
|
1
9
|
## 0.3.0 (2026-08-25)
|
|
2
10
|
|
|
3
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
|
data/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Where do AI coding agents store their session logs? This gem knows.
|
|
4
4
|
|
|
5
|
+

|
|
6
|
+
|
|
5
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.
|
|
6
8
|
|
|
7
9
|
Read-only by design. Runtime dependencies: `agent_homedir` and `zeitwerk`.
|
|
@@ -12,12 +14,22 @@ Two adapters, Qwen Code and Grok Build, are marked provisional: no such store ex
|
|
|
12
14
|
|
|
13
15
|
## Installation
|
|
14
16
|
|
|
15
|
-
|
|
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:
|
|
16
26
|
|
|
17
27
|
```ruby
|
|
18
28
|
gem "agent_sessions"
|
|
19
29
|
```
|
|
20
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
|
+
|
|
21
33
|
## Quick start
|
|
22
34
|
|
|
23
35
|
```sh
|
|
@@ -30,8 +42,32 @@ agent-sessions du --by project
|
|
|
30
42
|
|
|
31
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).
|
|
32
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
|
+
|
|
33
65
|
## Ruby API
|
|
34
66
|
|
|
67
|
+

|
|
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
|
+
|
|
35
71
|
```ruby
|
|
36
72
|
require "agent_sessions" # compatibility shim for Agent::Sessions
|
|
37
73
|
|
|
@@ -70,7 +106,7 @@ Agent::Sessions.for_project(Dir.pwd) # every agent's sessions for one
|
|
|
70
106
|
Agent::Sessions.projects(:codex) # distinct recorded project paths (reads headers)
|
|
71
107
|
```
|
|
72
108
|
|
|
73
|
-
|
|
109
|
+
Among the SQLite-backed agents, readers exist for opencode and Copilot CLI; Cursor IDE remains metadata-only.
|
|
74
110
|
|
|
75
111
|
Read a session's messages and token usage (Claude, Codex, Amp, opencode, pi, Gemini CLI, Copilot CLI, Qwen and Grok):
|
|
76
112
|
|
|
@@ -86,13 +122,45 @@ reader.usage&.input # disjoint buckets: input, output,
|
|
|
86
122
|
|
|
87
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.
|
|
88
124
|
|
|
125
|
+
### Errors
|
|
126
|
+
|
|
127
|
+
Every domain-specific error the gem raises descends from `Agent::Sessions::Error`:
|
|
128
|
+
|
|
129
|
+
- Catch `Agent::Sessions::UnknownAgent` for a name outside the supported-agents table; the message lists valid names.
|
|
130
|
+
- Catch `Agent::Sessions::MissingDependency` when a SQLite-backed agent is enumerated without the optional `sqlite3` gem installed.
|
|
131
|
+
- Catch `Agent::Sessions::UnsupportedFormat` when `read` is called on a session whose format has no reader (Cursor CLI and Cursor IDE).
|
|
132
|
+
- Catch `Agent::Sessions::UnreadableStore` when a store exists but cannot be opened.
|
|
133
|
+
|
|
89
134
|
## Roadmap
|
|
90
135
|
|
|
91
136
|
- 0.2: enumerate sessions, map them to projects (`list`, `du`)
|
|
92
137
|
- **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`
|
|
93
|
-
- 0.4: Cursor IDE
|
|
138
|
+
- 0.4: a reader for Cursor IDE, which is metadata-only today
|
|
94
139
|
- 0.5: `export` with secret redaction
|
|
95
140
|
|
|
141
|
+
## Contributing
|
|
142
|
+
|
|
143
|
+
Activate Ruby 3.2 or newer.
|
|
144
|
+
|
|
145
|
+
Run the tests before sending a change:
|
|
146
|
+
|
|
147
|
+
```sh
|
|
148
|
+
bundle install
|
|
149
|
+
bundle exec rake test
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Before a release, run:
|
|
153
|
+
|
|
154
|
+
```sh
|
|
155
|
+
bin/prepare_release
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
It runs the test suite, regenerates the API documentation and `llm.txt`, and builds the gem without publishing it.
|
|
159
|
+
|
|
160
|
+
## License
|
|
161
|
+
|
|
162
|
+
MIT
|
|
163
|
+
|
|
96
164
|
## History
|
|
97
165
|
|
|
98
166
|
View the [changelog](CHANGELOG.md).
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Class Agent::Sessions::Adapters::Amp <a id="class-Agent-Sessions-Adapters-Amp"></a>
|
|
2
|
+
|
|
3
|
+
| | |
|
|
4
|
+
| --- | --- |
|
|
5
|
+
| **Inherits** | [Agent::Sessions::Adapters::Base](Base.md) |
|
|
6
|
+
| **Defined in** | lib/agent/sessions/adapters/amp.rb |
|
|
7
|
+
|
|
8
|
+
## Public Class Methods
|
|
9
|
+
### `reader_class()` <a id="method-c-reader_class"></a> <a id="reader_class-class_method"></a>
|
|
10
|
+
Not documented.
|
|
11
|
+
|
|
12
|
+
## Public Instance Methods
|
|
13
|
+
### `project_path_for(path)` <a id="method-i-project_path_for"></a> <a id="project_path_for-instance_method"></a>
|
|
14
|
+
env.initial.trees is the array a workspace's roots live in (plural —the format
|
|
15
|
+
is shaped for more than one). Verified 2026-08-05: the one real thread on this
|
|
16
|
+
machine carries exactly one tree, so `trees` is reported and an actual
|
|
17
|
+
multi-root thread's other roots are silently unreachable through
|
|
18
|
+
sessions_for_project/project_paths — see the warning below, which is where a
|
|
19
|
+
user who hits that finds out, since nobody who has looked at real data has hit
|
|
20
|
+
it yet. Reporting only the first is still the right call: Session#project_path
|
|
21
|
+
is a single String, not a list, so supporting every root would be a data-model
|
|
22
|
+
change this task does not make, and the failure mode is a false NEGATIVE (a
|
|
23
|
+
real root that never matches), never a false positive.
|
|
24
|
+
|
|
25
|
+
started_at deliberately does NOT read `created` from this same JSON: that
|
|
26
|
+
would turn every session's stat-only listing into a content read, not just
|
|
27
|
+
project_path's already-deferred one, breaking the stat-only guarantee
|
|
28
|
+
`sessions` makes for every adapter (Base's class comment on `sessions`) — and
|
|
29
|
+
it cannot be fixed by memoizing the parse across both hooks, either:
|
|
30
|
+
build_session computes started_at_for eagerly at construction and defers only
|
|
31
|
+
project_path through Session's resolver block, so a memo would always be cold
|
|
32
|
+
when started_at runs. The ordering never reverses without a Session/Base
|
|
33
|
+
change, which is out of scope here. Base's stat.birthtime fallback stays in
|
|
34
|
+
effect, nil-on-Linux limitation and all (see rule 3) — that gap is not
|
|
35
|
+
Amp-specific; Claude's started_at has the identical gap from the identical
|
|
36
|
+
fallback. Worth revisiting if a caller needs started_at at all on a filesystem
|
|
37
|
+
without birthtime; nothing today does (Task 10 sorts by updated_at, matching
|
|
38
|
+
Codex's own note on the same trade-off).
|
|
39
|
+
|
|
40
|
+
URI.parse, not `uri.delete_prefix("file://")`: the naive strip mishandles the
|
|
41
|
+
authority-component form <code>file://localhost/Users/...</code> (it would
|
|
42
|
+
leave a leading "localhost/" in the path), which URI.parse's #path strips
|
|
43
|
+
correctly by design. That fix is only net-positive once its own new failure
|
|
44
|
+
modes are covered, and a partial mirror's JSON is exactly the kind of data
|
|
45
|
+
that can be present-but-wrong at every step, not merely absent (rule 1, one
|
|
46
|
+
level up: the CONTAINER at each step needs checking, not just the leaf):
|
|
47
|
+
- opaque form "file:relative/x" parses with scheme "file" but
|
|
48
|
+
#path nil — decode_uri_component(nil) raises NoMethodError.
|
|
49
|
+
- "file:", "file://", "file://localhost" all parse to path "" —
|
|
50
|
+
truthy, so left unchecked it becomes an empty-string project
|
|
51
|
+
instead of the unknown-project nil an empty path actually means.
|
|
52
|
+
- "file://nas/share" (a real host, e.g. a network share) parses to
|
|
53
|
+
path "/share" with host "nas" — a location this machine cannot
|
|
54
|
+
read as a local directory, silently rejected here rather than
|
|
55
|
+
reported as if it were one.
|
|
56
|
+
- a trailing slash ("file:///Users/you/app/") survives decoding
|
|
57
|
+
unchanged and would never equal a caller's expanded path.
|
|
58
|
+
- an unescaped character (a literal space) makes URI.parse itself
|
|
59
|
+
raise URI::InvalidURIError — file DATA, not an adapter bug, so
|
|
60
|
+
that raise is rescued rather than left to propagate and take
|
|
61
|
+
every agent's listing down with it (rule 2). The rescue is
|
|
62
|
+
scoped to the URI.parse call alone, not the whole method: Codex
|
|
63
|
+
and pi both wrap only Time.new the same way, for the same
|
|
64
|
+
reason — a method-scoped rescue would just as readily swallow a
|
|
65
|
+
raise from a genuine adapter bug above it. decode_uri_component
|
|
66
|
+
needs no rescue of its own: it only ever substitutes /%\h\h/, and
|
|
67
|
+
URI.parse has already rejected any malformed escape by the time
|
|
68
|
+
its #path reaches that call (verified against %FF%FE, %C3%28,
|
|
69
|
+
%80 — none raise).
|
|
70
|
+
|
|
71
|
+
Every JSON level below is unwrapped by hand and type-checked, rather than one
|
|
72
|
+
#dig("env", "initial", "trees", 0, "uri") call: #dig raises TypeError the
|
|
73
|
+
moment an intermediate value is present but not itself diggable (a String
|
|
74
|
+
"env", a top-level Array, "trees" holding a String instead of an Array...),
|
|
75
|
+
which is the same present-but-wrong risk as above, one level higher. Each
|
|
76
|
+
<code>[]</code>/<code>.first</code> below is only called once its receiver has
|
|
77
|
+
already been confirmed the right shape, so none of them can raise on their
|
|
78
|
+
own. uri.is_a?(String) is kept even though URI.parse's own rescue above would
|
|
79
|
+
also catch every non-String value JSON can produce here (Hash, Array, Integer,
|
|
80
|
+
Float, true, false, nil all raise URI::InvalidURIError when handed to
|
|
81
|
+
URI.parse, verified 2026-08-05) — the guard is not load-bearing against those
|
|
82
|
+
specific values today, but it keeps this method's contract with URI.parse
|
|
83
|
+
explicit rather than resting on an undocumented side effect of what that call
|
|
84
|
+
happens to do with the wrong type, and it matches every sibling adapter's
|
|
85
|
+
convention of checking a value's type before use.
|
|
86
|
+
|
|
87
|
+
### `warnings()` <a id="method-i-warnings"></a> <a id="warnings-instance_method"></a>
|
|
88
|
+
Gated, unlike the warning above. That one is a permanent property of the agent
|
|
89
|
+
and is worth reading before adopting the gem; this one is a "here is what
|
|
90
|
+
breaks, please send this back" report, and the plan's rule for those is that
|
|
91
|
+
they reach only people who can act on them. Same gate pi uses.
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# Class Agent::Sessions::Adapters::Base <a id="class-Agent-Sessions-Adapters-Base"></a>
|
|
2
|
+
|
|
3
|
+
| | |
|
|
4
|
+
| --- | --- |
|
|
5
|
+
| **Inherits** | Object |
|
|
6
|
+
| **Includes** | [Agent::Sessions::Adapters::Enumeration](Enumeration.md), [Agent::Sessions::HomeExpansion](../HomeExpansion.md) |
|
|
7
|
+
| **Defined in** | lib/agent/sessions/adapters/base.rb |
|
|
8
|
+
|
|
9
|
+
Base class for every agent adapter. Subclass this directly, never another
|
|
10
|
+
adapter: the DSL keeps its configuration in singleton instance variables,
|
|
11
|
+
which Ruby does not carry down a second level of inheritance, so a subclass of
|
|
12
|
+
a subclass would silently declare nothing.
|
|
13
|
+
|
|
14
|
+
An instance memoizes what it resolves. Build a new instance per resolution
|
|
15
|
+
rather than reusing one across changes to the env hash.
|
|
16
|
+
|
|
17
|
+
What lives here is Layer 1: where a store is, what it declares, and whether
|
|
18
|
+
disk agrees. Turning that store into sessions is Layer 2 and lives in
|
|
19
|
+
Enumeration, included below — the two halves met at 460 lines in one class and
|
|
20
|
+
were split before Layer 3 readers could make it three.
|
|
21
|
+
|
|
22
|
+
## Constants
|
|
23
|
+
### `FIDELITIES` <a id="constant-FIDELITIES"></a> <a id="FIDELITIES-constant"></a>
|
|
24
|
+
Not documented.
|
|
25
|
+
|
|
26
|
+
## Attributes
|
|
27
|
+
### `agent_name` [R] <a id="attribute-c-agent_name"></a> <a id="agent_name-class_method"></a>
|
|
28
|
+
Returns the value of attribute agent_name.
|
|
29
|
+
|
|
30
|
+
### `declared_warnings` [R] <a id="attribute-c-declared_warnings"></a> <a id="declared_warnings-class_method"></a>
|
|
31
|
+
Returns the value of attribute declared_warnings.
|
|
32
|
+
|
|
33
|
+
### `documented_value` [R] <a id="attribute-c-documented_value"></a> <a id="documented_value-class_method"></a>
|
|
34
|
+
Returns the value of attribute documented_value.
|
|
35
|
+
|
|
36
|
+
### `label_text` [R] <a id="attribute-c-label_text"></a> <a id="label_text-class_method"></a>
|
|
37
|
+
Returns the value of attribute label_text.
|
|
38
|
+
|
|
39
|
+
### `verified_on_date` [R] <a id="attribute-c-verified_on_date"></a> <a id="verified_on_date-class_method"></a>
|
|
40
|
+
Returns the value of attribute verified_on_date.
|
|
41
|
+
|
|
42
|
+
## Public Class Methods
|
|
43
|
+
### `fidelity_value()` <a id="method-c-fidelity_value"></a> <a id="fidelity_value-class_method"></a>
|
|
44
|
+
:unsupported is the honest default for an adapter that has not declared what a
|
|
45
|
+
reader could reconstruct from its format.
|
|
46
|
+
|
|
47
|
+
### `homedir_config()` <a id="method-c-homedir_config"></a> <a id="homedir_config-class_method"></a>
|
|
48
|
+
Not documented.
|
|
49
|
+
|
|
50
|
+
### `reader_class()` <a id="method-c-reader_class"></a> <a id="reader_class-class_method"></a>
|
|
51
|
+
The Layer 3 reader for this agent, or nil while it has none. nil is what makes
|
|
52
|
+
Agent::Sessions.read raise UnsupportedFormat instead of handing back a reader
|
|
53
|
+
that quietly yields nothing.
|
|
54
|
+
|
|
55
|
+
### `store_configs()` <a id="method-c-store_configs"></a> <a id="store_configs-class_method"></a>
|
|
56
|
+
Not documented.
|
|
57
|
+
|
|
58
|
+
## Public Instance Methods
|
|
59
|
+
### `base_dir()` <a id="method-i-base_dir"></a> <a id="base_dir-instance_method"></a>
|
|
60
|
+
Not documented.
|
|
61
|
+
|
|
62
|
+
### `bytes_for(_path, stat)` <a id="method-i-bytes_for"></a> <a id="bytes_for-instance_method"></a>
|
|
63
|
+
Bytes this session occupies on disk. The transcript alone for a store that
|
|
64
|
+
keeps one file per session; an adapter whose agent writes sidecar files beside
|
|
65
|
+
the transcript overrides this and adds them. Like the two time hooks it takes
|
|
66
|
+
the stat the enumerator already holds, so the common case still costs nothing
|
|
67
|
+
beyond the syscall already made.
|
|
68
|
+
|
|
69
|
+
An override runs EAGERLY for every session, so it carries build_session's
|
|
70
|
+
constraint: it must not raise on an unreadable path, or one bad sidecar takes
|
|
71
|
+
down the whole listing rather than its own row.
|
|
72
|
+
|
|
73
|
+
### `encode_project(_dir)` <a id="method-i-encode_project"></a> <a id="encode_project-instance_method"></a>
|
|
74
|
+
nil means this adapter has no directory-name fallback rule. Used only by
|
|
75
|
+
sessions_for_project, and only for a session whose own recorded cwd could not
|
|
76
|
+
be read. When overridden: dir arrives pre-expanded here from
|
|
77
|
+
sessions_for_project (File.expand_path), which is the precondition an override
|
|
78
|
+
may rely on — a direct caller must pass an absolute, expanded path itself, or
|
|
79
|
+
the encoding is nonsense ("app", "~/app", and a trailing slash all encode
|
|
80
|
+
differently from the canonical form real project directories were named from).
|
|
81
|
+
|
|
82
|
+
### `initialize(env: ENV)` <a id="method-i-initialize"></a> <a id="initialize-instance_method"></a>
|
|
83
|
+
- **@return** [Base] a new instance of Base
|
|
84
|
+
|
|
85
|
+
### `locate()` <a id="method-i-locate"></a> <a id="locate-instance_method"></a>
|
|
86
|
+
Not documented.
|
|
87
|
+
|
|
88
|
+
### `project_dir_name(path)` <a id="method-i-project_dir_name"></a> <a id="project_dir_name-instance_method"></a>
|
|
89
|
+
The directory whose name the encoding must match, when sessions_for_project
|
|
90
|
+
falls back to it. Overridable: not every store puts the encoded project
|
|
91
|
+
directly above the session file — cursor_ide nests
|
|
92
|
+
projects/<name>/agent-transcripts/*, where the immediate parent is
|
|
93
|
+
agent-transcripts and matching it would find nothing, silently.
|
|
94
|
+
|
|
95
|
+
### `project_path_for(_path)` <a id="method-i-project_path_for"></a> <a id="project_path_for-instance_method"></a>
|
|
96
|
+
nil means the project is unknown for this session. Adapters override with a
|
|
97
|
+
bounded read of their own metadata; Base cannot guess.
|
|
98
|
+
|
|
99
|
+
### `project_paths()` <a id="method-i-project_paths"></a> <a id="project_paths-instance_method"></a>
|
|
100
|
+
Distinct recorded project paths, sorted. This is the read-everything direction
|
|
101
|
+
(design doc section 7): the encodings cannot be reversed, so the recorded cwd
|
|
102
|
+
inside each file is the only reliable source. Sessions whose project cannot be
|
|
103
|
+
determined are excluded, not returned as nil.
|
|
104
|
+
|
|
105
|
+
Sorted rather than left in glob order because a stable order is what makes
|
|
106
|
+
`projects` output diffable and `du --by project` deterministic — and adapters
|
|
107
|
+
answering from a database would otherwise impose their own.
|
|
108
|
+
|
|
109
|
+
### `retention()` <a id="method-i-retention"></a> <a id="retention-instance_method"></a>
|
|
110
|
+
Not documented.
|
|
111
|
+
|
|
112
|
+
### `retention_source()` <a id="method-i-retention_source"></a> <a id="retention_source-instance_method"></a>
|
|
113
|
+
Not documented.
|
|
114
|
+
|
|
115
|
+
### `session_id_from(path)` <a id="method-i-session_id_from"></a> <a id="session_id_from-instance_method"></a>
|
|
116
|
+
--- Layer 2 hooks, overridable per adapter ---
|
|
117
|
+
|
|
118
|
+
### `sessions()` <a id="method-i-sessions"></a> <a id="sessions-instance_method"></a>
|
|
119
|
+
Lazily enumerates the primary store. Each consumed session costs one stat plus
|
|
120
|
+
filename parsing — never a content read. project_path is the exception and
|
|
121
|
+
pays for itself on first access.
|
|
122
|
+
|
|
123
|
+
A store the gem has no layout for is refused rather than reported empty:
|
|
124
|
+
Location#enumerable? exists precisely so "nothing here to enumerate" and
|
|
125
|
+
"enumerated, found none" stay distinguishable, and silently returning no
|
|
126
|
+
sessions is this gem's worst failure mode.
|
|
127
|
+
|
|
128
|
+
### `sessions_for_project(dir)` <a id="method-i-sessions_for_project"></a> <a id="sessions_for_project-instance_method"></a>
|
|
129
|
+
Match by RECORDED cwd, exact, per session (design doc section 7, revised
|
|
130
|
+
2026-08-05 — the third design for this method, kept honest here because the
|
|
131
|
+
next reader deserves to know why it is not "cheap"). The first two designs
|
|
132
|
+
were built and disproved against a real store, not in theory:
|
|
133
|
+
|
|
134
|
+
1. Directory-name matching (the original design) assumed a
|
|
135
|
+
session's parent directory equals encode(its own recorded cwd).
|
|
136
|
+
A project rename breaks that: the agent keeps writing under the
|
|
137
|
+
OLD encoded directory, so two directories can hold live sessions
|
|
138
|
+
for the SAME current cwd, and name-only matching silently
|
|
139
|
+
dropped the stale one — false negatives, the failure mode this
|
|
140
|
+
gem treats as worst (decision 11).
|
|
141
|
+
|
|
142
|
+
2. One-read-per-directory sampling (the first fix for #1) assumed
|
|
143
|
+
sessions within a directory share a cwd, to keep the match
|
|
144
|
+
sublinear. Reading a real renamed project's stale directory
|
|
145
|
+
disproved that: two of its three sessions had been resumed after
|
|
146
|
+
the rename and recorded the NEW cwd; the third was never resumed
|
|
147
|
+
and still recorded the OLD one. Sampling one session and
|
|
148
|
+
applying its verdict to the whole directory is wrong in BOTH
|
|
149
|
+
directions on the same store — it invented a false positive
|
|
150
|
+
here, and a different glob order would just as easily have
|
|
151
|
+
reproduced #1's false negative for that same directory.
|
|
152
|
+
Approximate cwd resolution doesn't make the error smaller; it
|
|
153
|
+
just moves where it lands.
|
|
154
|
+
|
|
155
|
+
Measured cost of reading every session instead of sampling: 0.17 ms per
|
|
156
|
+
session (68 real Claude sessions, full sweep, 0.012s total) — 0.7s
|
|
157
|
+
extrapolated to a 4,000-session store. That is what the sampling complexity
|
|
158
|
+
was buying, and it is not a trade worth making: the enumerator is already
|
|
159
|
+
lazy, so a caller taking first(n) never pays for sessions it never asked
|
|
160
|
+
about, and even the worst case (every session checked, no match) stays under a
|
|
161
|
+
second on a store two orders of magnitude larger than anything observed.
|
|
162
|
+
|
|
163
|
+
A session whose own cwd cannot be read (the scan gave up, the file is
|
|
164
|
+
unreadable, the adapter declares no reader) falls back to comparing ITS OWN
|
|
165
|
+
directory's name against the encoding, when the adapter declares one — this is
|
|
166
|
+
the only thing encode_project still buys: it keeps a session with an
|
|
167
|
+
unreadable header from becoming invisible, without resolving an unknown
|
|
168
|
+
project for every other session that happens to share its directory.
|
|
169
|
+
|
|
170
|
+
### `started_at_for(_path, stat)` <a id="method-i-started_at_for"></a> <a id="started_at_for-instance_method"></a>
|
|
171
|
+
Both time hooks take the stat the enumerator already holds, so a session still
|
|
172
|
+
costs one syscall. path is passed for adapters that answer from a sibling
|
|
173
|
+
metadata file instead.
|
|
174
|
+
|
|
175
|
+
nil beats a wrong guess when the filesystem cannot answer at all — and it says
|
|
176
|
+
so through ENOSYS/EPERM from statx as often as NotImplementedError.
|
|
177
|
+
|
|
178
|
+
### `updated_at_for(_path, stat)` <a id="method-i-updated_at_for"></a> <a id="updated_at_for-instance_method"></a>
|
|
179
|
+
Not documented.
|
|
180
|
+
|
|
181
|
+
### `verify()` <a id="method-i-verify"></a> <a id="verify-instance_method"></a>
|
|
182
|
+
Checks every declared store against disk. The design doc says each adapter
|
|
183
|
+
declares its own checks, and each one does: its store_configs decide what is
|
|
184
|
+
looked for and whether an absence is a failure or drift. Content-level checks
|
|
185
|
+
(first record type, encoding round-trip) need file reads and wait for Layer 3.
|
|
186
|
+
An adapter that needs its own can override this and call super.
|
|
187
|
+
|
|
188
|
+
The skip gate is the same signal Store#installed? uses: any declared store
|
|
189
|
+
exists. It is deliberately NOT base-dir existence — ~/.cursor is created by
|
|
190
|
+
the Cursor editor with no agent store in it (observed 2026-08-05), and the old
|
|
191
|
+
gate made doctor report FAIL while `where` said "(not installed)". A missing
|
|
192
|
+
store proves nothing on its own (never used? layout moved? the gem cannot
|
|
193
|
+
tell), so :fail is reserved for the one case with evidence: some store exists,
|
|
194
|
+
proving the agent records data here, while a required one is absent — the
|
|
195
|
+
layout-moved signature.
|
|
196
|
+
|
|
197
|
+
### `warnings()` <a id="method-i-warnings"></a> <a id="warnings-instance_method"></a>
|
|
198
|
+
Not documented.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Class Agent::Sessions::Adapters::Claude <a id="class-Agent-Sessions-Adapters-Claude"></a>
|
|
2
|
+
|
|
3
|
+
| | |
|
|
4
|
+
| --- | --- |
|
|
5
|
+
| **Inherits** | [Agent::Sessions::Adapters::Base](Base.md) |
|
|
6
|
+
| **Defined in** | lib/agent/sessions/adapters/claude.rb |
|
|
7
|
+
|
|
8
|
+
## Constants
|
|
9
|
+
### `DEFAULT_CLEANUP_PERIOD_DAYS` <a id="constant-DEFAULT_CLEANUP_PERIOD_DAYS"></a> <a id="DEFAULT_CLEANUP_PERIOD_DAYS-constant"></a>
|
|
10
|
+
Not documented.
|
|
11
|
+
|
|
12
|
+
## Public Class Methods
|
|
13
|
+
### `reader_class()` <a id="method-c-reader_class"></a> <a id="reader_class-class_method"></a>
|
|
14
|
+
Not documented.
|
|
15
|
+
|
|
16
|
+
## Public Instance Methods
|
|
17
|
+
### `bytes_for(path, stat)` <a id="method-i-bytes_for"></a> <a id="bytes_for-instance_method"></a>
|
|
18
|
+
Claude Code writes a directory beside each transcript, named after the session
|
|
19
|
+
id with the extension dropped: subagents/ holds the transcripts of agents this
|
|
20
|
+
session spawned, tool-results/ holds tool output too large to inline. Those
|
|
21
|
+
bytes are this session's, and until they were counted `du` reported 122.1 MB
|
|
22
|
+
for a store `audit` reported 173.0 MB for — 71% — because audit sums the store
|
|
23
|
+
directory whole while du sums sessions. Two commands, one directory, a 29%
|
|
24
|
+
disagreement.
|
|
25
|
+
|
|
26
|
+
Measured over 128 real sessions on 2026-08-10: 0.002 ms per session when there
|
|
27
|
+
is no sidecar (one stat, the common case on a fresh install) and 0.080 ms when
|
|
28
|
+
there is. That is under half what project_path's content read costs, and
|
|
29
|
+
unlike project_path this cannot be deferred —bytes is eager, and a
|
|
30
|
+
lazily-corrected byte total would leave `list` printing one number while `du`
|
|
31
|
+
summed another.
|
|
32
|
+
|
|
33
|
+
### `encode_project(dir)` <a id="method-i-encode_project"></a> <a id="encode_project-instance_method"></a>
|
|
34
|
+
Every non-alphanumeric character becomes "-" (design doc section 7). dir must
|
|
35
|
+
already be absolute and expanded — sessions_for_project guarantees that; a
|
|
36
|
+
direct caller passing "app", "~/app", or a trailing slash gets a nonsense
|
|
37
|
+
encoding (see Base#encode_project). Verified against real project directories
|
|
38
|
+
on 2026-08-05: /Users/dev/.local -> -Users-dev--local
|
|
39
|
+
|
|
40
|
+
### `project_path_for(path)` <a id="method-i-project_path_for"></a> <a id="project_path_for-instance_method"></a>
|
|
41
|
+
cwd is NOT on line 1. Real sessions open with a kebab-case preamble (ai-title,
|
|
42
|
+
agent-name, mode, permission-mode) followed by a variable-length run of
|
|
43
|
+
file-history-snapshot records — that run is what pushes the first cwd-bearing
|
|
44
|
+
record out further on some files, and nothing bounds its length. Observed on
|
|
45
|
+
this machine on 2026-08-05: line 3 (19 files), line 4 (48 files), line 9 (1
|
|
46
|
+
file, a longer snapshot run). limit: 25 is ~2.8x that observed maximum —
|
|
47
|
+
headroom for the variable-length run, not a tight fit to the common case — and
|
|
48
|
+
keeps this a few-KB read even on multi-GB files.
|
|
49
|
+
|
|
50
|
+
The block guards against a record that carries "cwd" but not usably: null
|
|
51
|
+
shadows a later valid record, and a wrong type (Integer, Hash) would otherwise
|
|
52
|
+
reach project_paths' .uniq.sort and raise there. scan_jsonl_for_key already
|
|
53
|
+
guarantees the key is present once the block accepts, so a plain fetch (no
|
|
54
|
+
default) is safe.
|
|
55
|
+
|
|
56
|
+
### `retention()` <a id="method-i-retention"></a> <a id="retention-instance_method"></a>
|
|
57
|
+
Not documented.
|
|
58
|
+
|
|
59
|
+
### `retention_source()` <a id="method-i-retention_source"></a> <a id="retention_source-instance_method"></a>
|
|
60
|
+
Not documented.
|
|
61
|
+
|
|
62
|
+
### `warnings()` <a id="method-i-warnings"></a> <a id="warnings-instance_method"></a>
|
|
63
|
+
Not documented.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Class Agent::Sessions::Adapters::Codex <a id="class-Agent-Sessions-Adapters-Codex"></a>
|
|
2
|
+
|
|
3
|
+
| | |
|
|
4
|
+
| --- | --- |
|
|
5
|
+
| **Inherits** | [Agent::Sessions::Adapters::Base](Base.md) |
|
|
6
|
+
| **Defined in** | lib/agent/sessions/adapters/codex.rb |
|
|
7
|
+
|
|
8
|
+
## Constants
|
|
9
|
+
### `FILENAME` <a id="constant-FILENAME"></a> <a id="FILENAME-constant"></a>
|
|
10
|
+
rollout-<YYYY-MM-DDTHH-MM-SS>-<uuid>.jsonl (verified 2026-08-05 against 360
|
|
11
|
+
real session files on this machine — every one matched). The timestamp uses
|
|
12
|
+
the local clock and dashes where ISO 8601 has colons. started_at_for's comment
|
|
13
|
+
says what "local" costs elsewhere. The uuid group is pinned to its actual
|
|
14
|
+
shape (8-4-4-4-12 hex), not (.+): greedy against .jsonlz, (.+) would swallow a
|
|
15
|
+
sync tool's or backup's " (conflicted copy)" suffix into what looks like a
|
|
16
|
+
canonical id rather than falling back to the basename, where such a copy is at
|
|
17
|
+
least visibly non-canonical.
|
|
18
|
+
|
|
19
|
+
## Public Class Methods
|
|
20
|
+
### `reader_class()` <a id="method-c-reader_class"></a> <a id="reader_class-class_method"></a>
|
|
21
|
+
Not documented.
|
|
22
|
+
|
|
23
|
+
## Public Instance Methods
|
|
24
|
+
### `project_path_for(path)` <a id="method-i-project_path_for"></a> <a id="project_path_for-instance_method"></a>
|
|
25
|
+
Line 1 is session_meta; the cwd lives in its payload (design doc section 6 and
|
|
26
|
+
8.2, verified 2026-08-05 — 360/360 real files carry a usable
|
|
27
|
+
session_meta/payload/cwd on line 1). limit: 3 is slack against that guarantee,
|
|
28
|
+
not a fit to any observed multi-line case: it tolerates a truncated or blank
|
|
29
|
+
first line without paying for an unbounded scan. "3" counts iterations of
|
|
30
|
+
File.foreach(path, "n", MAX_LINE_BYTES), not lines: a >1MB record is chunked
|
|
31
|
+
and each chunk is one iteration, so this is really 3MB of read headroom, not
|
|
32
|
+
"3 records." A future adapter copying this pattern with limit: 1 would lose
|
|
33
|
+
that tolerance entirely.
|
|
34
|
+
|
|
35
|
+
The predicate requires more than scan_jsonl_for_key's key-presence check can:
|
|
36
|
+
real sessions on this machine also carry a "payload" key on later,
|
|
37
|
+
non-session_meta records (turn_context observed 2026-08-05) whose payload
|
|
38
|
+
itself carries "cwd" — a presence-only scan would stop at whichever comes
|
|
39
|
+
first, right only by coincidence. Requiring type == "session_meta" pins the
|
|
40
|
+
read to the one documented source of truth (design doc 8.2), and requiring a
|
|
41
|
+
Hash payload with a String cwd stops a malformed record (payload not a Hash,
|
|
42
|
+
or cwd not a String) from permanently shadowing a later, usable session_meta
|
|
43
|
+
or reaching project_paths' .uniq.sort with the wrong type.
|
|
44
|
+
|
|
45
|
+
### `session_id_from(path)` <a id="method-i-session_id_from"></a> <a id="session_id_from-instance_method"></a>
|
|
46
|
+
Not documented.
|
|
47
|
+
|
|
48
|
+
### `sessions()` <a id="method-i-sessions"></a> <a id="sessions-instance_method"></a>
|
|
49
|
+
Codex writes rollout files to two stores, and Base enumerates only the primary
|
|
50
|
+
one. An archived session is still a session — a real one was found outside the
|
|
51
|
+
sessions/ glob on 2026-08-10 — and a session the gem does not report is the
|
|
52
|
+
silent under-reporting this design treats as its worst failure mode. Every
|
|
53
|
+
filename hook below applies unchanged: the archived files carry the same
|
|
54
|
+
rollout-<timestamp>-<uuid>.jsonl name.
|
|
55
|
+
|
|
56
|
+
`super` first, so the guard it raises when the primary store has no known
|
|
57
|
+
layout still fires, and so live sessions come out before archived ones.
|
|
58
|
+
Chained rather than concatenated to keep the result lazy: a caller taking
|
|
59
|
+
first(n) must not stat an archived file it never asked about.
|
|
60
|
+
|
|
61
|
+
### `started_at_for(path, stat)` <a id="method-i-started_at_for"></a> <a id="started_at_for-instance_method"></a>
|
|
62
|
+
The digit groups accept 00-99 each, which Time.new does not: month 13, minute
|
|
63
|
+
60, and similar out-of-range values raise ArgumentError rather than being
|
|
64
|
+
normalized. That is file DATA, not an adapter bug, so it must not cross the
|
|
65
|
+
line build_session draws between the two (a raising hook is meant to surface
|
|
66
|
+
as a programming error) — one such filename among many good ones would
|
|
67
|
+
otherwise take sessions, project_paths, and sessions_for_project down to zero
|
|
68
|
+
for every agent, not just Codex.
|
|
69
|
+
|
|
70
|
+
Local, not UTC: session_meta's own "timestamp" field is UTC and agrees with
|
|
71
|
+
this to within 1s across all 360 real files here, but a machine whose TZ
|
|
72
|
+
changed, or a store copied from another machine, would make this off by the
|
|
73
|
+
offset delta while Claude's birthtime-based started_at stays an absolute
|
|
74
|
+
instant. Harmless today because Task 10 sorts sessions by updated_at, not
|
|
75
|
+
started_at. The rescue wraps Time.new alone rather than the whole method. A
|
|
76
|
+
method-scoped rescue would also swallow an ArgumentError from a future
|
|
77
|
+
signature change — the commonest Ruby programming error — and silently return
|
|
78
|
+
birthtime for every Codex session: a plausible-looking wrong started_at with
|
|
79
|
+
no signal, which is worse than a crash.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Class Agent::Sessions::Adapters::Copilot <a id="class-Agent-Sessions-Adapters-Copilot"></a>
|
|
2
|
+
|
|
3
|
+
| | |
|
|
4
|
+
| --- | --- |
|
|
5
|
+
| **Inherits** | [Agent::Sessions::Adapters::Base](Base.md) |
|
|
6
|
+
| **Defined in** | lib/agent/sessions/adapters/copilot.rb |
|
|
7
|
+
|
|
8
|
+
GitHub Copilot CLI. Verified against a real store on this machine
|
|
9
|
+
(2026-08-24): ~/.copilot/session-store.db, schema_version 3, one session.
|
|
10
|
+
|
|
11
|
+
This store has MOVED since tokentelemetry's parser was written against it:
|
|
12
|
+
that reads ~/.copilot/session-state/<id>/events.jsonl, and no such file exists
|
|
13
|
+
here. The session-state/<id>/ directory does still exist as a companion
|
|
14
|
+
(workspace.yaml, checkpoints/, files/, research/), but the session record
|
|
15
|
+
itself is now a row in SQLite. An adapter following the older spec would
|
|
16
|
+
report nothing on a current install — the failure this gem's `verified_on`
|
|
17
|
+
dates exist to make visible.
|
|
18
|
+
|
|
19
|
+
## Constants
|
|
20
|
+
### `SESSION_COLUMNS` <a id="constant-SESSION_COLUMNS"></a> <a id="SESSION_COLUMNS-constant"></a>
|
|
21
|
+
created_at/updated_at are ISO 8601 strings here, not the epoch milliseconds
|
|
22
|
+
opencode and Cursor use — verified against a real row
|
|
23
|
+
("2026-05-26T04:36:01.288Z").
|
|
24
|
+
|
|
25
|
+
## Public Class Methods
|
|
26
|
+
### `reader_class()` <a id="method-c-reader_class"></a> <a id="reader_class-class_method"></a>
|
|
27
|
+
Not documented.
|
|
28
|
+
|
|
29
|
+
## Public Instance Methods
|
|
30
|
+
### `project_paths()` <a id="method-i-project_paths"></a> <a id="project_paths-instance_method"></a>
|
|
31
|
+
Not documented.
|
|
32
|
+
|
|
33
|
+
### `sessions()` <a id="method-i-sessions"></a> <a id="sessions-instance_method"></a>
|
|
34
|
+
Not documented.
|
|
35
|
+
|
|
36
|
+
### `sessions_for_project(dir)` <a id="method-i-sessions_for_project"></a> <a id="sessions_for_project-instance_method"></a>
|
|
37
|
+
cwd is a real column holding a real absolute path, so filtering is a WHERE
|
|
38
|
+
clause rather than a read-and-compare loop.
|