@1agents/session-reader 0.4.0 → 0.5.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.
- package/README.md +78 -9
- 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 +30 -8
- 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/search.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { adapters, listResolvedSessions, parseSince } from './resolver.js';
|
|
2
|
+
import { turnNoAt, turnStarts } from './turns.js';
|
|
2
3
|
import { canonicalizePath, isInside } from './util/paths.js';
|
|
3
4
|
const DEFAULT_CONTEXT = 100;
|
|
4
5
|
const DEFAULT_MAX_PER_SESSION = 5;
|
|
@@ -126,25 +127,115 @@ export function planQuery(query, options = {}) {
|
|
|
126
127
|
const run = longestCaselessRun(raw);
|
|
127
128
|
return run.length >= 2 ? { literal: run } : {};
|
|
128
129
|
}
|
|
129
|
-
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
// The searcher's own footprint
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
/**
|
|
134
|
+
* How recent a `1session` invocation has to be to be this very search rather
|
|
135
|
+
* than a historical one. The agent's tool call is written to its transcript
|
|
136
|
+
* seconds before the command runs, so the window only has to survive the
|
|
137
|
+
* indexing sweep — but a past session that genuinely ran the same query must
|
|
138
|
+
* stay a real hit, which is what keeps this narrow.
|
|
139
|
+
*/
|
|
140
|
+
const SELF_ECHO_WINDOW_MS = 5 * 60_000;
|
|
141
|
+
/** How far after the invocation its own output may land, as in `commandLedger`. */
|
|
142
|
+
const RESULT_SPAN = 3;
|
|
143
|
+
const INVOCATION = /(?:^|[\s"'`/])1session\s/;
|
|
144
|
+
/**
|
|
145
|
+
* A `1session` invocation written in the last few minutes.
|
|
146
|
+
*
|
|
147
|
+
* Only the live transcript can hold one: a session that ended yesterday cannot
|
|
148
|
+
* have an event timestamped now, so the window is what separates "this
|
|
149
|
+
* investigation, happening" from "someone once ran this", and no past session
|
|
150
|
+
* is ever touched by it.
|
|
151
|
+
*/
|
|
152
|
+
function isRunningInvocation(candidate, now) {
|
|
153
|
+
if (candidate.kind !== 'tool_call' || !candidate.timestamp)
|
|
154
|
+
return false;
|
|
155
|
+
const at = Date.parse(candidate.timestamp);
|
|
156
|
+
if (!Number.isFinite(at) || at < now - SELF_ECHO_WINDOW_MS || at > now + 60_000)
|
|
157
|
+
return false;
|
|
158
|
+
return INVOCATION.test(candidate.body);
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Folds the Read Plane's own footprint out of one session: an invocation
|
|
162
|
+
* running right now, and the result carrying what it printed.
|
|
163
|
+
*
|
|
164
|
+
* It deliberately does not require the invocation to carry *this* query. A
|
|
165
|
+
* `1session` call from a minute ago prints other sessions' content verbatim,
|
|
166
|
+
* so it matches queries it never mentioned — and whatever it echoed is still
|
|
167
|
+
* in the session it was quoting from, where the search finds it properly, with
|
|
168
|
+
* a handle that drills down to the real thing instead of to a screenful of
|
|
169
|
+
* this tool's output.
|
|
170
|
+
*
|
|
171
|
+
* Stateful because the second half is only knowable from the first, so it is
|
|
172
|
+
* built fresh per session and fed events in index order — which both search
|
|
173
|
+
* paths already produce.
|
|
174
|
+
*/
|
|
175
|
+
export function echoFolder(now) {
|
|
176
|
+
let lastEcho = Number.NEGATIVE_INFINITY;
|
|
177
|
+
return (candidate) => {
|
|
178
|
+
if (isRunningInvocation(candidate, now)) {
|
|
179
|
+
lastEcho = candidate.index;
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
return candidate.kind === 'tool_result' && candidate.index - lastEcho <= RESULT_SPAN;
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Whether a session is the one running the search. Accepts the canonical
|
|
187
|
+
* `provider:native_id`, the bare native id, or a 6+ character prefix of it —
|
|
188
|
+
* the same spellings `resolveSession` takes.
|
|
189
|
+
*/
|
|
190
|
+
export function isCallerSession(ref, callerId) {
|
|
191
|
+
const caller = callerId?.trim().toLowerCase();
|
|
192
|
+
if (!caller)
|
|
193
|
+
return false;
|
|
194
|
+
const native = ref.id.toLowerCase();
|
|
195
|
+
const canonical = `${ref.provider}:${native}`;
|
|
196
|
+
const bare = caller.includes(':') ? caller.slice(caller.indexOf(':') + 1) : caller;
|
|
197
|
+
return caller === canonical || bare === native || (bare.length >= 6 && native.startsWith(bare));
|
|
198
|
+
}
|
|
199
|
+
function accumulate(hits, sessionId, candidate, pattern, context, maxPerSession, echo) {
|
|
130
200
|
if (!candidate.body)
|
|
131
201
|
return;
|
|
132
202
|
pattern.lastIndex = 0;
|
|
133
203
|
const found = pattern.exec(candidate.body);
|
|
134
204
|
if (!found)
|
|
135
205
|
return;
|
|
136
|
-
const bucket = hits.get(sessionId) ?? { matches: [], totalMatches: 0 };
|
|
206
|
+
const bucket = hits.get(sessionId) ?? { matches: [], totalMatches: 0, suppressed: 0 };
|
|
207
|
+
hits.set(sessionId, bucket);
|
|
208
|
+
if (echo(candidate)) {
|
|
209
|
+
bucket.suppressed++;
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
137
212
|
bucket.totalMatches++;
|
|
138
213
|
if (bucket.matches.length < Math.min(maxPerSession, HARD_CAP)) {
|
|
139
214
|
bucket.matches.push({
|
|
140
215
|
index: candidate.index,
|
|
216
|
+
turn: 0, // filled in once the session's turn boundaries are known
|
|
141
217
|
kind: candidate.kind,
|
|
142
218
|
...(candidate.toolName ? { toolName: candidate.toolName } : {}),
|
|
143
219
|
...(candidate.timestamp ? { timestamp: candidate.timestamp } : {}),
|
|
144
220
|
excerpt: excerpt(candidate.body, found.index, found[0].length, context),
|
|
145
221
|
});
|
|
146
222
|
}
|
|
147
|
-
|
|
223
|
+
}
|
|
224
|
+
/** Stamps the drill-down handle onto every match of one session. */
|
|
225
|
+
function assignTurns(matches, starts) {
|
|
226
|
+
for (const match of matches)
|
|
227
|
+
match.turn = turnNoAt(starts, match.index);
|
|
228
|
+
}
|
|
229
|
+
/** A bucket as it leaves the search: empty ones only survive to be counted. */
|
|
230
|
+
function toHit(session, bucket, caller) {
|
|
231
|
+
const self = isCallerSession(session, caller) || (bucket.totalMatches === 0 && bucket.suppressed > 0);
|
|
232
|
+
return {
|
|
233
|
+
session,
|
|
234
|
+
matches: bucket.matches,
|
|
235
|
+
totalMatches: bucket.totalMatches,
|
|
236
|
+
...(bucket.suppressed ? { suppressed: bucket.suppressed } : {}),
|
|
237
|
+
...(self ? { self: true } : {}),
|
|
238
|
+
};
|
|
148
239
|
}
|
|
149
240
|
/**
|
|
150
241
|
* Full-text search across sessions.
|
|
@@ -159,16 +250,28 @@ export async function searchSessions(query, options = {}) {
|
|
|
159
250
|
const pattern = buildPattern(query, options);
|
|
160
251
|
const context = options.context ?? DEFAULT_CONTEXT;
|
|
161
252
|
const maxPerSession = options.maxPerSession ?? DEFAULT_MAX_PER_SESSION;
|
|
253
|
+
const now = Date.now();
|
|
254
|
+
const plan = {
|
|
255
|
+
pattern,
|
|
256
|
+
context,
|
|
257
|
+
maxPerSession,
|
|
258
|
+
newEchoFolder: () => echoFolder(now),
|
|
259
|
+
caller: options.selfSessionId ?? process.env.SESSION_READER_CALLER_SESSION,
|
|
260
|
+
};
|
|
162
261
|
const direct = options.useIndex === false || process.env.SESSION_READER_NO_INDEX === '1';
|
|
163
262
|
const hits = direct
|
|
164
|
-
? await searchByParsing(
|
|
165
|
-
: await searchByIndex(query, options,
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
263
|
+
? await searchByParsing(options, plan)
|
|
264
|
+
: await searchByIndex(query, options, plan);
|
|
265
|
+
hits.sort((a, b) => Date.parse(b.session.updatedAt ?? '') - Date.parse(a.session.updatedAt ?? ''));
|
|
266
|
+
// `limit` caps the sessions a caller has to read, so it counts the ones that
|
|
267
|
+
// will actually be shown; a folded self-hit carries nothing but its tally and
|
|
268
|
+
// must not push a real session out of the answer.
|
|
269
|
+
const limit = options.limit ?? Number.POSITIVE_INFINITY;
|
|
270
|
+
let shown = 0;
|
|
271
|
+
return hits.filter((hit) => (hit.self ? true : ++shown <= limit));
|
|
169
272
|
}
|
|
170
273
|
/** The oracle: parses every candidate from disk, touching no stored state. */
|
|
171
|
-
async function searchByParsing(
|
|
274
|
+
async function searchByParsing(options, plan) {
|
|
172
275
|
const kinds = options.kinds?.length ? new Set(options.kinds) : undefined;
|
|
173
276
|
const hits = [];
|
|
174
277
|
const handles = await listResolvedSessions({
|
|
@@ -177,10 +280,13 @@ async function searchByParsing(_query, options, pattern, context, maxPerSession)
|
|
|
177
280
|
scan: Number.POSITIVE_INFINITY,
|
|
178
281
|
});
|
|
179
282
|
for (const handle of handles) {
|
|
180
|
-
const session = await handle.adapter
|
|
283
|
+
const session = await handle.adapter
|
|
284
|
+
.parse(handle.candidate)
|
|
285
|
+
.catch(() => undefined);
|
|
181
286
|
if (!session)
|
|
182
287
|
continue;
|
|
183
288
|
const bucket = new Map();
|
|
289
|
+
const echo = plan.newEchoFolder();
|
|
184
290
|
for (const turn of session.turns) {
|
|
185
291
|
if (kinds && !kinds.has(turn.kind))
|
|
186
292
|
continue;
|
|
@@ -190,11 +296,13 @@ async function searchByParsing(_query, options, pattern, context, maxPerSession)
|
|
|
190
296
|
...(turn.toolName ? { toolName: turn.toolName } : {}),
|
|
191
297
|
...(turn.timestamp ? { timestamp: turn.timestamp } : {}),
|
|
192
298
|
body: haystack(turn),
|
|
193
|
-
}, pattern, context, maxPerSession);
|
|
299
|
+
}, plan.pattern, plan.context, plan.maxPerSession, echo);
|
|
194
300
|
}
|
|
195
301
|
const found = bucket.get(session.ref.id);
|
|
196
|
-
if (found)
|
|
197
|
-
|
|
302
|
+
if (!found)
|
|
303
|
+
continue;
|
|
304
|
+
assignTurns(found.matches, turnStarts(session));
|
|
305
|
+
hits.push(toHit(session.ref, found, plan.caller));
|
|
198
306
|
}
|
|
199
307
|
return hits;
|
|
200
308
|
}
|
|
@@ -204,11 +312,11 @@ async function searchByParsing(_query, options, pattern, context, maxPerSession)
|
|
|
204
312
|
* Sessions are never materialized — rebuilding 622 of them into objects cost
|
|
205
313
|
* more than every other part of a search put together.
|
|
206
314
|
*/
|
|
207
|
-
async function searchByIndex(query, options,
|
|
315
|
+
async function searchByIndex(query, options, plan) {
|
|
208
316
|
const { openStore } = await import('./store/db.js');
|
|
209
317
|
const { refreshSession } = await import('./store/indexer.js');
|
|
210
318
|
const { searchRows } = await import('./store/rows.js');
|
|
211
|
-
const { refOf } = await import('./store/read.js');
|
|
319
|
+
const { refOf, turnStartsOf } = await import('./store/read.js');
|
|
212
320
|
const db = await openStore();
|
|
213
321
|
const sinceMs = parseSince(options.since);
|
|
214
322
|
const ids = [];
|
|
@@ -223,16 +331,21 @@ async function searchByIndex(query, options, pattern, context, maxPerSession) {
|
|
|
223
331
|
ids.push(result.id);
|
|
224
332
|
}
|
|
225
333
|
}
|
|
226
|
-
const
|
|
334
|
+
const prefilter = planQuery(query, options);
|
|
227
335
|
const workspace = options.workspace ? canonicalizePath(options.workspace) : undefined;
|
|
228
336
|
const buckets = new Map();
|
|
337
|
+
let folding = { sessionId: '', echo: plan.newEchoFolder() };
|
|
229
338
|
for (const row of searchRows(db, {
|
|
230
339
|
ids,
|
|
231
340
|
...(workspace ? { workspace } : {}),
|
|
232
341
|
...(options.kinds?.length ? { kinds: options.kinds } : {}),
|
|
233
|
-
...(
|
|
234
|
-
...(
|
|
342
|
+
...(prefilter.literal ? { literal: prefilter.literal } : {}),
|
|
343
|
+
...(prefilter.fold ? { fold: true } : {}),
|
|
235
344
|
})) {
|
|
345
|
+
// `searchRows` orders by (session_id, idx), which is what the folder needs.
|
|
346
|
+
if (row.session_id !== folding.sessionId) {
|
|
347
|
+
folding = { sessionId: row.session_id, echo: plan.newEchoFolder() };
|
|
348
|
+
}
|
|
236
349
|
const args = row.tool_args_json ?? '';
|
|
237
350
|
accumulate(buckets, row.session_id, {
|
|
238
351
|
index: row.idx,
|
|
@@ -241,7 +354,7 @@ async function searchByIndex(query, options, pattern, context, maxPerSession) {
|
|
|
241
354
|
...(row.ts ? { timestamp: row.ts } : {}),
|
|
242
355
|
// Rebuilt exactly as `haystack` does, so an excerpt cannot shift.
|
|
243
356
|
body: [row.text ?? undefined, row.tool_result ?? undefined, args].filter(Boolean).join('\n'),
|
|
244
|
-
}, pattern, context, maxPerSession);
|
|
357
|
+
}, plan.pattern, plan.context, plan.maxPerSession, folding.echo);
|
|
245
358
|
}
|
|
246
359
|
const hits = [];
|
|
247
360
|
for (const [id, bucket] of buckets) {
|
|
@@ -251,7 +364,10 @@ async function searchByIndex(query, options, pattern, context, maxPerSession) {
|
|
|
251
364
|
const ref = refOf(row);
|
|
252
365
|
if (workspace && !isInside(workspace, ref.workspace ?? ''))
|
|
253
366
|
continue;
|
|
254
|
-
|
|
367
|
+
// Only the sessions that actually matched pay for this — a handful of
|
|
368
|
+
// index lookups, not the 622-session rebuild the row path exists to avoid.
|
|
369
|
+
assignTurns(bucket.matches, turnStartsOf(db, id, row.event_count));
|
|
370
|
+
hits.push(toHit(ref, bucket, plan.caller));
|
|
255
371
|
}
|
|
256
372
|
return hits;
|
|
257
373
|
}
|
package/dist/src/serve/http.d.ts
CHANGED
|
@@ -4,10 +4,13 @@
|
|
|
4
4
|
* 它把本地 Read Plane 原样暴露成网络能力:`1session overview <id>` 成为
|
|
5
5
|
* `sessions.read`。**不重新实现索引与事实层**,只是换一个调用入口。
|
|
6
6
|
*
|
|
7
|
-
*
|
|
7
|
+
* HTTP 层不引第三方框架,只用 node:http;端口与协议类型来自 L0 包。
|
|
8
8
|
*/
|
|
9
9
|
import http from 'node:http';
|
|
10
|
+
/** 约定端口,由 L0 协议包定义——Control Plane 的 pull 探测照着它找服务。 */
|
|
11
|
+
export declare const DEFAULT_PORT: 7777;
|
|
10
12
|
export interface ServeOptions {
|
|
13
|
+
/** 默认 {@link DEFAULT_PORT}。换端口就探测不到了,得自己 register。 */
|
|
11
14
|
port?: number;
|
|
12
15
|
/**
|
|
13
16
|
* Defaults to loopback. Session transcripts contain source code, shell
|
|
@@ -19,6 +22,11 @@ export interface ServeOptions {
|
|
|
19
22
|
token?: string;
|
|
20
23
|
/** Advertised base url, when behind a proxy or a different tailnet name. */
|
|
21
24
|
baseUrl?: string;
|
|
25
|
+
/**
|
|
26
|
+
* 向本机 node agent 报备自己。默认开——agent 没起时是静默 no-op,没有代价。
|
|
27
|
+
* 报备后外部探一个 36908 就能看见本服务,不必碰运气猜端口。
|
|
28
|
+
*/
|
|
29
|
+
report?: boolean;
|
|
22
30
|
}
|
|
23
31
|
export declare function createServer(options?: ServeOptions): http.Server;
|
|
24
32
|
export declare function serve(options?: ServeOptions): Promise<http.Server>;
|
package/dist/src/serve/http.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* 它把本地 Read Plane 原样暴露成网络能力:`1session overview <id>` 成为
|
|
5
5
|
* `sessions.read`。**不重新实现索引与事实层**,只是换一个调用入口。
|
|
6
6
|
*
|
|
7
|
-
*
|
|
7
|
+
* HTTP 层不引第三方框架,只用 node:http;端口与协议类型来自 L0 包。
|
|
8
8
|
*/
|
|
9
9
|
import http from 'node:http';
|
|
10
10
|
import { listRecentSessions, loadSession } from '../resolver.js';
|
|
@@ -13,6 +13,11 @@ import { summarizeTurns } from '../turns.js';
|
|
|
13
13
|
import { searchSessions } from '../search.js';
|
|
14
14
|
import { canonicalizePath } from '../util/paths.js';
|
|
15
15
|
import { buildManifest, nodeIdentity, sessionUri } from './node.js';
|
|
16
|
+
import { DEFAULT_PORTS } from '@1agents/dreammate-network';
|
|
17
|
+
import { reportAndHoldRegistration } from '@1agents/dreammate-node/client';
|
|
18
|
+
import { SESSION_CAPABILITIES } from './node.js';
|
|
19
|
+
/** 约定端口,由 L0 协议包定义——Control Plane 的 pull 探测照着它找服务。 */
|
|
20
|
+
export const DEFAULT_PORT = DEFAULT_PORTS['session-registry'];
|
|
16
21
|
const json = (res, status, body) => {
|
|
17
22
|
const payload = JSON.stringify(body, null, 2);
|
|
18
23
|
res.writeHead(status, {
|
|
@@ -53,14 +58,13 @@ async function noteRead(verb, target, caller) {
|
|
|
53
58
|
}
|
|
54
59
|
async function route(ctx) {
|
|
55
60
|
const { pathname } = ctx.url;
|
|
56
|
-
const identity = nodeIdentity();
|
|
61
|
+
const identity = await nodeIdentity();
|
|
57
62
|
if (pathname === '/health') {
|
|
58
63
|
return { status: 200, body: { status: 'ok', node_id: identity.node_id, service: 'session-registry' } };
|
|
59
64
|
}
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
return { status: 200, body: buildManifest(ctx.url.origin) };
|
|
65
|
+
// 只报本服务自己——节点全貌在本机 agent 的 :36908/manifest。
|
|
66
|
+
if (pathname === '/manifest') {
|
|
67
|
+
return { status: 200, body: await buildManifest(ctx.url.origin) };
|
|
64
68
|
}
|
|
65
69
|
if (pathname === '/v1/sessions') {
|
|
66
70
|
const refs = await listRecentSessions({
|
|
@@ -168,12 +172,30 @@ export function createServer(options = {}) {
|
|
|
168
172
|
export async function serve(options = {}) {
|
|
169
173
|
const host = options.host ?? '127.0.0.1';
|
|
170
174
|
const server = createServer(options);
|
|
171
|
-
await new Promise((resolve) => server.listen(options.port ??
|
|
175
|
+
await new Promise((resolve) => server.listen(options.port ?? DEFAULT_PORT, host, resolve));
|
|
172
176
|
const { port } = server.address();
|
|
173
|
-
const identity = nodeIdentity();
|
|
177
|
+
const identity = await nodeIdentity();
|
|
178
|
+
// 只听回环的服务,外部发现得了却连不上——如实说,别让调用方白跑一趟。
|
|
179
|
+
const reachability = host === '127.0.0.1' || host === 'localhost' || host === '::1' ? 'localhost' : 'network';
|
|
180
|
+
const reported = options.report === false
|
|
181
|
+
? undefined
|
|
182
|
+
: await reportAndHoldRegistration({
|
|
183
|
+
id: 'session-registry',
|
|
184
|
+
name: 'session-reader',
|
|
185
|
+
kind: 'session_registry',
|
|
186
|
+
capabilities: [...SESSION_CAPABILITIES],
|
|
187
|
+
port,
|
|
188
|
+
reachability,
|
|
189
|
+
resources: [{ scheme: 'session' }],
|
|
190
|
+
});
|
|
174
191
|
console.log(`1session serve — node ${identity.name} (${identity.node_id})`);
|
|
175
192
|
console.log(` http://${host}:${port}/manifest`);
|
|
176
193
|
console.log(` capabilities: sessions.list, sessions.read, sessions.turns, sessions.search, sessions.graph`);
|
|
194
|
+
if (reported) {
|
|
195
|
+
console.log(reported.ok
|
|
196
|
+
? ` 已向本机 node agent 报备(reachability=${reachability})`
|
|
197
|
+
: ` 未报备:${reported.reason}——不影响本服务,只是外部得靠约定端口找它`);
|
|
198
|
+
}
|
|
177
199
|
if (host !== '127.0.0.1' && host !== 'localhost' && !options.token) {
|
|
178
200
|
console.warn(` ⚠️ 绑定在 ${host} 且未设置 --token:会话原文(代码、shell 历史、密钥)将对该网络开放。`);
|
|
179
201
|
}
|
package/dist/src/serve/node.d.ts
CHANGED
|
@@ -1,26 +1,25 @@
|
|
|
1
|
-
import { PROTOCOL_VERSION, type AccessDescriptor, type NodeManifest, type Service as NetworkService, type SessionURI } from '@1agents/dreammate-network';
|
|
2
|
-
export { PROTOCOL_VERSION };
|
|
3
|
-
export type { AccessDescriptor, NetworkService, NodeManifest };
|
|
4
|
-
/** Where the node identity is kept, next to the index db. */
|
|
5
|
-
export declare function nodeIdentityPath(): string;
|
|
6
|
-
interface StoredIdentity {
|
|
7
|
-
node_id: string;
|
|
8
|
-
name: string;
|
|
9
|
-
type: string;
|
|
10
|
-
}
|
|
11
1
|
/**
|
|
12
|
-
*
|
|
13
|
-
* `~/.1agents/session-reader/node.json`.
|
|
2
|
+
* session-reader 作为 DreamMate Network Service 时的 manifest 构建。
|
|
14
3
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
4
|
+
* 节点身份不在这里实现——它是每台机器的公共事实,由 `@1agents/dreammate-node`
|
|
5
|
+
* 统一提供(优先 tailnet,拿不到回退本地)。本机所有服务读到同一份,不会各自
|
|
6
|
+
* 生成 id 把一台机器裂成几个 Node。
|
|
17
7
|
*/
|
|
18
|
-
|
|
8
|
+
import { nodeIdentity, type NodeIdentity } from '@1agents/dreammate-node';
|
|
9
|
+
import { PROTOCOL_VERSION, type AccessDescriptor, type NodeManifest, type Reachability, type Service as NetworkService, type SessionURI } from '@1agents/dreammate-network';
|
|
10
|
+
export { nodeIdentity, PROTOCOL_VERSION };
|
|
11
|
+
export type { AccessDescriptor, NetworkService, NodeIdentity, NodeManifest, Reachability };
|
|
19
12
|
/**
|
|
20
13
|
* The capabilities this Service answers for. Names are the network-facing
|
|
21
14
|
* spelling of the CLI verbs: `1session overview` is `sessions.read`.
|
|
22
15
|
*/
|
|
23
16
|
export declare const SESSION_CAPABILITIES: readonly ["sessions.list", "sessions.read", "sessions.turns", "sessions.search", "sessions.graph"];
|
|
24
|
-
export declare function buildManifest(baseUrl: string): NodeManifest
|
|
25
|
-
/**
|
|
17
|
+
export declare function buildManifest(baseUrl: string): Promise<NodeManifest>;
|
|
18
|
+
/**
|
|
19
|
+
* `session://<node>/<runtime>/<session_id>` — 一个会话在网络中的地址。
|
|
20
|
+
*
|
|
21
|
+
* `nodeName` 应该来自 {@link nodeIdentity},也就是 tailnet 的 DNSName 前缀。
|
|
22
|
+
* **不要传 `os.hostname()`**:iOS 设备的 hostname 全是 `localhost`,几台手机
|
|
23
|
+
* 接进来会产出一模一样的 `session://localhost/yima/...`。
|
|
24
|
+
*/
|
|
26
25
|
export declare function sessionUri(nodeName: string, provider: string, id: string): SessionURI;
|
package/dist/src/serve/node.js
CHANGED
|
@@ -1,57 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* session-reader 作为 DreamMate Network Service 时的 manifest 构建。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* 节点身份不在这里实现——它是每台机器的公共事实,由 `@1agents/dreammate-node`
|
|
5
|
+
* 统一提供(优先 tailnet,拿不到回退本地)。本机所有服务读到同一份,不会各自
|
|
6
|
+
* 生成 id 把一台机器裂成几个 Node。
|
|
6
7
|
*/
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
|
|
10
|
-
import { randomUUID } from 'node:crypto';
|
|
11
|
-
import { PROTOCOL_VERSION, } from '@1agents/dreammate-network';
|
|
12
|
-
export { PROTOCOL_VERSION };
|
|
13
|
-
/* ---------- 身份 ---------- */
|
|
14
|
-
/** Where the node identity is kept, next to the index db. */
|
|
15
|
-
export function nodeIdentityPath() {
|
|
16
|
-
return path.join(os.homedir(), '.1agents', 'session-reader', 'node.json');
|
|
17
|
-
}
|
|
18
|
-
const PLATFORM_TYPE = {
|
|
19
|
-
darwin: 'macos',
|
|
20
|
-
linux: 'linux',
|
|
21
|
-
win32: 'windows',
|
|
22
|
-
};
|
|
23
|
-
/**
|
|
24
|
-
* A stable node id that survives restarts, generated on first use and kept in
|
|
25
|
-
* `~/.1agents/session-reader/node.json`.
|
|
26
|
-
*
|
|
27
|
-
* `DREAMMATE_NODE_ID` / `DREAMMATE_NODE_NAME` override it without touching the
|
|
28
|
-
* file, which is what a container or a second instance on one host wants.
|
|
29
|
-
*/
|
|
30
|
-
export function nodeIdentity() {
|
|
31
|
-
const type = PLATFORM_TYPE[process.platform] ?? process.platform;
|
|
32
|
-
const envId = process.env.DREAMMATE_NODE_ID?.trim();
|
|
33
|
-
const envName = process.env.DREAMMATE_NODE_NAME?.trim();
|
|
34
|
-
if (envId && envName)
|
|
35
|
-
return { node_id: envId, name: envName, type };
|
|
36
|
-
const file = nodeIdentityPath();
|
|
37
|
-
let stored = {};
|
|
38
|
-
try {
|
|
39
|
-
stored = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
40
|
-
}
|
|
41
|
-
catch {
|
|
42
|
-
// First run, or an unreadable file we are about to overwrite.
|
|
43
|
-
}
|
|
44
|
-
if (!stored.node_id) {
|
|
45
|
-
stored = { node_id: `node_${randomUUID().replace(/-/g, '').slice(0, 12)}`, name: os.hostname(), type };
|
|
46
|
-
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
47
|
-
fs.writeFileSync(file, `${JSON.stringify(stored, null, 2)}\n`);
|
|
48
|
-
}
|
|
49
|
-
return {
|
|
50
|
-
node_id: envId ?? stored.node_id,
|
|
51
|
-
name: envName ?? stored.name ?? os.hostname(),
|
|
52
|
-
type,
|
|
53
|
-
};
|
|
54
|
-
}
|
|
8
|
+
import { nodeIdentity } from '@1agents/dreammate-node';
|
|
9
|
+
import { DEFAULT_PORTS, PROTOCOL_VERSION, } from '@1agents/dreammate-network';
|
|
10
|
+
export { nodeIdentity, PROTOCOL_VERSION };
|
|
55
11
|
/* ---------- Manifest ---------- */
|
|
56
12
|
/**
|
|
57
13
|
* The capabilities this Service answers for. Names are the network-facing
|
|
@@ -64,13 +20,20 @@ export const SESSION_CAPABILITIES = [
|
|
|
64
20
|
'sessions.search',
|
|
65
21
|
'sessions.graph',
|
|
66
22
|
];
|
|
67
|
-
export function buildManifest(baseUrl) {
|
|
68
|
-
const identity = nodeIdentity();
|
|
23
|
+
export async function buildManifest(baseUrl) {
|
|
24
|
+
const identity = await nodeIdentity();
|
|
25
|
+
// 有 MagicDNS 就用它:`http://scott-mac:7777` 跨网络稳定,不怕 IP 变,
|
|
26
|
+
// 而请求里的 Host 只是"调用方碰巧用了哪个地址"。
|
|
27
|
+
const advertised = identity.dnsName
|
|
28
|
+
? baseUrl.replace(/\/\/[^/]+/, `//${identity.dnsName}:${new URL(baseUrl).port || String(DEFAULT_PORTS['session-registry'])}`)
|
|
29
|
+
: baseUrl;
|
|
69
30
|
return {
|
|
70
|
-
|
|
71
|
-
|
|
31
|
+
node_id: identity.node_id,
|
|
32
|
+
name: identity.name,
|
|
33
|
+
type: identity.type,
|
|
34
|
+
tailscale_name: identity.dnsName ?? process.env.DREAMMATE_TAILSCALE_NAME?.trim() ?? null,
|
|
72
35
|
online: true,
|
|
73
|
-
metadata: { protocol_version: PROTOCOL_VERSION },
|
|
36
|
+
metadata: { protocol_version: PROTOCOL_VERSION, identity_source: identity.source },
|
|
74
37
|
services: [
|
|
75
38
|
{
|
|
76
39
|
id: 'session-registry',
|
|
@@ -78,12 +41,18 @@ export function buildManifest(baseUrl) {
|
|
|
78
41
|
kind: 'session_registry',
|
|
79
42
|
capabilities: [...SESSION_CAPABILITIES],
|
|
80
43
|
resources: [{ scheme: 'session', description: 'session://<node>/<runtime>/<session_id>' }],
|
|
81
|
-
access: [{ protocol: 'http', base_url: `${
|
|
44
|
+
access: [{ protocol: 'http', base_url: `${advertised}/v1` }],
|
|
82
45
|
},
|
|
83
46
|
],
|
|
84
47
|
};
|
|
85
48
|
}
|
|
86
|
-
/**
|
|
49
|
+
/**
|
|
50
|
+
* `session://<node>/<runtime>/<session_id>` — 一个会话在网络中的地址。
|
|
51
|
+
*
|
|
52
|
+
* `nodeName` 应该来自 {@link nodeIdentity},也就是 tailnet 的 DNSName 前缀。
|
|
53
|
+
* **不要传 `os.hostname()`**:iOS 设备的 hostname 全是 `localhost`,几台手机
|
|
54
|
+
* 接进来会产出一模一样的 `session://localhost/yima/...`。
|
|
55
|
+
*/
|
|
87
56
|
export function sessionUri(nodeName, provider, id) {
|
|
88
57
|
return `session://${nodeName}/${provider}/${id}`;
|
|
89
58
|
}
|
package/dist/src/store/read.d.ts
CHANGED
|
@@ -39,5 +39,13 @@ export interface ListQuery {
|
|
|
39
39
|
* workspace never silently loses its older sessions to a candidate cap.
|
|
40
40
|
*/
|
|
41
41
|
export declare function listSessionRows(db: DatabaseSync, query: ListQuery): SessionRow[];
|
|
42
|
+
/**
|
|
43
|
+
* Turn starts for one indexed session, without rebuilding it.
|
|
44
|
+
*
|
|
45
|
+
* The rule has to stay the rule `turnStarts` applies to a parsed session — a
|
|
46
|
+
* user event carrying real text opens a turn — so the two are fed to the same
|
|
47
|
+
* `turnStartsFrom`; only the source of the indices differs.
|
|
48
|
+
*/
|
|
49
|
+
export declare function turnStartsOf(db: DatabaseSync, sessionId: string, eventCount: number): number[];
|
|
42
50
|
export declare function refOf(row: SessionRow): SessionRef;
|
|
43
51
|
export declare function readSession(db: DatabaseSync, row: SessionRow): NormalizedSession;
|
package/dist/src/store/read.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { turnStartsFrom } from '../turns.js';
|
|
1
2
|
import { emptyProviderStats, } from '../types.js';
|
|
2
3
|
export function sessionRow(db, id) {
|
|
3
4
|
return db.prepare('SELECT * FROM sessions WHERE id = ?').get(id);
|
|
@@ -49,6 +50,21 @@ export function listSessionRows(db, query) {
|
|
|
49
50
|
'ORDER BY (ended_at IS NULL), ended_at DESC LIMIT ?')
|
|
50
51
|
.all(...params);
|
|
51
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Turn starts for one indexed session, without rebuilding it.
|
|
55
|
+
*
|
|
56
|
+
* The rule has to stay the rule `turnStarts` applies to a parsed session — a
|
|
57
|
+
* user event carrying real text opens a turn — so the two are fed to the same
|
|
58
|
+
* `turnStartsFrom`; only the source of the indices differs.
|
|
59
|
+
*/
|
|
60
|
+
export function turnStartsOf(db, sessionId, eventCount) {
|
|
61
|
+
const rows = db
|
|
62
|
+
.prepare(`SELECT idx FROM events
|
|
63
|
+
WHERE session_id = ? AND kind = 'user' AND trim(coalesce(text, '')) != ''
|
|
64
|
+
ORDER BY idx`)
|
|
65
|
+
.all(sessionId);
|
|
66
|
+
return turnStartsFrom(rows.map((row) => row.idx), eventCount);
|
|
67
|
+
}
|
|
52
68
|
export function refOf(row) {
|
|
53
69
|
return {
|
|
54
70
|
id: row.native_id,
|
package/dist/src/turns.d.ts
CHANGED
|
@@ -1,4 +1,23 @@
|
|
|
1
1
|
import type { NormalizedSession, TurnDetail, TurnEvent, TurnSummary } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Turn boundaries from the user messages that open them.
|
|
4
|
+
*
|
|
5
|
+
* Split out from `turnStarts` so the index path can feed it the same indices
|
|
6
|
+
* straight from SQL without materializing the session: a `T` printed by
|
|
7
|
+
* `search` then means exactly what a `T` printed by `turns` means.
|
|
8
|
+
*/
|
|
9
|
+
export declare function turnStartsFrom(userEventIndices: number[], eventCount: number): number[];
|
|
10
|
+
/**
|
|
11
|
+
* Turn boundaries: codex records them natively, the others start a new turn on
|
|
12
|
+
* every user message.
|
|
13
|
+
*/
|
|
14
|
+
export declare function turnStarts(session: NormalizedSession): number[];
|
|
15
|
+
/**
|
|
16
|
+
* The turn (1-based) an event index falls in, or 0 when it falls before the
|
|
17
|
+
* first one. Turns tile the event stream, so "the last start at or before the
|
|
18
|
+
* index" is the whole rule.
|
|
19
|
+
*/
|
|
20
|
+
export declare function turnNoAt(starts: number[], index: number): number;
|
|
2
21
|
export declare function summarizeTurns(session: NormalizedSession): TurnSummary[];
|
|
3
22
|
export declare function turnDetail(session: NormalizedSession, turnNo: number): TurnDetail;
|
|
4
23
|
export interface EventDetail extends TurnEvent {
|
|
@@ -7,6 +26,23 @@ export interface EventDetail extends TurnEvent {
|
|
|
7
26
|
/** Set when the transcript is short and the full copy could not be found. */
|
|
8
27
|
truncationNote?: string;
|
|
9
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* How many events one `--event` spec may expand to. Reading a tool call with
|
|
31
|
+
* its result and the assistant's verdict takes three; a spec asking for
|
|
32
|
+
* hundreds wanted `turn <n>` instead, and silently truncating would be worse
|
|
33
|
+
* than saying so.
|
|
34
|
+
*/
|
|
35
|
+
export declare const MAX_EVENT_SPAN = 50;
|
|
36
|
+
/**
|
|
37
|
+
* `214`, `214-218`, `214,216,218` and any mix, as ascending unique indices.
|
|
38
|
+
*
|
|
39
|
+
* Ranges are clipped to the session — asking for `210-999` on a 300-event
|
|
40
|
+
* session is a reasonable way to say "to the end" — while a bare index is left
|
|
41
|
+
* alone so an out-of-range one still errors naming the number that was typed.
|
|
42
|
+
*/
|
|
43
|
+
export declare function parseEventSpec(spec: string, eventCount: number): number[];
|
|
44
|
+
/** `eventDetail` over a spec: `214`, `214-218`, `214,216,218`. */
|
|
45
|
+
export declare function eventDetails(session: NormalizedSession, spec: string): Promise<EventDetail[]>;
|
|
10
46
|
/**
|
|
11
47
|
* Tier three: one event with its untruncated payload. Antigravity shortens long
|
|
12
48
|
* step output in the transcript and keeps the full copy under `steps/<n>/`.
|