@x1a0f3n9/dsh-session-query 0.1.5-alpha.2
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/LICENSE +21 -0
- package/README.i18n.yaml +6 -0
- package/README.md +167 -0
- package/README.zh.md +167 -0
- package/lib/index.js +1234 -0
- package/lib/types/cold-read.d.ts +27 -0
- package/lib/types/config.d.ts +29 -0
- package/lib/types/corpus.d.ts +66 -0
- package/lib/types/cursor.d.ts +11 -0
- package/lib/types/documents.d.ts +18 -0
- package/lib/types/extraction.d.ts +12 -0
- package/lib/types/filters.d.ts +35 -0
- package/lib/types/index.d.ts +155 -0
- package/lib/types/observation.d.ts +85 -0
- package/lib/types/sources.d.ts +9 -0
- package/lib/types/tracing.d.ts +33 -0
- package/lib/types/types.d.ts +265 -0
- package/package.json +62 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,1234 @@
|
|
|
1
|
+
import { Service } from "@deepseek-ai/cordis";
|
|
2
|
+
import { Session, SessionLogOffset, SessionSeq, foldSurface, interruptedTurnClosers, isSurfaceEvent, snapshotSessionEvent } from "@x1a0f3n9/dsh-session";
|
|
3
|
+
import { foldSessionTitle } from "@x1a0f3n9/dsh-session-title";
|
|
4
|
+
import { HarnessError } from "@x1a0f3n9/dsh-llm";
|
|
5
|
+
//#region lib/types/config.js
|
|
6
|
+
/** Public configuration and typed failures for the combined session-query service. */
|
|
7
|
+
/** Default maximum `before`/`after` raw-event window. */
|
|
8
|
+
const SESSION_QUERY_READ_WINDOW_MAX = 50;
|
|
9
|
+
/** Default maximum number of concurrent persisted-log reads in one batch read. */
|
|
10
|
+
const SESSION_QUERY_DEFAULT_PERSISTED_INSPECT_CONCURRENCY = 4;
|
|
11
|
+
/** Default maximum number of cold prepared-Session observations retained for reuse. */
|
|
12
|
+
const SESSION_QUERY_DEFAULT_PREPARED_SESSION_CACHE_SIZE = 5;
|
|
13
|
+
/** Typed session-query failure whose `code` is one closed taxonomy member. */
|
|
14
|
+
var SessionQueryError = class extends HarnessError {
|
|
15
|
+
constructor(message, code, options) {
|
|
16
|
+
super(message, code, options);
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
//#endregion
|
|
20
|
+
//#region lib/types/cold-read.js
|
|
21
|
+
/** One-shot cold session read through the handle-based persistence seam. */
|
|
22
|
+
/**
|
|
23
|
+
* Read one complete stored session log without taking ownership or mutating
|
|
24
|
+
* storage: open a read handle, read the validated contiguous log, close the
|
|
25
|
+
* handle, and append `interruptedTurnClosers` so a log whose writer crashed
|
|
26
|
+
* mid-turn folds as a balanced transcript. Backend failures propagate
|
|
27
|
+
* unmapped — each caller owns its error taxonomy.
|
|
28
|
+
* @param persistence - the mounted persistence service.
|
|
29
|
+
* @param sessionId - the stored session to read.
|
|
30
|
+
* @param signal - optional cancellation for the open and read work.
|
|
31
|
+
* @returns an adoptable seed in a caller-owned outer array, ready for in-place Session restoration.
|
|
32
|
+
*/
|
|
33
|
+
async function readColdSessionLog(persistence, sessionId, signal) {
|
|
34
|
+
const options = signal === void 0 ? void 0 : { signal };
|
|
35
|
+
const handle = await persistence.open(sessionId, "read", options);
|
|
36
|
+
let read;
|
|
37
|
+
try {
|
|
38
|
+
read = await handle.read(0, void 0, options);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
try {
|
|
41
|
+
await handle.close();
|
|
42
|
+
} catch {}
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
await handle.close();
|
|
46
|
+
const { events } = read;
|
|
47
|
+
return {
|
|
48
|
+
eventState: read.eventState,
|
|
49
|
+
header: handle.header,
|
|
50
|
+
inheritedEventCount: handle.inheritedEventCount,
|
|
51
|
+
events: [...events, ...interruptedTurnClosers(events)]
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
//#endregion
|
|
55
|
+
//#region lib/types/sources.js
|
|
56
|
+
/** Shared immutable-header checks for logical session source observers. */
|
|
57
|
+
/**
|
|
58
|
+
* Reject incompatible observations of one logical session source.
|
|
59
|
+
* @param a - first live, listed, or loaded header observation.
|
|
60
|
+
* @param b - second header observation expected to identify the same source.
|
|
61
|
+
*/
|
|
62
|
+
function assertSessionHeadersCompatible(a, b) {
|
|
63
|
+
if (a.id !== b.id || a.createdAt !== b.createdAt || a.cwd !== b.cwd || a.parentSession !== b.parentSession || a.isSeeded !== b.isSeeded || (a.delegationDepth ?? 0) !== (b.delegationDepth ?? 0)) throw new SessionQueryError(`session source headers conflict for session "${a.id}"`, "SESSION_QUERY_SOURCE_CONFLICT");
|
|
64
|
+
}
|
|
65
|
+
//#endregion
|
|
66
|
+
//#region lib/types/corpus.js
|
|
67
|
+
/** Live/persisted logical-corpus resolution for session-query. */
|
|
68
|
+
/** Resolves a live-preferred corpus against the persistence service mounted now. */
|
|
69
|
+
var SessionCorpus = class {
|
|
70
|
+
_ctx;
|
|
71
|
+
_persistedReadConcurrency;
|
|
72
|
+
_persistence;
|
|
73
|
+
_optionalPersistenceFiber;
|
|
74
|
+
constructor(_ctx, _persistedReadConcurrency) {
|
|
75
|
+
this._ctx = _ctx;
|
|
76
|
+
this._persistedReadConcurrency = _persistedReadConcurrency;
|
|
77
|
+
this._optionalPersistenceFiber = _ctx.inject(["sessionPersistence"], (childCtx) => {
|
|
78
|
+
const service = childCtx.sessionPersistence;
|
|
79
|
+
this._persistence = service;
|
|
80
|
+
childCtx.effect(() => () => {
|
|
81
|
+
/* v8 ignore next -- a stale optional-service disposer cannot clear a replacement */
|
|
82
|
+
if (this._persistence === service) this._persistence = void 0;
|
|
83
|
+
}, "sessionQuery.persistenceBinding");
|
|
84
|
+
});
|
|
85
|
+
_ctx.effect(() => {
|
|
86
|
+
return () => this._optionalPersistenceFiber.dispose();
|
|
87
|
+
}, "sessionQuery.optionalPersistence");
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* List the complete logical corpus with live precedence and cloned headers.
|
|
91
|
+
* @param signal - optional cancellation for persistence listing.
|
|
92
|
+
* @returns records in deterministic newest-first order.
|
|
93
|
+
*/
|
|
94
|
+
async listSessions(signal) {
|
|
95
|
+
signal?.throwIfAborted();
|
|
96
|
+
const persistence = this._persistence;
|
|
97
|
+
const persisted = persistence === void 0 ? [] : await listPersisted(persistence, signal);
|
|
98
|
+
signal?.throwIfAborted();
|
|
99
|
+
const records = /* @__PURE__ */ new Map();
|
|
100
|
+
for (const header of persisted) records.set(header.id, {
|
|
101
|
+
header: structuredClone(header),
|
|
102
|
+
live: false,
|
|
103
|
+
persisted: true
|
|
104
|
+
});
|
|
105
|
+
for (const session of this._ctx.sessions.list()) {
|
|
106
|
+
const durable = records.get(session.id);
|
|
107
|
+
if (durable !== void 0) assertSessionHeadersCompatible(session.header, durable.header);
|
|
108
|
+
records.set(session.id, {
|
|
109
|
+
header: structuredClone(session.header),
|
|
110
|
+
live: true,
|
|
111
|
+
persisted: durable !== void 0
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return [...records.values()].sort(compareSessions);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Load one logical source, preferring a detached live snapshot.
|
|
118
|
+
*
|
|
119
|
+
* A known live target never consults persistence, so an optional backend's
|
|
120
|
+
* failure cannot make current in-memory history unreadable.
|
|
121
|
+
* @param sessionId - session to resolve.
|
|
122
|
+
* @param signal - optional cancellation for persisted source resolution.
|
|
123
|
+
* @returns detached live-preferred header and events.
|
|
124
|
+
*/
|
|
125
|
+
async load(sessionId, signal) {
|
|
126
|
+
signal?.throwIfAborted();
|
|
127
|
+
const live = this._ctx.sessions.get(sessionId);
|
|
128
|
+
if (live !== void 0) {
|
|
129
|
+
const snapshot = snapshotLive(live);
|
|
130
|
+
signal?.throwIfAborted();
|
|
131
|
+
return snapshot;
|
|
132
|
+
}
|
|
133
|
+
const persistence = this._persistence;
|
|
134
|
+
if (persistence === void 0) throw notFound$1(sessionId);
|
|
135
|
+
const listed = (await listPersisted(persistence, signal)).find((header) => header.id === sessionId);
|
|
136
|
+
signal?.throwIfAborted();
|
|
137
|
+
if (listed === void 0) throw notFound$1(sessionId);
|
|
138
|
+
const loaded = await inspectPersisted(persistence, sessionId, signal);
|
|
139
|
+
signal?.throwIfAborted();
|
|
140
|
+
const attached = this._ctx.sessions.get(sessionId);
|
|
141
|
+
if (attached !== void 0) {
|
|
142
|
+
const snapshot = snapshotLive(attached);
|
|
143
|
+
signal?.throwIfAborted();
|
|
144
|
+
return snapshot;
|
|
145
|
+
}
|
|
146
|
+
assertSessionHeadersCompatible(loaded.header, listed);
|
|
147
|
+
const snapshot = {
|
|
148
|
+
header: structuredClone(loaded.header),
|
|
149
|
+
inheritedEventCount: loaded.inheritedEventCount,
|
|
150
|
+
events: loaded.events.map((event) => structuredClone(event))
|
|
151
|
+
};
|
|
152
|
+
signal?.throwIfAborted();
|
|
153
|
+
return snapshot;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Project unique logical sources immediately from one persistence listing.
|
|
157
|
+
*
|
|
158
|
+
* The synchronous projector runs before a persisted worker claims its next id.
|
|
159
|
+
* Full logs are borrowed only for that call and never retained by the batch.
|
|
160
|
+
* @param sessionIds - sessions to resolve in first-occurrence order.
|
|
161
|
+
* @param project - synchronous fold that owns/clones every retained value.
|
|
162
|
+
* @param signal - cancellation shared by listing and every persisted inspection.
|
|
163
|
+
* @returns one fulfilled or rejected projected result per unique requested id.
|
|
164
|
+
*/
|
|
165
|
+
async projectMany(sessionIds, project, signal) {
|
|
166
|
+
const ids = [...new Set(sessionIds)];
|
|
167
|
+
signal?.throwIfAborted();
|
|
168
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
169
|
+
const unresolved = [];
|
|
170
|
+
for (const id of ids) {
|
|
171
|
+
const session = this._ctx.sessions.get(id);
|
|
172
|
+
if (session === void 0) unresolved.push(id);
|
|
173
|
+
else resolved.set(id, projectSource(id, sourceLive(session), project, signal));
|
|
174
|
+
}
|
|
175
|
+
if (unresolved.length === 0) return orderedResults(ids, resolved);
|
|
176
|
+
const persistence = this._persistence;
|
|
177
|
+
if (persistence === void 0) {
|
|
178
|
+
for (const sessionId of unresolved) resolved.set(sessionId, {
|
|
179
|
+
sessionId,
|
|
180
|
+
status: "rejected",
|
|
181
|
+
reason: notFound$1(sessionId)
|
|
182
|
+
});
|
|
183
|
+
return orderedResults(ids, resolved);
|
|
184
|
+
}
|
|
185
|
+
let persisted;
|
|
186
|
+
try {
|
|
187
|
+
persisted = await listPersisted(persistence, signal);
|
|
188
|
+
signal?.throwIfAborted();
|
|
189
|
+
} catch (error) {
|
|
190
|
+
if (signal?.aborted) signal.throwIfAborted();
|
|
191
|
+
for (const sessionId of unresolved) resolved.set(sessionId, {
|
|
192
|
+
sessionId,
|
|
193
|
+
status: "rejected",
|
|
194
|
+
reason: error
|
|
195
|
+
});
|
|
196
|
+
return orderedResults(ids, resolved);
|
|
197
|
+
}
|
|
198
|
+
const persistedById = new Map(persisted.map((header) => [header.id, header]));
|
|
199
|
+
const resolvePersisted = async (sessionId) => {
|
|
200
|
+
const listed = persistedById.get(sessionId);
|
|
201
|
+
if (listed === void 0) {
|
|
202
|
+
const attached = this._ctx.sessions.get(sessionId);
|
|
203
|
+
resolved.set(sessionId, attached === void 0 ? {
|
|
204
|
+
sessionId,
|
|
205
|
+
status: "rejected",
|
|
206
|
+
reason: notFound$1(sessionId)
|
|
207
|
+
} : projectSource(sessionId, sourceLive(attached), project, signal));
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
try {
|
|
211
|
+
signal?.throwIfAborted();
|
|
212
|
+
const loaded = await inspectPersisted(persistence, sessionId, signal);
|
|
213
|
+
signal?.throwIfAborted();
|
|
214
|
+
const attached = this._ctx.sessions.get(sessionId);
|
|
215
|
+
if (attached !== void 0) {
|
|
216
|
+
resolved.set(sessionId, projectSource(sessionId, sourceLive(attached), project, signal));
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
assertSessionHeadersCompatible(loaded.header, listed);
|
|
220
|
+
resolved.set(sessionId, projectSource(sessionId, {
|
|
221
|
+
header: loaded.header,
|
|
222
|
+
events: loaded.events
|
|
223
|
+
}, project, signal));
|
|
224
|
+
} catch (error) {
|
|
225
|
+
if (signal?.aborted) signal.throwIfAborted();
|
|
226
|
+
resolved.set(sessionId, {
|
|
227
|
+
sessionId,
|
|
228
|
+
status: "rejected",
|
|
229
|
+
reason: error
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
let cursor = 0;
|
|
234
|
+
const worker = async () => {
|
|
235
|
+
for (;;) {
|
|
236
|
+
signal?.throwIfAborted();
|
|
237
|
+
const index = cursor;
|
|
238
|
+
if (index >= unresolved.length) return;
|
|
239
|
+
cursor += 1;
|
|
240
|
+
await resolvePersisted(unresolved[index]);
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
const workerCount = Math.min(this._persistedReadConcurrency, unresolved.length);
|
|
244
|
+
const settlements = await Promise.allSettled(Array.from({ length: workerCount }, () => worker()));
|
|
245
|
+
if (signal?.aborted) signal.throwIfAborted();
|
|
246
|
+
/* v8 ignore start -- per-id failures settle inside resolvePersisted; workers reject only on abort above */
|
|
247
|
+
for (const settlement of settlements) if (settlement.status === "rejected") throw settlement.reason;
|
|
248
|
+
/* v8 ignore stop */
|
|
249
|
+
signal?.throwIfAborted();
|
|
250
|
+
return orderedResults(ids, resolved);
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
function projectSource(sessionId, source, project, signal) {
|
|
254
|
+
try {
|
|
255
|
+
signal?.throwIfAborted();
|
|
256
|
+
const value = project(source);
|
|
257
|
+
signal?.throwIfAborted();
|
|
258
|
+
return {
|
|
259
|
+
sessionId,
|
|
260
|
+
status: "fulfilled",
|
|
261
|
+
value
|
|
262
|
+
};
|
|
263
|
+
} catch (reason) {
|
|
264
|
+
/* v8 ignore next -- the synchronous projector has no external cancellation yield */
|
|
265
|
+
if (signal?.aborted) signal.throwIfAborted();
|
|
266
|
+
return {
|
|
267
|
+
sessionId,
|
|
268
|
+
status: "rejected",
|
|
269
|
+
reason
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
function sourceLive(session) {
|
|
274
|
+
return {
|
|
275
|
+
header: session.header,
|
|
276
|
+
events: session.snapshotEvents()
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
function orderedResults(ids, resolved) {
|
|
280
|
+
return ids.map((sessionId) => resolved.get(sessionId));
|
|
281
|
+
}
|
|
282
|
+
async function listPersisted(persistence, signal) {
|
|
283
|
+
try {
|
|
284
|
+
return (await persistence.list(signal === void 0 ? void 0 : { signal })).map((snapshot) => snapshot.header);
|
|
285
|
+
} catch (error) {
|
|
286
|
+
if (signal?.aborted) signal.throwIfAborted();
|
|
287
|
+
throw new SessionQueryError(`session persistence listing failed: ${errorMessage$1(error)}`, "SESSION_QUERY_PERSISTENCE_FAILED", { cause: error });
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
async function inspectPersisted(persistence, sessionId, signal) {
|
|
291
|
+
try {
|
|
292
|
+
return await readColdSessionLog(persistence, sessionId, signal);
|
|
293
|
+
} catch (error) {
|
|
294
|
+
if (signal?.aborted) signal.throwIfAborted();
|
|
295
|
+
if (error instanceof Error && error.name === "SessionPersistenceCorruptionError") throw new SessionQueryError(`stored session "${sessionId}" is corrupt: ${errorMessage$1(error)}`, "SESSION_QUERY_CORRUPT_SESSION", { cause: error });
|
|
296
|
+
throw new SessionQueryError(`failed to read stored session "${sessionId}": ${errorMessage$1(error)}`, "SESSION_QUERY_PERSISTENCE_FAILED", { cause: error });
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
function snapshotLive(session) {
|
|
300
|
+
return {
|
|
301
|
+
header: structuredClone(session.header),
|
|
302
|
+
inheritedEventCount: session.inheritedEventCount,
|
|
303
|
+
events: session.snapshotEvents().map((event) => structuredClone(event))
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
function compareSessions(a, b) {
|
|
307
|
+
return b.header.createdAt - a.header.createdAt || a.header.id.localeCompare(b.header.id);
|
|
308
|
+
}
|
|
309
|
+
function notFound$1(sessionId) {
|
|
310
|
+
return new SessionQueryError(`session "${sessionId}" not found`, "SESSION_QUERY_SESSION_NOT_FOUND");
|
|
311
|
+
}
|
|
312
|
+
function errorMessage$1(error) {
|
|
313
|
+
return error instanceof Error ? error.message : "unknown error";
|
|
314
|
+
}
|
|
315
|
+
//#endregion
|
|
316
|
+
//#region lib/types/observation.js
|
|
317
|
+
/** Shared live/prepared observations for Session page and lifecycle consumers. */
|
|
318
|
+
/**
|
|
319
|
+
* Builds point observations without a corpus listing preflight.
|
|
320
|
+
*
|
|
321
|
+
* Cold reads are cached per session id, keyed by the persistence instance and
|
|
322
|
+
* the `stat` revision observed before the log read: an unchanged revision
|
|
323
|
+
* reuses the restored Session without re-reading the log. The cache is bounded
|
|
324
|
+
* (least-recently-used unpinned entries are evicted past the capacity), and
|
|
325
|
+
* entries pinned by active leases survive eviction and replacement — a lease's
|
|
326
|
+
* cut stays valid for the lease lifetime even after a newer revision lands.
|
|
327
|
+
*/
|
|
328
|
+
var SessionObservationReader = class {
|
|
329
|
+
ctx;
|
|
330
|
+
cacheCapacity;
|
|
331
|
+
cache = /* @__PURE__ */ new Map();
|
|
332
|
+
/**
|
|
333
|
+
* @param ctx - context carrying Session and optional persistence/projection services.
|
|
334
|
+
* @param cacheCapacity - maximum unpinned cold observations retained for reuse.
|
|
335
|
+
*/
|
|
336
|
+
constructor(ctx, cacheCapacity = 5) {
|
|
337
|
+
this.ctx = ctx;
|
|
338
|
+
this.cacheCapacity = cacheCapacity;
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Observe one live-preferred Session and retain a cold preparation until disposal.
|
|
342
|
+
* @param sessionId - logical Session identity.
|
|
343
|
+
* @param options - cancellation and all-or-none projection computation for this read.
|
|
344
|
+
* @returns one exact immutable observation.
|
|
345
|
+
*/
|
|
346
|
+
async read(sessionId, options = {}) {
|
|
347
|
+
const { signal, projectionMode = "all" } = options;
|
|
348
|
+
for (;;) {
|
|
349
|
+
throwIfObservationAborted(signal);
|
|
350
|
+
const live = this.ctx.sessions.get(sessionId);
|
|
351
|
+
if (live !== void 0) return this.live(live, projectionMode);
|
|
352
|
+
const persistence = this.ctx.get("sessionPersistence");
|
|
353
|
+
if (persistence === void 0) throw notFound(sessionId);
|
|
354
|
+
const snapshot = await this.statSource(persistence, sessionId, signal);
|
|
355
|
+
const attachedDuringStat = this.ctx.sessions.get(sessionId);
|
|
356
|
+
if (attachedDuringStat !== void 0) return this.live(attachedDuringStat, projectionMode);
|
|
357
|
+
let entry = this.cachedEntry(persistence, sessionId, snapshot.revision);
|
|
358
|
+
if (entry === void 0) {
|
|
359
|
+
const loaded = await this.loadSource(persistence, sessionId, signal);
|
|
360
|
+
throwIfObservationAborted(signal);
|
|
361
|
+
const attached = this.ctx.sessions.get(sessionId);
|
|
362
|
+
if (attached !== void 0) return this.live(attached, projectionMode);
|
|
363
|
+
const seed = loaded.events;
|
|
364
|
+
let session;
|
|
365
|
+
try {
|
|
366
|
+
session = this.ctx.sessions.prepare(sessionId, {
|
|
367
|
+
seed,
|
|
368
|
+
meta: structuredClone(loaded.header),
|
|
369
|
+
inheritedEventCount: loaded.inheritedEventCount,
|
|
370
|
+
eventState: loaded.eventState
|
|
371
|
+
});
|
|
372
|
+
} catch (error) {
|
|
373
|
+
if (this.ctx.sessions.get(sessionId) !== void 0) continue;
|
|
374
|
+
throw new SessionQueryError(`stored session "${sessionId}" is corrupt: ${errorMessage(error)}`, "SESSION_QUERY_CORRUPT_SESSION", { cause: error });
|
|
375
|
+
}
|
|
376
|
+
entry = {
|
|
377
|
+
persistence,
|
|
378
|
+
revision: snapshot.revision,
|
|
379
|
+
session,
|
|
380
|
+
events: Object.freeze(seed),
|
|
381
|
+
refs: 0
|
|
382
|
+
};
|
|
383
|
+
this.store(sessionId, entry);
|
|
384
|
+
}
|
|
385
|
+
let projections;
|
|
386
|
+
try {
|
|
387
|
+
projections = projectionMode === "none" ? void 0 : this.preparedProjections(entry);
|
|
388
|
+
} catch (error) {
|
|
389
|
+
throw new SessionQueryError(`failed to project session "${sessionId}": ${errorMessage(error)}`, "SESSION_QUERY_CORRUPT_SESSION", { cause: error });
|
|
390
|
+
}
|
|
391
|
+
return this.preparedLease(sessionId, entry, projections);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
/** Observe the stored snapshot, mapping absence and backend failures to the query taxonomy. */
|
|
395
|
+
async statSource(persistence, sessionId, signal) {
|
|
396
|
+
let snapshot;
|
|
397
|
+
try {
|
|
398
|
+
snapshot = await persistence.stat(sessionId, signal === void 0 ? void 0 : { signal });
|
|
399
|
+
} catch (error) {
|
|
400
|
+
throwIfObservationAborted(signal);
|
|
401
|
+
throw mapPersistenceFailure(sessionId, error);
|
|
402
|
+
}
|
|
403
|
+
throwIfObservationAborted(signal);
|
|
404
|
+
if (snapshot === void 0) throw notFound(sessionId);
|
|
405
|
+
if (snapshot.header.id !== sessionId) throw new SessionQueryError(`session persistence returned "${snapshot.header.id}" for "${sessionId}"`, "SESSION_QUERY_SOURCE_CONFLICT");
|
|
406
|
+
return snapshot;
|
|
407
|
+
}
|
|
408
|
+
/** Read the complete balanced cold log, mapping backend failures to the query taxonomy. */
|
|
409
|
+
async loadSource(persistence, sessionId, signal) {
|
|
410
|
+
try {
|
|
411
|
+
return await readColdSessionLog(persistence, sessionId, signal);
|
|
412
|
+
} catch (error) {
|
|
413
|
+
throwIfObservationAborted(signal);
|
|
414
|
+
throw mapPersistenceFailure(sessionId, error);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
/** Return a still-valid cached entry and mark it most recently used. */
|
|
418
|
+
cachedEntry(persistence, sessionId, revision) {
|
|
419
|
+
const cached = this.cache.get(sessionId);
|
|
420
|
+
if (cached === void 0 || cached.persistence !== persistence || cached.revision !== revision) return;
|
|
421
|
+
this.cache.delete(sessionId);
|
|
422
|
+
this.cache.set(sessionId, cached);
|
|
423
|
+
return cached;
|
|
424
|
+
}
|
|
425
|
+
/** Insert or replace the entry for one id, then evict past the capacity. */
|
|
426
|
+
store(sessionId, entry) {
|
|
427
|
+
this.cache.delete(sessionId);
|
|
428
|
+
this.cache.set(sessionId, entry);
|
|
429
|
+
this.evictPastCapacity(entry);
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Evict oldest unpinned entries until the cache fits its capacity again.
|
|
433
|
+
* Runs on store and whenever a lease release unpins an entry, so leases
|
|
434
|
+
* that pinned every candidate cannot leave the cache over budget for good.
|
|
435
|
+
* @param keep - the entry being stored, about to be leased; never evicted.
|
|
436
|
+
*/
|
|
437
|
+
evictPastCapacity(keep) {
|
|
438
|
+
if (this.cache.size <= this.cacheCapacity) return;
|
|
439
|
+
for (const [id, candidate] of this.cache) {
|
|
440
|
+
if (candidate === keep || candidate.refs > 0) continue;
|
|
441
|
+
this.cache.delete(id);
|
|
442
|
+
if (this.cache.size <= this.cacheCapacity) return;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
/** Build one disposable lease over a cached entry, pinning it until every lease releases. */
|
|
446
|
+
preparedLease(sessionId, entry, projections) {
|
|
447
|
+
entry.refs += 1;
|
|
448
|
+
const lease = () => {
|
|
449
|
+
let disposed = false;
|
|
450
|
+
return {
|
|
451
|
+
source: "prepared",
|
|
452
|
+
header: entry.session.header,
|
|
453
|
+
inheritedEventCount: entry.session.inheritedEventCount,
|
|
454
|
+
events: entry.events,
|
|
455
|
+
cursor: entry.events.at(-1)?.seq ?? -1,
|
|
456
|
+
revision: entry.revision,
|
|
457
|
+
...projections === void 0 ? {} : { projections },
|
|
458
|
+
retain: () => {
|
|
459
|
+
if (disposed) throw new Error(`session observation "${sessionId}" is disposed`);
|
|
460
|
+
entry.refs += 1;
|
|
461
|
+
return lease();
|
|
462
|
+
},
|
|
463
|
+
[Symbol.dispose]: () => {
|
|
464
|
+
if (disposed) return;
|
|
465
|
+
disposed = true;
|
|
466
|
+
entry.refs -= 1;
|
|
467
|
+
if (entry.refs === 0) this.evictPastCapacity();
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
};
|
|
471
|
+
return lease();
|
|
472
|
+
}
|
|
473
|
+
live(session, projectionMode) {
|
|
474
|
+
const seq = session.seq;
|
|
475
|
+
let materialized;
|
|
476
|
+
const projections = projectionMode === "none" ? void 0 : this.ctx.get("sessionProjections")?.snapshot(session);
|
|
477
|
+
const lease = () => {
|
|
478
|
+
let disposed = false;
|
|
479
|
+
return {
|
|
480
|
+
source: "live",
|
|
481
|
+
header: session.header,
|
|
482
|
+
inheritedEventCount: session.inheritedEventCount,
|
|
483
|
+
get events() {
|
|
484
|
+
materialized ??= session.snapshotEvents(SessionLogOffset(0), seq);
|
|
485
|
+
return materialized;
|
|
486
|
+
},
|
|
487
|
+
cursor: seq === 0 ? -1 : SessionSeq(seq - 1),
|
|
488
|
+
...projections === void 0 ? {} : { projections },
|
|
489
|
+
retain: () => {
|
|
490
|
+
if (disposed) throw new Error(`session observation "${session.id}" is disposed`);
|
|
491
|
+
return lease();
|
|
492
|
+
},
|
|
493
|
+
[Symbol.dispose]: () => {
|
|
494
|
+
disposed = true;
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
};
|
|
498
|
+
return lease();
|
|
499
|
+
}
|
|
500
|
+
preparedProjections(entry) {
|
|
501
|
+
const registry = this.ctx.get("sessionProjections");
|
|
502
|
+
if (registry === void 0) return void 0;
|
|
503
|
+
const cache = this.ctx.get("sessionProjectionCache");
|
|
504
|
+
return cache === void 0 ? registry.hydrate(entry.session, {}, entry.events, SessionLogOffset(0)) : cache.hydratePrepared(entry.session, entry.events);
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
function throwIfObservationAborted(signal) {
|
|
508
|
+
if (signal?.aborted !== true) return;
|
|
509
|
+
throw new SessionQueryError("session observation was aborted", "SESSION_QUERY_ABORTED", { cause: signal.reason });
|
|
510
|
+
}
|
|
511
|
+
function mapPersistenceFailure(sessionId, error) {
|
|
512
|
+
if (hasErrorName(error, "SessionPersistenceNotFoundError")) return notFound(sessionId, error);
|
|
513
|
+
if (hasErrorName(error, "SessionPersistenceCorruptionError")) return new SessionQueryError(`stored session "${sessionId}" is corrupt: ${error.message}`, "SESSION_QUERY_CORRUPT_SESSION", { cause: error });
|
|
514
|
+
return new SessionQueryError(`failed to observe session "${sessionId}": ${errorMessage(error)}`, "SESSION_QUERY_PERSISTENCE_FAILED", { cause: error });
|
|
515
|
+
}
|
|
516
|
+
function notFound(sessionId, cause) {
|
|
517
|
+
return new SessionQueryError(`session "${sessionId}" not found`, "SESSION_QUERY_SESSION_NOT_FOUND", cause === void 0 ? void 0 : { cause });
|
|
518
|
+
}
|
|
519
|
+
function errorMessage(error) {
|
|
520
|
+
return error instanceof Error ? error.message : "unknown error";
|
|
521
|
+
}
|
|
522
|
+
function hasErrorName(error, name) {
|
|
523
|
+
return error instanceof Error && error.name === name;
|
|
524
|
+
}
|
|
525
|
+
//#endregion
|
|
526
|
+
//#region lib/types/extraction.js
|
|
527
|
+
/** First-party semantic text extraction for session-query consumers. */
|
|
528
|
+
/**
|
|
529
|
+
* Extract searchable semantic text from one first-party session event.
|
|
530
|
+
*
|
|
531
|
+
* Structural boundaries, embedded raw streams, request envelopes, and unknown
|
|
532
|
+
* declaration-merged events contribute no text.
|
|
533
|
+
* @param event - event to inspect.
|
|
534
|
+
* @returns newline-joined semantic text, or an empty string when non-searchable.
|
|
535
|
+
*/
|
|
536
|
+
function extractSessionEventText(event) {
|
|
537
|
+
switch (event.type) {
|
|
538
|
+
case "user/message": return contentText(event.data.content);
|
|
539
|
+
case "assistant/message": return contentText(event.data.message.content);
|
|
540
|
+
case "tool/call": return joinText([event.data.name, event.data.arguments]);
|
|
541
|
+
case "tool/result": return joinText([
|
|
542
|
+
contentText(event.data.message.content),
|
|
543
|
+
event.data.error?.name ?? "",
|
|
544
|
+
event.data.error?.code ?? ""
|
|
545
|
+
]);
|
|
546
|
+
case "todo/write": return joinText(event.data.todos.flatMap((todo) => [todo.status, todo.content]));
|
|
547
|
+
case "turn/end": return turnEndText(event.data.reason);
|
|
548
|
+
case "turn/start":
|
|
549
|
+
case "step/start":
|
|
550
|
+
case "step/end":
|
|
551
|
+
case "assistant/attempt":
|
|
552
|
+
case "request/header": return "";
|
|
553
|
+
default: return "";
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
function turnEndText(reason) {
|
|
557
|
+
switch (reason.kind) {
|
|
558
|
+
case "error": return joinText(["error", reason.error.message]);
|
|
559
|
+
case "aborted": return "aborted";
|
|
560
|
+
case "max-tokens":
|
|
561
|
+
case "interrupted": return reason.kind;
|
|
562
|
+
case "completed": return "";
|
|
563
|
+
default: return "";
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
function contentText(content) {
|
|
567
|
+
return joinText(content.flatMap(blockText));
|
|
568
|
+
}
|
|
569
|
+
function blockText(block) {
|
|
570
|
+
switch (block.type) {
|
|
571
|
+
case "text": return [block.text];
|
|
572
|
+
case "reasoning": return [];
|
|
573
|
+
case "tool-call": return [block.name, block.arguments];
|
|
574
|
+
case "tool-result": return block.content.flatMap(blockText);
|
|
575
|
+
default: return [];
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
function joinText(parts) {
|
|
579
|
+
return parts.map((part) => part.trim()).filter(Boolean).join("\n");
|
|
580
|
+
}
|
|
581
|
+
//#endregion
|
|
582
|
+
//#region lib/types/documents.js
|
|
583
|
+
/** Shared event metadata and semantic-document projection. */
|
|
584
|
+
/**
|
|
585
|
+
* Project a raw log into lightweight surface-aware event records.
|
|
586
|
+
* @param sessionId - session that owns the log.
|
|
587
|
+
* @param events - complete contiguous raw event log.
|
|
588
|
+
* @returns one record per event in ascending seq order.
|
|
589
|
+
*/
|
|
590
|
+
function buildSessionEventRecords(sessionId, events) {
|
|
591
|
+
const surfaceBySeq = classifySurface(events);
|
|
592
|
+
return events.map((event) => ({
|
|
593
|
+
sessionId,
|
|
594
|
+
seq: event.seq,
|
|
595
|
+
type: event.type,
|
|
596
|
+
time: event.time,
|
|
597
|
+
surface: surfaceBySeq.get(event.seq) ?? "log-only"
|
|
598
|
+
}));
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Build first-party semantic documents for one complete raw event log.
|
|
602
|
+
* @param sessionId - session that owns the log.
|
|
603
|
+
* @param events - complete contiguous raw event log.
|
|
604
|
+
* @returns searchable documents in ascending seq order; structural events are omitted.
|
|
605
|
+
*/
|
|
606
|
+
function buildSessionEventSearchDocuments(sessionId, events) {
|
|
607
|
+
const surfaceBySeq = classifySurface(events);
|
|
608
|
+
const documents = [];
|
|
609
|
+
for (const event of events) {
|
|
610
|
+
const text = extractSessionEventText(event);
|
|
611
|
+
if (text.length === 0) continue;
|
|
612
|
+
documents.push({
|
|
613
|
+
sessionId,
|
|
614
|
+
seq: event.seq,
|
|
615
|
+
type: event.type,
|
|
616
|
+
time: event.time,
|
|
617
|
+
surface: surfaceBySeq.get(event.seq) ?? "log-only",
|
|
618
|
+
text
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
return documents;
|
|
622
|
+
}
|
|
623
|
+
function classifySurface(events) {
|
|
624
|
+
let folded;
|
|
625
|
+
try {
|
|
626
|
+
folded = foldSurface(events);
|
|
627
|
+
} catch (error) {
|
|
628
|
+
throw new SessionQueryError(
|
|
629
|
+
/* v8 ignore next -- foldSurface throws Error instances */
|
|
630
|
+
`invalid session surface: ${error instanceof Error ? error.message : "unknown error"}`,
|
|
631
|
+
"SESSION_QUERY_INVALID_SURFACE",
|
|
632
|
+
{ cause: error }
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
const result = /* @__PURE__ */ new Map();
|
|
636
|
+
for (const seq of folded.nodes) result.set(seq, "current");
|
|
637
|
+
for (const replacement of folded.replacements) for (const seq of replacement.shadowedSeqs) result.set(seq, "shadowed");
|
|
638
|
+
return result;
|
|
639
|
+
}
|
|
640
|
+
//#endregion
|
|
641
|
+
//#region lib/types/filters.js
|
|
642
|
+
/** Pure provider-independent predicates for logical sessions and event text. */
|
|
643
|
+
/**
|
|
644
|
+
* Apply ANDed logical-session filters while preserving input order.
|
|
645
|
+
* @param records - detached logical-session records to inspect.
|
|
646
|
+
* @param filters - clauses whose list values are ORed within each clause.
|
|
647
|
+
* @returns records accepted by every clause.
|
|
648
|
+
*/
|
|
649
|
+
function filterSessionResults(records, filters = []) {
|
|
650
|
+
const predicates = filters.map(sessionPredicate);
|
|
651
|
+
return records.filter((record) => predicates.every((predicate) => predicate(record)));
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Apply ANDed event filters to extracted semantic documents.
|
|
655
|
+
* @param documents - semantic documents produced by {@link buildSessionEventSearchDocuments}.
|
|
656
|
+
* @param filters - metadata and literal-text predicates.
|
|
657
|
+
* @returns documents accepted by every clause, in input order.
|
|
658
|
+
*/
|
|
659
|
+
function filterSessionEventDocuments(documents, filters = []) {
|
|
660
|
+
const predicates = filters.map(eventPredicate);
|
|
661
|
+
return documents.filter((document) => predicates.every((predicate) => predicate(document)));
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Copy and validate logical-session filters before an asynchronous boundary.
|
|
665
|
+
* @param filters - caller-owned clauses to materialize.
|
|
666
|
+
* @returns detached validated clauses.
|
|
667
|
+
*/
|
|
668
|
+
function materializeSessionResultFilters(filters) {
|
|
669
|
+
assertArray(filters);
|
|
670
|
+
return filters.map((filter) => {
|
|
671
|
+
switch (filter.kind) {
|
|
672
|
+
case "id": return {
|
|
673
|
+
kind: filter.kind,
|
|
674
|
+
values: copyStrings(filter.kind, filter.values)
|
|
675
|
+
};
|
|
676
|
+
case "cwd": return {
|
|
677
|
+
kind: filter.kind,
|
|
678
|
+
values: copyNullableStrings(filter.kind, filter.values)
|
|
679
|
+
};
|
|
680
|
+
case "created-at": return copyRange(filter.kind, filter);
|
|
681
|
+
case "parent": return {
|
|
682
|
+
kind: filter.kind,
|
|
683
|
+
values: copyNullableStrings(filter.kind, filter.values)
|
|
684
|
+
};
|
|
685
|
+
case "availability": {
|
|
686
|
+
const values = copyStrings(filter.kind, filter.values);
|
|
687
|
+
assertAllowedValues(filter.kind, values, ["live", "persisted"]);
|
|
688
|
+
return {
|
|
689
|
+
kind: filter.kind,
|
|
690
|
+
values
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
default: return unknownFilter(filter);
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* Copy and validate event filters before an asynchronous boundary.
|
|
699
|
+
* @param filters - caller-owned clauses to materialize.
|
|
700
|
+
* @returns detached validated clauses.
|
|
701
|
+
*/
|
|
702
|
+
function materializeSessionEventResultFilters(filters) {
|
|
703
|
+
assertArray(filters);
|
|
704
|
+
return filters.map((filter) => {
|
|
705
|
+
switch (filter.kind) {
|
|
706
|
+
case "seq":
|
|
707
|
+
case "time": return copyRange(filter.kind, filter);
|
|
708
|
+
case "type": return {
|
|
709
|
+
kind: filter.kind,
|
|
710
|
+
values: copyStrings(filter.kind, filter.values)
|
|
711
|
+
};
|
|
712
|
+
case "surface": {
|
|
713
|
+
const values = copyStrings(filter.kind, filter.values);
|
|
714
|
+
assertAllowedValues(filter.kind, values, [
|
|
715
|
+
"current",
|
|
716
|
+
"shadowed",
|
|
717
|
+
"log-only"
|
|
718
|
+
]);
|
|
719
|
+
return {
|
|
720
|
+
kind: filter.kind,
|
|
721
|
+
values
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
case "text":
|
|
725
|
+
if (typeof filter.text !== "string") throw invalidFilter("text filter text must be a string");
|
|
726
|
+
return {
|
|
727
|
+
kind: filter.kind,
|
|
728
|
+
text: filter.text
|
|
729
|
+
};
|
|
730
|
+
default: return unknownFilter(filter);
|
|
731
|
+
}
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* Compile a literal case-insensitive, whitespace-flexible semantic-text match.
|
|
736
|
+
* @param text - caller-provided literal text.
|
|
737
|
+
* @returns Unicode-aware regular expression safe from regex injection.
|
|
738
|
+
*/
|
|
739
|
+
function compileSessionTextFilter(text) {
|
|
740
|
+
const trimmed = text.trim();
|
|
741
|
+
if (trimmed.length === 0) throw new SessionQueryError("session text filter must contain non-whitespace text", "SESSION_QUERY_INVALID_FILTER");
|
|
742
|
+
const pattern = trimmed.split(/\s+/u).map((part) => part.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&")).join("\\s+");
|
|
743
|
+
return new RegExp(pattern, "iu");
|
|
744
|
+
}
|
|
745
|
+
function sessionPredicate(filter) {
|
|
746
|
+
switch (filter.kind) {
|
|
747
|
+
case "id": return (record) => filter.values.includes(record.header.id);
|
|
748
|
+
case "cwd": return (record) => filter.values.includes(record.header.cwd ?? null);
|
|
749
|
+
case "created-at": {
|
|
750
|
+
const range = validateRange(filter.kind, filter);
|
|
751
|
+
return (record) => matchesRange(record.header.createdAt, range);
|
|
752
|
+
}
|
|
753
|
+
case "parent": return (record) => filter.values.includes(record.header.parentSession ?? null);
|
|
754
|
+
case "availability":
|
|
755
|
+
assertAllowedValues(filter.kind, filter.values, ["live", "persisted"]);
|
|
756
|
+
return (record) => filter.values.some((value) => value === "live" ? record.live : record.persisted);
|
|
757
|
+
default: return unknownFilter(filter);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
function eventPredicate(filter) {
|
|
761
|
+
switch (filter.kind) {
|
|
762
|
+
case "seq": {
|
|
763
|
+
const range = validateRange(filter.kind, filter);
|
|
764
|
+
return (document) => matchesRange(document.seq, range);
|
|
765
|
+
}
|
|
766
|
+
case "time": {
|
|
767
|
+
const range = validateRange(filter.kind, filter);
|
|
768
|
+
return (document) => matchesRange(document.time, range);
|
|
769
|
+
}
|
|
770
|
+
case "type": return (document) => filter.values.includes(document.type);
|
|
771
|
+
case "surface":
|
|
772
|
+
assertAllowedValues(filter.kind, filter.values, [
|
|
773
|
+
"current",
|
|
774
|
+
"shadowed",
|
|
775
|
+
"log-only"
|
|
776
|
+
]);
|
|
777
|
+
return (document) => filter.values.includes(document.surface);
|
|
778
|
+
case "text": {
|
|
779
|
+
const pattern = compileSessionTextFilter(filter.text);
|
|
780
|
+
return (document) => pattern.test(document.text);
|
|
781
|
+
}
|
|
782
|
+
default: return unknownFilter(filter);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
function copyStrings(name, values) {
|
|
786
|
+
if (!isRuntimeArray(values) || values.some((value) => typeof value !== "string")) throw invalidFilter(`${name} filter values must be an array of strings`);
|
|
787
|
+
return [...values];
|
|
788
|
+
}
|
|
789
|
+
function assertArray(value) {
|
|
790
|
+
if (!Array.isArray(value)) throw invalidFilter("filters must be an array");
|
|
791
|
+
}
|
|
792
|
+
function copyNullableStrings(name, values) {
|
|
793
|
+
if (!isRuntimeArray(values) || values.some((value) => value !== null && typeof value !== "string")) throw invalidFilter(`${name} filter values must be an array of strings or null`);
|
|
794
|
+
return [...values];
|
|
795
|
+
}
|
|
796
|
+
function copyRange(kind, range) {
|
|
797
|
+
const copy = {
|
|
798
|
+
kind,
|
|
799
|
+
...range.from === void 0 ? {} : { from: range.from },
|
|
800
|
+
...range.to === void 0 ? {} : { to: range.to }
|
|
801
|
+
};
|
|
802
|
+
validateRange(kind, copy);
|
|
803
|
+
return copy;
|
|
804
|
+
}
|
|
805
|
+
function unknownFilter(filter) {
|
|
806
|
+
const kind = filter.kind;
|
|
807
|
+
throw invalidFilter(`unknown filter kind ${typeof kind === "string" ? `"${kind}"` : "(missing)"}`);
|
|
808
|
+
}
|
|
809
|
+
function assertAllowedValues(name, values, allowed) {
|
|
810
|
+
for (const value of values) if (!allowed.includes(value)) throw new SessionQueryError(`session ${name} filter contains unknown value "${value}"`, "SESSION_QUERY_INVALID_FILTER");
|
|
811
|
+
}
|
|
812
|
+
function validateRange(name, range) {
|
|
813
|
+
if (range.from !== void 0 && !Number.isFinite(range.from)) throw invalidRange(name, "from must be finite");
|
|
814
|
+
if (range.to !== void 0 && !Number.isFinite(range.to)) throw invalidRange(name, "to must be finite");
|
|
815
|
+
if (range.from !== void 0 && range.to !== void 0 && range.from > range.to) throw invalidRange(name, "from must be less than or equal to to");
|
|
816
|
+
return range;
|
|
817
|
+
}
|
|
818
|
+
function matchesRange(value, range) {
|
|
819
|
+
return (range.from === void 0 || value >= range.from) && (range.to === void 0 || value <= range.to);
|
|
820
|
+
}
|
|
821
|
+
function invalidRange(name, detail) {
|
|
822
|
+
return invalidFilter(`${name} filter ${detail}`);
|
|
823
|
+
}
|
|
824
|
+
function invalidFilter(detail) {
|
|
825
|
+
return new SessionQueryError(`session ${detail}`, "SESSION_QUERY_INVALID_FILTER");
|
|
826
|
+
}
|
|
827
|
+
function isRuntimeArray(value) {
|
|
828
|
+
return Array.isArray(value);
|
|
829
|
+
}
|
|
830
|
+
//#endregion
|
|
831
|
+
//#region lib/types/tracing.js
|
|
832
|
+
/** One-shot session-lineage and event-relationship tracing helpers. */
|
|
833
|
+
/**
|
|
834
|
+
* Classify a raw event log with one canonical surface fold.
|
|
835
|
+
* @param sessionId - owner of the event log.
|
|
836
|
+
* @param events - detached raw event log.
|
|
837
|
+
* @returns lightweight records in ascending log order.
|
|
838
|
+
*/
|
|
839
|
+
function eventRecords(sessionId, events) {
|
|
840
|
+
return analyzeEventLog(sessionId, events).records;
|
|
841
|
+
}
|
|
842
|
+
/**
|
|
843
|
+
* Fold and return the current model surface after validating the whole log.
|
|
844
|
+
* @param sessionId - owner used in query diagnostics.
|
|
845
|
+
* @param events - detached raw event log from one corpus observation.
|
|
846
|
+
* @returns detached current surface events in folded order.
|
|
847
|
+
*/
|
|
848
|
+
function currentSurfaceEvents(sessionId, events) {
|
|
849
|
+
return analyzeEventLog(sessionId, events).currentSeqs.map((seq) => {
|
|
850
|
+
const event = events[seq];
|
|
851
|
+
/* v8 ignore next 6 -- analyzeEventLog validated contiguous seqs and foldSurface returned only surface-event seqs. */
|
|
852
|
+
if (event === void 0 || event.seq !== seq || !isSurfaceEvent(event)) throw new SessionQueryError(`invalid session surface: current node ${seq} is not a surface event`, "SESSION_QUERY_INVALID_SURFACE");
|
|
853
|
+
return snapshotSessionEvent(event);
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* Trace one target after one canonical surface fold and whole-log validation.
|
|
858
|
+
* @param sessionId - owner of the event log.
|
|
859
|
+
* @param events - detached raw event log.
|
|
860
|
+
* @param seq - target event seq.
|
|
861
|
+
* @returns direct surface replacements and relationships to cited source events.
|
|
862
|
+
*/
|
|
863
|
+
function traceEvent(sessionId, events, seq) {
|
|
864
|
+
const target = events[seq];
|
|
865
|
+
if (target === void 0 || target.seq !== seq) throw new SessionQueryError(`session "${sessionId}" has no event at seq ${seq}`, "SESSION_QUERY_EVENT_NOT_FOUND");
|
|
866
|
+
const analysis = analyzeEventLog(sessionId, events);
|
|
867
|
+
const replacementChain = [];
|
|
868
|
+
let replacement = analysis.replacedBy.get(seq);
|
|
869
|
+
while (replacement !== void 0) {
|
|
870
|
+
replacementChain.push(replacement);
|
|
871
|
+
replacement = analysis.replacedBy.get(replacement);
|
|
872
|
+
}
|
|
873
|
+
const derivedEventSeqs = [];
|
|
874
|
+
for (const event of events) {
|
|
875
|
+
if (event.seq <= seq) continue;
|
|
876
|
+
if (eventSources(event).includes(seq)) derivedEventSeqs.push(event.seq);
|
|
877
|
+
}
|
|
878
|
+
const targetRecord = analysis.records[seq];
|
|
879
|
+
const replacedBy = analysis.replacedBy.get(seq);
|
|
880
|
+
return {
|
|
881
|
+
target: targetRecord,
|
|
882
|
+
...replacedBy === void 0 ? {} : { replacedBy },
|
|
883
|
+
replacementChain,
|
|
884
|
+
replacedEventSeqs: analysis.replacedEventSeqs.get(seq) ?? [],
|
|
885
|
+
sourceEventSeqs: [...eventSources(target)],
|
|
886
|
+
derivedEventSeqs
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* Trace one target's known ancestry and recursively known descendants.
|
|
891
|
+
* @param records - complete logical corpus from one observation.
|
|
892
|
+
* @param sessionId - target session id.
|
|
893
|
+
* @returns complete or explicitly partial lineage.
|
|
894
|
+
*/
|
|
895
|
+
function traceSession(records, sessionId) {
|
|
896
|
+
const byId = new Map(records.map((record) => [record.header.id, record]));
|
|
897
|
+
const target = byId.get(sessionId);
|
|
898
|
+
if (target === void 0) throw new SessionQueryError(`session "${sessionId}" not found`, "SESSION_QUERY_SESSION_NOT_FOUND");
|
|
899
|
+
const ancestors = [];
|
|
900
|
+
const ancestrySeen = new Set([sessionId]);
|
|
901
|
+
let unresolvedParentId;
|
|
902
|
+
let parentId = target.header.parentSession;
|
|
903
|
+
while (parentId !== void 0) {
|
|
904
|
+
if (ancestrySeen.has(parentId)) throw new SessionQueryError(`session lineage contains a cycle at "${parentId}"`, "SESSION_QUERY_INVALID_LINEAGE");
|
|
905
|
+
ancestrySeen.add(parentId);
|
|
906
|
+
const parent = byId.get(parentId);
|
|
907
|
+
if (parent === void 0) {
|
|
908
|
+
unresolvedParentId = parentId;
|
|
909
|
+
break;
|
|
910
|
+
}
|
|
911
|
+
ancestors.push(parent);
|
|
912
|
+
parentId = parent.header.parentSession;
|
|
913
|
+
}
|
|
914
|
+
const childrenByParent = /* @__PURE__ */ new Map();
|
|
915
|
+
for (const record of records) {
|
|
916
|
+
const parent = record.header.parentSession;
|
|
917
|
+
if (parent === void 0) continue;
|
|
918
|
+
const children = childrenByParent.get(parent) ?? [];
|
|
919
|
+
children.push(record);
|
|
920
|
+
childrenByParent.set(parent, children);
|
|
921
|
+
}
|
|
922
|
+
for (const children of childrenByParent.values()) children.sort((a, b) => a.header.createdAt - b.header.createdAt || a.header.id.localeCompare(b.header.id));
|
|
923
|
+
const descendants = buildDescendants(childrenByParent, sessionId);
|
|
924
|
+
const common = {
|
|
925
|
+
target: cloneRecord(target),
|
|
926
|
+
ancestors: ancestors.map(cloneRecord),
|
|
927
|
+
descendants
|
|
928
|
+
};
|
|
929
|
+
if (unresolvedParentId !== void 0) return {
|
|
930
|
+
...common,
|
|
931
|
+
complete: false,
|
|
932
|
+
unresolvedParentId
|
|
933
|
+
};
|
|
934
|
+
return {
|
|
935
|
+
...common,
|
|
936
|
+
complete: true,
|
|
937
|
+
root: cloneRecord(ancestors.at(-1) ?? target)
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
function analyzeEventLog(sessionId, events) {
|
|
941
|
+
let folded;
|
|
942
|
+
try {
|
|
943
|
+
folded = foldSurface(events);
|
|
944
|
+
} catch (error) {
|
|
945
|
+
throw new SessionQueryError(
|
|
946
|
+
/* v8 ignore next -- foldSurface throws Error instances */
|
|
947
|
+
`invalid session surface: ${error instanceof Error ? error.message : "unknown error"}`,
|
|
948
|
+
"SESSION_QUERY_INVALID_SURFACE",
|
|
949
|
+
{ cause: error }
|
|
950
|
+
);
|
|
951
|
+
}
|
|
952
|
+
const current = new Set(folded.nodes);
|
|
953
|
+
const replacedBy = /* @__PURE__ */ new Map();
|
|
954
|
+
const replacedEventSeqs = /* @__PURE__ */ new Map();
|
|
955
|
+
for (const replacement of folded.replacements) {
|
|
956
|
+
const removed = replacement.shadowedSeqs;
|
|
957
|
+
replacedEventSeqs.set(replacement.seq, removed);
|
|
958
|
+
for (const removedSeq of removed) replacedBy.set(removedSeq, replacement.seq);
|
|
959
|
+
}
|
|
960
|
+
return {
|
|
961
|
+
records: events.map((event) => ({
|
|
962
|
+
sessionId,
|
|
963
|
+
seq: event.seq,
|
|
964
|
+
type: event.type,
|
|
965
|
+
time: event.time,
|
|
966
|
+
surface: current.has(event.seq) ? "current" : replacedBy.has(event.seq) ? "shadowed" : "log-only"
|
|
967
|
+
})),
|
|
968
|
+
replacedBy,
|
|
969
|
+
replacedEventSeqs,
|
|
970
|
+
currentSeqs: [...folded.nodes]
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
function eventSources(event) {
|
|
974
|
+
return event.sourceEventSeqs ?? [];
|
|
975
|
+
}
|
|
976
|
+
function buildDescendants(childrenByParent, sessionId) {
|
|
977
|
+
const descendants = [];
|
|
978
|
+
const stack = [{
|
|
979
|
+
sessionId,
|
|
980
|
+
descendants
|
|
981
|
+
}];
|
|
982
|
+
while (stack.length > 0) {
|
|
983
|
+
const frame = stack.pop();
|
|
984
|
+
const nodes = [];
|
|
985
|
+
for (const child of childrenByParent.get(frame.sessionId) ?? []) {
|
|
986
|
+
const node = {
|
|
987
|
+
session: cloneRecord(child),
|
|
988
|
+
descendants: []
|
|
989
|
+
};
|
|
990
|
+
nodes.push(node);
|
|
991
|
+
frame.descendants.push(node);
|
|
992
|
+
}
|
|
993
|
+
for (let index = nodes.length - 1; index >= 0; index -= 1) {
|
|
994
|
+
const node = nodes[index];
|
|
995
|
+
stack.push({
|
|
996
|
+
sessionId: node.session.header.id,
|
|
997
|
+
descendants: node.descendants
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
return descendants;
|
|
1002
|
+
}
|
|
1003
|
+
function cloneRecord(record) {
|
|
1004
|
+
return {
|
|
1005
|
+
...record,
|
|
1006
|
+
header: structuredClone(record.header)
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
//#endregion
|
|
1010
|
+
//#region lib/types/cursor.js
|
|
1011
|
+
/** Opaque cursor identity for session-search pagination. */
|
|
1012
|
+
/**
|
|
1013
|
+
* Brand an encoded provider cursor for the public search contract.
|
|
1014
|
+
* @param value - opaque encoded cursor value.
|
|
1015
|
+
* @returns the same runtime string with session-search cursor identity.
|
|
1016
|
+
*/
|
|
1017
|
+
function SessionSearchCursor(value) {
|
|
1018
|
+
return value;
|
|
1019
|
+
}
|
|
1020
|
+
//#endregion
|
|
1021
|
+
//#region lib/types/index.js
|
|
1022
|
+
/**
|
|
1023
|
+
* Service Definition for combined session-history reads, traces, filters, and full-text search.
|
|
1024
|
+
*
|
|
1025
|
+
* @module @x1a0f3n9/dsh-session-query
|
|
1026
|
+
*/
|
|
1027
|
+
/**
|
|
1028
|
+
* Unified live-preferred session query service.
|
|
1029
|
+
*
|
|
1030
|
+
* Exact reads, filters, and traces are backend-independent concrete behavior.
|
|
1031
|
+
* A backend implements full-text observation, reconciliation, ranking, cursor
|
|
1032
|
+
* generations, and query execution on the same `ctx.sessionQuery` service.
|
|
1033
|
+
*/
|
|
1034
|
+
var SessionQueryEngine = class extends Service {
|
|
1035
|
+
static inject = ["sessions"];
|
|
1036
|
+
_readWindowMax;
|
|
1037
|
+
_corpus;
|
|
1038
|
+
_observations;
|
|
1039
|
+
constructor(ctx, config = {}) {
|
|
1040
|
+
super(ctx, "sessionQuery");
|
|
1041
|
+
this._readWindowMax = config.readWindowMax ?? 50;
|
|
1042
|
+
if (!Number.isInteger(this._readWindowMax) || this._readWindowMax < 0) throw new SessionQueryError("session-query: readWindowMax must be a non-negative integer", "SESSION_QUERY_INVALID_CONFIG");
|
|
1043
|
+
const persistedReadConcurrency = config.persistedReadConcurrency ?? 4;
|
|
1044
|
+
if (!Number.isSafeInteger(persistedReadConcurrency) || persistedReadConcurrency < 1) throw new SessionQueryError("session-query: persistedReadConcurrency must be a positive safe integer", "SESSION_QUERY_INVALID_CONFIG");
|
|
1045
|
+
const preparedSessionCacheSize = config.preparedSessionCacheSize ?? 5;
|
|
1046
|
+
if (!Number.isSafeInteger(preparedSessionCacheSize) || preparedSessionCacheSize < 1) throw new SessionQueryError("session-query: preparedSessionCacheSize must be a positive safe integer", "SESSION_QUERY_INVALID_CONFIG");
|
|
1047
|
+
this._corpus = new SessionCorpus(ctx, persistedReadConcurrency);
|
|
1048
|
+
this._observations = new SessionObservationReader(ctx, preparedSessionCacheSize);
|
|
1049
|
+
}
|
|
1050
|
+
/**
|
|
1051
|
+
* Observe one exact live or prepared Session without a persistence listing preflight.
|
|
1052
|
+
* @param sessionId - logical Session identity.
|
|
1053
|
+
* @param options - cancellation and projection selection for this read.
|
|
1054
|
+
* @returns a caller-owned observation lease.
|
|
1055
|
+
*/
|
|
1056
|
+
observeSession(sessionId, options = {}) {
|
|
1057
|
+
return this._observations.read(sessionId, options);
|
|
1058
|
+
}
|
|
1059
|
+
/**
|
|
1060
|
+
* List the complete logical corpus using live-preferred records.
|
|
1061
|
+
* @param signal - optional cancellation for persistence listing.
|
|
1062
|
+
* @returns deterministic newest-first cloned session records.
|
|
1063
|
+
*/
|
|
1064
|
+
listSessions(signal) {
|
|
1065
|
+
return this._corpus.listSessions(signal);
|
|
1066
|
+
}
|
|
1067
|
+
/**
|
|
1068
|
+
* Read and replay-validate one complete logical session log without making it live.
|
|
1069
|
+
* @param sessionId - live or persisted session id to read.
|
|
1070
|
+
* @returns cloned header and complete raw event log from one observation.
|
|
1071
|
+
* @throws when persistence, header compatibility, or replay validation fails.
|
|
1072
|
+
*/
|
|
1073
|
+
async readSession(sessionId) {
|
|
1074
|
+
const loaded = await this._corpus.load(sessionId);
|
|
1075
|
+
Session.create(sessionId, loaded.events, loaded.header, loaded.inheritedEventCount);
|
|
1076
|
+
return {
|
|
1077
|
+
session: structuredClone(loaded.header),
|
|
1078
|
+
inheritedEventCount: loaded.inheritedEventCount,
|
|
1079
|
+
events: loaded.events.map(snapshotSessionEvent)
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
/**
|
|
1083
|
+
* Filter the complete logical corpus with provider-independent predicates.
|
|
1084
|
+
* @param filters - ANDed session metadata and availability clauses.
|
|
1085
|
+
* @param signal - optional cancellation for persistence listing.
|
|
1086
|
+
* @returns matching cloned records in deterministic newest-first order.
|
|
1087
|
+
*/
|
|
1088
|
+
async filterSessions(filters, signal) {
|
|
1089
|
+
const ownedFilters = materializeSessionResultFilters(filters);
|
|
1090
|
+
return this._filterSessions(ownedFilters, signal);
|
|
1091
|
+
}
|
|
1092
|
+
/**
|
|
1093
|
+
* Fold the latest log-backed title from one live-preferred logical session.
|
|
1094
|
+
* @param sessionId - live or persisted session id to read.
|
|
1095
|
+
* @param signal - optional cancellation for source resolution and title folding.
|
|
1096
|
+
* @returns latest title snapshot, or `undefined` when the log has no title event.
|
|
1097
|
+
*/
|
|
1098
|
+
async readTitle(sessionId, signal) {
|
|
1099
|
+
return (await this.readTitleSnapshot(sessionId, signal)).title;
|
|
1100
|
+
}
|
|
1101
|
+
/**
|
|
1102
|
+
* Fold the latest title and return its source header from one corpus observation.
|
|
1103
|
+
* @param sessionId - live or persisted session id to read.
|
|
1104
|
+
* @param signal - optional cancellation for source resolution and title folding.
|
|
1105
|
+
* @returns cloned source header and optional latest title snapshot.
|
|
1106
|
+
*/
|
|
1107
|
+
async readTitleSnapshot(sessionId, signal) {
|
|
1108
|
+
const result = (await this.readTitleSnapshots([sessionId], signal))[0];
|
|
1109
|
+
if (result.status === "rejected") throw result.reason;
|
|
1110
|
+
return result.value;
|
|
1111
|
+
}
|
|
1112
|
+
/**
|
|
1113
|
+
* Fold titles for unique sessions from one cancellable corpus observation.
|
|
1114
|
+
*
|
|
1115
|
+
* Results preserve first-occurrence input order. Operational failures stay
|
|
1116
|
+
* isolated per session, while cancellation rejects the complete operation.
|
|
1117
|
+
* @param sessionIds - live or persisted session ids to observe.
|
|
1118
|
+
* @param signal - optional cancellation shared by all source reads.
|
|
1119
|
+
* @returns one fulfilled or rejected result per unique requested id.
|
|
1120
|
+
*/
|
|
1121
|
+
async readTitleSnapshots(sessionIds, signal) {
|
|
1122
|
+
return this._corpus.projectMany(sessionIds, (source) => {
|
|
1123
|
+
const title = foldSessionTitle(source.events);
|
|
1124
|
+
return {
|
|
1125
|
+
session: structuredClone(source.header),
|
|
1126
|
+
...title === void 0 ? {} : { title }
|
|
1127
|
+
};
|
|
1128
|
+
}, signal);
|
|
1129
|
+
}
|
|
1130
|
+
/**
|
|
1131
|
+
* List lightweight raw-log event records for one logical session.
|
|
1132
|
+
* @param sessionId - live-preferred session id to read.
|
|
1133
|
+
* @returns event records in ascending seq order.
|
|
1134
|
+
*/
|
|
1135
|
+
async listEvents(sessionId) {
|
|
1136
|
+
return eventRecords(sessionId, (await this._corpus.load(sessionId)).events);
|
|
1137
|
+
}
|
|
1138
|
+
/**
|
|
1139
|
+
* Scan first-party semantic event documents with provider-independent filters.
|
|
1140
|
+
* @param sessionId - live-preferred session id to scan.
|
|
1141
|
+
* @param filters - ANDed metadata and literal-text predicates.
|
|
1142
|
+
* @returns matching semantic documents in ascending seq order.
|
|
1143
|
+
*/
|
|
1144
|
+
async filterEvents(sessionId, filters) {
|
|
1145
|
+
const ownedFilters = materializeSessionEventResultFilters(filters);
|
|
1146
|
+
return this._filterEvents(sessionId, ownedFilters);
|
|
1147
|
+
}
|
|
1148
|
+
async _filterSessions(filters, signal) {
|
|
1149
|
+
return filterSessionResults(await this._corpus.listSessions(signal), filters);
|
|
1150
|
+
}
|
|
1151
|
+
async _filterEvents(sessionId, filters) {
|
|
1152
|
+
return filterSessionEventDocuments(buildSessionEventSearchDocuments(sessionId, (await this._corpus.load(sessionId)).events), filters);
|
|
1153
|
+
}
|
|
1154
|
+
/**
|
|
1155
|
+
* Read one session's complete current model surface from one corpus observation.
|
|
1156
|
+
* @param sessionId - live-preferred session id to read.
|
|
1157
|
+
* @returns cloned header, current surface, and the last sequence number included in the raw-log capture.
|
|
1158
|
+
* @throws when source resolution fails or the session surface is invalid.
|
|
1159
|
+
*/
|
|
1160
|
+
async readSurface(sessionId) {
|
|
1161
|
+
const loaded = await this._corpus.load(sessionId);
|
|
1162
|
+
return {
|
|
1163
|
+
session: structuredClone(loaded.header),
|
|
1164
|
+
inheritedEventCount: loaded.inheritedEventCount,
|
|
1165
|
+
capturedThroughSeq: loaded.events.at(-1)?.seq ?? null,
|
|
1166
|
+
events: currentSurfaceEvents(sessionId, loaded.events)
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
/**
|
|
1170
|
+
* Trace known ancestry and descendants from one corpus observation.
|
|
1171
|
+
* @param sessionId - logical session id to trace.
|
|
1172
|
+
* @param signal - optional cancellation for persistence listing.
|
|
1173
|
+
* @returns a complete lineage or the first parent that could not be resolved.
|
|
1174
|
+
* @throws when corpus resolution fails, the target is absent, or its known ancestry cycles.
|
|
1175
|
+
*/
|
|
1176
|
+
async traceSession(sessionId, signal) {
|
|
1177
|
+
const records = await this._corpus.listSessions(signal);
|
|
1178
|
+
signal?.throwIfAborted();
|
|
1179
|
+
return traceSession(records, sessionId);
|
|
1180
|
+
}
|
|
1181
|
+
/**
|
|
1182
|
+
* Trace one event's direct positional replacements and cited source events.
|
|
1183
|
+
* @param request - target session id and event seq.
|
|
1184
|
+
* @param signal - optional cancellation for persisted source resolution.
|
|
1185
|
+
* @returns source header, direct links, and the target's positional replacement chain.
|
|
1186
|
+
* @throws when source resolution fails, the target is absent, or surface/source-event validation fails.
|
|
1187
|
+
*/
|
|
1188
|
+
async traceEvent(request, signal) {
|
|
1189
|
+
const loaded = await this._corpus.load(request.sessionId, signal);
|
|
1190
|
+
signal?.throwIfAborted();
|
|
1191
|
+
return {
|
|
1192
|
+
session: loaded.header,
|
|
1193
|
+
...traceEvent(request.sessionId, loaded.events, request.seq)
|
|
1194
|
+
};
|
|
1195
|
+
}
|
|
1196
|
+
/**
|
|
1197
|
+
* Read one full event plus a bounded raw-log context window.
|
|
1198
|
+
* @param request - target session/seq and context sizes.
|
|
1199
|
+
* @param signal - optional cancellation for persisted source resolution.
|
|
1200
|
+
* @returns cloned target and neighboring events.
|
|
1201
|
+
*/
|
|
1202
|
+
async readEvent(request, signal) {
|
|
1203
|
+
const before = this._readWindow("before", request.before);
|
|
1204
|
+
const after = this._readWindow("after", request.after);
|
|
1205
|
+
const sessionId = request.sessionId;
|
|
1206
|
+
const seq = request.seq;
|
|
1207
|
+
return this._readEvent(sessionId, seq, before, after, signal);
|
|
1208
|
+
}
|
|
1209
|
+
async _readEvent(sessionId, seq, before, after, signal) {
|
|
1210
|
+
const loaded = await this._corpus.load(sessionId, signal);
|
|
1211
|
+
signal?.throwIfAborted();
|
|
1212
|
+
const target = loaded.events[seq];
|
|
1213
|
+
if (target === void 0 || target.seq !== seq) throw new SessionQueryError(`session "${sessionId}" has no event at seq ${seq}`, "SESSION_QUERY_EVENT_NOT_FOUND");
|
|
1214
|
+
const startSeq = SessionSeq(Math.max(0, seq - before));
|
|
1215
|
+
const endSeq = SessionSeq(Math.min(loaded.events.length - 1, seq + after));
|
|
1216
|
+
const targetSnapshot = snapshotSessionEvent(target);
|
|
1217
|
+
const events = loaded.events.slice(startSeq, endSeq + 1).map((event) => event === target ? targetSnapshot : snapshotSessionEvent(event));
|
|
1218
|
+
return {
|
|
1219
|
+
session: structuredClone(loaded.header),
|
|
1220
|
+
inheritedEventCount: loaded.inheritedEventCount,
|
|
1221
|
+
target: targetSnapshot,
|
|
1222
|
+
events,
|
|
1223
|
+
startSeq,
|
|
1224
|
+
endSeq
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
_readWindow(name, value) {
|
|
1228
|
+
if (value === void 0) return 0;
|
|
1229
|
+
if (!Number.isInteger(value) || value < 0 || value > this._readWindowMax) throw new SessionQueryError(`${name} must be an integer between 0 and ${this._readWindowMax}`, "SESSION_QUERY_INVALID_WINDOW");
|
|
1230
|
+
return value;
|
|
1231
|
+
}
|
|
1232
|
+
};
|
|
1233
|
+
//#endregion
|
|
1234
|
+
export { SESSION_QUERY_DEFAULT_PERSISTED_INSPECT_CONCURRENCY, SESSION_QUERY_DEFAULT_PREPARED_SESSION_CACHE_SIZE, SESSION_QUERY_READ_WINDOW_MAX, SessionQueryEngine, SessionQueryEngine as default, SessionQueryError, SessionSearchCursor, assertSessionHeadersCompatible, buildSessionEventRecords, buildSessionEventSearchDocuments, compileSessionTextFilter, extractSessionEventText, filterSessionEventDocuments, filterSessionResults, materializeSessionEventResultFilters, materializeSessionResultFilters, readColdSessionLog };
|