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,54 @@
1
+ # Class Agent::Sessions::Adapters::Cursor <a id="class-Agent-Sessions-Adapters-Cursor"></a>
2
+
3
+ | | |
4
+ | --- | --- |
5
+ | **Inherits** | [Agent::Sessions::Adapters::Base](Base.md) |
6
+ | **Defined in** | lib/agent/sessions/adapters/cursor.rb |
7
+
8
+ ## Public Instance Methods
9
+ ### `project_path_for(path)` <a id="method-i-project_path_for"></a> <a id="project_path_for-instance_method"></a>
10
+ cwd's presence in meta.json is not enough on its own (rule 1): the key can
11
+ hold a Hash, an Integer, anything JSON allows, and project_paths' .uniq.sort
12
+ raises on a non-String member. is_a?(String) is the guard, not merely a style
13
+ preference.
14
+
15
+ ### `session_id_from(path)` <a id="method-i-session_id_from"></a> <a id="session_id_from-instance_method"></a>
16
+ chats/<chat-id>/<uuid>/store.db — two nested ids (design doc 16 Q5), so the
17
+ session id keeps both. The blob store is never opened here; the sibling
18
+ meta.json is the metadata source (8.3), with stat as fallback.
19
+
20
+ Pure string manipulation on `path` — File.basename/File.dirname never raise
21
+ for any String input, so this hook cannot violate rule 3 (build_session lets a
22
+ raising hook propagate) regardless of shape. A path shallower than two
23
+ segments is not reachable through this store's OWN enumeration: the glob above
24
+ is "<strong>/</strong>/store.db", and Dir.glob's "*" never crosses a "/", so
25
+ every path this adapter actually enumerates is exactly two directories deep,
26
+ by construction, not by convention (see
27
+ test_session_id_from_does_not_raise_for_a_shallow_path, which calls this hook
28
+ directly to pin the behaviour for a caller that bypasses the glob).
29
+
30
+ ### `started_at_for(path, stat)` <a id="method-i-started_at_for"></a> <a id="started_at_for-instance_method"></a>
31
+ Not documented.
32
+
33
+ ### `updated_at_for(path, stat)` <a id="method-i-updated_at_for"></a> <a id="updated_at_for-instance_method"></a>
34
+ Not documented.
35
+
36
+ ### `warnings()` <a id="method-i-warnings"></a> <a id="warnings-instance_method"></a>
37
+ meta.json's field names (createdAtMs, updatedAtMs, cwd) are read from design
38
+ doc 8.3, itself written from a machine that had them to check against — this
39
+ machine has no ~/.cursor/chats (Cursor CLI is a separate product from the
40
+ Cursor editor and is simply not installed here). Gated, the same shape as pi's
41
+ identical warning about its own unverified header key and cursor_ide's about
42
+ its real session location below: a "here is what breaks, please act on it"
43
+ report reaches only someone whose declared store actually exists.
44
+
45
+ Why THIS unverified assumption specifically needs a warning, where some others
46
+ might get away without one: the failure is silent and looks correct. If
47
+ createdAtMs/updatedAtMs are the wrong keys, meta_time returns nil and
48
+ started_at_for/updated_at_for fall back to stat.birthtime/mtime — real file
49
+ timestamps, not an obviously broken value. If cwd is the wrong key,
50
+ project_path_for returns nil exactly the way it correctly does for a chat that
51
+ genuinely has no recorded cwd. Nothing in the output distinguishes "Cursor
52
+ recorded no project" from "the gem read the wrong key" — `projects`, `du --by
53
+ project`, and `sessions_for_project` all silently under-report Cursor, with no
54
+ error and no implausible-looking number anywhere to notice.
@@ -0,0 +1,49 @@
1
+ # Class Agent::Sessions::Adapters::CursorIde <a id="class-Agent-Sessions-Adapters-CursorIde"></a>
2
+
3
+ | | |
4
+ | --- | --- |
5
+ | **Inherits** | [Agent::Sessions::Adapters::Base](Base.md) |
6
+ | **Defined in** | lib/agent/sessions/adapters/cursor_ide.rb |
7
+
8
+ Cursor's IDE agent (Composer). Repointed 2026-08-24 at the store the 0.2
9
+ adapter's own warning named as the real one, now that it has been opened
10
+ rather than inferred: ~/Library/Application Support/Cursor/User/
11
+ globalStorage/state.vscdb, table cursorDiskKV, keys composerData:<uuid> — 6
12
+ real rows on the machine this was written on. The 0.2 declaration
13
+ (~/.cursor/projects/<strong>/agent-transcripts/</strong>) does not exist there
14
+ at all, so this adapter reported nothing for an agent that had sessions.
15
+
16
+ What is verified: the file, the table, the key prefix, and the record's own
17
+ composerId/createdAt. What is NOT: anything about the conversation itself —
18
+ all six records on this machine carry "conversation": [], so the shape of a
19
+ turn has never been seen here. fidelity stays :metadata and no reader exists,
20
+ which is the honest report: this adapter can say a session happened and when,
21
+ and must not pretend to say what was said.
22
+
23
+ ## Constants
24
+ ### `KEY_PREFIX` <a id="constant-KEY_PREFIX"></a> <a id="KEY_PREFIX-constant"></a>
25
+ Not documented.
26
+
27
+ ## Public Instance Methods
28
+ ### `project_path_for(_path)` <a id="method-i-project_path_for"></a> <a id="project_path_for-instance_method"></a>
29
+ Cursor's composer records do not name a project. context.fileSelections holds
30
+ paths of files ATTACHED to a turn — on this machine, a settings file from an
31
+ unrelated directory — and a workspace root inferred from one attachment would
32
+ be a guess dressed as a fact. nil is the honest answer, and `projects`
33
+ reporting nothing for this agent is correct rather than empty-looking.
34
+
35
+ ### `project_paths()` <a id="method-i-project_paths"></a> <a id="project_paths-instance_method"></a>
36
+ Not documented.
37
+
38
+ ### `sessions()` <a id="method-i-sessions"></a> <a id="sessions-instance_method"></a>
39
+ Rows, not files, so Base's glob enumeration is replaced the way opencode's is
40
+ — including the existence check FIRST, so a machine without Cursor never needs
41
+ the sqlite3 gem at all.
42
+
43
+ value is parsed for createdAt alone. It is the whole composer document
44
+ (context, capabilities, code blocks), which is why `bytes` stays nil: a row in
45
+ a shared database has no file size of its own, and the database's size belongs
46
+ to all 7 rows together.
47
+
48
+ ### `sessions_for_project(_dir)` <a id="method-i-sessions_for_project"></a> <a id="sessions_for_project-instance_method"></a>
49
+ Not documented.
@@ -0,0 +1,135 @@
1
+ # Module Agent::Sessions::Adapters::Enumeration <a id="module-Agent-Sessions-Adapters-Enumeration"></a>
2
+
3
+ | | |
4
+ | --- | --- |
5
+ | **Defined in** | lib/agent/sessions/adapters/enumeration.rb |
6
+
7
+ Layer 2: turning a resolved store into sessions. Split out of Base once it
8
+ held three concerns at 460 lines, and before Layer 3 readers add a fourth.
9
+
10
+ Mixed into Base rather than included per adapter, so every adapter keeps
11
+ inheriting all of this and overriding the hooks it needs — the extraction is a
12
+ move, not a change in how an adapter is written.
13
+
14
+ What this half needs from Layer 1 is deliberately small, and worth keeping
15
+ small: `primary_layer` (the store to enumerate and the format to stamp on each
16
+ session) and the class-level DSL readers `agent_name` and `fidelity_value`.
17
+ Nothing here resolves a path, reads an env override, or touches @env. A method
18
+ that needs to do any of those belongs in Base.
19
+
20
+ ## Constants
21
+ ### `MAX_LINE_BYTES` <a id="constant-MAX_LINE_BYTES"></a> <a id="MAX_LINE_BYTES-constant"></a>
22
+ Caps how many bytes one iteration of a JSONL scan may pull into memory. "One
23
+ line" is not a bounded quantity on disk: a record carrying a pasted file or a
24
+ base64 image is routinely tens of MB, and a truncated file may hold no newline
25
+ at all. An over-long line arrives as chunks of this size, which fail to parse
26
+ and are skipped, so the scan gives up rather than reading a 2.6 GB file into a
27
+ single String.
28
+
29
+ ## Public Instance Methods
30
+ ### `bytes_for(_path, stat)` <a id="method-i-bytes_for"></a> <a id="bytes_for-instance_method"></a>
31
+ Bytes this session occupies on disk. The transcript alone for a store that
32
+ keeps one file per session; an adapter whose agent writes sidecar files beside
33
+ the transcript overrides this and adds them. Like the two time hooks it takes
34
+ the stat the enumerator already holds, so the common case still costs nothing
35
+ beyond the syscall already made.
36
+
37
+ An override runs EAGERLY for every session, so it carries build_session's
38
+ constraint: it must not raise on an unreadable path, or one bad sidecar takes
39
+ down the whole listing rather than its own row.
40
+
41
+ ### `encode_project(_dir)` <a id="method-i-encode_project"></a> <a id="encode_project-instance_method"></a>
42
+ nil means this adapter has no directory-name fallback rule. Used only by
43
+ sessions_for_project, and only for a session whose own recorded cwd could not
44
+ be read. When overridden: dir arrives pre-expanded here from
45
+ sessions_for_project (File.expand_path), which is the precondition an override
46
+ may rely on — a direct caller must pass an absolute, expanded path itself, or
47
+ the encoding is nonsense ("app", "~/app", and a trailing slash all encode
48
+ differently from the canonical form real project directories were named from).
49
+
50
+ ### `project_dir_name(path)` <a id="method-i-project_dir_name"></a> <a id="project_dir_name-instance_method"></a>
51
+ The directory whose name the encoding must match, when sessions_for_project
52
+ falls back to it. Overridable: not every store puts the encoded project
53
+ directly above the session file — cursor_ide nests
54
+ projects/<name>/agent-transcripts/*, where the immediate parent is
55
+ agent-transcripts and matching it would find nothing, silently.
56
+
57
+ ### `project_path_for(_path)` <a id="method-i-project_path_for"></a> <a id="project_path_for-instance_method"></a>
58
+ nil means the project is unknown for this session. Adapters override with a
59
+ bounded read of their own metadata; Base cannot guess.
60
+
61
+ ### `project_paths()` <a id="method-i-project_paths"></a> <a id="project_paths-instance_method"></a>
62
+ Distinct recorded project paths, sorted. This is the read-everything direction
63
+ (design doc section 7): the encodings cannot be reversed, so the recorded cwd
64
+ inside each file is the only reliable source. Sessions whose project cannot be
65
+ determined are excluded, not returned as nil.
66
+
67
+ Sorted rather than left in glob order because a stable order is what makes
68
+ `projects` output diffable and `du --by project` deterministic — and adapters
69
+ answering from a database would otherwise impose their own.
70
+
71
+ ### `session_id_from(path)` <a id="method-i-session_id_from"></a> <a id="session_id_from-instance_method"></a>
72
+ --- Layer 2 hooks, overridable per adapter ---
73
+
74
+ ### `sessions()` <a id="method-i-sessions"></a> <a id="sessions-instance_method"></a>
75
+ Lazily enumerates the primary store. Each consumed session costs one stat plus
76
+ filename parsing — never a content read. project_path is the exception and
77
+ pays for itself on first access.
78
+
79
+ A store the gem has no layout for is refused rather than reported empty:
80
+ Location#enumerable? exists precisely so "nothing here to enumerate" and
81
+ "enumerated, found none" stay distinguishable, and silently returning no
82
+ sessions is this gem's worst failure mode.
83
+
84
+ ### `sessions_for_project(dir)` <a id="method-i-sessions_for_project"></a> <a id="sessions_for_project-instance_method"></a>
85
+ Match by RECORDED cwd, exact, per session (design doc section 7, revised
86
+ 2026-08-05 — the third design for this method, kept honest here because the
87
+ next reader deserves to know why it is not "cheap"). The first two designs
88
+ were built and disproved against a real store, not in theory:
89
+
90
+ 1. Directory-name matching (the original design) assumed a
91
+ session's parent directory equals encode(its own recorded cwd).
92
+ A project rename breaks that: the agent keeps writing under the
93
+ OLD encoded directory, so two directories can hold live sessions
94
+ for the SAME current cwd, and name-only matching silently
95
+ dropped the stale one — false negatives, the failure mode this
96
+ gem treats as worst (decision 11).
97
+
98
+ 2. One-read-per-directory sampling (the first fix for #1) assumed
99
+ sessions within a directory share a cwd, to keep the match
100
+ sublinear. Reading a real renamed project's stale directory
101
+ disproved that: two of its three sessions had been resumed after
102
+ the rename and recorded the NEW cwd; the third was never resumed
103
+ and still recorded the OLD one. Sampling one session and
104
+ applying its verdict to the whole directory is wrong in BOTH
105
+ directions on the same store — it invented a false positive
106
+ here, and a different glob order would just as easily have
107
+ reproduced #1's false negative for that same directory.
108
+ Approximate cwd resolution doesn't make the error smaller; it
109
+ just moves where it lands.
110
+
111
+ Measured cost of reading every session instead of sampling: 0.17 ms per
112
+ session (68 real Claude sessions, full sweep, 0.012s total) — 0.7s
113
+ extrapolated to a 4,000-session store. That is what the sampling complexity
114
+ was buying, and it is not a trade worth making: the enumerator is already
115
+ lazy, so a caller taking first(n) never pays for sessions it never asked
116
+ about, and even the worst case (every session checked, no match) stays under a
117
+ second on a store two orders of magnitude larger than anything observed.
118
+
119
+ A session whose own cwd cannot be read (the scan gave up, the file is
120
+ unreadable, the adapter declares no reader) falls back to comparing ITS OWN
121
+ directory's name against the encoding, when the adapter declares one — this is
122
+ the only thing encode_project still buys: it keeps a session with an
123
+ unreadable header from becoming invisible, without resolving an unknown
124
+ project for every other session that happens to share its directory.
125
+
126
+ ### `started_at_for(_path, stat)` <a id="method-i-started_at_for"></a> <a id="started_at_for-instance_method"></a>
127
+ Both time hooks take the stat the enumerator already holds, so a session still
128
+ costs one syscall. path is passed for adapters that answer from a sibling
129
+ metadata file instead.
130
+
131
+ nil beats a wrong guess when the filesystem cannot answer at all — and it says
132
+ so through ENOSYS/EPERM from statx as often as NotImplementedError.
133
+
134
+ ### `updated_at_for(_path, stat)` <a id="method-i-updated_at_for"></a> <a id="updated_at_for-instance_method"></a>
135
+ Not documented.
@@ -0,0 +1,67 @@
1
+ # Class Agent::Sessions::Adapters::Gemini <a id="class-Agent-Sessions-Adapters-Gemini"></a>
2
+
3
+ | | |
4
+ | --- | --- |
5
+ | **Inherits** | [Agent::Sessions::Adapters::Base](Base.md) |
6
+ | **Defined in** | lib/agent/sessions/adapters/gemini.rb |
7
+
8
+ Gemini CLI. Verified against a real store on this machine (2026-08-24): 9
9
+ project directories, 12 chat files, 121 records.
10
+
11
+ The store is keyed by an opaque project hash, not by an encoded path:
12
+ ~/.gemini/tmp/<projectHash>/chats/session-<UTC stamp>-<hex8>.json, with a
13
+ sibling logs.json holding a flat prompt log. Nothing anywhere under the store
14
+ names a working directory — grepping every JSON file in it for cwd, workspace,
15
+ projectPath, rootPath and directory found zero — which is why project
16
+ resolution below depends on a separate map file rather than on decoding the
17
+ hash.
18
+
19
+ ## Constants
20
+ ### `FILENAME` <a id="constant-FILENAME"></a> <a id="FILENAME-constant"></a>
21
+ session-2025-11-29T20-08-b20947ab.json. The trailing hex is NOT a session id:
22
+ two files in the real store share d4abc9ce while being different sessions, so
23
+ the whole basename is the id — unique, stable, and derivable without opening
24
+ the file, which is what Layer 2 is for. The agent's own sessionId lives inside
25
+ the document and reaches a caller through the reader.
26
+
27
+ ## Public Class Methods
28
+ ### `reader_class()` <a id="method-c-reader_class"></a> <a id="reader_class-class_method"></a>
29
+ Not documented.
30
+
31
+ ## Public Instance Methods
32
+ ### `encode_project(dir)` <a id="method-i-encode_project"></a> <a id="encode_project-instance_method"></a>
33
+ Not documented.
34
+
35
+ ### `project_dir_name(path)` <a id="method-i-project_dir_name"></a> <a id="project_dir_name-instance_method"></a>
36
+ <base>/tmp/<projectHash>/chats/<file>.json — two levels up from the file, not
37
+ one, so Base's default (the immediate parent) would answer "chats" for every
38
+ session.
39
+
40
+ ### `project_path_for(path)` <a id="method-i-project_path_for"></a> <a id="project_path_for-instance_method"></a>
41
+ The store groups sessions under a hash of the project directory that this gem
42
+ cannot reverse: it is not a plain SHA-256 of the path (tested directly against
43
+ real directories), and the store records the path nowhere else.
44
+ ~/.gemini/projects.json is the map Gemini itself keeps —when it exists, this
45
+ reads it; when it does not, nil is the honest answer and `projects` reports
46
+ nothing rather than inventing a name from the hash.
47
+
48
+ ### `project_paths()` <a id="method-i-project_paths"></a> <a id="project_paths-instance_method"></a>
49
+ Not documented.
50
+
51
+ ### `started_at_for(path, stat)` <a id="method-i-started_at_for"></a> <a id="started_at_for-instance_method"></a>
52
+ UTC, unlike Codex and pi, whose rollout filenames use the local clock.
53
+ Verified rather than assumed: four real filenames match their own document's
54
+ startTime to the minute when read as UTC (12-42 against
55
+ 2025-12-12T12:42:50.033Z), and would be three hours out as local time on the
56
+ machine this was written on.
57
+
58
+ Minute precision only — the document's startTime carries seconds, but reading
59
+ it would cost a parse per session, and Layer 2 is stat-only.
60
+
61
+ ### `warnings()` <a id="method-i-warnings"></a> <a id="warnings-instance_method"></a>
62
+ The delta-log variant: tokentelemetry's parser of this same store handles
63
+ chats written as JSONL with a header line and `$set` deltas. No such file
64
+ exists here (zero .jsonl anywhere under the store), so this adapter reads the
65
+ verified .json spelling only — and says so where a user with the other
66
+ spelling will see it, rather than silently enumerating nothing for half their
67
+ sessions.
@@ -0,0 +1,64 @@
1
+ # Class Agent::Sessions::Adapters::Grok <a id="class-Agent-Sessions-Adapters-Grok"></a>
2
+
3
+ | | |
4
+ | --- | --- |
5
+ | **Inherits** | [Agent::Sessions::Adapters::Base](Base.md) |
6
+ | **Defined in** | lib/agent/sessions/adapters/grok.rb |
7
+
8
+ Grok Build (xAI). PROVISIONAL: ~/.grok does not exist on the machine this was
9
+ written on (2026-08-24), so every claim follows tokentelemetry's working
10
+ parser of the same store (resources/tokentelemetry, _scan_grok_sessions and
11
+ _grok_usage_from_unified_log) rather than observation.
12
+
13
+ A Grok session is a DIRECTORY, not a file:
14
+ ~/.grok/sessions/<url-encoded cwd>/<session-uuid>/
15
+ summary.json chat_history.jsonl events.jsonl updates.jsonl
16
+ signals.json plan_mode.json subagents/<spawn-id>/meta.json
17
+
18
+ summary.json is what this adapter enumerates, because it is the record that
19
+ always exists and carries the session's own metadata. The transcript beside it
20
+ is what the reader reads, and `bytes` counts the whole directory — the same
21
+ choice Claude's adapter makes for its sidecar tree, and for the same reason:
22
+ those bytes belong to this session, and a `du` that ignored them would
23
+ disagree with the disk.
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
+ ### `bytes_for(path, stat)` <a id="method-i-bytes_for"></a> <a id="bytes_for-instance_method"></a>
31
+ The whole session directory, not just summary.json: the transcript and every
32
+ sibling log live in it.
33
+
34
+ ### `decode_project(name)` <a id="method-i-decode_project"></a> <a id="decode_project-instance_method"></a>
35
+ Not documented.
36
+
37
+ ### `encode_project(dir)` <a id="method-i-encode_project"></a> <a id="encode_project-instance_method"></a>
38
+ Not documented.
39
+
40
+ ### `project_dir_name(path)` <a id="method-i-project_dir_name"></a> <a id="project_dir_name-instance_method"></a>
41
+ Not documented.
42
+
43
+ ### `project_path_for(path)` <a id="method-i-project_path_for"></a> <a id="project_path_for-instance_method"></a>
44
+ The project bucket is a URL-encoded absolute path (tokentelemetry unquotes
45
+ it), so unlike Claude's and pi's dash encodings this one is losslessly
46
+ reversible. summary.json's own info.cwd is preferred where readable, because a
47
+ recorded path beats a decoded directory name; the decode is the fallback, and
48
+ a good one.
49
+
50
+ ### `session_id_from(path)` <a id="method-i-session_id_from"></a> <a id="session_id_from-instance_method"></a>
51
+ <sessions>/<url-encoded cwd>/<session-uuid>/summary.json — the id is the
52
+ directory holding the file, not the file's own basename, which is the constant
53
+ "summary".
54
+
55
+ ### `started_at_for(path, stat)` <a id="method-i-started_at_for"></a> <a id="started_at_for-instance_method"></a>
56
+ summary.json carries the session's own clock; the file's mtime is only ever a
57
+ proxy for it. Both are ISO 8601 strings per the reference parser, with
58
+ created_at standing in when updated_at is absent.
59
+
60
+ ### `updated_at_for(path, stat)` <a id="method-i-updated_at_for"></a> <a id="updated_at_for-instance_method"></a>
61
+ Not documented.
62
+
63
+ ### `warnings()` <a id="method-i-warnings"></a> <a id="warnings-instance_method"></a>
64
+ Not documented.
@@ -0,0 +1,93 @@
1
+ # Class Agent::Sessions::Adapters::Opencode <a id="class-Agent-Sessions-Adapters-Opencode"></a>
2
+
3
+ | | |
4
+ | --- | --- |
5
+ | **Inherits** | [Agent::Sessions::Adapters::Base](Base.md) |
6
+ | **Defined in** | lib/agent/sessions/adapters/opencode.rb |
7
+
8
+ ## Constants
9
+ ### `DATABASE_GLOB` <a id="constant-DATABASE_GLOB"></a> <a id="DATABASE_GLOB-constant"></a>
10
+ opencode names its database per release channel — opencode.db,
11
+ opencode-stable.db — so the filename is a glob, not a constant (tokentelemetry
12
+ globs the same pattern). Unverified here: this machine has only the plain
13
+ name.
14
+
15
+ ### `SESSION_COLUMNS` <a id="constant-SESSION_COLUMNS"></a> <a id="SESSION_COLUMNS-constant"></a>
16
+ Not documented.
17
+
18
+ ## Public Class Methods
19
+ ### `reader_class()` <a id="method-c-reader_class"></a> <a id="reader_class-class_method"></a>
20
+ Not documented.
21
+
22
+ ## Public Instance Methods
23
+ ### `base_dir()` <a id="method-i-base_dir"></a> <a id="base_dir-instance_method"></a>
24
+ The declared default stands unless another candidate actually holds a
25
+ database. Falling back to it rather than to the first candidate that merely
26
+ exists keeps `where` printing a concrete, conventional path on a machine with
27
+ no opencode at all.
28
+
29
+ ### `project_paths()` <a id="method-i-project_paths"></a> <a id="project_paths-instance_method"></a>
30
+ ORDER BY matches the sorted order Base guarantees, so `projects` output is
31
+ stable and diffable whichever adapter answers it. is_a?(String) excludes a row
32
+ whose directory is NULL (build_db_session's guard, same rule 2 container
33
+ check) rather than letting a literal nil sort in among real paths — "excluded,
34
+ not nil", matching Base's project_paths docstring. .uniq is needed on top of
35
+ SQL's own DISTINCT: SQLite's DISTINCT treats a BLOB and a byte-identical TEXT
36
+ value as different rows (confirmed directly — typeof reports "text" vs "blob"
37
+ for the same bytes even though the sqlite3 gem returns both to Ruby as String,
38
+ see build_db_session's comment), so without this a blob/text pair with
39
+ identical bytes would surface as two entries where Base's own
40
+ <code>.uniq.sort</code> would collapse them to one.
41
+
42
+ ### `sessions()` <a id="method-i-sessions"></a> <a id="sessions-instance_method"></a>
43
+ Sessions are rows, not files, so the Base glob enumeration is replaced by a
44
+ deferred query: it runs at first consumption, and only if the database exists.
45
+ The existence check comes FIRST so machines without opencode never need
46
+ sqlite3 at all (design doc section 9).
47
+
48
+ Row order is deliberately unspecified: no ORDER BY, so rows arrive in whatever
49
+ order SQLite's own scan produces (rowid order, absent an index that would
50
+ change it) — unlike the other six adapters, which are path-sorted for free by
51
+ Dir.glob. Invisible today because nothing here sorts before Task 10 does its
52
+ own sort_by(&:updated_at); stated so a future caller of THIS method directly
53
+ does not come to depend on insertion order looking stable.
54
+
55
+ The gap between this check and the open below is a real TOCTOU window
56
+ —opencode could delete or migrate the file in between — but the open that
57
+ follows a vanished file raises SQLite3::CantOpenException (verified directly),
58
+ which each_session_row already turns into UnreadableStore. That is judged the
59
+ right answer, not a bug to special-case: unlike Base's own glob-then-stat race
60
+ (one file silently missing from a multi-file listing, so build_session's
61
+ rescue drops it and moves on), a vanished DATABASE is the store's only source
62
+ for every session, so there is nothing partial to return — "the store I just
63
+ confirmed exists is now unreadable" is what happened, and UnreadableStore says
64
+ exactly that.
65
+
66
+ Opens once per consumption, not once per instance: `sessions`,
67
+ `sessions_for_project` and `project_paths` each open, query and close their
68
+ own connection through each_session_row. That costs an extra open when a
69
+ caller uses more than one of the three, but keeps every method independently
70
+ correct rather than threading a shared handle through them — and
71
+ Enumerator.new's block does not even run until the RETURNED lazy enumerator is
72
+ consumed, so a caller that builds `sessions` and never touches it opens
73
+ nothing at all. Confirmed empirically (not just assumed from Enumerator's
74
+ docs) that the `ensure db&.close` inside each_session_row fires promptly
75
+ either way a caller can stop early —`.lazy.first(n)` and an external `each {
76
+ break }` both unwind the generator fiber immediately, before the outer call
77
+ returns — so a caller taking <code>sessions.first</code> never leaves a
78
+ connection open waiting for GC to reclaim the fiber.
79
+
80
+ ### `sessions_for_project(dir)` <a id="method-i-sessions_for_project"></a> <a id="sessions_for_project-instance_method"></a>
81
+ The directory column holds the full recorded path, so filtering is a WHERE
82
+ clause instead of the Base read-and-compare loop.
83
+
84
+ ### `verify()` <a id="method-i-verify"></a> <a id="verify-instance_method"></a>
85
+ Base checks the declared path literally, which gets a machine holding only a
86
+ channel-named database wrong twice: its "is this agent installed" gate sees no
87
+ declared layer and skips the store checks entirely, and the store check itself
88
+ would report :fail on a real store that is merely called something the
89
+ declaration did not predict.
90
+
91
+ Any file matching the glob satisfies the claim. The detail names what was
92
+ actually found, so a non-canonical filename is visible rather than merely
93
+ tolerated — the same reason detail_for prints a file count.
@@ -0,0 +1,137 @@
1
+ # Class Agent::Sessions::Adapters::Pi <a id="class-Agent-Sessions-Adapters-Pi"></a>
2
+
3
+ | | |
4
+ | --- | --- |
5
+ | **Inherits** | [Agent::Sessions::Adapters::Base](Base.md) |
6
+ | **Defined in** | lib/agent/sessions/adapters/pi.rb |
7
+
8
+ ## Constants
9
+ ### `FILENAME` <a id="constant-FILENAME"></a> <a id="FILENAME-constant"></a>
10
+ FILENAME's h{8} id specifically contradicts the one written source available:
11
+ design doc section 8.6 says pi's **entries** carry an 8-character hex id,
12
+ while the section 8 table gives the filename itself as <timestamp>_<uuid>.
13
+ Both cannot be right, and nothing on this machine can settle which one pi's
14
+ own encoder does. h{8} is what is implemented here; if a real file uses a full
15
+ uuid instead, this regex simply never matches it, and session_id_from below
16
+ falls back to the basename — a real but visibly non-canonical id, not a crash.
17
+
18
+ ## Public Class Methods
19
+ ### `reader_class()` <a id="method-c-reader_class"></a> <a id="reader_class-class_method"></a>
20
+ The reader shares this adapter's provisional standing: written against
21
+ tokentelemetry's parser of the same format, since this machine's pi store
22
+ holds no session files to observe. Its own header comment says what remains
23
+ unverified.
24
+
25
+ ## Public Instance Methods
26
+ ### `encode_project(dir)` <a id="method-i-encode_project"></a> <a id="encode_project-instance_method"></a>
27
+ Verified against nine real pi project directories found on this machine on
28
+ 2026-08-05 (~/.pi/agent/sessions/--*--, empty of .jsonl but real encoder
29
+ output regardless — see the class comment above) — see
30
+ test_encode_project_round_trips_the_nine_real_pi_directories in
31
+ test/pi_adapter_test.rb. Design doc section 7 described this only as "wrap the
32
+ dashed cwd in double dashes," ambiguous on two points neither of us had
33
+ settled by observation. Both are now settled by real output rather than by
34
+ carrying Claude's rule over into pi's:
35
+
36
+ 1. The dash count. Read literally — dash-encode the WHOLE cwd,
37
+ including its leading "/", then wrap that in "--" — an
38
+ absolute path would get THREE leading dashes. Real pi output
39
+ has TWO: the leading "/" is absorbed into the wrap rather than
40
+ separately encoded, matching the store's own "--*--/*.jsonl"
41
+ glob (above).
42
+
43
+ 2. The character class. This used to read like Claude's "every
44
+ non-alphanumeric character becomes -" and that was WRONG: pi
45
+ preserves dots. Two of the nine real directories contain a
46
+ literal "." (a domain name in the path) unchanged, while the
47
+ "/" separators around it became "-". Claude has 45 project
48
+ directories on this same machine and not one contains a dot —
49
+ the two adapters' rules genuinely differ; they do not merely
50
+ happen to agree on every example seen before now.
51
+
52
+ What remains a guess: "_", spaces, and any other non-"/" separator never
53
+ appear in the nine real directories, so nothing here confirms whether pi
54
+ encodes them or preserves them too, the way it preserves ".". Do not widen
55
+ this gsub back into a character class without new evidence — that is exactly
56
+ the mistake being corrected here.
57
+
58
+ This directory-name encoding is what sessions_for_project falls back to when a
59
+ session's own header cwd cannot be read (see Base#encode_project) — pi's whole
60
+ safety net for project_path_for's still-unverified "cwd" header key
61
+ assumption. That fallback is now solid: a wrong "cwd" key degrades to accurate
62
+ name matching instead of two guesses compounding into silent failure.
63
+
64
+ Expects an absolute, expanded path; sessions_for_project expands first.
65
+
66
+ ### `project_path_for(path)` <a id="method-i-project_path_for"></a> <a id="project_path_for-instance_method"></a>
67
+ pi publishes its format: one header line, then typed entries (design doc 8.6)
68
+ — which argues for staying TIGHTER than Claude's 25, whose cwd genuinely was
69
+ not on line 1 and whose format was never published. But "publishes a spec" is
70
+ not the same evidence as "measured against a real file," and this machine has
71
+ zero pi sessions to measure against. limit: 25 matches Claude's number not
72
+ because pi is assumed to behave like Claude, but because scan_jsonl_for_key
73
+ returns as soon as it finds a usable record: the width costs nothing while the
74
+ line-1 assumption holds, and is only ever paid on the one case this file
75
+ cannot rule out — a preamble pi does not document, the same way Claude's
76
+ kebab-case preamble was not documented either. The one real cost of going
77
+ wide: the predicate below is type-checked but not otherwise selective, so a
78
+ longer window is more exposure to a later, unrelated record that happens to
79
+ carry a String "cwd" of its own — a decoy shadowing pi's real one —a risk this
80
+ file cannot bound without a real session to look at.
81
+
82
+ The predicate is mandatory, not decoration. scan_jsonl_for_key stops at the
83
+ first record merely CARRYING the key, so without a value guard a record
84
+ holding "cwd": null shadows a later usable one permanently, and a non-String
85
+ cwd reaches project_paths' .uniq.sort and raises.
86
+
87
+ ### `session_id_from(path)` <a id="method-i-session_id_from"></a> <a id="session_id_from-instance_method"></a>
88
+ Not documented.
89
+
90
+ ### `started_at_for(path, stat)` <a id="method-i-started_at_for"></a> <a id="started_at_for-instance_method"></a>
91
+ The rescue is not optional. d{2} accepts 00-99, and Time.new raises
92
+ ArgumentError on month 13, minute 60 and friends. build_session scopes its own
93
+ rescue to File.stat so that a raising hook surfaces as the adapter bug it
94
+ usually is — but this hook raises on FILE DATA, and without the rescue one
95
+ malformed filename returns zero sessions from `sessions`, `project_paths` and
96
+ `for_project` alike, and exits the CLI with a raw backtrace that takes every
97
+ other agent's rows with it. That failure mode was measured in Task 4, against
98
+ Codex — pi has no real filenames of its own to reproduce it against, but the
99
+ mechanism (Time.new rejecting digits d{2} happily accepted) belongs to Ruby,
100
+ not to any one adapter's data, so the same rescue applies here.
101
+
102
+ Local, not UTC: copied from Codex's VERIFIED behaviour (its rollout filenames
103
+ use the local clock, confirmed against 360 real files). pi's is UNVERIFIED —
104
+ no real pi filename exists on this machine to check it against. If pi instead
105
+ publishes UTC filenames, every pi started_at is silently off by the machine's
106
+ UTC offset, with no signal that it happened. The test fixture's header
107
+ timestamp and filename timestamp deliberately disagree (see build_fixture in
108
+ test/pi_adapter_test.rb) so that a started_at_for which quietly fell back to
109
+ reading the header would be caught returning the wrong hour, rather than
110
+ passing by coincidence on a UTC machine.
111
+
112
+ The rescue wraps Time.new alone rather than the whole method. A method-scoped
113
+ rescue would also swallow an ArgumentError from a future signature change —
114
+ the commonest Ruby programming error — and silently fall back to
115
+ stat.birthtime for every pi session: a plausible-looking wrong started_at with
116
+ no signal, which is worse than a crash.
117
+
118
+ ### `warnings()` <a id="method-i-warnings"></a> <a id="warnings-instance_method"></a>
119
+ pi's *session files* are still absent from this machine: all nine directories
120
+ under ~/.pi/agent/sessions (real pi output — see encode_project below) are
121
+ empty of .jsonl (2026-08-05). Everything about a session's CONTENT is
122
+ therefore still inference from design doc 8.6, not observation: the header's
123
+ "cwd" key (project_path_for below), which line it is on (the limit: argument
124
+ there), and whether a session's id segment is 8 hex characters or a full uuid
125
+ (FILENAME below). `warnings` below repeats the gist where a CLI user will
126
+ actually see it, gated on the store existing.
127
+
128
+ The directory NAMING scheme is a different story: it is real pi output, not a
129
+ guess — see encode_project's comment.
130
+
131
+ To check the remaining unverified points against a real session:
132
+ head -1 ~/.pi/agent/sessions/--*--/*.jsonl
133
+
134
+ and look for: which key actually holds the cwd (assumed "cwd"), which line it
135
+ is on (assumed line 1), and whether the id segment is 8 hex characters or a
136
+ full uuid (assumed 8 hex). A mismatch means fixing the matching line below and
137
+ the warning above it, not just the comment next to it.
@@ -0,0 +1,32 @@
1
+ # Class Agent::Sessions::Adapters::Qwen <a id="class-Agent-Sessions-Adapters-Qwen"></a>
2
+
3
+ | | |
4
+ | --- | --- |
5
+ | **Inherits** | [Agent::Sessions::Adapters::Base](Base.md) |
6
+ | **Defined in** | lib/agent/sessions/adapters/qwen.rb |
7
+
8
+ Qwen Code. PROVISIONAL: ~/.qwen does not exist on the machine this was written
9
+ on (2026-08-24), so every claim here comes from tokentelemetry's working
10
+ parser of the same store (resources/tokentelemetry, backend/main.py section 4)
11
+ rather than from observation — the same standing the pi reader carries, and
12
+ declared the same way.
13
+
14
+ Qwen is a Gemini CLI fork that kept Gemini's directory layout and adopted
15
+ Anthropic's message shape, which is why its store looks like ~/.gemini's while
16
+ its records read like Claude's.
17
+
18
+ ## Public Class Methods
19
+ ### `reader_class()` <a id="method-c-reader_class"></a> <a id="reader_class-class_method"></a>
20
+ Not documented.
21
+
22
+ ## Public Instance Methods
23
+ ### `project_path_for(path)` <a id="method-i-project_path_for"></a> <a id="project_path_for-instance_method"></a>
24
+ Unlike Gemini's, the project directory is reported as a name this gem cannot
25
+ decode into a path, so the recorded cwd inside the file is the only source —
26
+ the same position Claude and pi are in. The predicate is mandatory:
27
+ scan_jsonl_for_key stops at the first record merely CARRYING the key, so
28
+ without it a record holding "cwd": null shadows a later usable one
29
+ permanently.
30
+
31
+ ### `warnings()` <a id="method-i-warnings"></a> <a id="warnings-instance_method"></a>
32
+ Not documented.
@@ -0,0 +1,5 @@
1
+ # Module Agent::Sessions::Adapters <a id="module-Agent-Sessions-Adapters"></a>
2
+
3
+ | | |
4
+ | --- | --- |
5
+ | **Defined in** | lib/agent/sessions/adapters/pi.rb, lib/agent/sessions/adapters/amp.rb, lib/agent/sessions/adapters/base.rb, lib/agent/sessions/adapters/grok.rb, lib/agent/sessions/adapters/qwen.rb, lib/agent/sessions/adapters/codex.rb, lib/agent/sessions/adapters/claude.rb, lib/agent/sessions/adapters/cursor.rb, lib/agent/sessions/adapters/gemini.rb, lib/agent/sessions/adapters/copilot.rb, lib/agent/sessions/adapters/opencode.rb, lib/agent/sessions/adapters/cursor_ide.rb, lib/agent/sessions/adapters/enumeration.rb |