@khorsheed/dsh-context-lens 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.en.md +53 -0
  3. package/README.i18n.yaml +6 -0
  4. package/README.md +52 -2
  5. package/cordis.patch.yml +13 -0
  6. package/lib/client.js +26953 -0
  7. package/lib/index.js +3943 -0
  8. package/lib/typert.host.d.ts +3 -0
  9. package/lib/typert.host.js +1569 -0
  10. package/lib/typert.remote-client.d.ts +40 -0
  11. package/lib/typert.remote-client.js +1202 -0
  12. package/lib/types/changes.d.ts +72 -0
  13. package/lib/types/changes.js +328 -0
  14. package/lib/types/client/AgentTabs.d.ts +34 -0
  15. package/lib/types/client/AgentTabs.js +89 -0
  16. package/lib/types/client/ChangeList.d.ts +28 -0
  17. package/lib/types/client/ChangeList.js +109 -0
  18. package/lib/types/client/ConfigFacts.d.ts +21 -0
  19. package/lib/types/client/ConfigFacts.js +30 -0
  20. package/lib/types/client/HeaderUtility.d.ts +12 -0
  21. package/lib/types/client/HeaderUtility.js +8 -0
  22. package/lib/types/client/InspectorPanel.d.ts +5 -0
  23. package/lib/types/client/InspectorPanel.js +486 -0
  24. package/lib/types/client/RawView.d.ts +18 -0
  25. package/lib/types/client/RawView.js +8 -0
  26. package/lib/types/client/RequestFacts.d.ts +19 -0
  27. package/lib/types/client/RequestFacts.js +39 -0
  28. package/lib/types/client/RoomDecisionsView.d.ts +24 -0
  29. package/lib/types/client/RoomDecisionsView.js +52 -0
  30. package/lib/types/client/SchemaFields.d.ts +13 -0
  31. package/lib/types/client/SchemaFields.js +96 -0
  32. package/lib/types/client/SegCtl.d.ts +32 -0
  33. package/lib/types/client/SegCtl.js +12 -0
  34. package/lib/types/client/SegmentBlocks.d.ts +28 -0
  35. package/lib/types/client/SegmentBlocks.js +225 -0
  36. package/lib/types/client/TimingView.d.ts +17 -0
  37. package/lib/types/client/TimingView.js +98 -0
  38. package/lib/types/client/TokenView.d.ts +21 -0
  39. package/lib/types/client/TokenView.js +22 -0
  40. package/lib/types/client/ToolCatalog.d.ts +9 -0
  41. package/lib/types/client/ToolCatalog.js +51 -0
  42. package/lib/types/client/Trend.d.ts +25 -0
  43. package/lib/types/client/Trend.js +100 -0
  44. package/lib/types/client/TurnAction.d.ts +14 -0
  45. package/lib/types/client/TurnAction.js +31 -0
  46. package/lib/types/client/TurnMenu.d.ts +36 -0
  47. package/lib/types/client/TurnMenu.js +50 -0
  48. package/lib/types/client/aggregate.d.ts +141 -0
  49. package/lib/types/client/aggregate.js +213 -0
  50. package/lib/types/client/contract.d.ts +58 -0
  51. package/lib/types/client/contract.js +1 -0
  52. package/lib/types/client/definition.d.ts +47 -0
  53. package/lib/types/client/definition.js +25 -0
  54. package/lib/types/client/icons-local.d.ts +19 -0
  55. package/lib/types/client/icons-local.js +3 -0
  56. package/lib/types/client/icons.d.ts +18 -0
  57. package/lib/types/client/icons.js +2 -0
  58. package/lib/types/client/index.d.ts +42 -0
  59. package/lib/types/client/index.js +139 -0
  60. package/lib/types/client/locales.d.ts +365 -0
  61. package/lib/types/client/locales.js +705 -0
  62. package/lib/types/client/preset-visibility.d.ts +99 -0
  63. package/lib/types/client/preset-visibility.js +108 -0
  64. package/lib/types/client/scroll-sync.d.ts +54 -0
  65. package/lib/types/client/scroll-sync.js +118 -0
  66. package/lib/types/client/timing.d.ts +29 -0
  67. package/lib/types/client/timing.js +117 -0
  68. package/lib/types/client/wire.d.ts +13 -0
  69. package/lib/types/client/wire.js +11 -0
  70. package/lib/types/index.d.ts +32 -0
  71. package/lib/types/index.js +25 -0
  72. package/lib/types/log-lane.d.ts +198 -0
  73. package/lib/types/log-lane.js +841 -0
  74. package/lib/types/messages.d.ts +96 -0
  75. package/lib/types/messages.js +254 -0
  76. package/lib/types/prefix-diff.d.ts +4 -0
  77. package/lib/types/prefix-diff.js +20 -0
  78. package/lib/types/remote.d.ts +45 -0
  79. package/lib/types/remote.js +99 -0
  80. package/lib/types/room.d.ts +66 -0
  81. package/lib/types/room.js +101 -0
  82. package/lib/types/segmenter.d.ts +100 -0
  83. package/lib/types/segmenter.js +184 -0
  84. package/lib/types/service.d.ts +145 -0
  85. package/lib/types/service.js +493 -0
  86. package/lib/types/store.d.ts +424 -0
  87. package/lib/types/store.js +1920 -0
  88. package/lib/types/tool-summary.d.ts +6 -0
  89. package/lib/types/tool-summary.js +7 -0
  90. package/lib/types/types.d.ts +703 -0
  91. package/lib/types/types.js +18 -0
  92. package/locale/en.json +6 -0
  93. package/locale/zh.json +6 -0
  94. package/package.json +133 -3
@@ -0,0 +1,493 @@
1
+ import { InspectorStore, scopeAuthorized } from "./store.js";
2
+ import { recordAssembly } from "./segmenter.js";
3
+ import { fromRequestMessage, serializeMessage } from "./messages.js";
4
+ import { messageOfRow, meterOf, replacementFact, replacementFactsOf } from "./changes.js";
5
+ import { foldLogRows, IN_PROCESS_PROVIDERS, isSessionLogReader, openCursor, reconstructSession, } from "./log-lane.js";
6
+ /** The capture store's query face (Remote delegates here). */
7
+ export class InspectorService {
8
+ ctx;
9
+ config;
10
+ store;
11
+ disposers;
12
+ /** childId → open read handle (persistent: reads are offset-incremental). */
13
+ handles = new Map();
14
+ cursors = new Map();
15
+ /** In-flight refresh per childId — concurrent panel queries share one read. */
16
+ refreshing = new Map();
17
+ /** sessionId → the metering event (`compaction/summary` | `compaction/prune`) just seen; the host appends its replacement next. */
18
+ meters = new Map();
19
+ now;
20
+ logWarn;
21
+ persistenceOverride;
22
+ constructor(ctx, config = {}, deps = {}) {
23
+ this.ctx = ctx;
24
+ this.config = config;
25
+ this.now = deps.now ?? Date.now;
26
+ this.logWarn = deps.logger?.warn.bind(deps.logger) ?? ((message) => ctx.logger.warn(message));
27
+ this.persistenceOverride = deps.persistence;
28
+ const storeConfig = {
29
+ ...config.maxTurnsPerSession === undefined ? {} : { maxTurnsPerSession: config.maxTurnsPerSession },
30
+ ...config.maxBackfillRequestsPerSession === undefined ? {} : { maxBackfillRequestsPerSession: config.maxBackfillRequestsPerSession },
31
+ ...config.maxStoredCharsTotal === undefined ? {} : { maxStoredCharsTotal: config.maxStoredCharsTotal },
32
+ };
33
+ this.store = new InspectorStore(storeConfig, {
34
+ now: this.now,
35
+ onTurnEvicted: (_sessionId, turn) => this.dropEvictedChildState(turn),
36
+ ...deps.generation === undefined ? {} : { generation: deps.generation },
37
+ });
38
+ this.disposers = [
39
+ ctx.on('llm/stream', (options, next) => this.onStream(options, next)),
40
+ ctx.on('system-prompt/assemble', (_assembly, _context, next) => this.onAssemble(next)),
41
+ ctx.on('session/event', (session, event) => this.onSessionEvent(session, event)),
42
+ ];
43
+ }
44
+ // ---------- lane 1: llm/stream (in-process, full fidelity) ----------
45
+ /**
46
+ * The waterfall tee: snapshot the (possibly deep-frozen) request at dispatch,
47
+ * then forward every chunk unchanged while observing. A capture-side failure
48
+ * at setup degrades to the bare downstream stream; mid-stream failures only
49
+ * lose the remaining observations.
50
+ */
51
+ onStream(options, next) {
52
+ let request;
53
+ try {
54
+ request = this.store.beginRequest(snapshotOptions(options), this.now());
55
+ }
56
+ catch (error) {
57
+ this.logWarn(`context-lens: capture setup failed, forwarding uncaptured (${String(error)})`);
58
+ return next();
59
+ }
60
+ if (request === undefined)
61
+ return next();
62
+ return this.tee(request, next);
63
+ }
64
+ tee(request, next) {
65
+ const { store, now } = this;
66
+ const warn = this.logWarn;
67
+ return (async function* () {
68
+ let thrown;
69
+ try {
70
+ for await (const chunk of next()) {
71
+ try {
72
+ store.observeChunk(request, chunk, now());
73
+ }
74
+ catch (error) {
75
+ warn(`context-lens: chunk observation dropped (${String(error)})`);
76
+ }
77
+ yield chunk;
78
+ }
79
+ }
80
+ catch (error) {
81
+ thrown = error;
82
+ throw error;
83
+ }
84
+ finally {
85
+ try {
86
+ store.settleRequest(request, now(), thrown);
87
+ }
88
+ catch (error) {
89
+ warn(`context-lens: request settlement dropped (${String(error)})`);
90
+ }
91
+ }
92
+ })();
93
+ }
94
+ // ---------- lane 2: system-prompt/assemble (read-only attribution feed) ----------
95
+ /**
96
+ * Record the post-waterfall assembly for section attribution. The returned
97
+ * value passes through unchanged — this listener observes, it never rewrites
98
+ * a prompt.
99
+ */
100
+ async onAssemble(next) {
101
+ const result = await next();
102
+ try {
103
+ recordAssembly(this.store.assemblyRing, result, this.now());
104
+ }
105
+ catch (error) {
106
+ this.logWarn(`context-lens: assembly record dropped (${String(error)})`);
107
+ }
108
+ return result;
109
+ }
110
+ // ---------- lane 3: session/event (turn brackets + tool pairing + catalog) ----------
111
+ onSessionEvent(session, event) {
112
+ try {
113
+ this.dispatchSessionEvent(session, event);
114
+ }
115
+ catch (error) {
116
+ this.logWarn(`context-lens: session event "${String(event.type)}" dropped (${String(error)})`);
117
+ }
118
+ }
119
+ dispatchSessionEvent(session, event) {
120
+ const sessionId = session.id;
121
+ const at = event.time;
122
+ const eventType = event.type;
123
+ this.noteReplacement(session, event);
124
+ this.store.noteLogActivity(sessionId);
125
+ if (eventType.startsWith('compaction/'))
126
+ this.store.noteCompaction(sessionId, eventType, event.data);
127
+ if (eventType.startsWith('room/')) {
128
+ this.store.noteRoomEvent(sessionId, eventType, event.data, event.seq, at);
129
+ return;
130
+ }
131
+ if (event.type === 'tool/result' && event.surfaceOp !== 'append') {
132
+ const original = session.eventAt(event.surfaceOp.startSeq);
133
+ if (original?.type === 'tool/result') {
134
+ this.store.notePruning(sessionId, event.data.message.id, serializeMessage(fromRequestMessage(original.data.message)).length, serializeMessage(fromRequestMessage(event.data.message)).length);
135
+ }
136
+ // A replacement updates context, not the original execution's result.
137
+ return;
138
+ }
139
+ switch (event.type) {
140
+ case 'turn/start':
141
+ this.store.turnStart(sessionId, event.data.turn, at, event.seq);
142
+ return;
143
+ case 'turn/end':
144
+ this.store.turnEnd(sessionId, event.data.turn, event.data.reason, at);
145
+ return;
146
+ case 'user/message': {
147
+ const message = fromRequestMessage(event.data);
148
+ if (message.compactionId !== undefined && event.surfaceOp !== 'append')
149
+ this.store.noteCompaction(sessionId, 'compaction/applied', { compactionId: message.compactionId });
150
+ if (message.id)
151
+ this.store.noteMessageTurn(sessionId, message.id);
152
+ this.store.noteUserMessage(sessionId, message.sourceKind, serializeMessage(message));
153
+ return;
154
+ }
155
+ case 'assistant/message':
156
+ this.store.noteMessageTurn(sessionId, event.data.message.id, event.data.turn);
157
+ this.store.noteAssistantMessage(sessionId, event.data.message.id);
158
+ return;
159
+ case 'tool/call':
160
+ this.store.toolCall(sessionId, event.data.callId, event.data.name, event.data.arguments, at);
161
+ return;
162
+ case 'tool/result': {
163
+ this.store.noteMessageTurn(sessionId, event.data.message.id, event.data.turn);
164
+ const message = fromRequestMessage(event.data.message);
165
+ this.store.toolResult(sessionId, event.data.message.toolCallId, event.data.message.isError === true, serializeMessage(message), at);
166
+ return;
167
+ }
168
+ default:
169
+ break;
170
+ }
171
+ // `subagent/catalog` belongs to the subagent package's SessionEventMap
172
+ // merge — read structurally so this plugin neither depends on that package
173
+ // nor breaks when the composition lacks it.
174
+ const type = event.type;
175
+ if (type === 'subagent/descriptor') {
176
+ const provider = event.data['provider'];
177
+ if (typeof provider === 'string') {
178
+ this.store.noteChildProvider(sessionId, provider);
179
+ if (!IN_PROCESS_PROVIDERS.has(provider))
180
+ this.store.markLogDriven(sessionId);
181
+ }
182
+ return;
183
+ }
184
+ if (type === 'subagent/catalog') {
185
+ const data = event.data;
186
+ const childId = data['childId'];
187
+ if (typeof childId === 'string' && childId.length > 0) {
188
+ const label = data['label'];
189
+ this.store.subagentCatalog(sessionId, childId, typeof label === 'string' && label.length > 0 ? label : undefined, at);
190
+ }
191
+ }
192
+ }
193
+ // ---------- lane 4: child logs (fresh-process subagent fallback) ----------
194
+ /** The probed `sessionPersistence` service, when the composition mounts one. */
195
+ reader() {
196
+ if (this.persistenceOverride !== undefined)
197
+ return this.persistenceOverride;
198
+ try {
199
+ const candidate = this.ctx.get('sessionPersistence');
200
+ return isSessionLogReader(candidate) ? candidate : undefined;
201
+ }
202
+ catch {
203
+ return undefined;
204
+ }
205
+ }
206
+ /**
207
+ * Refresh the log-sourced child scopes of one session (query-time,
208
+ * incremental). A scope the query's {@link ScopeFilter} excludes is skipped
209
+ * outright — its log is not opened on an unauthorized query's behalf.
210
+ */
211
+ async refreshChildren(rootSessionId, filter) {
212
+ const reader = this.reader();
213
+ if (reader === undefined)
214
+ return;
215
+ const scopes = this.store.childScopes(rootSessionId);
216
+ await Promise.all(scopes.map(scope => scopeAuthorized(scope.scopeId, filter)
217
+ ? this.refreshChild(rootSessionId, scope.scopeId, reader, scope.live)
218
+ : Promise.resolve()));
219
+ }
220
+ refreshChild(rootSessionId, childId, reader, live) {
221
+ if (live)
222
+ return Promise.resolve();
223
+ const pending = this.refreshing.get(childId);
224
+ if (pending !== undefined)
225
+ return pending;
226
+ const job = this.doRefreshChild(rootSessionId, childId, reader)
227
+ .finally(() => {
228
+ this.refreshing.delete(childId);
229
+ });
230
+ this.refreshing.set(childId, job);
231
+ return job;
232
+ }
233
+ async doRefreshChild(rootSessionId, childId, reader) {
234
+ let cursor = this.cursors.get(childId);
235
+ if (cursor?.broken !== undefined)
236
+ return;
237
+ if (cursor === undefined) {
238
+ cursor = openCursor();
239
+ this.cursors.set(childId, cursor);
240
+ }
241
+ try {
242
+ let handle = this.handles.get(childId);
243
+ if (handle === undefined) {
244
+ handle = await reader.open(childId, 'read');
245
+ this.handles.set(childId, handle);
246
+ }
247
+ const { events } = await handle.read(cursor.consumed);
248
+ const descriptor = events.find(row => row.type === 'subagent/descriptor');
249
+ const provider = descriptor?.data?.['provider'];
250
+ if (typeof provider === 'string')
251
+ this.store.noteChildProvider(childId, provider);
252
+ for (const request of foldLogRows(cursor, events)) {
253
+ this.store.appendLogRequest(rootSessionId, childId, request);
254
+ }
255
+ this.store.refreshExecutionResults(rootSessionId, childId);
256
+ }
257
+ catch (error) {
258
+ // A not-yet-materialized child log is transient (the child process flushes
259
+ // on its own schedule) — retry on the next refresh. Anything else is a
260
+ // durable unreadability (format drift, unknown event vocabulary): stop
261
+ // hammering it and keep the scope's honest empty state.
262
+ if (error instanceof Error && error.name === 'SessionPersistenceNotFoundError')
263
+ return;
264
+ cursor.broken = error instanceof Error ? error.message : String(error);
265
+ this.logWarn(`context-lens: child log "${childId}" unreadable (${cursor.broken})`);
266
+ }
267
+ }
268
+ /** Turn eviction drops the child cursors/handles whose scopes lived on it. */
269
+ dropEvictedChildState(turn) {
270
+ for (const scopeId of turn.scopes.keys()) {
271
+ if (scopeId === 'main')
272
+ continue;
273
+ this.cursors.delete(scopeId);
274
+ const handle = this.handles.get(scopeId);
275
+ if (handle !== undefined) {
276
+ this.handles.delete(scopeId);
277
+ void handle.close().catch(() => undefined);
278
+ }
279
+ }
280
+ }
281
+ // ---------- lane 5: main-session history backfill ----------
282
+ /**
283
+ * Reconstruct the session's pre-mount history from its own log, once per
284
+ * session (the store's watermark holds). The read is one full pass at first
285
+ * query; the store keeps the newest turns that fit the budget behind the
286
+ * live ones. A not-yet-materialized log is transient (retry next query); any
287
+ * other read failure marks the session broken and stops hammering it.
288
+ */
289
+ /** Pair a surface replacement with what it shadowed, typed by the metering event right before it. */
290
+ noteReplacement(session, row) {
291
+ const meter = this.meters.get(session.id);
292
+ this.meters.delete(session.id);
293
+ const next = meterOf(row);
294
+ if (next !== undefined)
295
+ this.meters.set(session.id, next);
296
+ const fact = replacementFact(row, seq => {
297
+ const shadowed = session.eventAt(seq);
298
+ return shadowed === undefined ? undefined : messageOfRow(shadowed)?.id;
299
+ }, meter);
300
+ if (fact !== undefined)
301
+ this.store.noteReplacement(session.id, fact);
302
+ }
303
+ async backfillMainSession(sessionId) {
304
+ const reader = this.reader();
305
+ if (reader === undefined)
306
+ return;
307
+ // Retryable until complete: a partial fill re-reads on later queries
308
+ // (eviction may have freed room); the read is one zstd decode.
309
+ const state = this.store.backfillState(sessionId);
310
+ if (state === 'broken' || (state === 'complete' && !this.store.needsRequestNumbers(sessionId) && !this.store.needsLogRead(sessionId)))
311
+ return;
312
+ const jobKey = `main:${sessionId}`;
313
+ const pending = this.refreshing.get(jobKey);
314
+ if (pending !== undefined)
315
+ return pending;
316
+ const job = this.doBackfillMainSession(sessionId, reader)
317
+ .finally(() => {
318
+ this.refreshing.delete(jobKey);
319
+ });
320
+ this.refreshing.set(jobKey, job);
321
+ return job;
322
+ }
323
+ async doBackfillMainSession(sessionId, reader) {
324
+ try {
325
+ const handle = await reader.open(sessionId, 'read');
326
+ try {
327
+ const { events } = await handle.read(0);
328
+ // Materialize only the window's requests: every request carries its
329
+ // whole context, so a long log rebuilt at once grows quadratically.
330
+ const hosts = events.filter(row => row.type === 'turn/start')
331
+ .map(row => row.data?.turn).filter((turn) => typeof turn === 'number');
332
+ // An external agent's own session: its requests reach only this log.
333
+ const provider = events.find(row => row.type === 'subagent/descriptor')?.data?.['provider'];
334
+ if (typeof provider === 'string' && !IN_PROCESS_PROVIDERS.has(provider))
335
+ this.store.markLogDriven(sessionId);
336
+ const keep = this.store.windowHosts(sessionId, hosts);
337
+ this.store.insertLogTurns(sessionId, reconstructSession(events, host => host === undefined || keep.has(host)));
338
+ for (const fact of replacementFactsOf(events))
339
+ this.store.noteReplacement(sessionId, fact);
340
+ for (const row of events) {
341
+ if (row.type.startsWith('room/'))
342
+ this.store.noteRoomEvent(sessionId, row.type, row.data, row.seq, row.time);
343
+ }
344
+ await this.resolveParent(sessionId, events, reader);
345
+ }
346
+ finally {
347
+ void handle.close().catch(() => undefined);
348
+ }
349
+ }
350
+ catch (error) {
351
+ if (error instanceof Error && error.name === 'SessionPersistenceNotFoundError')
352
+ return;
353
+ const reason = error instanceof Error ? error.message : String(error);
354
+ this.store.markBackfillBroken(sessionId, reason);
355
+ this.logWarn(`context-lens: session "${sessionId}" log unreadable, history backfill skipped (${reason})`);
356
+ }
357
+ }
358
+ /**
359
+ * A session opened on its own whose log header names a `parentSession`:
360
+ * read the parent's log once for its title, the turn whose catalog row
361
+ * started this child, and that row's label. Best effort — an unreadable
362
+ * parent leaves just its id.
363
+ */
364
+ async resolveParent(sessionId, events, reader) {
365
+ const header = events[0];
366
+ const parentId = header?.type === 'session' ? header.parentSession : undefined;
367
+ if (typeof parentId !== 'string' || parentId.length === 0 || this.store.hasParent(sessionId))
368
+ return;
369
+ const parent = { sessionId: parentId };
370
+ try {
371
+ const handle = await reader.open(parentId, 'read');
372
+ try {
373
+ const { events: rows } = await handle.read(0);
374
+ let turn;
375
+ for (const row of rows) {
376
+ const data = (row.data ?? {});
377
+ if (row.type === 'turn/start' && typeof data['turn'] === 'number')
378
+ turn = data['turn'];
379
+ if (row.type === 'session/title' && typeof data['title'] === 'string')
380
+ parent.title = data['title'];
381
+ if (row.type === 'subagent/catalog' && data['childId'] === sessionId) {
382
+ if (turn !== undefined)
383
+ parent.hostTurn = turn;
384
+ if (typeof data['label'] === 'string')
385
+ parent.label = data['label'];
386
+ }
387
+ }
388
+ }
389
+ finally {
390
+ void handle.close().catch(() => undefined);
391
+ }
392
+ }
393
+ catch {
394
+ // The id alone still says whose child this is.
395
+ }
396
+ this.store.noteParent(sessionId, parent);
397
+ }
398
+ // ---------- queries (the Remote face delegates here) ----------
399
+ /**
400
+ * Light turn list of one session plus its backfill watermark. The optional
401
+ * {@link ScopeFilter} restricts rows and aggregates to the authorized
402
+ * scopes; absent is the unfiltered local semantics.
403
+ */
404
+ async listTurns(sessionId, filter) {
405
+ await this.backfillMainSession(sessionId);
406
+ await this.refreshChildren(sessionId, filter);
407
+ return this.store.listTurns(sessionId, filter);
408
+ }
409
+ /**
410
+ * Move the session's backfill window to the turns ending at `end` (the
411
+ * latest turns when null) and answer the refreshed turn list. Live turns
412
+ * stay; the window refills from the log in the same call.
413
+ */
414
+ async loadTurns(sessionId, end, filter) {
415
+ const pending = this.refreshing.get(`main:${sessionId}`);
416
+ if (pending !== undefined)
417
+ await pending;
418
+ this.store.setWindow(sessionId, end ?? undefined);
419
+ return this.listTurns(sessionId, filter);
420
+ }
421
+ /** One turn's scope/request overview, without segments. */
422
+ async getTurn(sessionId, turnSeq, filter) {
423
+ await this.backfillMainSession(sessionId);
424
+ await this.refreshChildren(sessionId, filter);
425
+ return this.store.getTurn(sessionId, turnSeq, filter);
426
+ }
427
+ /** One request's full four-segment breakdown (a {@link ScopedRefusal} when the filter excludes its scope). */
428
+ async getRequest(sessionId, seq, filter) {
429
+ await this.backfillMainSession(sessionId);
430
+ await this.refreshChildren(sessionId, filter);
431
+ const request = this.store.getRequest(sessionId, seq, filter);
432
+ const tools = request !== undefined && 'prefix' in request ? request.prefix?.toolChanges : undefined;
433
+ if (tools === undefined || tools.length === 0)
434
+ return request;
435
+ return { ...request, prefix: { ...request.prefix, toolChanges: tools.map(tool => {
436
+ const owner = this.toolOwner(tool.name);
437
+ return owner === undefined ? tool : { ...tool, owner };
438
+ }) } };
439
+ }
440
+ /** The declaring package from a registered tool's host-side origin tag (capability-catalog convention); absent when untagged. */
441
+ toolOwner(name) {
442
+ try {
443
+ const registry = this.ctx.get('tools');
444
+ const definition = registry?.get?.(name);
445
+ if (typeof definition !== 'object' || definition === null)
446
+ return undefined;
447
+ const origin = definition[Symbol.for('dsh.tool.origin')];
448
+ return typeof origin?.owner === 'string' ? origin.owner : undefined;
449
+ }
450
+ catch {
451
+ return undefined;
452
+ }
453
+ }
454
+ /** Read the session map through the same backfill and scope-filtered path. */
455
+ async getSessionTrend(sessionId, filter) {
456
+ await this.listTurns(sessionId, filter);
457
+ return this.store.getSessionTrend(sessionId, filter);
458
+ }
459
+ /** One turn's whole-turn timing aggregation. */
460
+ async getTurnTiming(sessionId, turnSeq, filter) {
461
+ await this.backfillMainSession(sessionId);
462
+ await this.refreshChildren(sessionId, filter);
463
+ return this.store.getTurnTiming(sessionId, turnSeq, filter);
464
+ }
465
+ /** Release every session buffer, listener, and open child-log handle (plugin dispose). */
466
+ dispose() {
467
+ for (const dispose of this.disposers.splice(0))
468
+ dispose();
469
+ this.store.dispose();
470
+ for (const handle of this.handles.values()) {
471
+ void handle.close().catch(() => undefined);
472
+ }
473
+ this.handles.clear();
474
+ this.cursors.clear();
475
+ }
476
+ }
477
+ /** The dispatch-time snapshot of one GenerateOptions (never mutates the frozen input). */
478
+ function snapshotOptions(options) {
479
+ return {
480
+ ...options.sessionId === undefined ? {} : { sessionId: options.sessionId },
481
+ ...options.purpose === undefined ? {} : { purpose: options.purpose },
482
+ config: {
483
+ provider: options.provider,
484
+ model: options.model,
485
+ ...options.reasoningEffort === undefined ? {} : { reasoningEffort: options.reasoningEffort },
486
+ ...options.temperature === undefined ? {} : { temperature: options.temperature },
487
+ ...options.maxTokens === undefined ? {} : { maxTokens: options.maxTokens },
488
+ },
489
+ messages: options.messages.map(fromRequestMessage),
490
+ ...options.system === undefined ? {} : { systemSlot: options.system },
491
+ ...options.tools === undefined ? {} : { tools: options.tools },
492
+ };
493
+ }