@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/README.md +77 -7
- package/dist/bin/1session.js +63 -22
- package/dist/src/index.d.ts +3 -3
- package/dist/src/index.js +3 -3
- package/dist/src/ledger.js +3 -6
- package/dist/src/search.d.ts +48 -0
- package/dist/src/search.js +136 -20
- package/dist/src/serve/http.d.ts +9 -1
- package/dist/src/serve/http.js +28 -5
- package/dist/src/serve/node.d.ts +16 -17
- package/dist/src/serve/node.js +27 -58
- package/dist/src/store/read.d.ts +8 -0
- package/dist/src/store/read.js +16 -0
- package/dist/src/turns.d.ts +36 -0
- package/dist/src/turns.js +87 -13
- package/package.json +3 -2
- package/skills/1session/SKILL.md +47 -6
- package/skills/1session/references/cli.md +39 -5
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
|
|
44
|
-
*
|
|
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
|
|
47
|
-
const starts =
|
|
48
|
-
|
|
49
|
-
|
|
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 &&
|
|
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,
|
|
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 =
|
|
97
|
+
let owner = startTimes.findIndex((start) => start !== undefined && Date.parse(start) >= at);
|
|
69
98
|
if (owner === -1)
|
|
70
|
-
owner =
|
|
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 =
|
|
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.
|
|
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.
|
|
60
|
+
"@1agents/dreammate-network": "^0.3.0",
|
|
61
|
+
"@1agents/dreammate-node": "^0.1.0"
|
|
61
62
|
}
|
|
62
63
|
}
|
package/skills/1session/SKILL.md
CHANGED
|
@@ -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
|
|
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
|
-
| "
|
|
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
|
-
`
|
|
86
|
-
|
|
87
|
-
|
|
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)
|
|
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]
|
|
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
|
|
95
|
-
arguments and **untruncated**
|
|
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.
|