@1agents/session-reader 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/src/turns.js CHANGED
@@ -40,34 +40,63 @@ function assessTurn(events, native, nextPrompt, nudgeCount) {
40
40
  return { status, evidence };
41
41
  }
42
42
  /**
43
- * Turn boundaries: codex records them natively, the others start a new turn on
44
- * every user message.
43
+ * Turn boundaries from the user messages that open them.
44
+ *
45
+ * Split out from `turnStarts` so the index path can feed it the same indices
46
+ * straight from SQL without materializing the session: a `T` printed by
47
+ * `search` then means exactly what a `T` printed by `turns` means.
45
48
  */
46
- function boundaries(session) {
47
- const starts = session.turns
48
- .filter((turn) => turn.kind === 'user' && turn.text?.trim())
49
- .map((turn) => turn.index);
50
- if (!starts.length && session.turns.length)
51
- return [0];
49
+ export function turnStartsFrom(userEventIndices, eventCount) {
50
+ const starts = [...userEventIndices];
51
+ if (!starts.length)
52
+ return eventCount ? [0] : [];
52
53
  // Anything before the first user message belongs to turn 1.
53
- if (starts[0] !== 0 && session.turns.length)
54
+ if (starts[0] !== 0 && eventCount)
54
55
  starts[0] = 0;
55
56
  return starts;
56
57
  }
58
+ /**
59
+ * Turn boundaries: codex records them natively, the others start a new turn on
60
+ * every user message.
61
+ */
62
+ export function turnStarts(session) {
63
+ return turnStartsFrom(session.turns.filter((turn) => turn.kind === 'user' && turn.text?.trim()).map((turn) => turn.index), session.turns.length);
64
+ }
65
+ /**
66
+ * The turn (1-based) an event index falls in, or 0 when it falls before the
67
+ * first one. Turns tile the event stream, so "the last start at or before the
68
+ * index" is the whole rule.
69
+ */
70
+ export function turnNoAt(starts, index) {
71
+ let lo = 0;
72
+ let hi = starts.length - 1;
73
+ let found = 0;
74
+ while (lo <= hi) {
75
+ const mid = (lo + hi) >> 1;
76
+ if (starts[mid] <= index) {
77
+ found = mid + 1;
78
+ lo = mid + 1;
79
+ }
80
+ else {
81
+ hi = mid - 1;
82
+ }
83
+ }
84
+ return found;
85
+ }
57
86
  /**
58
87
  * Providers fire `task_started` a few seconds *before* the first event of the
59
88
  * turn it opens, so a boundary belongs to the next turn that starts, not to
60
89
  * the turn whose window happens to contain it.
61
90
  */
62
- function boundariesByTurn(declared, turnStarts) {
91
+ function boundariesByTurn(declared, startTimes) {
63
92
  const byTurn = new Map();
64
93
  for (const boundary of declared) {
65
94
  if (!boundary.startedAt)
66
95
  continue;
67
96
  const at = Date.parse(boundary.startedAt);
68
- let owner = turnStarts.findIndex((start) => start !== undefined && Date.parse(start) >= at);
97
+ let owner = startTimes.findIndex((start) => start !== undefined && Date.parse(start) >= at);
69
98
  if (owner === -1)
70
- owner = turnStarts.length - 1;
99
+ owner = startTimes.length - 1;
71
100
  const list = byTurn.get(owner) ?? [];
72
101
  list.push(boundary);
73
102
  byTurn.set(owner, list);
@@ -75,7 +104,7 @@ function boundariesByTurn(declared, turnStarts) {
75
104
  return byTurn;
76
105
  }
77
106
  export function summarizeTurns(session) {
78
- const starts = boundaries(session);
107
+ const starts = turnStarts(session);
79
108
  const workspace = session.ref.workspace;
80
109
  const declared = session.stats.turnBoundaries;
81
110
  const nativeByTurn = boundariesByTurn(declared, starts.map((start) => session.turns[start]?.timestamp));
@@ -138,6 +167,51 @@ export function turnDetail(session, turnNo) {
138
167
  throw new Error(`no turn ${turnNo} (session has ${summaries.length})`);
139
168
  return { summary, events: session.turns.slice(summary.events[0], summary.events[1] + 1) };
140
169
  }
170
+ /**
171
+ * How many events one `--event` spec may expand to. Reading a tool call with
172
+ * its result and the assistant's verdict takes three; a spec asking for
173
+ * hundreds wanted `turn <n>` instead, and silently truncating would be worse
174
+ * than saying so.
175
+ */
176
+ export const MAX_EVENT_SPAN = 50;
177
+ /**
178
+ * `214`, `214-218`, `214,216,218` and any mix, as ascending unique indices.
179
+ *
180
+ * Ranges are clipped to the session — asking for `210-999` on a 300-event
181
+ * session is a reasonable way to say "to the end" — while a bare index is left
182
+ * alone so an out-of-range one still errors naming the number that was typed.
183
+ */
184
+ export function parseEventSpec(spec, eventCount) {
185
+ const indices = new Set();
186
+ for (const part of spec.split(',').map((piece) => piece.trim()).filter(Boolean)) {
187
+ const range = /^(\d+)\s*[-–]\s*(\d+)$/.exec(part);
188
+ if (range) {
189
+ const [from, to] = [Number(range[1]), Number(range[2])].sort((a, b) => a - b);
190
+ for (let i = Math.max(0, from); i <= Math.min(to, eventCount - 1); i++)
191
+ indices.add(i);
192
+ continue;
193
+ }
194
+ if (!/^\d+$/.test(part)) {
195
+ throw new Error(`--event 无法解析:${part}(用 214、214-218 或 214,216,218)`);
196
+ }
197
+ indices.add(Number(part));
198
+ }
199
+ if (!indices.size)
200
+ throw new Error(`--event ${spec} 不含该会话的任何事件(0–${eventCount - 1})`);
201
+ if (indices.size > MAX_EVENT_SPAN) {
202
+ throw new Error(`--event ${spec} 展开为 ${indices.size} 个事件,超过上限 ${MAX_EVENT_SPAN};` +
203
+ `缩小区间,或用 1session turn <id> <轮次> 看整轮概要`);
204
+ }
205
+ return [...indices].sort((a, b) => a - b);
206
+ }
207
+ /** `eventDetail` over a spec: `214`, `214-218`, `214,216,218`. */
208
+ export async function eventDetails(session, spec) {
209
+ const indices = parseEventSpec(spec, session.turns.length);
210
+ const details = [];
211
+ for (const index of indices)
212
+ details.push(await eventDetail(session, index));
213
+ return details;
214
+ }
141
215
  /**
142
216
  * Tier three: one event with its untruncated payload. Antigravity shortens long
143
217
  * step output in the transcript and keeps the full copy under `steps/<n>/`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1agents/session-reader",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Read Plane: cross-agent session discovery, turn inspection, workspace aggregation and distillation from raw local session files.",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -57,6 +57,7 @@
57
57
  "typescript": "^5.7.2"
58
58
  },
59
59
  "dependencies": {
60
- "@1agents/dreammate-network": "^0.1.0"
60
+ "@1agents/dreammate-network": "^0.3.0",
61
+ "@1agents/dreammate-node": "^0.1.0"
61
62
  }
62
63
  }
@@ -29,7 +29,7 @@ build, not a hang.
29
29
 
30
30
  ## Pick the command from the question
31
31
 
32
- Users ask about the past in roughly five shapes. Match the shape, don't run the
32
+ Users ask about the past in a handful of shapes. Match the shape, don't run the
33
33
  whole ladder by reflex:
34
34
 
35
35
  | The user is asking | Start with |
@@ -37,9 +37,17 @@ whole ladder by reflex:
37
37
  | "what have I been doing / what sessions exist" | `list` |
38
38
  | "where did I discuss X" (a word, path, error string, package name) | `search` |
39
39
  | "what happened in that session" (they named or you found one) | `overview` |
40
- | "what exactly did it do at step N" | `turns`, then `turn <n>` |
40
+ | "find where we talked about X / which turn was that" | `turns` |
41
+ | "what exactly did it do at step N" | `turn <n>`, then `--event k` |
41
42
  | "what did all the agents do in this project" | `workspace` |
42
43
 
44
+ "What happened in that session" and "find where we talked about X" look like the
45
+ same question and are not. `overview` compresses a session into statistics and
46
+ an end state — the answer to *what it did*, and the one form that throws away
47
+ what locating a conversation needs. `turns` keeps the ▸user/◂agent rhythm one
48
+ line at a time, so you find the moment by scanning: an order of magnitude faster
49
+ for "where in here did that happen".
50
+
43
51
  `<session-id>` accepts a full id, a prefix of 6+ characters, or a raw file path.
44
52
  Session ids shown by `list` and `search` are 8-char prefixes — pass them straight
45
53
  back in.
@@ -52,6 +60,16 @@ why a choice was made, what was tried and abandoned, an error and how it was
52
60
  worked around, work done over ssh or in a UI, and anything spanning projects or
53
61
  agents. The strongest answers use both — git for what changed, sessions for why.
54
62
 
63
+ **Sessions date, and the end state dates fastest.** For any question about how
64
+ things *are right now* — a credential, an env var, which version is installed,
65
+ whether a file still exists — a session can only tell you when it was last true.
66
+ Go and check. A transcript records what was said and what exit code came back,
67
+ never the effect: a command can exit 0 having read an empty input and written an
68
+ empty value; a turn can conclude "there are three copies now" when one was a
69
+ shell function that never hit disk; "X is configured" can be accurate and three
70
+ weeks stale. Say which turn the claim comes from and when, then verify it with
71
+ the system itself before the user acts on it.
72
+
55
73
  ## Scope is the thing people get wrong
56
74
 
57
75
  `list`, `search` and `index --all` default to **the current pwd and everything
@@ -80,11 +98,32 @@ Each rung narrows the evidence, so climb only as far as the question needs.
80
98
  1session overview 3ab9fe0e # layer 1: what that session did
81
99
  1session turns 3ab9fe0e # layer 2: turn-by-turn summary
82
100
  1session turn 3ab9fe0e 9 --event 491 # layer 3: one tool call, untruncated
101
+ 1session turn 3ab9fe0e 9 --event 491-493 # …with its result and the verdict
83
102
  ```
84
103
 
85
- `overview` is the highest-value single call: goal, instruction trail, end state
86
- (last request, last successful command, last failed command, last file touched),
87
- and counts of turns/files/commands/failures/commits/tokens.
104
+ Every `search` hit is prefixed with the handle that drills into it, and each
105
+ session prints the command ready to paste:
106
+
107
+ ```text
108
+ ### claude 3ab9fe0e 2026-09-13T03:33:32Z → 2026-09-13T05:52:59Z (2 命中)
109
+ T9 · E491 tool_call(Bash) …curl --max-time 5 …
110
+ ↳ 1session turn 3ab9fe0e 9 --event 491
111
+ ```
112
+
113
+ Reading one tool call usually means reading three events — the call, its result,
114
+ and what the agent concluded — so `--event` takes `491-493` and `491,495,502` as
115
+ well as a single number.
116
+
117
+ `search` hides the invocation you are running right now (and what it printed)
118
+ from its own results, since a live session is indexed as it happens and would
119
+ otherwise match itself first. It says how many it folded; `--include-self` shows
120
+ them.
121
+
122
+ `overview` is the highest-value single call *for what a session did*: goal,
123
+ instruction trail, end state (last request, last successful command, last failed
124
+ command, last file touched), and counts of
125
+ turns/files/commands/failures/commits/tokens. For *where in a session something
126
+ happened*, start at `turns` instead.
88
127
 
89
128
  Useful narrower ledgers when the question is specifically about one dimension:
90
129
  `commands <id> [--failed]`, `files <id>`, `errors <id>`, `jobs <id>`,
@@ -108,7 +147,9 @@ you "the session is blocked on Y" — sections that would require interpretation
108
147
  say so explicitly instead of guessing. **That interpretation is your job**, and
109
148
  you should keep the two layers visibly separate when you answer.
110
149
 
111
- Every fact carries an evidence handle like `E221 · T9` (event 221, turn 9). When
150
+ Every fact carries an evidence handle like `E221 · T9` (event 221, turn 9), and
151
+ so does every search hit — `T9 · E221`, in the order `turn <id> 9 --event 221`
152
+ wants it. When
112
153
  you assert something happened, carry the handle or the session id into your
113
154
  answer so the user can verify it with one command. A claim about the past that
114
155
  can't be traced back to a turn is worth less than saying you didn't find it.
@@ -40,11 +40,24 @@ workspace basename, title.
40
40
  1session search <query> [--scope <path>|cwd|global] [--global] [--since 24h]
41
41
  [--limit n] [--provider name]
42
42
  [--kind user,assistant,thinking,tool_call,tool_result]
43
- [--regex] [--case] [--context n] [--max-hits n] [--json]
43
+ [--regex] [--case] [--context n] [--max-hits n]
44
+ [--include-self] [--json]
44
45
  ```
45
46
 
46
47
  Full-text across sessions; prints matching turns with surrounding context.
47
48
 
49
+ Every hit carries `T<turn> · E<event>`, in the order the drill-down wants, and
50
+ each session ends with the command already assembled:
51
+
52
+ ```text
53
+ ### claude 3ab9fe0e 2026-09-13T03:33:32Z → 2026-09-13T05:52:59Z (2 命中)
54
+ T9 · E491 tool_call(Bash) …curl --max-time 5 …
55
+ ↳ 1session turn 3ab9fe0e 9 --event 491
56
+ ```
57
+
58
+ `T?` means the session recorded no turn containing that event — drill in with
59
+ `turns` first. In `--json`, the handle is `match.turn` and `match.index`.
60
+
48
61
  - `--kind user` is the sharpest filter for "what did I ask about X" — it drops
49
62
  the tool noise and leaves only the human's own words.
50
63
  - `--kind tool_result` finds error text that an agent saw but never quoted back.
@@ -54,6 +67,13 @@ Full-text across sessions; prints matching turns with surrounding context.
54
67
  - `--max-hits n` raises the per-session cap when a session is truncated with
55
68
  "另有 N 处".
56
69
  - `--context n` widens the excerpt around each hit.
70
+ - `--include-self` stops hiding the search's own footprint. By default a
71
+ `1session` invocation written in the last five minutes, and the result
72
+ carrying what it printed, are folded out of the hits — the calling agent's
73
+ session is indexed live, so the query is in it because this command put it
74
+ there. Only the live transcript can hold an event timestamped now, so no past
75
+ session is affected, and the count of what was folded is always printed. In
76
+ `--json` these sessions stay in the array, tagged `self` and `suppressed`.
57
77
 
58
78
  Search is a SQL prefilter that narrows candidate lines, then a regex verifier
59
79
  that decides. Empty queries are rejected rather than matching everything.
@@ -89,12 +109,19 @@ Layer 2. One line per turn: time, duration, event range, file/command/failure
89
109
  counts, what the user said, what the agent replied. Use it to find the turn
90
110
  number to drill into.
91
111
 
92
- ### `turn <id> <n> [--event k]`
112
+ ### `turn <id> <n> [--event k|a-b|a,b,c]`
93
113
 
94
- Layer 3. Every event in a turn. With `--event k`, a single tool call with full
95
- arguments and **untruncated** result — this is the only way to see what a command
114
+ Layer 3. Every event in a turn. With `--event`, the named events with full
115
+ arguments and **untruncated** results — the only way to see what a command
96
116
  actually printed.
97
117
 
118
+ `--event` takes one index (`491`), a range (`491-493`), a list (`491,495,502`),
119
+ or a mix, up to 50 events in one call; ranges are clipped to the session, so
120
+ `560-999` means "to the end". Reading a tool call usually means reading its
121
+ result and the assistant's verdict too, which is the range form in one process
122
+ instead of three. `--json` returns an object for a bare index and an array for
123
+ anything that asked for more than one.
124
+
98
125
  ### `digest <id> [--focus marketing|review|full]`
99
126
 
100
127
  Compact narrative: goal, changed files, commands, key moments (需求 / 转向 / 受阻 /
@@ -138,13 +165,20 @@ import the library instead of shelling out repeatedly:
138
165
  import {
139
166
  listRecentSessions, findSessionsByWorkspace, loadSession, parseSession,
140
167
  distillSession, aggregateWorkspaceSessions, searchSessions,
141
- buildOverview, summarizeTurns, turnDetail, eventDetail,
168
+ buildOverview, summarizeTurns, turnDetail, eventDetail, eventDetails,
169
+ turnStarts, turnNoAt,
142
170
  } from '@1agents/session-reader';
143
171
 
144
172
  const hits = await searchSessions('小红书', { workspace: process.cwd(), since: '24h', kinds: ['user'] });
173
+ hits[0].matches[0].turn; // the T of the T·E handle
145
174
  const overview = buildOverview(await loadSession('01a0907c'));
146
175
  const full = await eventDetail(session, 11); // untruncated tool output
176
+ const around = await eventDetails(session, '11-13');
147
177
  ```
148
178
 
179
+ `searchSessions` takes `selfSessionId` to name the caller explicitly (it
180
+ defaults to `SESSION_READER_CALLER_SESSION`); hits from it come back tagged
181
+ `self` rather than dropped, so a caller decides whether to show them.
182
+
149
183
  `loadSession` goes through the index; `parseSession` reads the source file
150
184
  directly. Requires Node.js >= 22.5 (built-in `node:sqlite`), zero runtime deps.