agent_sessions 0.3.1 → 0.4.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 +13 -0
- data/README.md +18 -2
- data/doc/Agent/Sessions/Readers/Base.md +31 -0
- data/doc/Agent/Sessions/Readers/Claude.md +3 -0
- data/doc/Agent/Sessions/Readers/Codex.md +7 -7
- data/doc/Agent/Sessions/RoundTrip.md +57 -0
- data/doc/Agent/Sessions.md +2 -1
- data/doc/Agent.md +1 -1
- data/doc/CHANGELOG.md +13 -0
- data/doc/README.md +20 -0
- data/doc/index.csv +14 -0
- data/lib/agent/sessions/readers/base.rb +95 -0
- data/lib/agent/sessions/readers/claude.rb +10 -0
- data/lib/agent/sessions/readers/codex.rb +39 -26
- data/lib/agent/sessions/round_trip.rb +39 -0
- data/lib/agent/sessions/version.rb +1 -1
- data/llm.txt +2 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 96029e1b23c71e1b1703abf8bcd1d09640a35c469da194d6e2cd0879a2da0786
|
|
4
|
+
data.tar.gz: 91f556777c0afd1af418606a8fe4fa71329688c2a6f40090e152b1cef308dbab
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b6eaa333075f8dc86b887b7b00bf02c06591bfe158b217c0d0d0f439520eeb99a755c38b92c06e276512825fe2b912dff87b97b68a2fa980d1039fc94999ff21
|
|
7
|
+
data.tar.gz: d8c484f661db60b910a650617192737051c0f275f74363e78447bea5ee10480253285a001c661ec7cc7207a5d397181f06090d027fd16f498cece2452592b940
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
## 0.4.1 (2026-09-10)
|
|
2
|
+
|
|
3
|
+
- Recognize Codex `token_usage_record` accounting metadata without adding conversation messages, round trips, or unknown-record warnings, including with `include_events: true`.
|
|
4
|
+
- Read Codex session usage from the latest usable cumulative totals in file order across legacy `token_count` events and new `thread_token_usage` records, without double-counting. Malformed or empty totals preserve earlier valid usage; existing token normalization and missing-value semantics are retained.
|
|
5
|
+
|
|
6
|
+
## 0.4.0 (2026-09-10)
|
|
7
|
+
|
|
8
|
+
- Round-trip grouping in every reader: `RoundTrip` (`index`, `messages`, `usage`, `recorded`, plus `parts`, `calls`, `roles`), `reader.each_round_trip` (streams), `reader.round_trips` (eager), `reader.round_trips_recorded?`
|
|
9
|
+
- Claude names its round-trip id — `message.id`, the id Anthropic's API gives one response — and is the only reader today where `round_trips_recorded?` answers true; every other reader falls back to one round trip per message
|
|
10
|
+
- The reappearing-id warning fires only when nothing explains the split. Measured over the 60 most recent real Claude transcripts (2026-09-10): 3,315 distinct message ids, 261 of them (7.9%) split across more than one run in 33 of the 60 files; 759 of those splits are a tool_result record, 66 a last-prompt, 12 a file-history-delta — all benign, so warning on them would put four or five lines under every real session
|
|
11
|
+
- Conformance suite case C9: `round_trips` must never lose or duplicate a message, across every reader
|
|
12
|
+
- The loop view (`Loop`, `ToolCall`, `LoopView`) and its CLI command (`show`) moved to the `agent_session_context` gem before release, so they never shipped here
|
|
13
|
+
|
|
1
14
|
## 0.3.1 (2026-08-26)
|
|
2
15
|
|
|
3
16
|
- Add tracked Markdown YARD documentation and a consolidated `llm.txt`, package both with the gem, and validate every local documentation link
|
data/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Where do AI coding agents store their session logs? This gem knows.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
<img width="1200" height="620" alt="demo" src="https://github.com/user-attachments/assets/d13e7c68-0cb4-404c-bb31-d2c3b24d3522" />
|
|
6
6
|
|
|
7
7
|
Resolves session store paths for Claude Code, Codex CLI, Cursor (CLI and IDE), Amp CLI, opencode, pi, Gemini CLI, GitHub Copilot CLI, Qwen Code, and Grok Build. Verifies those paths against disk. Audits whether plaintext transcripts sit inside anything that syncs.
|
|
8
8
|
|
|
@@ -64,7 +64,7 @@ Ruby callers passing any other symbol get `Agent::Sessions::UnknownAgent`; its m
|
|
|
64
64
|
|
|
65
65
|
## Ruby API
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
<img width="1200" height="620" alt="demo-ruby" src="https://github.com/user-attachments/assets/a0dd54b6-d597-4dad-85ae-97ce07aba2a4" />
|
|
68
68
|
|
|
69
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
70
|
|
|
@@ -122,6 +122,22 @@ reader.usage&.input # disjoint buckets: input, output,
|
|
|
122
122
|
|
|
123
123
|
`Session#bytes` is what that session occupies on disk, not just its transcript. Claude Code writes a sidecar directory beside each transcript — `<id>/subagents/`, `<id>/tool-results/` — and those bytes belong to the session that produced them, which is why `du` and `audit` agree on the same store.
|
|
124
124
|
|
|
125
|
+
### Round trips
|
|
126
|
+
|
|
127
|
+
A round trip is one model response, however many records the store split it across:
|
|
128
|
+
|
|
129
|
+
```ruby
|
|
130
|
+
reader.round_trips # eager: an Array of RoundTrip
|
|
131
|
+
reader.each_round_trip { |trip| ... } # streams; breaking early reads one group, not the file
|
|
132
|
+
reader.round_trips_recorded? # true only where the store names its own groups
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
A `RoundTrip` carries `index`, `messages`, `usage`, and `recorded`, and derives `parts` (every part, in file order), `calls` (its `:tool_use` parts), and `roles` (the distinct roles seen, first-seen order).
|
|
136
|
+
|
|
137
|
+
Claude names its groups (`message.id`); every other reader falls back to one round trip per message, and `round_trips_recorded?` answers `false` there — "not recorded" must never read as "none".
|
|
138
|
+
|
|
139
|
+
Pairing a call with the result that answers it, and rendering a session as a loop, live in the sibling [`agent_session_context`](https://github.com/lucianghinda/agent_session_context) gem, via its `agent-session-context loop` command.
|
|
140
|
+
|
|
125
141
|
### Errors
|
|
126
142
|
|
|
127
143
|
Every domain-specific error the gem raises descends from `Agent::Sessions::Error`:
|
|
@@ -47,6 +47,29 @@ every message to get them.
|
|
|
47
47
|
Streams. Yields each message as it is parsed; a caller that breaks after one
|
|
48
48
|
has read one record, not the file.
|
|
49
49
|
|
|
50
|
+
### `each_round_trip()` <a id="method-i-each_round_trip"></a> <a id="each_round_trip-instance_method"></a>
|
|
51
|
+
Streams, exactly as each_message does. A group is a RUN of neighbouring
|
|
52
|
+
records sharing one round-trip id, not every record sharing that id wherever
|
|
53
|
+
it sits in the file — collecting scattered records cannot stream, and rule 3
|
|
54
|
+
says no code path here may assume a file fits in memory.
|
|
55
|
+
|
|
56
|
+
So an id that reappears after its run closed opens a NEW round trip rather
|
|
57
|
+
than reopening the old one. That is ORDINARY for Claude, not an anomaly:
|
|
58
|
+
Claude Code writes each tool_result immediately after the tool_use it answers,
|
|
59
|
+
while every content block of the one API response keeps the same message.id,
|
|
60
|
+
so a response making two tool calls has its records split by the result in
|
|
61
|
+
between. Measured over the 60 most recent real Claude transcripts on this
|
|
62
|
+
machine (2026-09-10): 3,315 distinct message.ids, 261 of them (7.9%) split
|
|
63
|
+
across more than one run, in 33 of the 60 files; 759 of the splits are a
|
|
64
|
+
tool_result record, 66 a last-prompt, 12 a file-history-delta.
|
|
65
|
+
|
|
66
|
+
Hence the warning fires only where NOTHING answered a tool between the two
|
|
67
|
+
runs — the case that is genuinely unexplained and would mean the format
|
|
68
|
+
drifted. Warning on the benign split would put four or five lines under every
|
|
69
|
+
real session's loop view, which teaches a caller that these warnings are
|
|
70
|
+
noise.
|
|
71
|
+
- **@yield** [build_round_trip(index + 1, open_messages, open_id)]
|
|
72
|
+
|
|
50
73
|
### `fidelity()` <a id="method-i-fidelity"></a> <a id="fidelity-instance_method"></a>
|
|
51
74
|
Not documented.
|
|
52
75
|
|
|
@@ -63,6 +86,14 @@ True where the local file is not the whole story — Amp, whose server holds the
|
|
|
63
86
|
canonical copy. Overridden there, false everywhere else.
|
|
64
87
|
- **@return** [Boolean]
|
|
65
88
|
|
|
89
|
+
### `round_trips()` <a id="method-i-round_trips"></a> <a id="round_trips-instance_method"></a>
|
|
90
|
+
Not documented.
|
|
91
|
+
|
|
92
|
+
### `round_trips_recorded?()` <a id="method-i-round_trips_recorded-3F"></a> <a id="round_trips_recorded?-instance_method"></a>
|
|
93
|
+
False here: most stores are an append-only list of records and no format names
|
|
94
|
+
which of them belong to one model response.
|
|
95
|
+
- **@return** [Boolean]
|
|
96
|
+
|
|
66
97
|
### `tree()` <a id="method-i-tree"></a> <a id="tree-instance_method"></a>
|
|
67
98
|
The conversation as roots and their continuations, for an agent that records
|
|
68
99
|
parent links. Unlike every other method here this cannot stream — a tree is
|
|
@@ -56,6 +56,9 @@ tree from.
|
|
|
56
56
|
### `initialize(session, resolve_spills: true, **rest)` <a id="method-i-initialize"></a> <a id="initialize-instance_method"></a>
|
|
57
57
|
- **@return** [Claude] a new instance of Claude
|
|
58
58
|
|
|
59
|
+
### `round_trips_recorded?()` <a id="method-i-round_trips_recorded-3F"></a> <a id="round_trips_recorded?-instance_method"></a>
|
|
60
|
+
- **@return** [Boolean]
|
|
61
|
+
|
|
59
62
|
### `subagents()` <a id="method-i-subagents"></a> <a id="subagents-instance_method"></a>
|
|
60
63
|
The transcripts of agents this session spawned, as readers of their own.
|
|
61
64
|
Exposed rather than inlined, per design doc 8.1: a subagent's turns are not
|
|
@@ -39,8 +39,8 @@ caller must learn to ignore is worse than no warning.
|
|
|
39
39
|
|
|
40
40
|
### `NON_MESSAGE_TYPES` <a id="constant-NON_MESSAGE_TYPES"></a> <a id="NON_MESSAGE_TYPES-constant"></a>
|
|
41
41
|
Known, and deliberately not messages: the session header, per-turn
|
|
42
|
-
configuration,
|
|
43
|
-
a judgement, not an oversight —
|
|
42
|
+
configuration, state records, and cumulative token accounting. Silence here is
|
|
43
|
+
a judgement, not an oversight —these are not conversation, and warning about
|
|
44
44
|
them would train a caller to ignore warnings.
|
|
45
45
|
|
|
46
46
|
### `ROLES` <a id="constant-ROLES"></a> <a id="ROLES-constant"></a>
|
|
@@ -61,11 +61,11 @@ Not documented.
|
|
|
61
61
|
## Public Instance Methods
|
|
62
62
|
### `usage()` <a id="method-i-usage"></a> <a id="usage-instance_method"></a>
|
|
63
63
|
Session totals. Codex writes no usage on its messages; it writes token_count
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
turn.
|
|
64
|
+
events with info.total_token_usage, or token_usage_record records with
|
|
65
|
+
thread_token_usage (observed 2026-09-10). Both hold cumulative totals. The
|
|
66
|
+
legacy format was verified on 2026-08-24: consecutive records report total
|
|
67
|
+
33,751 then 69,135 while their last_token_usage differ, so the latest usable
|
|
68
|
+
record is the session and summing would multiply-count every earlier turn.
|
|
69
69
|
|
|
70
70
|
Two normalizations, both from that same file:
|
|
71
71
|
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Class Agent::Sessions::RoundTrip <a id="class-Agent-Sessions-RoundTrip"></a>
|
|
2
|
+
|
|
3
|
+
| | |
|
|
4
|
+
| --- | --- |
|
|
5
|
+
| **Inherits** | Data |
|
|
6
|
+
| **Defined in** | lib/agent/sessions/round_trip.rb |
|
|
7
|
+
|
|
8
|
+
One model response, grouped from however many records a format split it
|
|
9
|
+
across. Claude Code writes one record PER CONTENT BLOCK of an API response: in
|
|
10
|
+
one real transcript on this machine, 260 assistant records share only 124
|
|
11
|
+
message.id values, so a caller counting records over- counts what the session
|
|
12
|
+
actually did by roughly two to one. A round trip is the unit that actually
|
|
13
|
+
happened.
|
|
14
|
+
|
|
15
|
+
`recorded` is the load-bearing field. False means this group was ASSUMED — the
|
|
16
|
+
format names no round-trip id, so grouping fell back to one message per round
|
|
17
|
+
trip — never "this session has none". Same rule tree()/branching? already
|
|
18
|
+
enforce for parent links: "not recorded" must never read as "recorded and
|
|
19
|
+
empty".
|
|
20
|
+
|
|
21
|
+
A round trip deliberately carries NO tool results: a result can arrive long
|
|
22
|
+
after its call, and grouping streams one record at a time, so pairing a call
|
|
23
|
+
with its eventual result is a separate, eager layer (Agent::Sessions::Loop,
|
|
24
|
+
landing next) rather than this one's job.
|
|
25
|
+
|
|
26
|
+
## Attributes
|
|
27
|
+
### `index` [R] <a id="attribute-i-index"></a> <a id="index-instance_method"></a>
|
|
28
|
+
Returns the value of attribute index
|
|
29
|
+
- **@return** [Object] the current value of index
|
|
30
|
+
|
|
31
|
+
### `messages` [R] <a id="attribute-i-messages"></a> <a id="messages-instance_method"></a>
|
|
32
|
+
Returns the value of attribute messages
|
|
33
|
+
- **@return** [Object] the current value of messages
|
|
34
|
+
|
|
35
|
+
### `recorded` [R] <a id="attribute-i-recorded"></a> <a id="recorded-instance_method"></a>
|
|
36
|
+
Returns the value of attribute recorded
|
|
37
|
+
- **@return** [Object] the current value of recorded
|
|
38
|
+
|
|
39
|
+
### `usage` [R] <a id="attribute-i-usage"></a> <a id="usage-instance_method"></a>
|
|
40
|
+
Returns the value of attribute usage
|
|
41
|
+
- **@return** [Object] the current value of usage
|
|
42
|
+
|
|
43
|
+
## Public Instance Methods
|
|
44
|
+
### `at()` <a id="method-i-at"></a> <a id="at-instance_method"></a>
|
|
45
|
+
The first recorded Time among the messages, or nil when none carried one.
|
|
46
|
+
|
|
47
|
+
### `calls()` <a id="method-i-calls"></a> <a id="calls-instance_method"></a>
|
|
48
|
+
The :tool_use parts only, [] when this round trip asked for no tool.
|
|
49
|
+
|
|
50
|
+
### `model()` <a id="method-i-model"></a> <a id="model-instance_method"></a>
|
|
51
|
+
The first recorded model String, or nil.
|
|
52
|
+
|
|
53
|
+
### `parts()` <a id="method-i-parts"></a> <a id="parts-instance_method"></a>
|
|
54
|
+
Every part of every message, in file order.
|
|
55
|
+
|
|
56
|
+
### `roles()` <a id="method-i-roles"></a> <a id="roles-instance_method"></a>
|
|
57
|
+
The distinct roles in this group, in first-seen order.
|
data/doc/Agent/Sessions.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
| | |
|
|
4
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 |
|
|
5
|
+
| **Defined in** | lib/agent/sessions.rb, lib/agent/sessions/cli.rb, lib/agent/sessions/node.rb, lib/agent/sessions/part.rb, lib/agent/sessions/audit.rb, lib/agent/sessions/check.rb, lib/agent/sessions/error.rb, lib/agent/sessions/store.rb, lib/agent/sessions/usage.rb, lib/agent/sessions/sqlite.rb, lib/agent/sessions/message.rb, lib/agent/sessions/session.rb, lib/agent/sessions/version.rb, lib/agent/sessions/location.rb, lib/agent/sessions/compaction.rb, lib/agent/sessions/readers/pi.rb, lib/agent/sessions/round_trip.rb, lib/agent/sessions/adapters/pi.rb, lib/agent/sessions/readers/amp.rb, lib/agent/sessions/adapters/amp.rb, lib/agent/sessions/env_override.rb, lib/agent/sessions/readers/base.rb, lib/agent/sessions/readers/grok.rb, lib/agent/sessions/readers/qwen.rb, lib/agent/sessions/adapters/base.rb, lib/agent/sessions/adapters/grok.rb, lib/agent/sessions/adapters/qwen.rb, lib/agent/sessions/readers/codex.rb, lib/agent/sessions/unknown_agent.rb, lib/agent/sessions/adapters/codex.rb, lib/agent/sessions/home_expansion.rb, lib/agent/sessions/readers/claude.rb, lib/agent/sessions/readers/gemini.rb, lib/agent/sessions/adapters/claude.rb, lib/agent/sessions/adapters/cursor.rb, lib/agent/sessions/adapters/gemini.rb, lib/agent/sessions/readers/copilot.rb, lib/agent/sessions/adapters/copilot.rb, lib/agent/sessions/readers/opencode.rb, lib/agent/sessions/unreadable_store.rb, lib/agent/sessions/adapters/opencode.rb, lib/agent/sessions/missing_dependency.rb, lib/agent/sessions/unsupported_format.rb, lib/agent/sessions/adapters/cursor_ide.rb, lib/agent/sessions/adapters/enumeration.rb |
|
|
6
6
|
|
|
7
7
|
## Constants
|
|
8
8
|
### `LOADER` <a id="constant-LOADER"></a> <a id="LOADER-constant"></a>
|
|
@@ -168,6 +168,7 @@ Not documented.
|
|
|
168
168
|
- [Sessions/Readers/Opencode.md](Sessions/Readers/Opencode.md)
|
|
169
169
|
- [Sessions/Readers/Pi.md](Sessions/Readers/Pi.md)
|
|
170
170
|
- [Sessions/Readers/Qwen.md](Sessions/Readers/Qwen.md)
|
|
171
|
+
- [Sessions/RoundTrip.md](Sessions/RoundTrip.md)
|
|
171
172
|
- [Sessions/Session.md](Sessions/Session.md)
|
|
172
173
|
- [Sessions/Sqlite.md](Sessions/Sqlite.md)
|
|
173
174
|
- [Sessions/Store.md](Sessions/Store.md)
|
data/doc/Agent.md
CHANGED
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
|
|
3
3
|
| | |
|
|
4
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 |
|
|
5
|
+
| **Defined in** | lib/agent/sessions.rb, lib/agent/sessions/cli.rb, lib/agent/sessions/node.rb, lib/agent/sessions/part.rb, lib/agent/sessions/audit.rb, lib/agent/sessions/check.rb, lib/agent/sessions/error.rb, lib/agent/sessions/store.rb, lib/agent/sessions/usage.rb, lib/agent/sessions/sqlite.rb, lib/agent/sessions/message.rb, lib/agent/sessions/session.rb, lib/agent/sessions/version.rb, lib/agent/sessions/location.rb, lib/agent/sessions/compaction.rb, lib/agent/sessions/readers/pi.rb, lib/agent/sessions/round_trip.rb, lib/agent/sessions/adapters/pi.rb, lib/agent/sessions/readers/amp.rb, lib/agent/sessions/adapters/amp.rb, lib/agent/sessions/env_override.rb, lib/agent/sessions/readers/base.rb, lib/agent/sessions/readers/grok.rb, lib/agent/sessions/readers/qwen.rb, lib/agent/sessions/adapters/base.rb, lib/agent/sessions/adapters/grok.rb, lib/agent/sessions/adapters/qwen.rb, lib/agent/sessions/readers/codex.rb, lib/agent/sessions/unknown_agent.rb, lib/agent/sessions/adapters/codex.rb, lib/agent/sessions/home_expansion.rb, lib/agent/sessions/readers/claude.rb, lib/agent/sessions/readers/gemini.rb, lib/agent/sessions/adapters/claude.rb, lib/agent/sessions/adapters/cursor.rb, lib/agent/sessions/adapters/gemini.rb, lib/agent/sessions/readers/copilot.rb, lib/agent/sessions/adapters/copilot.rb, lib/agent/sessions/readers/opencode.rb, lib/agent/sessions/unreadable_store.rb, lib/agent/sessions/adapters/opencode.rb, lib/agent/sessions/missing_dependency.rb, lib/agent/sessions/unsupported_format.rb, lib/agent/sessions/adapters/cursor_ide.rb, lib/agent/sessions/adapters/enumeration.rb |
|
data/doc/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
## 0.4.1 (2026-09-10)
|
|
2
|
+
|
|
3
|
+
- Recognize Codex `token_usage_record` accounting metadata without adding conversation messages, round trips, or unknown-record warnings, including with `include_events: true`.
|
|
4
|
+
- Read Codex session usage from the latest usable cumulative totals in file order across legacy `token_count` events and new `thread_token_usage` records, without double-counting. Malformed or empty totals preserve earlier valid usage; existing token normalization and missing-value semantics are retained.
|
|
5
|
+
|
|
6
|
+
## 0.4.0 (2026-09-10)
|
|
7
|
+
|
|
8
|
+
- Round-trip grouping in every reader: `RoundTrip` (`index`, `messages`, `usage`, `recorded`, plus `parts`, `calls`, `roles`), `reader.each_round_trip` (streams), `reader.round_trips` (eager), `reader.round_trips_recorded?`
|
|
9
|
+
- Claude names its round-trip id — `message.id`, the id Anthropic's API gives one response — and is the only reader today where `round_trips_recorded?` answers true; every other reader falls back to one round trip per message
|
|
10
|
+
- The reappearing-id warning fires only when nothing explains the split. Measured over the 60 most recent real Claude transcripts (2026-09-10): 3,315 distinct message ids, 261 of them (7.9%) split across more than one run in 33 of the 60 files; 759 of those splits are a tool_result record, 66 a last-prompt, 12 a file-history-delta — all benign, so warning on them would put four or five lines under every real session
|
|
11
|
+
- Conformance suite case C9: `round_trips` must never lose or duplicate a message, across every reader
|
|
12
|
+
- The loop view (`Loop`, `ToolCall`, `LoopView`) and its CLI command (`show`) moved to the `agent_session_context` gem before release, so they never shipped here
|
|
13
|
+
|
|
1
14
|
## 0.3.1 (2026-08-26)
|
|
2
15
|
|
|
3
16
|
- Add tracked Markdown YARD documentation and a consolidated `llm.txt`, package both with the gem, and validate every local documentation link
|
data/doc/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
|
+
<img width="1200" height="620" alt="demo" src="https://github.com/user-attachments/assets/d13e7c68-0cb4-404c-bb31-d2c3b24d3522" />
|
|
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`.
|
|
@@ -62,6 +64,8 @@ Ruby callers passing any other symbol get `Agent::Sessions::UnknownAgent`; its m
|
|
|
62
64
|
|
|
63
65
|
## Ruby API
|
|
64
66
|
|
|
67
|
+
<img width="1200" height="620" alt="demo-ruby" src="https://github.com/user-attachments/assets/a0dd54b6-d597-4dad-85ae-97ce07aba2a4" />
|
|
68
|
+
|
|
65
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.
|
|
66
70
|
|
|
67
71
|
```ruby
|
|
@@ -118,6 +122,22 @@ reader.usage&.input # disjoint buckets: input, output,
|
|
|
118
122
|
|
|
119
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.
|
|
120
124
|
|
|
125
|
+
### Round trips
|
|
126
|
+
|
|
127
|
+
A round trip is one model response, however many records the store split it across:
|
|
128
|
+
|
|
129
|
+
```ruby
|
|
130
|
+
reader.round_trips # eager: an Array of RoundTrip
|
|
131
|
+
reader.each_round_trip { |trip| ... } # streams; breaking early reads one group, not the file
|
|
132
|
+
reader.round_trips_recorded? # true only where the store names its own groups
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
A `RoundTrip` carries `index`, `messages`, `usage`, and `recorded`, and derives `parts` (every part, in file order), `calls` (its `:tool_use` parts), and `roles` (the distinct roles seen, first-seen order).
|
|
136
|
+
|
|
137
|
+
Claude names its groups (`message.id`); every other reader falls back to one round trip per message, and `round_trips_recorded?` answers `false` there — "not recorded" must never read as "none".
|
|
138
|
+
|
|
139
|
+
Pairing a call with the result that answers it, and rendering a session as a loop, live in the sibling [`agent_session_context`](https://github.com/lucianghinda/agent_session_context) gem, via its `agent-session-context loop` command.
|
|
140
|
+
|
|
121
141
|
### Errors
|
|
122
142
|
|
|
123
143
|
Every domain-specific error the gem raises descends from `Agent::Sessions::Error`:
|
data/doc/index.csv
CHANGED
|
@@ -139,6 +139,16 @@ Agent::Sessions::Readers::Pi,Class,Agent/Sessions/Readers/Pi.md
|
|
|
139
139
|
Agent::Sessions::Readers::Pi.NON_MESSAGE_TYPES,Constant,Agent/Sessions/Readers/Pi.md#constant-NON_MESSAGE_TYPES
|
|
140
140
|
Agent::Sessions::Readers::Pi.ROLES,Constant,Agent/Sessions/Readers/Pi.md#constant-ROLES
|
|
141
141
|
Agent::Sessions::Readers::Pi.usage,Method,Agent/Sessions/Readers/Pi.md#method-i-usage
|
|
142
|
+
Agent::Sessions::RoundTrip,Class,Agent/Sessions/RoundTrip.md
|
|
143
|
+
Agent::Sessions::RoundTrip.at,Method,Agent/Sessions/RoundTrip.md#method-i-at
|
|
144
|
+
Agent::Sessions::RoundTrip.calls,Method,Agent/Sessions/RoundTrip.md#method-i-calls
|
|
145
|
+
Agent::Sessions::RoundTrip.model,Method,Agent/Sessions/RoundTrip.md#method-i-model
|
|
146
|
+
Agent::Sessions::RoundTrip.parts,Method,Agent/Sessions/RoundTrip.md#method-i-parts
|
|
147
|
+
Agent::Sessions::RoundTrip.roles,Method,Agent/Sessions/RoundTrip.md#method-i-roles
|
|
148
|
+
Agent::Sessions::RoundTrip.index,Attribute,Agent/Sessions/RoundTrip.md#attribute-i-index
|
|
149
|
+
Agent::Sessions::RoundTrip.messages,Attribute,Agent/Sessions/RoundTrip.md#attribute-i-messages
|
|
150
|
+
Agent::Sessions::RoundTrip.recorded,Attribute,Agent/Sessions/RoundTrip.md#attribute-i-recorded
|
|
151
|
+
Agent::Sessions::RoundTrip.usage,Attribute,Agent/Sessions/RoundTrip.md#attribute-i-usage
|
|
142
152
|
Agent::Sessions::Adapters::Pi,Class,Agent/Sessions/Adapters/Pi.md
|
|
143
153
|
Agent::Sessions::Adapters::Pi.FILENAME,Constant,Agent/Sessions/Adapters/Pi.md#constant-FILENAME
|
|
144
154
|
Agent::Sessions::Adapters::Pi.encode_project,Method,Agent/Sessions/Adapters/Pi.md#method-i-encode_project
|
|
@@ -165,10 +175,13 @@ Agent::Sessions::Readers::Base.MAX_RECORD_BYTES,Constant,Agent/Sessions/Readers/
|
|
|
165
175
|
Agent::Sessions::Readers::Base.branching?,Method,Agent/Sessions/Readers/Base.md#method-i-branching-3F
|
|
166
176
|
Agent::Sessions::Readers::Base.compactions,Method,Agent/Sessions/Readers/Base.md#method-i-compactions
|
|
167
177
|
Agent::Sessions::Readers::Base.each_message,Method,Agent/Sessions/Readers/Base.md#method-i-each_message
|
|
178
|
+
Agent::Sessions::Readers::Base.each_round_trip,Method,Agent/Sessions/Readers/Base.md#method-i-each_round_trip
|
|
168
179
|
Agent::Sessions::Readers::Base.fidelity,Method,Agent/Sessions/Readers/Base.md#method-i-fidelity
|
|
169
180
|
Agent::Sessions::Readers::Base.initialize,Method,Agent/Sessions/Readers/Base.md#method-i-initialize
|
|
170
181
|
Agent::Sessions::Readers::Base.messages,Method,Agent/Sessions/Readers/Base.md#method-i-messages
|
|
171
182
|
Agent::Sessions::Readers::Base.partial?,Method,Agent/Sessions/Readers/Base.md#method-i-partial-3F
|
|
183
|
+
Agent::Sessions::Readers::Base.round_trips,Method,Agent/Sessions/Readers/Base.md#method-i-round_trips
|
|
184
|
+
Agent::Sessions::Readers::Base.round_trips_recorded?,Method,Agent/Sessions/Readers/Base.md#method-i-round_trips_recorded-3F
|
|
172
185
|
Agent::Sessions::Readers::Base.tree,Method,Agent/Sessions/Readers/Base.md#method-i-tree
|
|
173
186
|
Agent::Sessions::Readers::Base.usage,Method,Agent/Sessions/Readers/Base.md#method-i-usage
|
|
174
187
|
Agent::Sessions::Readers::Base.warnings,Method,Agent/Sessions/Readers/Base.md#method-i-warnings
|
|
@@ -252,6 +265,7 @@ Agent::Sessions::Readers::Claude.SPILL,Constant,Agent/Sessions/Readers/Claude.md
|
|
|
252
265
|
Agent::Sessions::Readers::Claude.MAX_SPILL_BYTES,Constant,Agent/Sessions/Readers/Claude.md#constant-MAX_SPILL_BYTES
|
|
253
266
|
Agent::Sessions::Readers::Claude.branching?,Method,Agent/Sessions/Readers/Claude.md#method-i-branching-3F
|
|
254
267
|
Agent::Sessions::Readers::Claude.initialize,Method,Agent/Sessions/Readers/Claude.md#method-i-initialize
|
|
268
|
+
Agent::Sessions::Readers::Claude.round_trips_recorded?,Method,Agent/Sessions/Readers/Claude.md#method-i-round_trips_recorded-3F
|
|
255
269
|
Agent::Sessions::Readers::Claude.subagents,Method,Agent/Sessions/Readers/Claude.md#method-i-subagents
|
|
256
270
|
Agent::Sessions::Readers::Claude.usage,Method,Agent/Sessions/Readers/Claude.md#method-i-usage
|
|
257
271
|
Agent::Sessions::Readers::Gemini,Class,Agent/Sessions/Readers/Gemini.md
|
|
@@ -60,6 +60,75 @@ module Agent
|
|
|
60
60
|
# `each_message` is what a 2.6 GB file requires.
|
|
61
61
|
def messages = each_message.to_a
|
|
62
62
|
|
|
63
|
+
# Streams, exactly as each_message does. A group is a RUN of
|
|
64
|
+
# neighbouring records sharing one round-trip id, not every record
|
|
65
|
+
# sharing that id wherever it sits in the file — collecting scattered
|
|
66
|
+
# records cannot stream, and rule 3 says no code path here may assume
|
|
67
|
+
# a file fits in memory.
|
|
68
|
+
#
|
|
69
|
+
# So an id that reappears after its run closed opens a NEW round trip
|
|
70
|
+
# rather than reopening the old one. That is ORDINARY for Claude, not
|
|
71
|
+
# an anomaly: Claude Code writes each tool_result immediately after
|
|
72
|
+
# the tool_use it answers, while every content block of the one API
|
|
73
|
+
# response keeps the same message.id, so a response making two tool
|
|
74
|
+
# calls has its records split by the result in between. Measured over
|
|
75
|
+
# the 60 most recent real Claude transcripts on this machine
|
|
76
|
+
# (2026-09-10): 3,315 distinct message.ids, 261 of them (7.9%) split
|
|
77
|
+
# across more than one run, in 33 of the 60 files; 759 of the splits
|
|
78
|
+
# are a tool_result record, 66 a last-prompt, 12 a file-history-delta.
|
|
79
|
+
#
|
|
80
|
+
# Hence the warning fires only where NOTHING answered a tool between
|
|
81
|
+
# the two runs — the case that is genuinely unexplained and would mean
|
|
82
|
+
# the format drifted. Warning on the benign split would put four or
|
|
83
|
+
# five lines under every real session's loop view, which teaches a
|
|
84
|
+
# caller that these warnings are noise.
|
|
85
|
+
def each_round_trip
|
|
86
|
+
return enum_for(:each_round_trip) unless block_given?
|
|
87
|
+
|
|
88
|
+
open_messages = []
|
|
89
|
+
open_id = nil
|
|
90
|
+
closed_ids = {}
|
|
91
|
+
answers_seen = 0
|
|
92
|
+
index = 0
|
|
93
|
+
|
|
94
|
+
each_message do |message|
|
|
95
|
+
id = round_trip_id_for(message.raw)
|
|
96
|
+
|
|
97
|
+
if id && id == open_id
|
|
98
|
+
open_messages << message
|
|
99
|
+
answers_seen += 1 if answers_a_tool?(message)
|
|
100
|
+
next
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
if open_messages.any?
|
|
104
|
+
index += 1
|
|
105
|
+
yield build_round_trip(index, open_messages, open_id)
|
|
106
|
+
end
|
|
107
|
+
# What this id's run closed at, counted in tool answers seen so
|
|
108
|
+
# far. Comparing that count against the count now is what
|
|
109
|
+
# separates "a tool result split one response" from "this id came
|
|
110
|
+
# back for a reason nothing here explains".
|
|
111
|
+
closed_ids[open_id] = answers_seen if open_id
|
|
112
|
+
|
|
113
|
+
if id && closed_ids[id] == answers_seen
|
|
114
|
+
warn_about("round-trip id #{id} reappears after its run closed with no tool answered " \
|
|
115
|
+
"in between; grouped as a new round trip")
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
open_messages = [message]
|
|
119
|
+
open_id = id
|
|
120
|
+
answers_seen += 1 if answers_a_tool?(message)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
yield build_round_trip(index + 1, open_messages, open_id) if open_messages.any?
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def round_trips = each_round_trip.to_a
|
|
127
|
+
|
|
128
|
+
# False here: most stores are an append-only list of records and no
|
|
129
|
+
# format names which of them belong to one model response.
|
|
130
|
+
def round_trips_recorded? = false
|
|
131
|
+
|
|
63
132
|
# Whether this agent records which turn each turn followed. False here:
|
|
64
133
|
# most stores are an append-only list and a tree would have to be invented.
|
|
65
134
|
def branching? = false
|
|
@@ -117,6 +186,32 @@ module Agent
|
|
|
117
186
|
def node_id_for(_record) = nil
|
|
118
187
|
def parent_id_for(_record) = nil
|
|
119
188
|
|
|
189
|
+
# nil means "this format records no round-trip id". Same shape of
|
|
190
|
+
# hook as node_id_for/parent_id_for: an agent says WHERE its id
|
|
191
|
+
# lives, never how the grouping is assembled.
|
|
192
|
+
def round_trip_id_for(_record) = nil
|
|
193
|
+
|
|
194
|
+
# Whether this message is a tool answering a call. Used only to tell a
|
|
195
|
+
# benign split of one response's records from an unexplained one.
|
|
196
|
+
def answers_a_tool?(message) = message.parts.any? { |part| part.type == :tool_result }
|
|
197
|
+
|
|
198
|
+
def build_round_trip(index, messages, id)
|
|
199
|
+
RoundTrip.new(index: index, messages: messages, usage: usage_for(messages), recorded: !id.nil?)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# NEVER a sum. Claude repeats one API response's usage byte-for-byte
|
|
203
|
+
# on each record of that response (94 of 124 message ids in one real
|
|
204
|
+
# transcript), so summing the group would report roughly double what
|
|
205
|
+
# the vendor billed.
|
|
206
|
+
def usage_for(messages)
|
|
207
|
+
found = messages.filter_map(&:usage)
|
|
208
|
+
return nil if found.empty?
|
|
209
|
+
return found.first if found.uniq.size == 1
|
|
210
|
+
|
|
211
|
+
warn_about("round trip records disagree on usage; reporting the first")
|
|
212
|
+
found.first
|
|
213
|
+
end
|
|
214
|
+
|
|
120
215
|
# Two passes over one session. The first records every uuid-bearing
|
|
121
216
|
# record's parent and which of them became messages; the second links
|
|
122
217
|
# each message to the nearest ANCESTOR that is also a message.
|
|
@@ -52,6 +52,8 @@ module Agent
|
|
|
52
52
|
# trustworthy enough to build a tree from.
|
|
53
53
|
def branching? = true
|
|
54
54
|
|
|
55
|
+
def round_trips_recorded? = true
|
|
56
|
+
|
|
55
57
|
def initialize(session, resolve_spills: true, **rest)
|
|
56
58
|
super(session, **rest)
|
|
57
59
|
@resolve_spills = resolve_spills
|
|
@@ -112,6 +114,14 @@ module Agent
|
|
|
112
114
|
def node_id_for(record) = record["uuid"]
|
|
113
115
|
def parent_id_for(record) = record["parentUuid"]
|
|
114
116
|
|
|
117
|
+
# message.id is the id Anthropic's API gives one response; Claude
|
|
118
|
+
# Code writes one record PER CONTENT BLOCK, all repeating it. The
|
|
119
|
+
# reader already dedups session usage by exactly this id (usage,
|
|
120
|
+
# above), so the fact is known and only needs exposing. A record
|
|
121
|
+
# with no message.id (a user turn, an event) yields nil and becomes
|
|
122
|
+
# its own assumed round trip.
|
|
123
|
+
def round_trip_id_for(record) = record.dig("message", "id")
|
|
124
|
+
|
|
115
125
|
def message_for(record, line_number)
|
|
116
126
|
type = record["type"]
|
|
117
127
|
return nil if NON_MESSAGE_TYPES.include?(type)
|
|
@@ -15,11 +15,12 @@ module Agent
|
|
|
15
15
|
# been inference. A reference implementation has to be falsifiable.
|
|
16
16
|
class Codex < Base
|
|
17
17
|
# Known, and deliberately not messages: the session header, per-turn
|
|
18
|
-
# configuration,
|
|
19
|
-
# here is a judgement, not an oversight —
|
|
20
|
-
# warning about them would train a
|
|
18
|
+
# configuration, state records, and cumulative token accounting.
|
|
19
|
+
# Silence here is a judgement, not an oversight —
|
|
20
|
+
# these are not conversation, and warning about them would train a
|
|
21
|
+
# caller to ignore warnings.
|
|
21
22
|
NON_MESSAGE_TYPES = %w[session_meta turn_context world_state
|
|
22
|
-
inter_agent_communication_metadata].freeze
|
|
23
|
+
inter_agent_communication_metadata token_usage_record].freeze
|
|
23
24
|
|
|
24
25
|
# "developer" is what Codex writes where the normalized vocabulary says
|
|
25
26
|
# :system. It is 101 of 292 role-bearing records in the sample, so this is
|
|
@@ -60,11 +61,12 @@ module Agent
|
|
|
60
61
|
CALL_OUTPUTS = %w[output tools].freeze
|
|
61
62
|
|
|
62
63
|
# Session totals. Codex writes no usage on its messages; it writes
|
|
63
|
-
# token_count
|
|
64
|
-
#
|
|
64
|
+
# token_count events with info.total_token_usage, or token_usage_record
|
|
65
|
+
# records with thread_token_usage (observed 2026-09-10). Both hold
|
|
66
|
+
# cumulative totals. The legacy format was verified on 2026-08-24:
|
|
65
67
|
# consecutive records report total 33,751 then 69,135 while their
|
|
66
|
-
# last_token_usage differ, so the
|
|
67
|
-
# would multiply-count every earlier turn.
|
|
68
|
+
# last_token_usage differ, so the latest usable record is the session
|
|
69
|
+
# and summing would multiply-count every earlier turn.
|
|
68
70
|
#
|
|
69
71
|
# Two normalizations, both from that same file:
|
|
70
72
|
#
|
|
@@ -79,25 +81,12 @@ module Agent
|
|
|
79
81
|
# any bucket it landed in would be double-counted by a caller summing
|
|
80
82
|
# buckets.
|
|
81
83
|
def usage
|
|
82
|
-
|
|
84
|
+
mapped = nil
|
|
83
85
|
each_record do |record, _line_number|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
candidate = record.dig("payload", "info", "total_token_usage")
|
|
87
|
-
info = candidate if record.dig("payload", "type") == "token_count" && candidate.is_a?(Hash)
|
|
86
|
+
candidate = cumulative_usage(record)
|
|
87
|
+
mapped = candidate if candidate
|
|
88
88
|
end
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
input = count_from(info["input_tokens"])
|
|
92
|
-
cached = count_from(info["cached_input_tokens"])
|
|
93
|
-
mapped = Usage.new(input: input && cached ? [input - cached, 0].max : input,
|
|
94
|
-
output: count_from(info["output_tokens"]),
|
|
95
|
-
cache_read: cached,
|
|
96
|
-
cache_creation: count_from(info["cache_write_input_tokens"]),
|
|
97
|
-
reasoning: count_from(info["reasoning_output_tokens"]))
|
|
98
|
-
# Same rule as Claude's usage_from: a token_count record whose every
|
|
99
|
-
# field failed the count check answers nil, not an all-nil Usage.
|
|
100
|
-
mapped.to_h.each_value.any? ? mapped : nil
|
|
89
|
+
mapped
|
|
101
90
|
end
|
|
102
91
|
|
|
103
92
|
private
|
|
@@ -198,7 +187,31 @@ module Agent
|
|
|
198
187
|
def event_message(record)
|
|
199
188
|
return nil unless include_events
|
|
200
189
|
|
|
201
|
-
|
|
190
|
+
payload = record["payload"]
|
|
191
|
+
type = payload.is_a?(Hash) ? payload["type"] : nil
|
|
192
|
+
build(record, :system, [Part.new(type: :unknown, text: type)])
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def cumulative_usage(record)
|
|
196
|
+
source = case record["type"]
|
|
197
|
+
when "event_msg"
|
|
198
|
+
payload = record["payload"]
|
|
199
|
+
info = payload["info"] if payload.is_a?(Hash) && payload["type"] == "token_count"
|
|
200
|
+
info["total_token_usage"] if info.is_a?(Hash)
|
|
201
|
+
when "token_usage_record"
|
|
202
|
+
payload = record["payload"]
|
|
203
|
+
payload["thread_token_usage"] if payload.is_a?(Hash)
|
|
204
|
+
end
|
|
205
|
+
return unless source.is_a?(Hash)
|
|
206
|
+
|
|
207
|
+
input = count_from(source["input_tokens"])
|
|
208
|
+
cached = count_from(source["cached_input_tokens"])
|
|
209
|
+
result = Usage.new(input: input && cached ? [input - cached, 0].max : input,
|
|
210
|
+
output: count_from(source["output_tokens"]),
|
|
211
|
+
cache_read: cached,
|
|
212
|
+
cache_creation: count_from(source["cache_write_input_tokens"]),
|
|
213
|
+
reasoning: count_from(source["reasoning_output_tokens"]))
|
|
214
|
+
result if result.to_h.each_value.any?
|
|
202
215
|
end
|
|
203
216
|
|
|
204
217
|
def unknown_message(record)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Agent
|
|
4
|
+
module Sessions
|
|
5
|
+
# One model response, grouped from however many records a format split it
|
|
6
|
+
# across. Claude Code writes one record PER CONTENT BLOCK of an API
|
|
7
|
+
# response: in one real transcript on this machine, 260 assistant records
|
|
8
|
+
# share only 124 message.id values, so a caller counting records over-
|
|
9
|
+
# counts what the session actually did by roughly two to one. A round
|
|
10
|
+
# trip is the unit that actually happened.
|
|
11
|
+
#
|
|
12
|
+
# `recorded` is the load-bearing field. False means this group was
|
|
13
|
+
# ASSUMED — the format names no round-trip id, so grouping fell back to
|
|
14
|
+
# one message per round trip — never "this session has none". Same rule
|
|
15
|
+
# tree()/branching? already enforce for parent links: "not recorded"
|
|
16
|
+
# must never read as "recorded and empty".
|
|
17
|
+
#
|
|
18
|
+
# A round trip deliberately carries NO tool results: a result can arrive
|
|
19
|
+
# long after its call, and grouping streams one record at a time, so
|
|
20
|
+
# pairing a call with its eventual result is a separate, eager layer
|
|
21
|
+
# (Agent::Sessions::Loop, landing next) rather than this one's job.
|
|
22
|
+
RoundTrip = Data.define(:index, :messages, :usage, :recorded) do
|
|
23
|
+
# Every part of every message, in file order.
|
|
24
|
+
def parts = messages.flat_map(&:parts)
|
|
25
|
+
|
|
26
|
+
# The :tool_use parts only, [] when this round trip asked for no tool.
|
|
27
|
+
def calls = parts.select { |part| part.type == :tool_use }
|
|
28
|
+
|
|
29
|
+
# The distinct roles in this group, in first-seen order.
|
|
30
|
+
def roles = messages.map(&:role).uniq
|
|
31
|
+
|
|
32
|
+
# The first recorded Time among the messages, or nil when none carried one.
|
|
33
|
+
def at = messages.filter_map(&:at).first
|
|
34
|
+
|
|
35
|
+
# The first recorded model String, or nil.
|
|
36
|
+
def model = messages.filter_map(&:model).first
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
data/llm.txt
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
| | |
|
|
4
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 |
|
|
5
|
+
| **Defined in** | lib/agent/sessions.rb, lib/agent/sessions/cli.rb, lib/agent/sessions/node.rb, lib/agent/sessions/part.rb, lib/agent/sessions/audit.rb, lib/agent/sessions/check.rb, lib/agent/sessions/error.rb, lib/agent/sessions/store.rb, lib/agent/sessions/usage.rb, lib/agent/sessions/sqlite.rb, lib/agent/sessions/message.rb, lib/agent/sessions/session.rb, lib/agent/sessions/version.rb, lib/agent/sessions/location.rb, lib/agent/sessions/compaction.rb, lib/agent/sessions/readers/pi.rb, lib/agent/sessions/round_trip.rb, lib/agent/sessions/adapters/pi.rb, lib/agent/sessions/readers/amp.rb, lib/agent/sessions/adapters/amp.rb, lib/agent/sessions/env_override.rb, lib/agent/sessions/readers/base.rb, lib/agent/sessions/readers/grok.rb, lib/agent/sessions/readers/qwen.rb, lib/agent/sessions/adapters/base.rb, lib/agent/sessions/adapters/grok.rb, lib/agent/sessions/adapters/qwen.rb, lib/agent/sessions/readers/codex.rb, lib/agent/sessions/unknown_agent.rb, lib/agent/sessions/adapters/codex.rb, lib/agent/sessions/home_expansion.rb, lib/agent/sessions/readers/claude.rb, lib/agent/sessions/readers/gemini.rb, lib/agent/sessions/adapters/claude.rb, lib/agent/sessions/adapters/cursor.rb, lib/agent/sessions/adapters/gemini.rb, lib/agent/sessions/readers/copilot.rb, lib/agent/sessions/adapters/copilot.rb, lib/agent/sessions/readers/opencode.rb, lib/agent/sessions/unreadable_store.rb, lib/agent/sessions/adapters/opencode.rb, lib/agent/sessions/missing_dependency.rb, lib/agent/sessions/unsupported_format.rb, lib/agent/sessions/adapters/cursor_ide.rb, lib/agent/sessions/adapters/enumeration.rb |
|
|
6
6
|
|
|
7
7
|
## Constants
|
|
8
8
|
### `LOADER` <a id="constant-LOADER"></a> <a id="LOADER-constant"></a>
|
|
@@ -168,6 +168,7 @@ Not documented.
|
|
|
168
168
|
- [doc/Agent/Sessions/Readers/Opencode.md](doc/Agent/Sessions/Readers/Opencode.md)
|
|
169
169
|
- [doc/Agent/Sessions/Readers/Pi.md](doc/Agent/Sessions/Readers/Pi.md)
|
|
170
170
|
- [doc/Agent/Sessions/Readers/Qwen.md](doc/Agent/Sessions/Readers/Qwen.md)
|
|
171
|
+
- [doc/Agent/Sessions/RoundTrip.md](doc/Agent/Sessions/RoundTrip.md)
|
|
171
172
|
- [doc/Agent/Sessions/Session.md](doc/Agent/Sessions/Session.md)
|
|
172
173
|
- [doc/Agent/Sessions/Sqlite.md](doc/Agent/Sessions/Sqlite.md)
|
|
173
174
|
- [doc/Agent/Sessions/Store.md](doc/Agent/Sessions/Store.md)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: agent_sessions
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Lucian Ghinda
|
|
@@ -91,6 +91,7 @@ files:
|
|
|
91
91
|
- doc/Agent/Sessions/Readers/Opencode.md
|
|
92
92
|
- doc/Agent/Sessions/Readers/Pi.md
|
|
93
93
|
- doc/Agent/Sessions/Readers/Qwen.md
|
|
94
|
+
- doc/Agent/Sessions/RoundTrip.md
|
|
94
95
|
- doc/Agent/Sessions/Session.md
|
|
95
96
|
- doc/Agent/Sessions/Sqlite.md
|
|
96
97
|
- doc/Agent/Sessions/Store.md
|
|
@@ -138,6 +139,7 @@ files:
|
|
|
138
139
|
- lib/agent/sessions/readers/opencode.rb
|
|
139
140
|
- lib/agent/sessions/readers/pi.rb
|
|
140
141
|
- lib/agent/sessions/readers/qwen.rb
|
|
142
|
+
- lib/agent/sessions/round_trip.rb
|
|
141
143
|
- lib/agent/sessions/session.rb
|
|
142
144
|
- lib/agent/sessions/sqlite.rb
|
|
143
145
|
- lib/agent/sessions/store.rb
|