@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
package/lib/index.js ADDED
@@ -0,0 +1,3943 @@
1
+ import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
2
+ //#region lib/types/tool-summary.js
3
+ /** Counts calls by exact tool name, retaining first-occurrence order. */
4
+ function countToolCalls(names) {
5
+ const counts = /* @__PURE__ */ new Map();
6
+ for (const name of names) counts.set(name, (counts.get(name) ?? 0) + 1);
7
+ return [...counts].map(([name, count]) => ({
8
+ name,
9
+ count
10
+ }));
11
+ }
12
+ //#endregion
13
+ //#region lib/types/messages.js
14
+ /**
15
+ * User-role producers that ride along every turn rather than start one: a
16
+ * turn with no human message is attributed to its first message from any
17
+ * other producer (a room delivery, a schedule, a plugin trigger).
18
+ */
19
+ const AMBIENT_SOURCES = /* @__PURE__ */ new Set([
20
+ "user",
21
+ "runtime-context",
22
+ "skill-catalog",
23
+ "skill-invocation",
24
+ "repeat-tool-reminder",
25
+ "compact-checkpoint"
26
+ ]);
27
+ /**
28
+ * The trigger preview: injected triggers lead with standing instructions and
29
+ * end with what this turn delivers (a report, a forwarded message), so the
30
+ * last paragraph is the part that differs turn to turn.
31
+ */
32
+ function lastParagraph(text) {
33
+ return previewLine(text.split(/\n\s*\n/).map((p) => p.trim()).filter((p) => p.length > 0).at(-1) ?? text, 120);
34
+ }
35
+ /** Collapse whitespace for the single-line preview, capped at `max` chars. */
36
+ function previewLine(text, max = 80) {
37
+ const collapsed = text.replace(/\s+/g, " ").trim();
38
+ return collapsed.length <= max ? collapsed : `${collapsed.slice(0, max - 1)}…`;
39
+ }
40
+ /** Model-facing text of non-assistant content: text verbatim, file/image as placeholder lines. */
41
+ function plainText(blocks) {
42
+ const parts = [];
43
+ for (const block of blocks) switch (block.type) {
44
+ case "text":
45
+ parts.push(block.text ?? "");
46
+ break;
47
+ case "file":
48
+ parts.push(`[file: ${block.ref ?? "attachment"}]`);
49
+ break;
50
+ case "image": parts.push(`[image${block.ref === void 0 ? "" : `: ${block.ref}`}]`);
51
+ }
52
+ return parts.join("\n");
53
+ }
54
+ /**
55
+ * The verbatim wire form of one message — what `SegmentItem.full` carries.
56
+ * Assistant rows serialize to a JSON string `{reasoning?, text?, calls?}`
57
+ * (calls keep the raw argument JSON), the shape the reference panel renders;
58
+ * every other role is its plain text. The content is pre-projection: the
59
+ * waterfall sees file/image blocks before the adapter rewrites them to handle
60
+ * text, so they appear as bracketed placeholders.
61
+ */
62
+ function serializeMessage(message) {
63
+ if (message.role === "assistant") {
64
+ const reasoning = message.content.filter((block) => block.type === "reasoning").map((block) => block.text ?? "").join("");
65
+ const text = message.content.filter((block) => block.type === "text").map((block) => block.text ?? "").join("");
66
+ const calls = message.content.filter((block) => block.type === "tool-call").map((block) => ({
67
+ id: block.id ?? "",
68
+ name: block.name ?? "",
69
+ arguments: block.arguments ?? ""
70
+ }));
71
+ return JSON.stringify({
72
+ ...reasoning.length > 0 ? { reasoning } : {},
73
+ ...text.length > 0 ? { text } : {},
74
+ ...calls.length > 0 ? { calls } : {}
75
+ });
76
+ }
77
+ if (message.role === "developer") return JSON.stringify(message.content);
78
+ return plainText(message.content);
79
+ }
80
+ /** Tool names one assistant message invoked (the chip on assistant rows). */
81
+ function toolCallNames(message) {
82
+ return message.content.filter((block) => block.type === "tool-call").map((block) => block.name ?? "").filter((name) => name.length > 0);
83
+ }
84
+ /** Single-line summary of one message for the collapsed row. */
85
+ function previewMessage(message) {
86
+ if (message.role === "assistant") {
87
+ const names = toolCallNames(message);
88
+ if (names.length > 0) return `→ ${names.join(", ")}`;
89
+ return previewLine(message.content.filter((block) => block.type === "text").map((block) => block.text ?? "").join(""));
90
+ }
91
+ if (message.role === "developer") {
92
+ const changes = message.content.map((block) => block.type === "tool-addition" ? `+${block.toolName ?? ""}` : block.type === "tool-removal" ? `-${block.toolName ?? ""}` : "").filter((part) => part.length > 1);
93
+ return previewLine(changes.length > 0 ? `tools ${changes.join(" ")}` : serializeMessage(message));
94
+ }
95
+ return previewLine(plainText(message.content));
96
+ }
97
+ /**
98
+ * Classify one message into its panel category. Role decides first; user-role
99
+ * rows refine by producer `kind`, with `form: 'snapshot'` as the fallback
100
+ * signal for runtime snapshots from producers the explicit map predates.
101
+ */
102
+ function classifyMessage(message) {
103
+ switch (message.role) {
104
+ case "system": return "system";
105
+ case "assistant": return "assistant";
106
+ case "tool": return "tool-result";
107
+ case "developer": return "tool-change";
108
+ }
109
+ switch (message.sourceKind) {
110
+ case "user": return "user";
111
+ case "runtime-context": return "runtime-snapshot";
112
+ case "skill-catalog":
113
+ case "skill-invocation": return "skill-injection";
114
+ case "compact-checkpoint": return "compaction";
115
+ case "repeat-tool-reminder": return "reminder-injection";
116
+ default: return message.sourceForm === "snapshot" ? "runtime-snapshot" : "plugin-injection";
117
+ }
118
+ }
119
+ /** The label a row shows as its origin — producer kind, refined where the panel knows more. */
120
+ function messageSourceLabel(message, toolName) {
121
+ if (message.role === "tool" && toolName !== void 0) return toolName;
122
+ return message.sourceKind;
123
+ }
124
+ /**
125
+ * Diff identity for one message: the durable id when present, else a content
126
+ * hash. Tool-result pruning preserves ids while changing content; the hash
127
+ * fallback keeps hand-built id-less inputs (aux requests) diffable.
128
+ */
129
+ function messageKey(message, hash) {
130
+ return `${message.id === void 0 ? "h" : `id:${message.id}`}:${hash(JSON.stringify({
131
+ ..."turn" in message && typeof message.turn === "number" ? { sourceTurn: message.turn } : {},
132
+ role: message.role,
133
+ kind: message.sourceKind,
134
+ content: message.content,
135
+ ...message.toolCallId === void 0 ? {} : { toolCallId: message.toolCallId }
136
+ }))}`;
137
+ }
138
+ /** Normalize one typed waterfall message (loop-built or hand-built). Never mutates the input. */
139
+ function fromRequestMessage(message) {
140
+ const content = message.content.map(normalizeBlock);
141
+ const id = "id" in message && typeof message.id === "string" ? message.id : void 0;
142
+ const source = "source" in message && typeof message.source === "object" ? message.source : void 0;
143
+ const base = {
144
+ ...id === void 0 ? {} : { id },
145
+ ..."turn" in message && typeof message.turn === "number" ? { sourceTurn: message.turn } : {},
146
+ role: message.role,
147
+ ...source?.compactionId === void 0 ? {} : { compactionId: source.compactionId },
148
+ sourceKind: source?.kind ?? "user",
149
+ ...source?.form === void 0 ? {} : { sourceForm: source.form },
150
+ content
151
+ };
152
+ if (message.role === "tool") return {
153
+ ...base,
154
+ toolCallId: message.toolCallId,
155
+ ...message.isError === void 0 ? {} : { isError: message.isError }
156
+ };
157
+ return base;
158
+ }
159
+ /** Flatten one typed content block. Unknown block types keep only their tag. */
160
+ function normalizeBlock(block) {
161
+ switch (block.type) {
162
+ case "text":
163
+ case "reasoning": return {
164
+ type: block.type,
165
+ text: block.text
166
+ };
167
+ case "tool-call": return {
168
+ type: "tool-call",
169
+ id: block.id,
170
+ name: block.name,
171
+ arguments: block.arguments
172
+ };
173
+ case "tool-addition":
174
+ case "tool-removal": return {
175
+ type: block.type,
176
+ toolName: block.toolName
177
+ };
178
+ case "file":
179
+ case "image": {
180
+ const ref = attachmentLabel(block.attachment);
181
+ return ref === void 0 ? { type: block.type } : {
182
+ type: block.type,
183
+ ref
184
+ };
185
+ }
186
+ default: return { type: block.type };
187
+ }
188
+ }
189
+ /** Best-effort display label out of an attachment reference. */
190
+ function attachmentLabel(attachment) {
191
+ if (typeof attachment !== "object" || attachment === null) return void 0;
192
+ const name = attachment.name;
193
+ return typeof name === "string" && name.length > 0 ? name : void 0;
194
+ }
195
+ /** Structural read of one log-lane message (untrusted JSON). Returns undefined for unreadable rows. */
196
+ function fromLogMessage(raw) {
197
+ if (typeof raw !== "object" || raw === null) return void 0;
198
+ const value = raw;
199
+ const role = value["role"];
200
+ if (role !== "system" && role !== "user" && role !== "assistant" && role !== "tool" && role !== "developer") return;
201
+ const content = Array.isArray(value["content"]) ? value["content"].map(readBlock).filter(isDefined) : [];
202
+ const source = typeof value["source"] === "object" && value["source"] !== null ? value["source"] : void 0;
203
+ const sourceKind = typeof source?.["kind"] === "string" ? source["kind"] : role === "user" ? "user" : role;
204
+ return {
205
+ ...typeof value["id"] === "string" ? { id: value["id"] } : {},
206
+ ...typeof value["turn"] === "number" ? { sourceTurn: value["turn"] } : {},
207
+ role,
208
+ sourceKind,
209
+ ...typeof source?.["compactionId"] === "string" ? { compactionId: source["compactionId"] } : {},
210
+ ...typeof source?.["form"] === "string" ? { sourceForm: source["form"] } : {},
211
+ content,
212
+ ...typeof value["toolCallId"] === "string" ? { toolCallId: value["toolCallId"] } : {},
213
+ ...value["isError"] === void 0 ? {} : { isError: value["isError"] === true }
214
+ };
215
+ }
216
+ /** Structural read of one log-lane content block. */
217
+ function readBlock(raw) {
218
+ if (typeof raw !== "object" || raw === null) return void 0;
219
+ const value = raw;
220
+ const type = value["type"];
221
+ if (typeof type !== "string") return void 0;
222
+ return {
223
+ type,
224
+ ...typeof value["text"] === "string" ? { text: value["text"] } : {},
225
+ ...typeof value["id"] === "string" ? { id: value["id"] } : {},
226
+ ...typeof value["name"] === "string" ? { name: value["name"] } : {},
227
+ ...typeof value["arguments"] === "string" ? { arguments: value["arguments"] } : {},
228
+ ...typeof value["toolName"] === "string" ? { toolName: value["toolName"] } : {}
229
+ };
230
+ }
231
+ function isDefined(value) {
232
+ return value !== void 0;
233
+ }
234
+ const VARIABLE_REFERENCE = /\{\{([a-z][a-z0-9_]*)\}\}/g;
235
+ /**
236
+ * Interpolate one section the way the host's renderer does, minus its error
237
+ * paths: a reference whose variable is missing keeps its literal form so the
238
+ * rendered text still lines up with whatever the host produced (or simply
239
+ * fails to match, degrading attribution to a single row).
240
+ */
241
+ function interpolateSection(text, variables) {
242
+ return text.replace(VARIABLE_REFERENCE, (raw, name) => variables[name] ?? raw);
243
+ }
244
+ /**
245
+ * Record one post-waterfall assembly into the ring. Adjacent duplicates (a
246
+ * listener-free re-assembly between steps) collapse to one entry.
247
+ */
248
+ function recordAssembly(ring, assembly, at) {
249
+ const sections = assembly.sections.map((section) => ({
250
+ name: section.name,
251
+ text: section.interpolate === false ? section.text : interpolateSection(section.text, assembly.variables)
252
+ })).filter((section) => section.text.length > 0);
253
+ const rendered = sections.map((section) => section.text).join("\n\n");
254
+ const last = ring[ring.length - 1];
255
+ if (last !== void 0 && last.rendered === rendered) {
256
+ last.at = at;
257
+ return;
258
+ }
259
+ ring.push({
260
+ rendered,
261
+ sections,
262
+ at
263
+ });
264
+ if (ring.length > 32) ring.splice(0, ring.length - 32);
265
+ }
266
+ /** Split one request's payload into the four segments. Pure: no I/O, no clock. */
267
+ function splitRequest(input) {
268
+ const { messages, context } = input;
269
+ const keys = messages.map((message) => messageKey(message, contentHash));
270
+ const freshnessByIndex = messageFreshness(keys, context.previous);
271
+ const previousToolNames = new Set(context.previous?.toolNames ?? []);
272
+ const system = [];
273
+ if (input.systemSlot !== void 0 && input.systemSlot.length > 0) system.push({
274
+ index: -1,
275
+ category: "system",
276
+ source: "system",
277
+ preview: previewLine(input.systemSlot),
278
+ full: input.systemSlot,
279
+ chars: input.systemSlot.length,
280
+ freshness: context.previous?.systemSlot === input.systemSlot ? "history" : "new"
281
+ });
282
+ const history = [];
283
+ const added = [];
284
+ messages.forEach((message, index) => {
285
+ const freshness = freshnessByIndex[index];
286
+ if (message.role === "system") {
287
+ system.push(...attributeSystem(message, index, freshness, context.assemblies));
288
+ return;
289
+ }
290
+ const full = serializeMessage(message);
291
+ const toolCalls = message.role === "assistant" ? toolCallNames(message) : [];
292
+ const durationMs = message.role === "assistant" && message.id !== void 0 ? context.durationFor?.(message.id) : void 0;
293
+ const sourceTurn = message.sourceTurn ?? (message.id === void 0 ? void 0 : context.turnFor?.(message.id));
294
+ const item = {
295
+ index,
296
+ ...message.id === void 0 ? {} : { messageId: message.id },
297
+ ...message.compactionId === void 0 ? {} : { compactionId: message.compactionId },
298
+ ...message.pruning === void 0 ? {} : { pruning: message.pruning },
299
+ category: classifyMessage(message),
300
+ source: messageSourceLabel(message, message.toolCallId === void 0 ? void 0 : context.resolveToolName?.(message.toolCallId)),
301
+ ...toolCalls.length > 0 ? { toolCalls } : {},
302
+ preview: previewMessage(message),
303
+ full,
304
+ chars: full.length,
305
+ ...durationMs === void 0 ? {} : { durationMs },
306
+ ...message.isError === void 0 ? {} : { isError: message.isError },
307
+ ...message.toolCallId === void 0 ? {} : { toolCallId: message.toolCallId },
308
+ ...sourceTurn === void 0 ? {} : { sourceTurn },
309
+ freshness
310
+ };
311
+ (freshness === "history" ? history : added).push(item);
312
+ });
313
+ const tools = (input.tools ?? []).map((tool) => {
314
+ const parameters = JSON.stringify(tool.parameters, null, 1);
315
+ return {
316
+ name: tool.name,
317
+ description: tool.description,
318
+ parameters,
319
+ chars: tool.name.length + tool.description.length + parameters.length,
320
+ added: context.previous !== void 0 && !previousToolNames.has(tool.name)
321
+ };
322
+ });
323
+ const totalChars = system.reduce((n, item) => n + item.chars, 0) + tools.reduce((n, item) => n + item.chars, 0) + history.reduce((n, item) => n + item.chars, 0) + added.reduce((n, item) => n + item.chars, 0);
324
+ return {
325
+ system,
326
+ tools,
327
+ history,
328
+ added,
329
+ fingerprint: {
330
+ messageKeys: keys,
331
+ toolNames: (input.tools ?? []).map((tool) => tool.name),
332
+ ...input.systemSlot === void 0 ? {} : { systemSlot: input.systemSlot }
333
+ },
334
+ totalChars
335
+ };
336
+ }
337
+ /** Match occurrences, preserving unchanged tails across surface replacements. */
338
+ function messageFreshness(keys, previous) {
339
+ const remaining = /* @__PURE__ */ new Map();
340
+ for (const key of previous?.messageKeys ?? []) remaining.set(key, (remaining.get(key) ?? 0) + 1);
341
+ return keys.map((key) => {
342
+ const count = remaining.get(key) ?? 0;
343
+ if (count > 0) remaining.set(key, count - 1);
344
+ return count > 0 ? "history" : "new";
345
+ });
346
+ }
347
+ /** A late log backfill can supply the baseline that was missing at capture. */
348
+ function rebaseSegments(segments, current, previous) {
349
+ const freshness = messageFreshness(current.messageKeys, previous);
350
+ const refresh = (item) => ({
351
+ ...item,
352
+ freshness: item.index === -1 ? current.systemSlot === previous.systemSlot ? "history" : "new" : freshness[item.index] ?? item.freshness
353
+ });
354
+ const messages = [...segments.history, ...segments.added].sort((a, b) => a.index - b.index).map(refresh);
355
+ const tools = new Set(previous.toolNames);
356
+ return {
357
+ system: segments.system.map(refresh),
358
+ tools: segments.tools.map((tool) => ({
359
+ ...tool,
360
+ added: !tools.has(tool.name)
361
+ })),
362
+ history: messages.filter((item) => item.freshness === "history"),
363
+ added: messages.filter((item) => item.freshness === "new")
364
+ };
365
+ }
366
+ /** Attribute one system message to assembly sections when its text matches a rendered assembly exactly. */
367
+ function attributeSystem(message, index, freshness, assemblies) {
368
+ const full = serializeMessage(message);
369
+ for (let i = assemblies.length - 1; i >= 0; i -= 1) {
370
+ const assembly = assemblies[i];
371
+ if (assembly === void 0 || assembly.rendered !== full || assembly.sections.length === 0) continue;
372
+ return assembly.sections.map((section) => ({
373
+ index,
374
+ ...message.id === void 0 ? {} : { messageId: message.id },
375
+ ...message.compactionId === void 0 ? {} : { compactionId: message.compactionId },
376
+ ...message.pruning === void 0 ? {} : { pruning: message.pruning },
377
+ category: "system",
378
+ source: section.name,
379
+ preview: previewLine(section.text),
380
+ full: section.text,
381
+ chars: section.text.length,
382
+ ...message.isError === void 0 ? {} : { isError: message.isError },
383
+ ...message.toolCallId === void 0 ? {} : { toolCallId: message.toolCallId },
384
+ ...message.sourceTurn === void 0 ? {} : { sourceTurn: message.sourceTurn },
385
+ freshness
386
+ }));
387
+ }
388
+ return [{
389
+ index,
390
+ ...message.id === void 0 ? {} : { messageId: message.id },
391
+ category: "system",
392
+ source: message.sourceKind,
393
+ preview: previewLine(full),
394
+ full,
395
+ chars: full.length,
396
+ freshness
397
+ }];
398
+ }
399
+ /** FNV-1a 32-bit — enough to keep id-less messages diffable, no crypto import needed. */
400
+ function contentHash(text) {
401
+ let hash = 2166136261;
402
+ for (let i = 0; i < text.length; i += 1) {
403
+ hash ^= text.charCodeAt(i);
404
+ hash = Math.imul(hash, 16777619);
405
+ }
406
+ return (hash >>> 0).toString(36);
407
+ }
408
+ //#endregion
409
+ //#region lib/types/prefix-diff.js
410
+ /** Linear comparison on full text; never infer a change from already-truncated previews. */
411
+ function prefixTextChange(before, after) {
412
+ const a = Array.from(before), b = Array.from(after);
413
+ let start = 0, end = 0;
414
+ while (start < a.length && start < b.length && a[start] === b[start]) start++;
415
+ while (end < a.length - start && end < b.length - start && a[a.length - end - 1] === b[b.length - end - 1]) end++;
416
+ return {
417
+ offset: start,
418
+ leading: a.slice(Math.max(0, start - 48), start).join(""),
419
+ trailing: end ? a.slice(a.length - end, a.length - end + 48).join("") : "",
420
+ leadingOmitted: start > 48,
421
+ trailingOmitted: end > 48,
422
+ removed: a.slice(start, Math.min(a.length - end, start + 160)).join(""),
423
+ added: b.slice(start, Math.min(b.length - end, start + 160)).join(""),
424
+ removedChars: a.length - start - end,
425
+ addedChars: b.length - start - end
426
+ };
427
+ }
428
+ //#endregion
429
+ //#region lib/types/changes.js
430
+ /**
431
+ * The change list between two consecutive requests of one scope. Every entry
432
+ * is a fact the host recorded or an exact comparison — never an inference:
433
+ *
434
+ * - Messages pair by durable id (content hash for id-less ones): ids only in
435
+ * the previous request were removed, ids only in the current one were added,
436
+ * a shared id whose text differs was rewritten in place.
437
+ * - A surface replacement event (`surfaceOp: replace`) names the ids it
438
+ * shadowed and the id it put in their place; that pairing — and its
439
+ * producer's `kind · op` — comes from the {@link ReplacementFact} registry,
440
+ * fed by the live `session/event` lane and by log backfill alike. The
441
+ * metering event the host appends right before a replacement
442
+ * (`compaction/summary`, `compaction/prune`) types it as a compaction or a
443
+ * prune. Without a fact the same rows stay an honest remove + add.
444
+ * - System rows group into one entry (the user's question is "did the system
445
+ * prompt change"), diffed head against head, with a checkable equality when
446
+ * the new head repeats a removed in-history system update verbatim.
447
+ * - Unexplained additions after the last kept row are the ordinary tail
448
+ * append, summarized rather than listed.
449
+ *
450
+ * @module @khorsheed/dsh-context-lens/changes
451
+ */
452
+ /** Read the meter a `compaction/*` row announces for the next replacement. */
453
+ function meterOf(row) {
454
+ if (row.type === "compaction/prune") return { kind: "prune" };
455
+ if (row.type !== "compaction/summary") return void 0;
456
+ const id = record(row.data)?.["compactionId"];
457
+ return {
458
+ kind: "compaction",
459
+ ...typeof id === "string" ? { compactionId: id } : {}
460
+ };
461
+ }
462
+ function record(value) {
463
+ return typeof value === "object" && value !== null ? value : void 0;
464
+ }
465
+ /** The message a surface event carries: `data.message` for step events, `data` itself for user messages. */
466
+ function messageOfRow(row) {
467
+ const data = record(row.data);
468
+ if (data === void 0) return void 0;
469
+ const message = record(data["message"]) ?? data;
470
+ const id = message["id"];
471
+ const source = record(message["source"]);
472
+ return {
473
+ ...typeof id === "string" ? { id } : {},
474
+ ...source === void 0 ? {} : { source }
475
+ };
476
+ }
477
+ /**
478
+ * The fact one replacement row records, or undefined for appends and rows
479
+ * whose ids cannot be read.
480
+ * @param idAt - resolve a shadowed log seq to its message id.
481
+ * @param meter - the metering row immediately before this one, if any.
482
+ */
483
+ function replacementFact(row, idAt, meter) {
484
+ const op = record(row.surfaceOp);
485
+ if (op === void 0 || op["op"] !== "replace") return void 0;
486
+ const message = messageOfRow(row);
487
+ if (message?.id === void 0) return void 0;
488
+ const seqs = row.sourceEventSeqs !== void 0 && row.sourceEventSeqs.length > 0 ? [...row.sourceEventSeqs] : typeof op["startSeq"] === "number" && typeof op["endSeq"] === "number" && op["endSeq"] >= op["startSeq"] ? Array.from({ length: op["endSeq"] - op["startSeq"] + 1 }, (_, i) => op["startSeq"] + i) : [];
489
+ const replacedIds = seqs.map(idAt).filter((id) => id !== void 0);
490
+ const sourceKind = typeof message.source?.["kind"] === "string" ? message.source["kind"] : row.type;
491
+ const sourceOp = typeof message.source?.["op"] === "string" ? message.source["op"] : void 0;
492
+ const compactionId = typeof message.source?.["compactionId"] === "string" ? message.source["compactionId"] : meter?.compactionId;
493
+ const kind = meter?.kind ?? (compactionId !== void 0 ? "compaction" : "replace");
494
+ return {
495
+ newId: message.id,
496
+ replacedIds,
497
+ kind,
498
+ sourceKind,
499
+ ...sourceOp === void 0 ? {} : { sourceOp },
500
+ ...row.seq === void 0 ? {} : { eventSeq: row.seq },
501
+ ...seqs.length === 0 ? {} : { replacedSeqs: seqs },
502
+ ...kind === "compaction" && compactionId !== void 0 ? { compactionId } : {}
503
+ };
504
+ }
505
+ /** Every replacement fact of one whole log (the backfill path). */
506
+ function replacementFactsOf(rows) {
507
+ const ids = /* @__PURE__ */ new Map();
508
+ const facts = [];
509
+ let meter;
510
+ for (const row of rows) {
511
+ const fact = replacementFact(row, (seq) => ids.get(seq), meter);
512
+ if (fact !== void 0) facts.push(fact);
513
+ const id = messageOfRow(row)?.id;
514
+ if (row.seq !== void 0 && id !== void 0) ids.set(row.seq, id);
515
+ meter = meterOf(row);
516
+ }
517
+ return facts;
518
+ }
519
+ function messagesOf(rows) {
520
+ const byIndex = /* @__PURE__ */ new Map();
521
+ for (const row of rows) byIndex.set(row.index, [...byIndex.get(row.index) ?? [], row]);
522
+ const seen = /* @__PURE__ */ new Map();
523
+ return [...byIndex.entries()].sort((a, b) => a[0] - b[0]).map(([index, parts]) => {
524
+ const first = parts[0];
525
+ const sectioned = parts.length > 1;
526
+ const sections = [];
527
+ let cursor = 0;
528
+ for (const part of parts) {
529
+ sections.push({
530
+ name: part.source,
531
+ start: cursor,
532
+ end: cursor + Array.from(part.full).length
533
+ });
534
+ cursor += Array.from(part.full).length + 2;
535
+ }
536
+ const text = parts.map((part) => part.full).join("\n\n");
537
+ const id = parts.find((part) => part.messageId !== void 0)?.messageId;
538
+ const base = id === void 0 ? `h:${first.category}:${text.length}:${hashText(text)}` : `id:${id}`;
539
+ const n = seen.get(base) ?? 0;
540
+ seen.set(base, n + 1);
541
+ return {
542
+ index,
543
+ key: n === 0 ? base : `${base}#${n}`,
544
+ ...id === void 0 ? {} : { id },
545
+ category: first.category,
546
+ source: first.category === "system" && sectioned ? "system-prompt" : first.source,
547
+ text,
548
+ sections: sectioned ? sections : []
549
+ };
550
+ });
551
+ }
552
+ function hashText(text) {
553
+ let hash = 2166136261;
554
+ for (let i = 0; i < text.length; i += 1) {
555
+ hash ^= text.charCodeAt(i);
556
+ hash = Math.imul(hash, 16777619);
557
+ }
558
+ return (hash >>> 0).toString(36);
559
+ }
560
+ function rowOf(message) {
561
+ return {
562
+ index: message.index,
563
+ ...message.id === void 0 ? {} : { messageId: message.id },
564
+ category: message.category,
565
+ source: message.source,
566
+ preview: message.text.replace(/\s+/g, " ").trim().slice(0, 120),
567
+ chars: Array.from(message.text).length
568
+ };
569
+ }
570
+ function sectionAt(message, offset) {
571
+ return message.sections.find((section) => offset >= section.start && offset <= section.end)?.name;
572
+ }
573
+ /** Contiguous runs (by index) of rows; system and non-system rows never share a run. */
574
+ function runs(messages) {
575
+ const out = [];
576
+ for (const message of messages) {
577
+ const last = out[out.length - 1];
578
+ const tail = last?.[last.length - 1];
579
+ if (last !== void 0 && tail !== void 0 && message.index === tail.index + 1 && message.category === "system" === (tail.category === "system")) last.push(message);
580
+ else out.push([message]);
581
+ }
582
+ return out;
583
+ }
584
+ /**
585
+ * Diff two consecutive requests.
586
+ * @param breakIndex - the message index where the exact prefix comparison first differs, if it does.
587
+ */
588
+ function diffRequests(previous, current, facts, breakIndex) {
589
+ const prev = messagesOf(previous.rows), cur = messagesOf(current.rows);
590
+ const prevByKey = new Map(prev.map((m) => [m.key, m])), curByKey = new Map(cur.map((m) => [m.key, m]));
591
+ const removed = prev.filter((m) => !curByKey.has(m.key));
592
+ const added = cur.filter((m) => !prevByKey.has(m.key));
593
+ const rewritten = cur.filter((m) => m.id !== void 0 && prevByKey.has(m.key) && prevByKey.get(m.key).text !== m.text);
594
+ const resolve = (ids, depth = 0) => ids.flatMap((id) => {
595
+ if (prev.some((m) => m.id === id) || depth > 8) return [id];
596
+ const inner = facts.byNewId(id);
597
+ return inner === void 0 || inner.newId === id ? [id] : resolve(inner.replacedIds, depth + 1);
598
+ });
599
+ const explained = /* @__PURE__ */ new Set();
600
+ const changes = [];
601
+ const seenFacts = /* @__PURE__ */ new Set();
602
+ const candidates = [];
603
+ const want = (fact) => {
604
+ if (fact === void 0 || seenFacts.has(fact.newId)) return;
605
+ seenFacts.add(fact.newId);
606
+ candidates.push(fact);
607
+ };
608
+ for (const m of added) if (m.id !== void 0) want(facts.byNewId(m.id));
609
+ for (const m of rewritten) want(facts.byNewId(m.id));
610
+ for (const m of removed) if (m.id !== void 0) want(facts.byReplacedId(m.id));
611
+ for (const fact of candidates) {
612
+ const shadowed = new Set(resolve(fact.replacedIds));
613
+ const gone = removed.filter((m) => m.id !== void 0 && shadowed.has(m.id) && !explained.has(m.key));
614
+ const fresh = cur.filter((m) => m.id === fact.newId);
615
+ const inPlace = fresh.filter((m) => prevByKey.has(m.key));
616
+ const freshAdded = fresh.filter((m) => !prevByKey.has(m.key));
617
+ if (gone.length === 0 && freshAdded.length === 0 && inPlace.length === 0) continue;
618
+ for (const m of [
619
+ ...gone,
620
+ ...freshAdded,
621
+ ...inPlace
622
+ ]) explained.add(m.key);
623
+ const before = inPlace.length > 0 ? prevByKey.get(inPlace[0].key) : gone.length === 1 ? gone[0] : void 0;
624
+ const after = inPlace[0] ?? freshAdded[0];
625
+ const text = before !== void 0 && after !== void 0 ? prefixTextChange(before.text, after.text) : void 0;
626
+ changes.push({
627
+ kind: fact.kind,
628
+ removed: inPlace.length > 0 ? inPlace.map((m) => rowOf(prevByKey.get(m.key))) : gone.map(rowOf),
629
+ added: [...inPlace, ...freshAdded].map(rowOf),
630
+ source: {
631
+ kind: fact.sourceKind,
632
+ ...fact.sourceOp === void 0 ? {} : { op: fact.sourceOp }
633
+ },
634
+ ...fact.eventSeq === void 0 ? {} : { eventSeq: fact.eventSeq },
635
+ ...fact.replacedSeqs === void 0 ? {} : { replacedSeqs: fact.replacedSeqs },
636
+ ...fact.compactionId === void 0 ? {} : { compactionId: fact.compactionId },
637
+ ...text === void 0 ? {} : { text }
638
+ });
639
+ }
640
+ for (const m of rewritten) {
641
+ if (explained.has(m.key)) continue;
642
+ explained.add(m.key);
643
+ const before = prevByKey.get(m.key);
644
+ changes.push({
645
+ kind: "replace",
646
+ removed: [rowOf(before)],
647
+ added: [rowOf(m)],
648
+ text: prefixTextChange(before.text, m.text)
649
+ });
650
+ }
651
+ const keptPrev = prev.filter((m) => curByKey.has(m.key));
652
+ const keptCur = cur.filter((m) => prevByKey.has(m.key));
653
+ const lastKeptPrev = Math.max(-1, ...keptPrev.map((m) => m.index));
654
+ const anchor = Math.max(-1, ...keptCur.map((m) => m.index), ...cur.filter((m) => explained.has(m.key)).map((m) => m.index));
655
+ const looseRemoved = removed.filter((m) => !explained.has(m.key));
656
+ const looseAdded = added.filter((m) => !explained.has(m.key));
657
+ const tailRows = looseRemoved.some((m) => m.index > lastKeptPrev) ? [] : looseAdded.filter((m) => m.index > anchor && m.category !== "system");
658
+ const tailKeys = new Set(tailRows.map((m) => m.key));
659
+ for (const run of runs(looseRemoved)) changes.push({
660
+ kind: "remove",
661
+ removed: run.map(rowOf),
662
+ added: []
663
+ });
664
+ for (const run of runs(looseAdded.filter((m) => !tailKeys.has(m.key)))) changes.push({
665
+ kind: "add",
666
+ removed: [],
667
+ added: run.map(rowOf)
668
+ });
669
+ const grouped = groupSystem(changes, prev, cur);
670
+ grouped.sort((a, b) => firstIndex(a) - firstIndex(b));
671
+ const tools = diffTools(previous.tools, current.tools);
672
+ const breakAt = breakIndex === void 0 ? void 0 : grouped.find((c) => c.added.some((r) => r.index === breakIndex)) ?? grouped.find((c) => c.removed.some((r) => r.index === breakIndex));
673
+ if (breakAt !== void 0) breakAt.isBreak = true;
674
+ else if (breakIndex === void 0 && tools[0] !== void 0) tools[0].isBreak = true;
675
+ return {
676
+ changes: grouped,
677
+ tools,
678
+ ...tailRows.length === 0 ? {} : { tail: {
679
+ count: tailRows.length,
680
+ sources: [...new Set(tailRows.map((m) => m.source))]
681
+ } }
682
+ };
683
+ }
684
+ function firstIndex(change) {
685
+ return Math.min(...change.added.map((r) => r.index), ...change.removed.map((r) => r.index));
686
+ }
687
+ /**
688
+ * Fold every all-system change into one entry. The text diff compares the
689
+ * head system message of both requests (the cache prefix); when the new head
690
+ * repeats a removed system message verbatim, that equality is recorded.
691
+ */
692
+ function groupSystem(changes, prev, cur) {
693
+ const isSystem = (c) => [...c.removed, ...c.added].length > 0 && [...c.removed, ...c.added].every((r) => r.category === "system");
694
+ const system = changes.filter(isSystem);
695
+ if (system.length === 0) return changes;
696
+ const rest = changes.filter((c) => !isSystem(c));
697
+ const prevHead = prev.find((m) => m.category === "system"), curHead = cur.find((m) => m.category === "system");
698
+ let text;
699
+ let section;
700
+ if (prevHead !== void 0 && curHead !== void 0 && prevHead.index === curHead.index && prevHead.text !== curHead.text) {
701
+ text = prefixTextChange(prevHead.text, curHead.text);
702
+ section = sectionAt(curHead, text.offset);
703
+ } else {
704
+ const effective = (list) => list.filter((m) => m.category === "system" && m.text.length > 0).at(-1);
705
+ const before = effective(prev), after = effective(cur);
706
+ if (before !== void 0 && after !== void 0 && before.text !== after.text) text = prefixTextChange(before.text, after.text);
707
+ }
708
+ const removed = system.flatMap((c) => c.removed);
709
+ const sameAs = curHead === void 0 ? void 0 : removed.find((r) => r.index !== curHead.index && prev.find((m) => m.index === r.index)?.text === curHead.text);
710
+ return [{
711
+ kind: system.some((c) => c.kind !== "add" && c.kind !== "remove") ? "replace" : removed.length > 0 ? "remove" : "add",
712
+ system: true,
713
+ removed,
714
+ added: system.flatMap((c) => c.added),
715
+ source: { kind: "system-prompt" },
716
+ parts: system.map((c) => ({
717
+ kind: c.kind,
718
+ removed: c.removed,
719
+ added: c.added,
720
+ ...c.eventSeq === void 0 ? {} : { eventSeq: c.eventSeq },
721
+ ...c.replacedSeqs === void 0 ? {} : { replacedSeqs: c.replacedSeqs }
722
+ })),
723
+ ...text === void 0 ? {} : { text },
724
+ ...section === void 0 ? {} : { section },
725
+ ...sameAs === void 0 ? {} : { sameAs }
726
+ }, ...rest];
727
+ }
728
+ /** Tool definitions pair by name; description and parameters diff separately. */
729
+ function diffTools(previous, current) {
730
+ const before = new Map(previous.map((tool) => [tool.name, tool]));
731
+ const after = new Set(current.map((tool) => tool.name));
732
+ const out = [];
733
+ for (const tool of current) {
734
+ const old = before.get(tool.name);
735
+ if (old === void 0) {
736
+ out.push({
737
+ name: tool.name,
738
+ change: "added"
739
+ });
740
+ continue;
741
+ }
742
+ const description = old.description === tool.description ? void 0 : prefixTextChange(old.description, tool.description);
743
+ const parameters = old.parameters === tool.parameters ? void 0 : prefixTextChange(old.parameters, tool.parameters);
744
+ if (description === void 0 && parameters === void 0) continue;
745
+ out.push({
746
+ name: tool.name,
747
+ change: "modified",
748
+ ...description === void 0 ? {} : { description },
749
+ ...parameters === void 0 ? {} : { parameters }
750
+ });
751
+ }
752
+ for (const tool of previous) if (!after.has(tool.name)) out.push({
753
+ name: tool.name,
754
+ change: "removed"
755
+ });
756
+ return out;
757
+ }
758
+ //#endregion
759
+ //#region lib/types/room.js
760
+ function emptyRoom() {
761
+ return {
762
+ members: /* @__PURE__ */ new Map(),
763
+ dispatches: [],
764
+ runs: /* @__PURE__ */ new Map(),
765
+ handoffs: [],
766
+ decisions: [],
767
+ seen: /* @__PURE__ */ new Set()
768
+ };
769
+ }
770
+ const str = (value) => typeof value === "string" && value.length > 0 ? value : void 0;
771
+ const num = (value) => typeof value === "number" && Number.isFinite(value) ? value : void 0;
772
+ /**
773
+ * Apply one `room/*` event. Unknown types and malformed payloads are ignored.
774
+ * @returns whether the event was a room event this module reads.
775
+ */
776
+ function applyRoomEvent(room, type, data, seq, at) {
777
+ if (!type.startsWith("room/")) return false;
778
+ if (seq !== void 0) {
779
+ if (room.seen.has(seq)) return true;
780
+ room.seen.add(seq);
781
+ }
782
+ const d = typeof data === "object" && data !== null ? data : {};
783
+ switch (type) {
784
+ case "room/member-added":
785
+ case "room/member-updated": {
786
+ const id = str(d["id"]), name = str(d["name"]);
787
+ if (id !== void 0 && name !== void 0) room.members.set(id, name);
788
+ return true;
789
+ }
790
+ case "room/dispatch": {
791
+ if (d["origin"] !== "human" || seq === void 0 || at === void 0) return true;
792
+ const targets = Array.isArray(d["targets"]) ? d["targets"].filter((t) => typeof t === "string") : [];
793
+ room.dispatches.push({
794
+ seq,
795
+ at,
796
+ text: str(d["text"]) ?? "",
797
+ targets
798
+ });
799
+ return true;
800
+ }
801
+ case "room/run-state": {
802
+ const runId = str(d["runId"]);
803
+ if (runId === void 0) return true;
804
+ const [head, ...rest] = runId.split(":");
805
+ const dispatchSeq = Number(head);
806
+ if (!Number.isSafeInteger(dispatchSeq)) return true;
807
+ const run = room.runs.get(runId) ?? {
808
+ runId,
809
+ dispatchSeq,
810
+ memberId: rest.join(":")
811
+ };
812
+ const member = str(d["member"]), state = str(d["state"]), child = str(d["childSessionId"]);
813
+ const startedAt = num(d["startedAt"]), elapsed = num(d["elapsedMs"]), childTurn = num(d["childTurn"]);
814
+ if (member !== void 0) run.member = member;
815
+ if (state !== void 0) run.state = state;
816
+ if (startedAt !== void 0) run.startedAt = startedAt;
817
+ if (startedAt !== void 0 && elapsed !== void 0) run.endedAt = startedAt + elapsed;
818
+ else if ((state === "done" || state === "failed") && at !== void 0) run.endedAt ??= at;
819
+ if (child !== void 0) run.childSessionId = child;
820
+ if (childTurn !== void 0) run.childTurn = childTurn;
821
+ room.runs.set(runId, run);
822
+ return true;
823
+ }
824
+ case "room/decision": {
825
+ const call = typeof d["call"] === "object" && d["call"] !== null ? d["call"] : void 0;
826
+ const id = str(d["id"]), scenario = str(d["scenario"]), outcome = str(d["outcome"]);
827
+ if (call === void 0 || id === void 0 || scenario === void 0 || outcome === void 0 || seq === void 0 || at === void 0) return true;
828
+ const request = typeof call["request"] === "object" && call["request"] !== null ? call["request"] : {};
829
+ const failure = call["failure"];
830
+ const about = typeof d["about"] === "object" && d["about"] !== null ? d["about"] : {};
831
+ room.decisions.push({
832
+ id,
833
+ seq,
834
+ at,
835
+ scenario,
836
+ outcome,
837
+ model: str(call["model"]) ?? "typesafe",
838
+ about,
839
+ request: {
840
+ state: request["state"] ?? null,
841
+ questions: request["questions"] ?? null
842
+ },
843
+ ...str(d["rule"]) === void 0 ? {} : { rule: str(d["rule"]) },
844
+ ...num(d["probability"]) === void 0 ? {} : { probability: num(d["probability"]) },
845
+ ...num(call["latencyMs"]) === void 0 ? {} : { latencyMs: num(call["latencyMs"]) },
846
+ ...typeof call["cached"] === "boolean" ? { cached: call["cached"] } : {},
847
+ ...num(call["inputTokens"]) === void 0 ? {} : { inputTokens: num(call["inputTokens"]) },
848
+ ...num(call["outputTokens"]) === void 0 ? {} : { outputTokens: num(call["outputTokens"]) },
849
+ ...call["response"] === void 0 ? {} : { response: call["response"] },
850
+ ...typeof failure?.reason === "string" ? { failure: {
851
+ reason: failure.reason,
852
+ ...typeof failure.detail === "string" ? { detail: failure.detail } : {}
853
+ } } : {}
854
+ });
855
+ return true;
856
+ }
857
+ case "room/coordinator": {
858
+ const memberId = str(d["memberId"]);
859
+ if (memberId !== void 0 && seq !== void 0 && at !== void 0) room.handoffs.push({
860
+ seq,
861
+ at,
862
+ memberId
863
+ });
864
+ return true;
865
+ }
866
+ default: return true;
867
+ }
868
+ }
869
+ /** The runs a dispatch started, in start order. */
870
+ function runsOf(room, dispatchSeq) {
871
+ return [...room.runs.values()].filter((run) => run.dispatchSeq === dispatchSeq).sort((a, b) => (a.startedAt ?? 0) - (b.startedAt ?? 0));
872
+ }
873
+ /** A synthetic, stable panel address for a room round (far below any purpose bucket). */
874
+ const ROOM_TURN_BASE = -1e6;
875
+ const roomTurnSeq = (dispatchSeq) => ROOM_TURN_BASE - dispatchSeq;
876
+ //#endregion
877
+ //#region lib/types/store.js
878
+ /**
879
+ * The context-lens capture store: an in-memory ring of sessions → turns →
880
+ * agent scopes → requests, plus the correlation tables the lanes feed.
881
+ *
882
+ * Correlation model:
883
+ * - **Turns** come from `turn/start` / `turn/end` on a root session; a request
884
+ * whose `sessionId` is the root lands in the turn open at dispatch time.
885
+ * - **Aux requests** (`purpose: compaction | session-title | …`) are not
886
+ * conversation steps even when one fires mid-turn: they accumulate in a
887
+ * per-session pseudo-turn keyed by purpose, sorted into the turn list at
888
+ * first sight.
889
+ * - **Scopes**: `main` is the conversation itself; each `subagent/catalog`
890
+ * event opens a child scope pinned to the turn that delegated it. A request
891
+ * whose `sessionId` is a known child joins that scope (in-process drivers);
892
+ * fresh-process children fill the same scope from the log lane instead.
893
+ * - **Tool timing** pairs `tool/call` → `tool/result` by callId and attributes
894
+ * the interval to the request whose response issued the call (callId owner
895
+ * table, fed from tool-call stream blocks).
896
+ *
897
+ * Eviction is turn-granular (never a half turn): per-session turn cap and a
898
+ * global request cap both evict the oldest CLOSED turn; a turn holding a live
899
+ * stream is skipped, so the caps are soft while streams are open.
900
+ *
901
+ * @module @khorsheed/dsh-context-lens/store
902
+ */
903
+ /** Replacement facts kept per session; older facts only matter for requests long evicted. */
904
+ const REPLACEMENT_CAP = 2048;
905
+ const DEFAULT_MAX_TURNS_PER_SESSION = 30;
906
+ const DEFAULT_MAX_BACKFILL_REQUESTS_PER_SESSION = 500;
907
+ const DEFAULT_MAX_STORED_CHARS_TOTAL = 15e7;
908
+ /**
909
+ * Whether one scope passes a read's allow-list: unfiltered when the filter
910
+ * carries no `scopes`, otherwise membership. Shared by the store's query
911
+ * methods and the service's child-log gate.
912
+ */
913
+ function scopeAuthorized(scopeId, filter) {
914
+ return filter?.scopes === void 0 || filter.scopes.includes(scopeId);
915
+ }
916
+ /** The scope's fidelity marker for the wire; absent while it holds no captured content. */
917
+ function scopeOrigin(scope) {
918
+ if (scope.logFilled) return "log-reconstructed";
919
+ if (scope.live || scope.requests.length > 0) return "live";
920
+ }
921
+ /** The in-memory capture store. All mutation enters through the `note*`/`observe*` methods. */
922
+ var InspectorStore = class {
923
+ sessions = /* @__PURE__ */ new Map();
924
+ childToParent = /* @__PURE__ */ new Map();
925
+ /** A session opened on its own that its log header names as a child (`parentSession`). */
926
+ parents = /* @__PURE__ */ new Map();
927
+ /** callId → the request whose response issued the call (exec attribution). */
928
+ callOwners = /* @__PURE__ */ new Map();
929
+ callNames = /* @__PURE__ */ new Map();
930
+ /** Recently observed rendered assemblies (system-section attribution). */
931
+ assemblies = [];
932
+ now;
933
+ onTurnEvicted;
934
+ maxTurnsPerSession;
935
+ maxBackfillRequestsPerSession;
936
+ maxStoredCharsTotal;
937
+ /** The instance epoch stamped on every query result (restart invalidation). */
938
+ generation;
939
+ seqCounter = 0;
940
+ /** Stored segment text characters across all sessions (the memory driver). */
941
+ totalChars = 0;
942
+ constructor(config = {}, deps = {}) {
943
+ this.maxTurnsPerSession = config.maxTurnsPerSession ?? DEFAULT_MAX_TURNS_PER_SESSION;
944
+ this.maxBackfillRequestsPerSession = config.maxBackfillRequestsPerSession ?? DEFAULT_MAX_BACKFILL_REQUESTS_PER_SESSION;
945
+ this.maxStoredCharsTotal = config.maxStoredCharsTotal ?? DEFAULT_MAX_STORED_CHARS_TOTAL;
946
+ this.now = deps.now ?? Date.now;
947
+ this.onTurnEvicted = deps.onTurnEvicted;
948
+ this.generation = deps.generation ?? `${this.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
949
+ }
950
+ /** The shared assembly ring (service feeds it from the assemble waterfall). */
951
+ get assemblyRing() {
952
+ return this.assemblies;
953
+ }
954
+ /** Whether `sessionId` is a known subagent child of some captured session. */
955
+ isChild(sessionId) {
956
+ return this.childToParent.has(sessionId);
957
+ }
958
+ /** The root session one request/event belongs to (children resolve up). */
959
+ rootOf(sessionId) {
960
+ let cursor = sessionId;
961
+ const seen = /* @__PURE__ */ new Set();
962
+ while (this.childToParent.has(cursor) && !seen.has(cursor)) {
963
+ seen.add(cursor);
964
+ cursor = this.childToParent.get(cursor);
965
+ }
966
+ return cursor;
967
+ }
968
+ session(id) {
969
+ let record = this.sessions.get(id);
970
+ if (record === void 0) {
971
+ record = {
972
+ id,
973
+ turns: [],
974
+ activeCompactions: /* @__PURE__ */ new Map(),
975
+ prunings: /* @__PURE__ */ new Map(),
976
+ replacements: /* @__PURE__ */ new Map(),
977
+ lastAccess: this.now(),
978
+ turnSpans: /* @__PURE__ */ new Map(),
979
+ replacedBy: /* @__PURE__ */ new Map(),
980
+ openTurn: void 0,
981
+ buckets: /* @__PURE__ */ new Map(),
982
+ nextBucketSeq: -1,
983
+ nextScopeSeq: 1,
984
+ backfillState: "none",
985
+ backfillReason: void 0,
986
+ backfilledRequests: 0,
987
+ pendingCalls: /* @__PURE__ */ new Map(),
988
+ messageTurns: /* @__PURE__ */ new Map(),
989
+ messageDurations: /* @__PURE__ */ new Map(),
990
+ evictedTurns: 0,
991
+ nextRequestNumbers: /* @__PURE__ */ new Map()
992
+ };
993
+ this.sessions.set(id, record);
994
+ }
995
+ return record;
996
+ }
997
+ /** Allocate one turn row; the caller sets the address (`turnSeq`) in `init`. */
998
+ mintTurn(session, at, init) {
999
+ const turn = {
1000
+ liveContent: false,
1001
+ query: "",
1002
+ startedAt: at,
1003
+ scopes: /* @__PURE__ */ new Map(),
1004
+ ...init
1005
+ };
1006
+ session.turns.push(turn);
1007
+ return turn;
1008
+ }
1009
+ mainScope(session, turn, at) {
1010
+ let scope = turn.scopes.get("main");
1011
+ if (scope === void 0) {
1012
+ scope = {
1013
+ scopeId: "main",
1014
+ scopeSeq: session.nextScopeSeq,
1015
+ kind: "main",
1016
+ status: "running",
1017
+ config: {},
1018
+ requests: [],
1019
+ startedAt: at,
1020
+ live: false,
1021
+ logFilled: false
1022
+ };
1023
+ session.nextScopeSeq += 1;
1024
+ turn.scopes.set("main", scope);
1025
+ }
1026
+ return scope;
1027
+ }
1028
+ /** Observe durable compression boundaries without manufacturing model calls. */
1029
+ noteCompaction(sessionId, type, data) {
1030
+ const session = this.session(this.rootOf(sessionId));
1031
+ const id = data["compactionId"];
1032
+ if (typeof id !== "string") return;
1033
+ if (type === "compaction/start") session.activeCompactions.set(sessionId, {
1034
+ id,
1035
+ ...typeof data["turn"] === "number" ? { hostTurn: data["turn"] } : {}
1036
+ });
1037
+ if (type === "compaction/end") session.activeCompactions.delete(sessionId);
1038
+ if (type === "compaction/summary" || type === "compaction/applied") for (const turn of session.turns) for (const scope of turn.scopes.values()) for (const request of scope.requests) {
1039
+ if (request.compaction?.id !== id) continue;
1040
+ if (Array.isArray(data["shadowedSeqs"])) request.compaction.replacedCount = data["shadowedSeqs"].length;
1041
+ if (type === "compaction/applied") request.compaction.applied = true;
1042
+ }
1043
+ }
1044
+ /** Preserve pruning metadata across later requests, with bounded observation state. */
1045
+ notePruning(sessionId, messageId, beforeChars, afterChars) {
1046
+ const state = this.session(this.rootOf(sessionId)).prunings;
1047
+ const key = `${sessionId}:${messageId}`;
1048
+ if (afterChars >= beforeChars && !state.has(key)) return;
1049
+ state.set(key, {
1050
+ beforeChars: state.get(key)?.beforeChars ?? beforeChars,
1051
+ afterChars
1052
+ });
1053
+ if (state.size > 512) state.delete(state.keys().next().value);
1054
+ }
1055
+ /** Record one host surface replacement (live event or log backfill) for the change list. */
1056
+ noteReplacement(sessionId, fact) {
1057
+ const session = this.session(this.rootOf(sessionId));
1058
+ session.replacements.set(fact.newId, fact);
1059
+ for (const id of fact.replacedIds) if (id !== fact.newId) session.replacedBy.set(id, fact);
1060
+ while (session.replacements.size > REPLACEMENT_CAP) session.replacements.delete(session.replacements.keys().next().value);
1061
+ while (session.replacedBy.size > REPLACEMENT_CAP * 4) session.replacedBy.delete(session.replacedBy.keys().next().value);
1062
+ }
1063
+ /**
1064
+ * Begin one captured request: correlate session/turn/scope, split the
1065
+ * segments against the scope's previous fingerprint, assign the sequence.
1066
+ *
1067
+ * @returns the stored row, or `undefined` when the request carries no
1068
+ * session identity the capture can place (a host call with no sessionId
1069
+ * and no purpose is out of the panel's scope).
1070
+ */
1071
+ beginRequest(snapshot, startedAt) {
1072
+ if (snapshot.sessionId === void 0) return void 0;
1073
+ const rootId = this.rootOf(snapshot.sessionId);
1074
+ const session = this.session(rootId);
1075
+ const isChild = snapshot.sessionId !== rootId;
1076
+ const compaction = snapshot.purpose === "compaction" ? session.activeCompactions.get(snapshot.sessionId) : void 0;
1077
+ let turn;
1078
+ let scopeId;
1079
+ if (isChild) {
1080
+ const found = this.findScope(session, snapshot.sessionId);
1081
+ if (found === void 0 || found.scope.logFilled) return void 0;
1082
+ turn = found.turn;
1083
+ scopeId = found.scope.scopeId;
1084
+ found.scope.live = true;
1085
+ } else if (snapshot.purpose !== void 0 || session.openTurn === void 0) {
1086
+ const purpose = snapshot.purpose ?? "ad-hoc";
1087
+ const bucketKey = compaction ? `${purpose}:${compaction.id}` : purpose;
1088
+ let bucket = session.buckets.get(bucketKey);
1089
+ if (bucket === void 0) {
1090
+ bucket = this.mintTurn(session, startedAt, {
1091
+ purpose,
1092
+ query: purpose,
1093
+ turnSeq: session.nextBucketSeq
1094
+ });
1095
+ session.nextBucketSeq -= 1;
1096
+ session.buckets.set(bucketKey, bucket);
1097
+ }
1098
+ turn = bucket;
1099
+ scopeId = "main";
1100
+ } else {
1101
+ turn = session.openTurn;
1102
+ scopeId = "main";
1103
+ }
1104
+ const scope = scopeId === "main" ? this.mainScope(session, turn, startedAt) : turn.scopes.get(scopeId);
1105
+ if (scope === void 0) return void 0;
1106
+ const previous = scope.previous ?? (snapshot.purpose === void 0 ? this.previousRequest(session, scopeId, startedAt, this.seqCounter)?.fingerprint : void 0);
1107
+ const segments = splitRequest({
1108
+ messages: snapshot.messages.map((message) => {
1109
+ const pruning = message.id === void 0 ? void 0 : session.prunings.get(`${snapshot.sessionId}:${message.id}`);
1110
+ return pruning === void 0 ? message : {
1111
+ ...message,
1112
+ pruning
1113
+ };
1114
+ }),
1115
+ ...snapshot.systemSlot === void 0 ? {} : { systemSlot: snapshot.systemSlot },
1116
+ ...snapshot.tools === void 0 ? {} : { tools: snapshot.tools },
1117
+ context: {
1118
+ ...previous === void 0 || snapshot.purpose === "compaction" ? {} : { previous },
1119
+ assemblies: this.assemblies,
1120
+ resolveToolName: (callId) => this.callNames.get(callId),
1121
+ durationFor: (messageId) => session.messageDurations.get(messageId),
1122
+ turnFor: (messageId) => session.messageTurns.get(messageId)
1123
+ }
1124
+ });
1125
+ if (snapshot.purpose !== "compaction") scope.previous = segments.fingerprint;
1126
+ const request = {
1127
+ seq: this.seqCounter,
1128
+ sessionId: rootId,
1129
+ scopeId: scope.scopeId,
1130
+ turnSeq: turn.turnSeq,
1131
+ ...snapshot.purpose === void 0 ? {} : { purpose: snapshot.purpose },
1132
+ ...compaction === void 0 ? {} : { compaction: { ...compaction } },
1133
+ startedAt,
1134
+ status: "streaming",
1135
+ config: snapshot.config,
1136
+ responseSummary: { kind: snapshot.purpose === "compaction" ? "compaction" : "none" },
1137
+ segments: {
1138
+ system: segments.system,
1139
+ tools: segments.tools,
1140
+ history: segments.history,
1141
+ added: segments.added
1142
+ },
1143
+ totalChars: segments.totalChars,
1144
+ blocks: [],
1145
+ execs: [],
1146
+ fingerprint: segments.fingerprint,
1147
+ ...snapshot.purpose === void 0 && previous === void 0 ? { needsHistoryBaseline: true } : {}
1148
+ };
1149
+ this.seqCounter += 1;
1150
+ const nextNumber = snapshot.purpose === void 0 ? session.nextRequestNumbers.get(scopeId) : void 0;
1151
+ if (nextNumber !== void 0) {
1152
+ request.requestNumber = nextNumber;
1153
+ session.nextRequestNumbers.set(scopeId, nextNumber + 1);
1154
+ }
1155
+ scope.requests.push(request);
1156
+ turn.liveContent = true;
1157
+ this.totalChars += request.totalChars;
1158
+ if (scope.config.provider === void 0 && snapshot.config.provider !== void 0) scope.config = snapshot.config;
1159
+ this.enforceCaps();
1160
+ return request;
1161
+ }
1162
+ /** Attach a purpose-built (log-lane) request row to an existing child scope. */
1163
+ appendLogRequest(rootSessionId, scopeId, built) {
1164
+ const session = this.sessions.get(rootSessionId);
1165
+ if (session === void 0) return void 0;
1166
+ const found = this.findScope(session, scopeId);
1167
+ if (found === void 0 || found.scope.live) return void 0;
1168
+ found.scope.logFilled = true;
1169
+ const request = {
1170
+ ...built,
1171
+ seq: this.seqCounter,
1172
+ sessionId: rootSessionId,
1173
+ scopeId,
1174
+ turnSeq: found.turn.turnSeq
1175
+ };
1176
+ this.seqCounter += 1;
1177
+ found.scope.requests.push(request);
1178
+ if (built.purpose === void 0) found.scope.previous = built.fingerprint;
1179
+ this.totalChars += request.totalChars;
1180
+ this.attachResponse(request, built.response);
1181
+ this.accountExecutionResults(request);
1182
+ this.enforceCaps();
1183
+ return request;
1184
+ }
1185
+ findScope(session, scopeId) {
1186
+ for (const turn of session.turns) {
1187
+ const scope = turn.scopes.get(scopeId);
1188
+ if (scope !== void 0) return {
1189
+ turn,
1190
+ scope
1191
+ };
1192
+ }
1193
+ }
1194
+ /** One stream chunk observed on a live request's tee. */
1195
+ observeChunk(request, chunk, at) {
1196
+ if (request.firstChunkAt === void 0) request.firstChunkAt = at;
1197
+ switch (chunk.type) {
1198
+ case "block-start":
1199
+ if (chunk.index === void 0) return;
1200
+ request.blocks.push({
1201
+ kind: chunk.blockType ?? "unknown",
1202
+ index: chunk.index,
1203
+ start: at
1204
+ });
1205
+ return;
1206
+ case "text-delta":
1207
+ case "reasoning-delta": {
1208
+ const span = request.blocks.find((block) => block.index === chunk.index && block.end === void 0);
1209
+ if (span === void 0 || chunk.text === void 0) return;
1210
+ span.text = (span.text ?? "") + chunk.text;
1211
+ return;
1212
+ }
1213
+ case "tool-call-delta": {
1214
+ const span = request.blocks.find((block) => block.index === chunk.index && block.end === void 0);
1215
+ if (span === void 0 || chunk.argumentsDelta === void 0) return;
1216
+ span.args = (span.args ?? "") + chunk.argumentsDelta;
1217
+ return;
1218
+ }
1219
+ case "block-end": {
1220
+ const span = request.blocks.find((block) => block.index === chunk.index && block.end === void 0);
1221
+ if (span === void 0) return;
1222
+ span.end = at;
1223
+ if (chunk.block?.text !== void 0) span.text = chunk.block.text;
1224
+ if (chunk.block?.type === "tool-call") {
1225
+ if (chunk.block.name !== void 0) span.toolName = chunk.block.name;
1226
+ if (chunk.block.id !== void 0) {
1227
+ span.callId = chunk.block.id;
1228
+ this.callOwners.set(chunk.block.id, request);
1229
+ if (chunk.block.name !== void 0) this.callNames.set(chunk.block.id, chunk.block.name);
1230
+ }
1231
+ if (chunk.block.arguments !== void 0) span.args = chunk.block.arguments;
1232
+ }
1233
+ return;
1234
+ }
1235
+ case "usage":
1236
+ if (chunk.usage !== void 0) request.usage = chunk.usage;
1237
+ return;
1238
+ case "finish":
1239
+ if (chunk.reason !== void 0) request.finishKind = chunk.reason.kind;
1240
+ if (chunk.reason?.kind === "error" || chunk.reason?.kind === "aborted") request.error = chunk.reason.failure?.message ?? chunk.reason.kind;
1241
+ return;
1242
+ }
1243
+ }
1244
+ /** Settle one request: ok on a clean finish, failed on an error finish or a raised error. */
1245
+ settleRequest(request, at, thrown) {
1246
+ if (request.status !== "streaming") return;
1247
+ request.settledAt = at;
1248
+ if (thrown !== void 0) {
1249
+ request.status = "failed";
1250
+ request.error = thrown instanceof Error ? thrown.message : String(thrown);
1251
+ } else if (request.error !== void 0) request.status = "failed";
1252
+ else if (request.finishKind === void 0) {
1253
+ request.status = "failed";
1254
+ request.error = "stream closed before its finish chunk";
1255
+ } else request.status = "ok";
1256
+ request.responseSummary = request.purpose === "compaction" ? { kind: "compaction" } : summarizeResponse(request);
1257
+ this.attachResponse(request, responseFromSpans(request.blocks));
1258
+ const session = this.sessions.get(request.sessionId);
1259
+ if (session !== void 0 && request.purpose === void 0) session.lastSettledMain = request;
1260
+ }
1261
+ /** Set the request's response and count its chars into the store budget. */
1262
+ attachResponse(request, response) {
1263
+ if (response === void 0) return;
1264
+ request.response = response;
1265
+ const chars = responseSize(response);
1266
+ request.responseChars = chars;
1267
+ this.totalChars += chars;
1268
+ }
1269
+ accountExecutionResults(request) {
1270
+ const chars = executionSize(request);
1271
+ this.totalChars += chars - (request.executionChars ?? 0);
1272
+ request.executionChars = chars;
1273
+ }
1274
+ /** Incremental child folds mutate shared execution spans after insertion. */
1275
+ refreshExecutionResults(sessionId, scopeId) {
1276
+ const session = this.sessions.get(sessionId);
1277
+ const scope = session && this.findScope(session, scopeId)?.scope;
1278
+ for (const request of scope?.requests ?? []) this.accountExecutionResults(request);
1279
+ this.enforceCaps();
1280
+ }
1281
+ /** turn/start on a root session (child sessions route their events to the parent instead). */
1282
+ /** @param seq - the `turn/start` event's log seq (the chat's history-jump target). */
1283
+ turnStart(sessionId, hostTurn, at, seq) {
1284
+ const rootId = this.rootOf(sessionId);
1285
+ const session = this.session(rootId);
1286
+ const scopeId = sessionId === rootId ? "main" : sessionId;
1287
+ if (hostTurn === 1 && !session.nextRequestNumbers.has(scopeId)) session.nextRequestNumbers.set(scopeId, 1);
1288
+ if (this.isChild(sessionId)) return;
1289
+ if (!session.turnSpans.has(hostTurn)) session.turnSpans.set(hostTurn, { start: at });
1290
+ if (session.logDriven) {
1291
+ session.logDirty = true;
1292
+ return;
1293
+ }
1294
+ if (session.openTurn !== void 0) return;
1295
+ const existing = session.turns.find((turn) => turn.hostTurn === hostTurn && turn.purpose === void 0);
1296
+ if (existing !== void 0) {
1297
+ session.openTurn = existing;
1298
+ if (seq !== void 0) existing.startSeq = seq;
1299
+ return;
1300
+ }
1301
+ session.openTurn = this.mintTurn(session, at, {
1302
+ hostTurn,
1303
+ turnSeq: hostTurn,
1304
+ liveContent: true,
1305
+ liveStartObserved: true
1306
+ });
1307
+ if (seq !== void 0) session.openTurn.startSeq = seq;
1308
+ }
1309
+ /** turn/end: close the open turn, resolve its main scope, sweep abandoned streams. */
1310
+ turnEnd(sessionId, hostTurn, reason, at) {
1311
+ if (this.isChild(sessionId)) return;
1312
+ const span = this.sessions.get(sessionId)?.turnSpans.get(hostTurn);
1313
+ if (span !== void 0) span.end = at;
1314
+ const session = this.sessions.get(sessionId);
1315
+ if (session === void 0) return;
1316
+ if (session.logDriven) {
1317
+ session.logDirty = true;
1318
+ return;
1319
+ }
1320
+ let turn = session.openTurn;
1321
+ if (turn === void 0) {
1322
+ const bucket = session.buckets.get("ad-hoc");
1323
+ const drifted = bucket?.scopes.get("main")?.requests.filter((request) => request.purpose === void 0) ?? [];
1324
+ if (drifted.length === 0) return;
1325
+ turn = this.mintTurn(session, at, {
1326
+ hostTurn,
1327
+ turnSeq: hostTurn,
1328
+ liveContent: true
1329
+ });
1330
+ const main = this.mainScope(session, turn, drifted[0]?.startedAt ?? at);
1331
+ for (const request of drifted) {
1332
+ request.turnSeq = turn.turnSeq;
1333
+ main.requests.push(request);
1334
+ }
1335
+ const bucketScope = bucket?.scopes.get("main");
1336
+ if (bucket !== void 0 && bucketScope !== void 0) {
1337
+ bucketScope.requests = bucketScope.requests.filter((request) => !drifted.includes(request));
1338
+ if (bucketScope.requests.length === 0) {
1339
+ bucket.scopes.delete("main");
1340
+ session.buckets.delete("ad-hoc");
1341
+ session.turns = session.turns.filter((candidate) => candidate !== bucket);
1342
+ }
1343
+ }
1344
+ }
1345
+ turn.endedAt = at;
1346
+ session.openTurn = void 0;
1347
+ const main = turn.scopes.get("main");
1348
+ if (main !== void 0) {
1349
+ main.status = reason.kind === "completed" || reason.kind === "max-tokens" ? "ok" : "failed";
1350
+ if (main.status === "failed") main.error = reason.kind === "error" ? reason.error?.message ?? "turn failed" : reason.kind;
1351
+ main.endedAt = at;
1352
+ }
1353
+ for (const scope of turn.scopes.values()) for (const request of scope.requests) if (request.status === "streaming") this.settleRequest(request, at, /* @__PURE__ */ new Error("stream still open at turn end"));
1354
+ this.enforceCaps();
1355
+ }
1356
+ /** A human message entering the turn — the turn list's query preview. */
1357
+ noteUserMessage(sessionId, sourceKind, text) {
1358
+ if (this.isChild(sessionId)) return;
1359
+ const turn = this.sessions.get(sessionId)?.openTurn;
1360
+ if (turn === void 0 || turn.query !== "") return;
1361
+ if (sourceKind !== "user") {
1362
+ if (turn.trigger === void 0 && !AMBIENT_SOURCES.has(sourceKind)) turn.trigger = {
1363
+ source: sourceKind,
1364
+ text: lastParagraph(text)
1365
+ };
1366
+ return;
1367
+ }
1368
+ turn.query = text.replace(/\s+/g, " ").trim().slice(0, 120);
1369
+ }
1370
+ /** The turn's closing assistant message id + generation-time backfill for history rows. */
1371
+ noteAssistantMessage(sessionId, messageId) {
1372
+ const session = this.sessions.get(this.rootOf(sessionId));
1373
+ if (session === void 0) return;
1374
+ if (!this.isChild(sessionId) && session.openTurn !== void 0) session.openTurn.closingMessageId = messageId;
1375
+ const producer = session.lastSettledMain;
1376
+ if (producer !== void 0) {
1377
+ const total = producer.settledAt === void 0 ? void 0 : producer.settledAt - producer.startedAt;
1378
+ if (total !== void 0) session.messageDurations.set(messageId, total);
1379
+ }
1380
+ }
1381
+ /** tool/call: open the exec interval (when the issuing request is known) and queue delegation binding. */
1382
+ toolCall(sessionId, callId, name, args, at) {
1383
+ const rootId = this.rootOf(sessionId);
1384
+ const session = this.session(rootId);
1385
+ this.callNames.set(callId, name);
1386
+ const owner = this.callOwners.get(callId);
1387
+ if (owner !== void 0) owner.execs.push({
1388
+ callId,
1389
+ name,
1390
+ start: at
1391
+ });
1392
+ if (isDelegationTool$1(name)) {
1393
+ const pending = {
1394
+ callId,
1395
+ name,
1396
+ at,
1397
+ ...delegationLabel$1(args)
1398
+ };
1399
+ session.pendingCalls.set(callId, pending);
1400
+ }
1401
+ }
1402
+ /** Source turns belong to session events, not the immutable message payload. */
1403
+ noteMessageTurn(sessionId, messageId, turn) {
1404
+ const root = this.childToParent.get(sessionId) ?? sessionId;
1405
+ const session = this.sessions.get(root);
1406
+ const value = turn ?? session?.openTurn?.hostTurn;
1407
+ if (session && value !== void 0) session.messageTurns.set(messageId, value);
1408
+ }
1409
+ /** tool/result: close the exec interval; resolve a delegation scope's outcome. */
1410
+ toolResult(sessionId, callId, isError, result, at) {
1411
+ const rootId = this.rootOf(sessionId);
1412
+ const session = this.sessions.get(rootId);
1413
+ const owner = this.callOwners.get(callId);
1414
+ if (owner !== void 0) {
1415
+ const span = owner.execs.find((exec) => exec.callId === callId && exec.end === void 0);
1416
+ if (span !== void 0) {
1417
+ span.end = at;
1418
+ span.isError = isError;
1419
+ span.result = result;
1420
+ this.accountExecutionResults(owner);
1421
+ this.enforceCaps();
1422
+ }
1423
+ }
1424
+ if (session === void 0) return;
1425
+ const pending = session.pendingCalls.get(callId);
1426
+ if (pending === void 0) return;
1427
+ session.pendingCalls.delete(callId);
1428
+ const scopeId = pending.childId ?? `call:${callId}`;
1429
+ const scope = this.findScope(session, scopeId)?.scope ?? this.openChildScope(session, scopeId, pending.label, at, callId);
1430
+ if (scope === void 0) return;
1431
+ scope.status = isError ? "failed" : "ok";
1432
+ if (isError) scope.error = previewLine(result);
1433
+ scope.endedAt = at;
1434
+ }
1435
+ /** subagent/catalog: open the child scope on the current turn and bind the pending call. */
1436
+ subagentCatalog(sessionId, childId, label, at) {
1437
+ if (this.isChild(sessionId)) return;
1438
+ const session = this.session(sessionId);
1439
+ this.childToParent.set(childId, sessionId);
1440
+ const scope = this.openChildScope(session, childId, label, at);
1441
+ if (scope === void 0) return;
1442
+ for (const pending of session.pendingCalls.values()) if (pending.childId === void 0) {
1443
+ pending.childId = childId;
1444
+ scope.callId = pending.callId;
1445
+ if (pending.label !== void 0 && scope.label === void 0) scope.label = pending.label;
1446
+ break;
1447
+ }
1448
+ }
1449
+ /** Open one child scope on the session's current turn (or latest turn as a fallback). */
1450
+ openChildScope(session, scopeId, label, at, callId) {
1451
+ const existing = this.findScope(session, scopeId);
1452
+ if (existing !== void 0) return existing.scope;
1453
+ const turn = session.openTurn ?? this.holderTurn(session, at);
1454
+ const scope = {
1455
+ scopeId,
1456
+ scopeSeq: session.nextScopeSeq,
1457
+ kind: "subagent",
1458
+ ...label === void 0 ? {} : { label },
1459
+ status: "running",
1460
+ config: {},
1461
+ requests: [],
1462
+ startedAt: at,
1463
+ live: false,
1464
+ logFilled: false,
1465
+ ...callId === void 0 ? {} : { callId }
1466
+ };
1467
+ session.nextScopeSeq += 1;
1468
+ turn.scopes.set(scopeId, scope);
1469
+ return scope;
1470
+ }
1471
+ /** Every child scope of one session (the log lane iterates these). */
1472
+ childScopes(sessionId) {
1473
+ const session = this.sessions.get(sessionId);
1474
+ if (session === void 0) return [];
1475
+ const out = [];
1476
+ for (const turn of session.turns) for (const scope of turn.scopes.values()) if (scope.kind === "subagent" && !scope.scopeId.startsWith("call:")) out.push({
1477
+ scopeId: scope.scopeId,
1478
+ live: scope.live,
1479
+ status: scope.status
1480
+ });
1481
+ return out;
1482
+ }
1483
+ /** The session's backfill watermark (retryable until 'complete'). */
1484
+ backfillState(sessionId) {
1485
+ return this.sessions.get(sessionId)?.backfillState ?? "none";
1486
+ }
1487
+ /** A durably unreadable log stops the backfill from retrying it on every query. */
1488
+ markBackfillBroken(sessionId, reason) {
1489
+ const session = this.session(sessionId);
1490
+ session.backfillState = "broken";
1491
+ session.backfillReason = reason;
1492
+ }
1493
+ /**
1494
+ * Insert log-reconstructed turns (the pre-mount history), newest-first within
1495
+ * budget: live-content turns win the budget outright, a host turn with any
1496
+ * record is never merged into (the overlap rule), and one oversized turn is
1497
+ * SKIPPED without aborting the older ones. Scope ordinals are renumbered
1498
+ * afterwards so the chips' `#N` stay chronological across the live/backfill
1499
+ * boundary (scopeSeq is a display anchor, not an address).
1500
+ *
1501
+ * The watermark is honest: 'complete' only when every candidate fit; a
1502
+ * budget-dropped candidate keeps the state 'partial', and the service
1503
+ * re-reads the log on later queries (eviction may have freed room). The
1504
+ * insert itself never evicts a live turn: char pressure evicts the oldest
1505
+ * BACKFILLED turns first.
1506
+ *
1507
+ * @returns the number of turns actually inserted.
1508
+ */
1509
+ insertLogTurns(sessionId, logTurns) {
1510
+ const session = this.session(sessionId);
1511
+ session.lastAccess = this.now();
1512
+ this.reconcileRequestNumbers(session, logTurns);
1513
+ const hosts = logTurns.filter((turn) => turn.purpose === void 0).map((turn) => turn.hostTurn);
1514
+ if (hosts.length > 0) session.logRange = {
1515
+ first: Math.min(...hosts),
1516
+ last: Math.max(...hosts)
1517
+ };
1518
+ for (const turn of logTurns) if (turn.purpose === void 0) session.turnSpans.set(turn.hostTurn, {
1519
+ start: turn.startedAt,
1520
+ ...turn.endedAt === void 0 ? {} : { end: turn.endedAt }
1521
+ });
1522
+ for (const holder of logTurns.filter((turn) => turn.purpose === "room-members")) {
1523
+ const target = this.holderTurn(session, holder.startedAt);
1524
+ for (const delegation of holder.delegations) if (delegation.childId === void 0 || this.findScope(session, delegation.childId) === void 0) this.addDelegationScope(session, target, delegation);
1525
+ }
1526
+ let refreshed = 0;
1527
+ if (session.logDriven && session.backfillState !== "broken") {
1528
+ const materialized = this.windowOf(session, hosts);
1529
+ for (const loaded of session.turns.filter((turn) => turn.purpose === void 0 && turn.hostTurn !== void 0)) {
1530
+ const logged = logTurns.find((turn) => turn.purpose === void 0 && turn.hostTurn === loaded.hostTurn);
1531
+ if (logged === void 0 || !materialized.has(logged.hostTurn)) continue;
1532
+ if ((loaded.scopes.get("main")?.requests.length ?? 0) === logged.requests.length && loaded.endedAt === this.logTurnEnd(session, logged, logTurns)) continue;
1533
+ this.evictTurn(session, loaded, false);
1534
+ this.insertLogTurn(session, logged, logTurns);
1535
+ refreshed += 1;
1536
+ }
1537
+ if (refreshed > 0) this.renumberScopes(session);
1538
+ }
1539
+ const dirty = session.logDirty === true;
1540
+ session.logDirty = false;
1541
+ if (session.backfillState === "broken" || session.backfillState === "complete" && !dirty) return refreshed;
1542
+ const keep = this.windowOf(session, hosts);
1543
+ const window = logTurns.filter((turn) => turn.purpose === void 0 && keep.has(turn.hostTurn));
1544
+ if (window.length === 0) {
1545
+ session.backfillState = "complete";
1546
+ return 0;
1547
+ }
1548
+ const lo = Math.min(...window.map((turn) => turn.hostTurn)), hi = Math.max(...window.map((turn) => turn.hostTurn));
1549
+ const startAt = window.find((turn) => turn.hostTurn === lo).startedAt;
1550
+ const nextAfter = logTurns.filter((turn) => turn.purpose === void 0 && turn.hostTurn > hi).sort((x, y) => x.hostTurn - y.hostTurn)[0]?.startedAt ?? Number.POSITIVE_INFINITY;
1551
+ const inWindow = (turn) => {
1552
+ if (turn.purpose === "room-members") return false;
1553
+ if (turn.purpose === void 0) return turn.hostTurn >= lo && turn.hostTurn <= hi;
1554
+ const host = turn.requests[0]?.compaction?.hostTurn;
1555
+ return host !== void 0 ? host >= lo && host <= hi : turn.startedAt >= startAt && turn.startedAt < nextAfter;
1556
+ };
1557
+ const candidates = logTurns.filter((logTurn) => inWindow(logTurn) && !session.turns.some((turn) => logTurn.purpose === void 0 ? turn.purpose === void 0 && turn.hostTurn === logTurn.hostTurn : [...turn.scopes.values()].some((scope) => scope.requests.some((request) => request.compaction?.id === logTurn.requests[0]?.compaction?.id))));
1558
+ let dropped = 0;
1559
+ let inserted = 0;
1560
+ const stop = (i) => {
1561
+ if (inserted === 0) {
1562
+ dropped += 1;
1563
+ return false;
1564
+ }
1565
+ dropped += i + 1;
1566
+ return true;
1567
+ };
1568
+ for (let i = candidates.length - 1; i >= 0; i -= 1) {
1569
+ const candidate = candidates[i];
1570
+ const candidateChars = candidate.requests.reduce((n, request) => n + request.totalChars + executionSize(request) + responseSize(request.response), 0);
1571
+ if (candidate.requests.length > this.maxBackfillRequestsPerSession - session.backfilledRequests) {
1572
+ if (stop(i)) break;
1573
+ continue;
1574
+ }
1575
+ if (this.totalChars + candidateChars > this.maxStoredCharsTotal) this.freeChars(session, candidateChars);
1576
+ if (this.totalChars + candidateChars > this.maxStoredCharsTotal) {
1577
+ if (stop(i)) break;
1578
+ continue;
1579
+ }
1580
+ this.insertLogTurn(session, candidate, logTurns);
1581
+ inserted += 1;
1582
+ }
1583
+ session.backfillState = dropped > 0 ? "partial" : "complete";
1584
+ if (inserted > 0) this.renumberScopes(session);
1585
+ return inserted;
1586
+ }
1587
+ /**
1588
+ * The backfill window over a log's host turns: the newest
1589
+ * `maxTurnsPerSession` at or before the window's end. At the latest, live
1590
+ * turns the log does not hold yet take window slots too. The log lane
1591
+ * materializes only these turns' requests.
1592
+ */
1593
+ windowHosts(sessionId, hosts) {
1594
+ return this.windowOf(this.session(sessionId), hosts);
1595
+ }
1596
+ windowOf(session, hosts) {
1597
+ const end = session.windowEnd ?? Number.POSITIVE_INFINITY;
1598
+ const logged = new Set(hosts);
1599
+ const liveAhead = session.windowEnd === void 0 ? session.turns.filter((turn) => turn.liveContent && turn.hostTurn !== void 0 && !logged.has(turn.hostTurn)).length : 0;
1600
+ return new Set([...logged].filter((host) => host <= end).sort((x, y) => y - x).slice(0, Math.max(0, this.maxTurnsPerSession - liveAhead)));
1601
+ }
1602
+ /**
1603
+ * Make room for `needed` chars by evicting other sessions' backfilled
1604
+ * turns, least recently read session first, oldest turn first. Live
1605
+ * content never yields. A session that lost backfill re-reads it when next
1606
+ * read (its state drops back to partial).
1607
+ */
1608
+ freeChars(current, needed) {
1609
+ const others = [...this.sessions.values()].filter((other) => other !== current).sort((x, y) => x.lastAccess - y.lastAccess);
1610
+ for (const other of others) {
1611
+ const victims = other.turns.filter((turn) => !turn.liveContent && !this.turnIsLive(turn)).sort((x, y) => x.startedAt - y.startedAt);
1612
+ for (const victim of victims) {
1613
+ if (this.totalChars + needed <= this.maxStoredCharsTotal) return;
1614
+ this.evictTurn(other, victim, false);
1615
+ if (other.backfillState === "complete") other.backfillState = "partial";
1616
+ }
1617
+ }
1618
+ }
1619
+ /**
1620
+ * Move a session's backfill window: drop its backfilled turns (live turns
1621
+ * stay) and let the next read refill the window ending at `end` (the
1622
+ * latest turns when absent).
1623
+ */
1624
+ setWindow(sessionId, end) {
1625
+ const session = this.session(sessionId);
1626
+ if (end === void 0) delete session.windowEnd;
1627
+ else session.windowEnd = end;
1628
+ for (const turn of [...session.turns]) if (!turn.liveContent && !this.turnIsLive(turn)) this.evictTurn(session, turn, false);
1629
+ if (session.backfillState !== "broken") session.backfillState = "none";
1630
+ }
1631
+ /** A complete log can anchor live turns without retaining old request bodies. */
1632
+ reconcileRequestNumbers(session, logTurns) {
1633
+ for (const logged of logTurns) {
1634
+ if (logged.purpose !== void 0) continue;
1635
+ const live = session.turns.find((t) => t.hostTurn === logged.hostTurn && t.purpose === void 0 && t.liveStartObserved);
1636
+ const first = logged.requests[0]?.requestNumber;
1637
+ if (!live || first === void 0) continue;
1638
+ const requests = live.scopes.get("main")?.requests ?? [];
1639
+ for (let i = 0; i < requests.length; i++) requests[i].requestNumber ??= first + i;
1640
+ }
1641
+ const liveRequests = session.turns.filter((t) => t.liveContent && t.purpose === void 0).sort((a, b) => a.startedAt - b.startedAt || a.turnSeq - b.turnSeq).flatMap((t) => t.scopes.get("main")?.requests ?? []);
1642
+ if (liveRequests.length && liveRequests.at(-1)?.requestNumber === void 0) return;
1643
+ let next = session.nextRequestNumbers.get("main");
1644
+ for (const r of [...logTurns.flatMap((t) => t.requests), ...liveRequests]) if (r.requestNumber !== void 0) next = Math.max(next ?? 1, r.requestNumber + 1);
1645
+ if (next !== void 0) session.nextRequestNumbers.set("main", next);
1646
+ }
1647
+ /** Retry numbering after a streaming turn’s first durable response becomes available. */
1648
+ /**
1649
+ * Make a session log-driven: an external agent's session opened on its own
1650
+ * (a CLI in its own process, so no request passes through this host). Its
1651
+ * turns come from its log only; the live turns opened so far held nothing
1652
+ * and give way to the log's.
1653
+ */
1654
+ markLogDriven(sessionId) {
1655
+ const session = this.session(sessionId);
1656
+ if (session.logDriven) return;
1657
+ session.logDriven = true;
1658
+ session.logDirty = true;
1659
+ for (const turn of [...session.turns]) {
1660
+ if (turn.purpose !== void 0 || !turn.liveContent) continue;
1661
+ if ([...turn.scopes.values()].some((scope) => scope.requests.length > 0)) continue;
1662
+ if (session.openTurn === turn) session.openTurn = void 0;
1663
+ this.evictTurn(session, turn, false);
1664
+ }
1665
+ }
1666
+ /** Any live event of a log-driven session: the next read re-reads its log. */
1667
+ noteLogActivity(sessionId) {
1668
+ const session = this.sessions.get(sessionId);
1669
+ if (session?.logDriven) session.logDirty = true;
1670
+ }
1671
+ /** Whether a read should re-read a log-driven session's log. */
1672
+ needsLogRead(sessionId) {
1673
+ return this.sessions.get(sessionId)?.logDirty === true;
1674
+ }
1675
+ /** A log turn's end as the store records it: still open when it is a log-driven session's last turn. */
1676
+ logTurnEnd(session, logTurn, logTurns) {
1677
+ if (this.logTurnOpen(session, logTurn, logTurns)) return void 0;
1678
+ const lastSettled = logTurn.requests.reduce((latest, request) => request.settledAt !== void 0 && (latest === void 0 || request.settledAt > latest) ? request.settledAt : latest, void 0);
1679
+ return logTurn.endedAt ?? lastSettled ?? logTurn.startedAt;
1680
+ }
1681
+ logTurnOpen(session, logTurn, logTurns) {
1682
+ if (!session.logDriven || logTurn.endReasonKind !== void 0 || logTurn.purpose !== void 0) return false;
1683
+ return !logTurns.some((turn) => turn.purpose === void 0 && turn.hostTurn > logTurn.hostTurn);
1684
+ }
1685
+ needsRequestNumbers(sessionId) {
1686
+ return this.sessions.get(sessionId)?.turns.some((t) => t.liveStartObserved && (t.scopes.get("main")?.requests.some((r) => r.purpose === void 0 && r.requestNumber === void 0) ?? false)) ?? false;
1687
+ }
1688
+ /** One reconstructed turn → one TurnRecord (main scope + delegation stubs). */
1689
+ insertLogTurn(session, logTurn, logTurns = []) {
1690
+ const turn = this.mintTurn(session, logTurn.startedAt, {
1691
+ ...logTurn.purpose === void 0 ? { hostTurn: logTurn.hostTurn } : { purpose: logTurn.purpose },
1692
+ turnSeq: logTurn.purpose === void 0 ? logTurn.hostTurn : session.nextBucketSeq--,
1693
+ query: logTurn.query
1694
+ });
1695
+ if (logTurn.trigger !== void 0) turn.trigger = logTurn.trigger;
1696
+ if (logTurn.startSeq !== void 0) turn.startSeq = logTurn.startSeq;
1697
+ if (logTurn.closingMessageId !== void 0) turn.closingMessageId = logTurn.closingMessageId;
1698
+ const open = this.logTurnOpen(session, logTurn, logTurns);
1699
+ const end = this.logTurnEnd(session, logTurn, logTurns);
1700
+ if (end !== void 0) turn.endedAt = end;
1701
+ const mainStatus = open ? "running" : logTurn.endReasonKind === "completed" || logTurn.endReasonKind === "max-tokens" ? "ok" : "failed";
1702
+ const main = {
1703
+ scopeId: "main",
1704
+ scopeSeq: 0,
1705
+ kind: "main",
1706
+ status: mainStatus,
1707
+ config: logTurn.requests[0]?.config ?? {},
1708
+ requests: [],
1709
+ startedAt: logTurn.startedAt,
1710
+ ...turn.endedAt === void 0 ? {} : { endedAt: turn.endedAt },
1711
+ live: false,
1712
+ logFilled: true
1713
+ };
1714
+ if (mainStatus === "failed") main.error = logTurn.endReasonKind === void 0 ? "interrupted (no turn/end in the log)" : logTurn.endError ?? logTurn.endReasonKind;
1715
+ turn.scopes.set("main", main);
1716
+ for (const built of logTurn.requests) {
1717
+ const request = {
1718
+ ...built,
1719
+ seq: this.seqCounter,
1720
+ sessionId: session.id,
1721
+ scopeId: "main",
1722
+ turnSeq: turn.turnSeq
1723
+ };
1724
+ this.seqCounter += 1;
1725
+ main.requests.push(request);
1726
+ this.totalChars += request.totalChars;
1727
+ session.backfilledRequests += 1;
1728
+ this.attachResponse(request, built.response);
1729
+ this.accountExecutionResults(request);
1730
+ }
1731
+ for (const delegation of logTurn.delegations) this.addDelegationScope(session, turn, delegation);
1732
+ }
1733
+ /** One reconstructed delegation → its child scope on `turn`. */
1734
+ addDelegationScope(session, turn, delegation) {
1735
+ const scopeId = delegation.childId ?? `call:${delegation.callId}`;
1736
+ if (delegation.childId !== void 0) this.childToParent.set(delegation.childId, session.id);
1737
+ const label = delegation.catalogLabel ?? delegation.label;
1738
+ const scope = {
1739
+ scopeId,
1740
+ scopeSeq: 0,
1741
+ kind: "subagent",
1742
+ ...label === void 0 ? {} : { label },
1743
+ status: delegation.callId === "" ? "ok" : delegation.isError === true || delegation.resultAt === void 0 ? "failed" : "ok",
1744
+ ...delegation.isError === true && delegation.errorPreview !== void 0 ? { error: delegation.errorPreview } : delegation.resultAt === void 0 && delegation.callId !== "" ? { error: "no tool/result in the log" } : {},
1745
+ config: {},
1746
+ requests: [],
1747
+ startedAt: delegation.at,
1748
+ ...delegation.resultAt === void 0 ? {} : { endedAt: delegation.resultAt },
1749
+ live: false,
1750
+ logFilled: false,
1751
+ ...delegation.callId === "" ? {} : { callId: delegation.callId }
1752
+ };
1753
+ turn.scopes.set(scopeId, scope);
1754
+ }
1755
+ /** The hidden turn holding children started outside any turn (never listed; room rounds read it). */
1756
+ holderTurn(session, at) {
1757
+ const found = session.turns.find((turn) => turn.purpose === "room-members");
1758
+ if (found !== void 0) return found;
1759
+ const turn = this.mintTurn(session, at, {
1760
+ purpose: "room-members",
1761
+ query: "",
1762
+ turnSeq: session.nextBucketSeq
1763
+ });
1764
+ session.nextBucketSeq -= 1;
1765
+ return turn;
1766
+ }
1767
+ /** Renumber one session's scope ordinals chronologically (turn order, then creation order). */
1768
+ renumberScopes(session) {
1769
+ let next = 1;
1770
+ const ordered = [...session.turns].sort((a, b) => a.startedAt - b.startedAt || a.turnSeq - b.turnSeq);
1771
+ for (const turn of ordered) for (const scope of turn.scopes.values()) {
1772
+ scope.scopeSeq = next;
1773
+ next += 1;
1774
+ }
1775
+ }
1776
+ /**
1777
+ * Light turn list of one session plus its backfill watermark (sorted newest
1778
+ * last). A {@link ScopeFilter} restricts every aggregate to the authorized
1779
+ * scopes; the turn rows themselves (query preview, timestamps) are
1780
+ * session-level metadata and stay listed.
1781
+ */
1782
+ listTurns(sessionId, filter) {
1783
+ const session = this.sessions.get(sessionId);
1784
+ if (session !== void 0) session.lastAccess = this.now();
1785
+ const backfill = session === void 0 ? { state: "none" } : {
1786
+ state: session.backfillState,
1787
+ ...session.backfillReason === void 0 ? {} : { reason: session.backfillReason }
1788
+ };
1789
+ if (session === void 0) return {
1790
+ turns: [],
1791
+ backfill,
1792
+ generation: this.generation,
1793
+ ...this.parents.has(sessionId) ? { parent: this.parents.get(sessionId) } : {}
1794
+ };
1795
+ const ordinals = this.ordinals(session);
1796
+ const listed = [...session.turns, ...this.roomTurns(session, filter)].filter((turn) => turn.purpose !== "room-members" && this.attachedTurn(session, turn) === void 0).sort((a, b) => a.startedAt - b.startedAt || a.turnSeq - b.turnSeq);
1797
+ const handoffs = (session.room?.handoffs ?? []).map((h) => ({
1798
+ at: h.at,
1799
+ name: session.room.members.get(h.memberId) ?? h.memberId
1800
+ }));
1801
+ const decisions = scopeAuthorized("main", filter) ? this.decisionsByTurn(session, filter) : /* @__PURE__ */ new Map();
1802
+ return {
1803
+ turns: listed.map((turn) => {
1804
+ const held = decisions.get(turn.turnSeq);
1805
+ const item = {
1806
+ ...this.turnItem(sessionId, turn, filter),
1807
+ ...held === void 0 ? {} : { roomDecisionIds: held.map((row) => row.id) }
1808
+ };
1809
+ const ordinal = ordinals.get(turn.hostTurn !== void 0 ? `h${turn.hostTurn}` : `t${turn.turnSeq}`);
1810
+ return ordinal === void 0 ? item : {
1811
+ ...item,
1812
+ ordinal
1813
+ };
1814
+ }),
1815
+ backfill,
1816
+ total: ordinals.size,
1817
+ unloaded: this.unloadedRuns(session, ordinals),
1818
+ ...handoffs.length === 0 ? {} : { handoffs },
1819
+ generation: this.generation,
1820
+ ...this.parents.has(sessionId) ? { parent: this.parents.get(sessionId) } : {},
1821
+ ...session.logRange === void 0 ? {} : { range: session.logRange },
1822
+ ...session.evictedTurns === 0 ? {} : { evictedTurnCount: session.evictedTurns },
1823
+ ...filter?.scopes === void 0 ? {} : { authorizedScopes: this.authorizedScopes(session, filter) }
1824
+ };
1825
+ }
1826
+ /** The session's existing scopes that pass the filter (turn order, then creation order). */
1827
+ authorizedScopes(session, filter) {
1828
+ const allowed = new Set(filter.scopes);
1829
+ const out = [];
1830
+ for (const turn of [...session.turns].sort((a, b) => a.startedAt - b.startedAt || a.turnSeq - b.turnSeq)) for (const scope of turn.scopes.values()) if (allowed.has(scope.scopeId) && !out.includes(scope.scopeId)) out.push(scope.scopeId);
1831
+ return out;
1832
+ }
1833
+ turnItem(sessionId, turn, filter) {
1834
+ let requestCount = 0;
1835
+ let scopeCount = 0;
1836
+ let failedScopeCount = 0;
1837
+ let totalInputTokens = 0;
1838
+ let totalOutputTokens = 0;
1839
+ for (const scope of turn.scopes.values()) {
1840
+ if (!scopeAuthorized(scope.scopeId, filter)) continue;
1841
+ scopeCount += 1;
1842
+ requestCount += scope.requests.length;
1843
+ if (scope.status === "failed") failedScopeCount += 1;
1844
+ for (const request of scope.requests) {
1845
+ totalInputTokens += (request.usage?.inputTokens ?? 0) + (request.usage?.cacheReadTokens ?? 0) + (request.usage?.cacheWriteTokens ?? 0);
1846
+ totalOutputTokens += request.usage?.outputTokens ?? 0;
1847
+ }
1848
+ }
1849
+ return {
1850
+ sessionId,
1851
+ turnSeq: turn.turnSeq,
1852
+ ...turn.hostTurn === void 0 ? {} : { hostTurn: turn.hostTurn },
1853
+ query: turn.query,
1854
+ ...turn.query === "" && turn.trigger !== void 0 ? { trigger: turn.trigger } : {},
1855
+ ...turn.room === void 0 ? {} : { room: turn.room },
1856
+ ...turn.startSeq === void 0 ? {} : { startSeq: turn.startSeq },
1857
+ startedAt: turn.startedAt,
1858
+ ...turn.endedAt === void 0 ? {} : { endedAt: turn.endedAt },
1859
+ requestCount,
1860
+ scopeCount,
1861
+ failedScopeCount,
1862
+ totalInputTokens,
1863
+ totalOutputTokens,
1864
+ origin: turn.liveContent ? "live" : "log-reconstructed",
1865
+ ...turn.closingMessageId === void 0 ? {} : { closingMessageId: turn.closingMessageId }
1866
+ };
1867
+ }
1868
+ /** One turn's scope/request overview, without segments. */
1869
+ getTurn(sessionId, turnSeq, filter) {
1870
+ const session = this.sessions.get(sessionId);
1871
+ if (session !== void 0) session.lastAccess = this.now();
1872
+ const turn = session === void 0 ? void 0 : this.findTurn(session, turnSeq, filter);
1873
+ if (session === void 0 || turn === void 0) return void 0;
1874
+ const item = this.turnItem(sessionId, turn, filter);
1875
+ const scopes = [...turn.scopes.values()].filter((scope) => scopeAuthorized(scope.scopeId, filter)).map((scope) => this.scopeOverview(session, turn, scope));
1876
+ if (turn.hostTurn !== void 0 && scopeAuthorized("main", filter)) for (const pseudo of session.turns) {
1877
+ if (this.attachedTurn(session, pseudo) !== turn) continue;
1878
+ for (const scope of pseudo.scopes.values()) scopes.push(this.scopeOverview(session, pseudo, scope));
1879
+ }
1880
+ const roomDecisions = scopeAuthorized("main", filter) ? this.decisionsByTurn(session, filter).get(turn.turnSeq) : void 0;
1881
+ return {
1882
+ turn: item,
1883
+ scopes,
1884
+ ...roomDecisions === void 0 ? {} : { roomDecisions },
1885
+ generation: this.generation,
1886
+ ...filter?.scopes === void 0 ? {} : { authorizedScopes: scopes.map((scope) => scope.scopeId) }
1887
+ };
1888
+ }
1889
+ /**
1890
+ * The room's decision-model calls by the turn they happened in: each goes to
1891
+ * the latest listed turn (main turn or room round) that started at or
1892
+ * before it.
1893
+ */
1894
+ decisionsByTurn(session, filter) {
1895
+ const byTurn = /* @__PURE__ */ new Map();
1896
+ const decisions = session.room?.decisions ?? [];
1897
+ if (decisions.length === 0) return byTurn;
1898
+ const listed = [...session.turns, ...this.roomTurns(session, filter)].filter((turn) => turn.purpose !== "room-members" && this.attachedTurn(session, turn) === void 0).sort((a, b) => a.startedAt - b.startedAt || a.turnSeq - b.turnSeq);
1899
+ for (const decision of decisions) {
1900
+ const owner = listed.findLast((turn) => turn.startedAt <= decision.at);
1901
+ if (owner === void 0) continue;
1902
+ const rows = byTurn.get(owner.turnSeq) ?? [];
1903
+ rows.push(decision);
1904
+ byTurn.set(owner.turnSeq, rows);
1905
+ }
1906
+ return byTurn;
1907
+ }
1908
+ /** Record one `room/*` event of a main session (log backfill or live). */
1909
+ noteRoomEvent(sessionId, type, data, seq, at) {
1910
+ if (this.isChild(sessionId) || !type.startsWith("room/")) return;
1911
+ const session = this.session(sessionId);
1912
+ session.room ??= emptyRoom();
1913
+ applyRoomEvent(session.room, type, data, seq, at);
1914
+ }
1915
+ /** A listed turn or a room round, by its panel address. */
1916
+ findTurn(session, turnSeq, filter) {
1917
+ return session.turns.find((candidate) => candidate.turnSeq === turnSeq) ?? (turnSeq < -999999 ? this.roomTurns(session, filter).find((candidate) => candidate.turnSeq === turnSeq) : void 0);
1918
+ }
1919
+ /** Whether a moment falls inside some main turn (loaded or not). */
1920
+ insideMainTurn(session, at) {
1921
+ for (const span of session.turnSpans.values()) if (at >= span.start && at <= (span.end ?? Number.POSITIVE_INFINITY)) return true;
1922
+ return false;
1923
+ }
1924
+ /**
1925
+ * Every human room dispatch made outside a main turn, as a round: the
1926
+ * dispatch's message, and per member run the child session's requests of
1927
+ * that run (its recorded child turn, else the run's time span). Built on
1928
+ * read from the room state and the children's scopes; nothing is stored.
1929
+ */
1930
+ roomTurns(session, filter) {
1931
+ const room = session.room;
1932
+ if (room === void 0) return [];
1933
+ return room.dispatches.filter((dispatch) => !this.insideMainTurn(session, dispatch.at)).map((dispatch) => {
1934
+ const scopes = /* @__PURE__ */ new Map();
1935
+ const runs = runsOf(room, dispatch.seq);
1936
+ for (const run of runs) {
1937
+ const child = this.childOfRun(session, run);
1938
+ if (child !== void 0 && !scopeAuthorized(child.scopeId, filter)) continue;
1939
+ const scopeId = child?.scopeId ?? `room:${dispatch.seq}:${run.memberId}`;
1940
+ const from = (run.startedAt ?? dispatch.at) - 2e3, to = (run.endedAt ?? this.now()) + 2e3;
1941
+ const requests = (child?.requests ?? []).filter((request) => run.childTurn !== void 0 && request.hostTurn !== void 0 ? request.hostTurn === run.childTurn : request.startedAt >= from && request.startedAt <= to);
1942
+ scopes.set(scopeId, {
1943
+ scopeId,
1944
+ scopeSeq: child?.scopeSeq ?? 0,
1945
+ kind: "subagent",
1946
+ label: run.member ?? room.members.get(run.memberId) ?? child?.label ?? run.memberId,
1947
+ ...child?.provider === void 0 ? {} : { provider: child.provider },
1948
+ status: run.state === "failed" ? "failed" : run.state === "done" ? "ok" : "running",
1949
+ config: child?.config ?? {},
1950
+ requests,
1951
+ startedAt: run.startedAt ?? dispatch.at,
1952
+ ...run.endedAt === void 0 ? {} : { endedAt: run.endedAt },
1953
+ live: false,
1954
+ logFilled: true
1955
+ });
1956
+ }
1957
+ const ends = runs.map((run) => run.endedAt).filter((end) => end !== void 0);
1958
+ return {
1959
+ turnSeq: roomTurnSeq(dispatch.seq),
1960
+ purpose: "room",
1961
+ query: dispatch.text,
1962
+ room: { targets: dispatch.targets },
1963
+ startedAt: dispatch.at,
1964
+ ...ends.length === 0 ? {} : { endedAt: Math.max(...ends) },
1965
+ liveContent: false,
1966
+ scopes
1967
+ };
1968
+ });
1969
+ }
1970
+ /** The child session a run went to: named by the run, else the child catalogued closest to the run's start. */
1971
+ childOfRun(session, run) {
1972
+ if (run.childSessionId !== void 0) {
1973
+ const named = this.findScope(session, run.childSessionId)?.scope;
1974
+ if (named !== void 0) return named;
1975
+ }
1976
+ if (run.startedAt === void 0) return void 0;
1977
+ let best;
1978
+ for (const turn of session.turns) for (const scope of turn.scopes.values()) {
1979
+ if (scope.kind !== "subagent" || Math.abs(scope.startedAt - run.startedAt) > 1e4) continue;
1980
+ if (best === void 0 || Math.abs(scope.startedAt - run.startedAt) < Math.abs(best.startedAt - run.startedAt)) best = scope;
1981
+ }
1982
+ return best;
1983
+ }
1984
+ /**
1985
+ * One numbering for the whole session, by time: every main turn (loaded or
1986
+ * not) and every room round. Keys: `h<hostTurn>` for main turns,
1987
+ * `t<turnSeq>` for room rounds.
1988
+ */
1989
+ ordinals(session) {
1990
+ const units = [];
1991
+ const hosts = /* @__PURE__ */ new Set();
1992
+ for (const [host, span] of session.turnSpans) {
1993
+ units.push({
1994
+ key: `h${host}`,
1995
+ at: span.start,
1996
+ host
1997
+ });
1998
+ hosts.add(host);
1999
+ }
2000
+ for (const turn of session.turns) if (turn.hostTurn !== void 0 && !hosts.has(turn.hostTurn)) units.push({
2001
+ key: `h${turn.hostTurn}`,
2002
+ at: turn.startedAt,
2003
+ host: turn.hostTurn
2004
+ });
2005
+ for (const round of this.roomTurns(session)) units.push({
2006
+ key: `t${round.turnSeq}`,
2007
+ at: round.startedAt,
2008
+ host: Number.POSITIVE_INFINITY
2009
+ });
2010
+ units.sort((a, b) => a.at - b.at || a.host - b.host);
2011
+ return new Map(units.map((unit, i) => [unit.key, i + 1]));
2012
+ }
2013
+ /** Runs of main turns the log holds but the store has not loaded, in ordinals, with their host-turn ends. */
2014
+ unloadedRuns(session, ordinals) {
2015
+ const loaded = new Set(session.turns.filter((turn) => turn.hostTurn !== void 0).map((turn) => turn.hostTurn));
2016
+ const missing = [...session.turnSpans.keys()].filter((host) => !loaded.has(host)).map((host) => ({
2017
+ host,
2018
+ ordinal: ordinals.get(`h${host}`)
2019
+ })).sort((a, b) => a.ordinal - b.ordinal);
2020
+ const out = [];
2021
+ for (const { host, ordinal } of missing) {
2022
+ const last = out.at(-1);
2023
+ if (last !== void 0 && last.to === ordinal - 1) {
2024
+ last.to = ordinal;
2025
+ last.lastHost = host;
2026
+ } else out.push({
2027
+ from: ordinal,
2028
+ to: ordinal,
2029
+ firstHost: host,
2030
+ lastHost: host
2031
+ });
2032
+ }
2033
+ return out;
2034
+ }
2035
+ /** A compaction pseudo-turn's host turn: the one it started in, else the next turn to begin; undefined for any other turn. */
2036
+ attachedTurn(session, turn) {
2037
+ if (turn.purpose !== "compaction") return void 0;
2038
+ const real = session.turns.filter((t) => t.hostTurn !== void 0).sort((a, b) => a.startedAt - b.startedAt);
2039
+ const hostTurn = [...turn.scopes.values()].flatMap((scope) => scope.requests).find((r) => r.compaction?.hostTurn !== void 0)?.compaction?.hostTurn;
2040
+ return (hostTurn === void 0 ? void 0 : real.find((t) => t.hostTurn === hostTurn)) ?? real.find((t) => t.startedAt >= turn.startedAt) ?? real.at(-1);
2041
+ }
2042
+ /** Where the panel addresses a request: compaction requests live in their host turn's `compaction:` view. */
2043
+ addressOf(session, request) {
2044
+ const pseudo = session.turns.find((t) => t.turnSeq === request.turnSeq);
2045
+ const host = pseudo === void 0 ? void 0 : this.attachedTurn(session, pseudo);
2046
+ return {
2047
+ turnSeq: host?.turnSeq ?? request.turnSeq,
2048
+ scopeId: host === void 0 ? request.scopeId : `compaction:${request.turnSeq}`,
2049
+ seq: request.seq,
2050
+ ...request.requestNumber === void 0 ? {} : { requestNumber: request.requestNumber }
2051
+ };
2052
+ }
2053
+ scopeOverview(session, turn, scope) {
2054
+ const origin = scopeOrigin(scope);
2055
+ const host = this.attachedTurn(session, turn);
2056
+ const scopeId = host === void 0 ? scope.scopeId : `compaction:${turn.turnSeq}`;
2057
+ const main = turn.scopes.get("main");
2058
+ const delegator = scope.callId === void 0 ? void 0 : main?.requests.find((r) => r.response?.toolCalls.some((call) => call.callId === scope.callId));
2059
+ const compaction = host === void 0 ? void 0 : scope.requests.find((r) => r.compaction !== void 0)?.compaction;
2060
+ const after = host === void 0 ? void 0 : host.scopes.get("main")?.requests.filter((r) => r.startedAt <= turn.startedAt).sort((a, b) => b.startedAt - a.startedAt)[0];
2061
+ return {
2062
+ scopeId,
2063
+ scopeSeq: scope.scopeSeq,
2064
+ kind: host !== void 0 ? "compaction" : scope.kind,
2065
+ ...scope.label === void 0 ? {} : { label: scope.label },
2066
+ ...scope.provider === void 0 ? {} : { provider: scope.provider },
2067
+ ...delegator === void 0 ? {} : { delegatedBy: this.addressOf(session, delegator) },
2068
+ ...compaction === void 0 ? {} : { compaction: {
2069
+ ...compaction.replacedCount === void 0 ? {} : { replacedCount: compaction.replacedCount },
2070
+ ...after === void 0 ? {} : { after: this.addressOf(session, after) },
2071
+ ...compaction.hostTurn === void 0 ? { betweenTurns: true } : {}
2072
+ } },
2073
+ status: scope.status,
2074
+ ...scope.error === void 0 ? {} : { error: scope.error },
2075
+ config: scope.config,
2076
+ startedAt: scope.startedAt,
2077
+ ...scope.endedAt === void 0 ? {} : { endedAt: scope.endedAt },
2078
+ ...origin === void 0 ? {} : { origin },
2079
+ requests: scope.requests.map((request) => ({
2080
+ seq: request.seq,
2081
+ ...request.requestNumber === void 0 ? {} : { requestNumber: request.requestNumber },
2082
+ scopeId,
2083
+ status: request.status,
2084
+ config: request.config,
2085
+ responseSummary: request.responseSummary,
2086
+ startedAt: request.startedAt,
2087
+ ...request.usage === void 0 ? {} : { usage: request.usage },
2088
+ ...request.settledAt === void 0 ? {} : { totalMs: request.settledAt - request.startedAt },
2089
+ ...request.compaction?.inputPartial ? { timingApproximate: true } : {},
2090
+ totalChars: request.totalChars,
2091
+ toolErrors: request.execs.filter((exec) => exec.isError).length
2092
+ }))
2093
+ };
2094
+ }
2095
+ /** Record the child's provider from its `subagent/descriptor` (live child events or its log). */
2096
+ noteChildProvider(childId, provider) {
2097
+ const root = this.childToParent.get(childId);
2098
+ const session = root === void 0 ? void 0 : this.sessions.get(root);
2099
+ const found = session === void 0 ? void 0 : this.findScope(session, childId);
2100
+ if (found !== void 0) found.scope.provider = provider;
2101
+ }
2102
+ /** A session opened on its own that is some other session's child. */
2103
+ noteParent(sessionId, parent) {
2104
+ this.parents.set(sessionId, parent);
2105
+ }
2106
+ /** Whether a child's parent link is already known (the service resolves it once). */
2107
+ hasParent(sessionId) {
2108
+ return this.parents.has(sessionId);
2109
+ }
2110
+ /**
2111
+ * One request's full four-segment breakdown. A request whose scope the
2112
+ * filter excludes answers with a {@link ScopedRefusal} — distinguishable
2113
+ * from `undefined` (no such seq on this instance).
2114
+ */
2115
+ getRequest(sessionId, seq, filter) {
2116
+ const session = this.sessions.get(sessionId);
2117
+ if (session === void 0) return void 0;
2118
+ for (const turn of session.turns) for (const scope of turn.scopes.values()) {
2119
+ const request = scope.requests.find((candidate) => candidate.seq === seq);
2120
+ if (request === void 0) continue;
2121
+ if (!scopeAuthorized(scope.scopeId, filter)) return {
2122
+ refused: "no-authorized-content",
2123
+ generation: this.generation
2124
+ };
2125
+ const previous = session.turns.flatMap((t) => [...t.scopes.values()]).filter((s) => s.scopeId === scope.scopeId).flatMap((s) => s.requests).filter((r) => r.purpose === request.purpose && (r.startedAt < request.startedAt || r.startedAt === request.startedAt && r.seq < request.seq)).sort((a, b) => b.startedAt - a.startedAt || b.seq - a.seq)[0];
2126
+ return {
2127
+ ...this.requestView(request),
2128
+ ...previous === void 0 || request.compaction?.inputPartial || previous.compaction?.inputPartial ? {} : { prefix: this.withLinks(session, request, previous, comparePrefix(previous, request, factLookup(session))) }
2129
+ };
2130
+ }
2131
+ }
2132
+ /** Resolve change entries to retained requests: the compaction call, and the request that still held un-pruned text. */
2133
+ withLinks(session, request, previous, prefix) {
2134
+ if (prefix.changes === void 0) return prefix;
2135
+ const link = (r) => this.addressOf(session, r);
2136
+ const retained = session.turns.flatMap((turn) => [...turn.scopes.values()]).flatMap((scope) => scope.requests);
2137
+ return {
2138
+ ...prefix,
2139
+ changes: prefix.changes.map((change) => {
2140
+ if (change.kind === "compaction" && change.compactionId !== void 0) {
2141
+ const target = retained.find((r) => r.compaction?.id === change.compactionId && r.seq !== request.seq);
2142
+ return target === void 0 ? change : {
2143
+ ...change,
2144
+ compactionRequest: link(target)
2145
+ };
2146
+ }
2147
+ return change.kind === "prune" ? {
2148
+ ...change,
2149
+ pruneBefore: link(previous)
2150
+ } : change;
2151
+ })
2152
+ };
2153
+ }
2154
+ previousRequest(session, scopeId, at, seq) {
2155
+ return session.turns.flatMap((turn) => turn.scopes.get(scopeId)?.requests ?? []).filter((r) => r.purpose === void 0 && (r.startedAt < at || r.startedAt === at && r.seq < seq)).sort((a, b) => b.startedAt - a.startedAt || b.seq - a.seq)[0];
2156
+ }
2157
+ requestView(request) {
2158
+ const session = this.sessions.get(request.sessionId);
2159
+ const baseline = request.needsHistoryBaseline && session !== void 0 ? this.previousRequest(session, request.scopeId, request.startedAt, request.seq) : void 0;
2160
+ const segments = baseline === void 0 ? request.segments : rebaseSegments(request.segments, request.fingerprint, baseline.fingerprint);
2161
+ const owners = /* @__PURE__ */ new Map();
2162
+ for (const turn of this.sessions.get(request.sessionId)?.turns ?? []) for (const r of turn.scopes.get(request.scopeId)?.requests ?? []) if (r.requestNumber !== void 0) {
2163
+ for (const c of r.response?.toolCalls ?? []) if (c.callId !== void 0) owners.set(c.callId, r.requestNumber);
2164
+ }
2165
+ const retained = (this.sessions.get(request.sessionId)?.turns ?? []).flatMap((turn) => turn.scopes.get(request.scopeId)?.requests ?? []);
2166
+ const link = (r) => session === void 0 ? {
2167
+ turnSeq: r.turnSeq,
2168
+ scopeId: r.scopeId,
2169
+ seq: r.seq
2170
+ } : this.addressOf(session, r);
2171
+ const before = retained.filter((r) => r.purpose === void 0 && r.startedAt < request.startedAt).sort((a, b) => b.startedAt - a.startedAt)[0];
2172
+ const provenance = (items) => items.map((item) => {
2173
+ const number = item.toolCallId === void 0 ? void 0 : owners.get(item.toolCallId);
2174
+ const compression = item.compactionId === void 0 ? void 0 : retained.find((r) => r.compaction?.id === item.compactionId);
2175
+ const original = item.pruning === void 0 ? void 0 : retained.filter((r) => r.startedAt < request.startedAt && [...r.segments.history, ...r.segments.added].some((old) => old.messageId === item.messageId && !old.pruning)).sort((a, b) => b.startedAt - a.startedAt)[0];
2176
+ return {
2177
+ ...item,
2178
+ ...number === void 0 ? {} : { sourceRequestNumber: number },
2179
+ ...compression === void 0 ? {} : { compactionRequest: link(compression) },
2180
+ ...item.pruning === void 0 ? {} : { pruning: {
2181
+ ...item.pruning,
2182
+ ...original === void 0 ? {} : { before: link(original) }
2183
+ } }
2184
+ };
2185
+ });
2186
+ const response = request.response && {
2187
+ ...request.response,
2188
+ toolCalls: request.response.toolCalls.map((call) => {
2189
+ const exec = call.callId === void 0 ? void 0 : request.execs.find((e) => e.callId === call.callId);
2190
+ return exec === void 0 ? call : {
2191
+ ...call,
2192
+ execution: {
2193
+ status: exec.isError === true ? "failed" : exec.end !== void 0 || exec.isError === false ? "ok" : "running",
2194
+ ...exec.result === void 0 ? {} : { result: exec.result }
2195
+ }
2196
+ };
2197
+ })
2198
+ };
2199
+ return {
2200
+ seq: request.seq,
2201
+ ...request.requestNumber === void 0 ? {} : { requestNumber: request.requestNumber },
2202
+ scopeId: session === void 0 ? request.scopeId : this.addressOf(session, request).scopeId,
2203
+ sessionId: request.sessionId,
2204
+ ...request.purpose === void 0 ? {} : { purpose: request.purpose },
2205
+ ...request.compaction === void 0 ? {} : { compaction: {
2206
+ ...request.compaction,
2207
+ ...before === void 0 ? {} : { before: link(before) }
2208
+ } },
2209
+ startedAt: request.startedAt,
2210
+ status: request.status,
2211
+ ...request.error === void 0 ? {} : { error: request.error },
2212
+ config: request.config,
2213
+ ...request.usage === void 0 ? {} : { usage: request.usage },
2214
+ timing: requestTiming(request),
2215
+ generation: this.generation,
2216
+ responseSummary: request.responseSummary,
2217
+ ...response === void 0 ? {} : { response },
2218
+ segments: {
2219
+ ...segments,
2220
+ history: provenance(segments.history),
2221
+ added: provenance(segments.added)
2222
+ }
2223
+ };
2224
+ }
2225
+ /** One turn's whole-turn timing aggregation, filtered to the authorized scopes. */
2226
+ getTurnTiming(sessionId, turnSeq, filter) {
2227
+ const session = this.sessions.get(sessionId);
2228
+ const turn = session === void 0 ? void 0 : this.findTurn(session, turnSeq, filter);
2229
+ if (session === void 0 || turn === void 0) return void 0;
2230
+ const lanes = [];
2231
+ const tools = /* @__PURE__ */ new Map();
2232
+ for (const scope of turn.scopes.values()) {
2233
+ if (!scopeAuthorized(scope.scopeId, filter)) continue;
2234
+ for (const request of scope.requests) {
2235
+ lanes.push(...requestLanes(request));
2236
+ for (const exec of request.execs) {
2237
+ if (exec.end === void 0) continue;
2238
+ lanes.push({
2239
+ kind: "tool-exec",
2240
+ requestSeq: request.seq,
2241
+ scopeId: request.scopeId,
2242
+ startedAt: exec.start,
2243
+ durationMs: exec.end - exec.start,
2244
+ tool: exec.name,
2245
+ callId: exec.callId,
2246
+ ...exec.isError === void 0 ? {} : { isError: exec.isError }
2247
+ });
2248
+ const argsMs = request.blocks.filter((block) => block.callId === exec.callId && block.end !== void 0).reduce((n, block) => n + (block.end - block.start), 0);
2249
+ const aggregate = tools.get(exec.name) ?? {
2250
+ tool: exec.name,
2251
+ calls: 0,
2252
+ totalMs: 0,
2253
+ argsMs: 0
2254
+ };
2255
+ aggregate.calls += 1;
2256
+ aggregate.totalMs += exec.end - exec.start;
2257
+ aggregate.argsMs += argsMs;
2258
+ tools.set(exec.name, aggregate);
2259
+ }
2260
+ }
2261
+ }
2262
+ lanes.sort((a, b) => a.startedAt - b.startedAt || a.requestSeq - b.requestSeq);
2263
+ const sum = (kind) => lanes.filter((lane) => lane.kind === kind).reduce((n, lane) => n + lane.durationMs, 0);
2264
+ const endedAt = turn.endedAt ?? this.now();
2265
+ return {
2266
+ turnSeq: turn.turnSeq,
2267
+ startedAt: turn.startedAt,
2268
+ totalMs: Math.max(0, endedAt - turn.startedAt),
2269
+ buckets: {
2270
+ waitMs: sum("wait"),
2271
+ reasoningMs: sum("reasoning"),
2272
+ toolArgsMs: sum("tool-args"),
2273
+ textMs: sum("text"),
2274
+ toolExecMs: sum("tool-exec"),
2275
+ otherMs: sum("other")
2276
+ },
2277
+ lanes,
2278
+ tools: [...tools.values()].sort((a, b) => b.totalMs - a.totalMs),
2279
+ generation: this.generation,
2280
+ ...filter?.scopes === void 0 ? {} : { authorizedScopes: [...turn.scopes.values()].filter((scope) => scopeAuthorized(scope.scopeId, filter)).map((scope) => scope.scopeId) }
2281
+ };
2282
+ }
2283
+ /** Session request headers only; excluded scopes contribute no rows or aggregates. */
2284
+ getSessionTrend(sessionId, filter) {
2285
+ const list = this.listTurns(sessionId, filter);
2286
+ return {
2287
+ ...list,
2288
+ requests: list.turns.flatMap((turn) => (this.getTurn(sessionId, turn.turnSeq, filter)?.scopes ?? []).flatMap((scope) => scope.requests.map((request) => ({
2289
+ ...request,
2290
+ turnSeq: turn.turnSeq
2291
+ }))))
2292
+ };
2293
+ }
2294
+ /** Release every session buffer (plugin dispose). */
2295
+ dispose() {
2296
+ const sessionIds = [...this.sessions.keys()];
2297
+ for (const sessionId of sessionIds) {
2298
+ const session = this.sessions.get(sessionId);
2299
+ if (session === void 0) continue;
2300
+ for (const turn of [...session.turns]) this.evictTurn(session, turn);
2301
+ }
2302
+ this.sessions.clear();
2303
+ this.childToParent.clear();
2304
+ this.callOwners.clear();
2305
+ this.callNames.clear();
2306
+ this.totalChars = 0;
2307
+ }
2308
+ /** @param counted - false for a window move or cross-session room, which the "collected then dropped" signal does not report. */
2309
+ evictTurn(session, turn, counted = true) {
2310
+ session.turns = session.turns.filter((candidate) => candidate !== turn);
2311
+ if (counted) session.evictedTurns += 1;
2312
+ for (const [purpose, bucket] of session.buckets) if (bucket === turn) session.buckets.delete(purpose);
2313
+ if (session.openTurn === turn) session.openTurn = void 0;
2314
+ for (const scope of turn.scopes.values()) {
2315
+ for (const request of scope.requests) {
2316
+ for (const block of request.blocks) if (block.callId !== void 0) {
2317
+ this.callOwners.delete(block.callId);
2318
+ this.callNames.delete(block.callId);
2319
+ }
2320
+ this.totalChars -= request.totalChars + (request.responseChars ?? 0) + (request.executionChars ?? 0);
2321
+ }
2322
+ if (scope.kind === "subagent") session.nextRequestNumbers.delete(scope.scopeId);
2323
+ if (!turn.liveContent && scope.kind === "main") session.backfilledRequests -= scope.requests.length;
2324
+ }
2325
+ const end = turn.endedAt ?? this.now();
2326
+ for (const [callId, pending] of session.pendingCalls) if (pending.at >= turn.startedAt && pending.at <= end) session.pendingCalls.delete(callId);
2327
+ this.onTurnEvicted?.(session.id, turn);
2328
+ if (session.turns.length === 0 && counted) {
2329
+ this.sessions.delete(session.id);
2330
+ for (const [childId, parentId] of this.childToParent) if (parentId === session.id) this.childToParent.delete(childId);
2331
+ }
2332
+ }
2333
+ turnIsLive(turn) {
2334
+ if (turn.endedAt === void 0 && turn.purpose === void 0) return true;
2335
+ for (const scope of turn.scopes.values()) if (scope.requests.some((request) => request.status === "streaming")) return true;
2336
+ return false;
2337
+ }
2338
+ /** The globally oldest closed backfilled turn (char-pressure eviction order). */
2339
+ oldestBackfilledTurn() {
2340
+ let victim;
2341
+ for (const session of this.sessions.values()) for (const turn of session.turns) {
2342
+ if (turn.liveContent || this.turnIsLive(turn)) continue;
2343
+ if (victim === void 0 || turn.startedAt < victim.turn.startedAt) victim = {
2344
+ session,
2345
+ turn
2346
+ };
2347
+ }
2348
+ return victim;
2349
+ }
2350
+ enforceCaps() {
2351
+ for (const session of this.sessions.values()) {
2352
+ const real = (live) => session.turns.filter((turn) => turn.hostTurn !== void 0 && turn.liveContent === live);
2353
+ while (real(true).length > this.maxTurnsPerSession) {
2354
+ const victim = real(true).sort((x, y) => x.startedAt - y.startedAt).find((turn) => !this.turnIsLive(turn));
2355
+ if (victim === void 0) break;
2356
+ this.evictTurn(session, victim);
2357
+ }
2358
+ while (real(false).length > this.maxTurnsPerSession) {
2359
+ const victim = real(false).sort((x, y) => x.startedAt - y.startedAt)[0];
2360
+ if (victim === void 0) break;
2361
+ this.evictTurn(session, victim);
2362
+ }
2363
+ }
2364
+ while (this.totalChars > this.maxStoredCharsTotal) {
2365
+ const victim = this.oldestBackfilledTurn();
2366
+ if (victim === void 0) return;
2367
+ this.evictTurn(victim.session, victim.turn);
2368
+ if (victim.session.backfillState === "complete") victim.session.backfillState = "partial";
2369
+ }
2370
+ for (const session of [...this.sessions.values()]) if (session.turns.length === 0 && session.openTurn === void 0) {
2371
+ this.sessions.delete(session.id);
2372
+ for (const [childId, parentId] of this.childToParent) if (parentId === session.id) this.childToParent.delete(childId);
2373
+ }
2374
+ }
2375
+ };
2376
+ /** The six-way timing split of one request, computed from its recorded spans. */
2377
+ function requestTiming(request) {
2378
+ const closed = request.blocks.filter((block) => block.end !== void 0);
2379
+ const sum = (kind) => closed.filter((block) => block.kind === kind).reduce((n, block) => n + (block.end - block.start), 0);
2380
+ const closedExecs = request.execs.filter((exec) => exec.end !== void 0);
2381
+ const toolExecMs = closedExecs.reduce((n, exec) => n + (exec.end - exec.start), 0);
2382
+ return {
2383
+ ...request.firstChunkAt === void 0 ? {} : { waitMs: request.firstChunkAt - request.startedAt },
2384
+ ...closed.some((block) => block.kind === "reasoning") ? { reasoningMs: sum("reasoning") } : {},
2385
+ ...closed.some((block) => block.kind === "tool-call") ? { toolArgsMs: sum("tool-call") } : {},
2386
+ ...closed.some((block) => block.kind === "text") ? { textMs: sum("text") } : {},
2387
+ ...closedExecs.length > 0 ? { toolExecMs } : {},
2388
+ ...request.settledAt === void 0 ? {} : { totalMs: request.settledAt - request.startedAt }
2389
+ };
2390
+ }
2391
+ const LANE_KIND = {
2392
+ reasoning: "reasoning",
2393
+ "tool-call": "tool-args",
2394
+ text: "text"
2395
+ };
2396
+ /**
2397
+ * One request's lane partition of [startedAt, settledAt]: wait, then blocks in
2398
+ * time order, with the gaps between known activity as `other` lanes. Overlaps
2399
+ * clamp to the running cursor, so the lanes never double-count wall time.
2400
+ */
2401
+ function requestLanes(request) {
2402
+ if (request.settledAt === void 0) return [];
2403
+ const lanes = [];
2404
+ const base = {
2405
+ requestSeq: request.seq,
2406
+ scopeId: request.scopeId
2407
+ };
2408
+ let cursor = request.startedAt;
2409
+ const push = (kind, start, end, tool) => {
2410
+ if (end <= start) return;
2411
+ lanes.push({
2412
+ kind,
2413
+ ...base,
2414
+ startedAt: start,
2415
+ durationMs: end - start,
2416
+ ...tool === void 0 ? {} : { tool }
2417
+ });
2418
+ };
2419
+ if (request.firstChunkAt !== void 0 && request.firstChunkAt > cursor) {
2420
+ push("wait", cursor, request.firstChunkAt);
2421
+ cursor = request.firstChunkAt;
2422
+ }
2423
+ const closed = request.blocks.filter((block) => block.end !== void 0).sort((a, b) => a.start - b.start);
2424
+ for (const block of closed) {
2425
+ const start = Math.max(block.start, cursor);
2426
+ if (start > cursor) push("other", cursor, start);
2427
+ push(LANE_KIND[block.kind] ?? "other", start, Math.max(start, block.end), block.toolName);
2428
+ cursor = Math.max(cursor, block.end);
2429
+ }
2430
+ if (request.settledAt > cursor) push("other", cursor, request.settledAt);
2431
+ return lanes;
2432
+ }
2433
+ /**
2434
+ * Assemble a request's own response from its recorded block spans: the
2435
+ * authoritative block-end content, falling back to delta-accumulated text for
2436
+ * blocks a cancelled stream never closed. `undefined` when nothing was
2437
+ * produced (a failure before any block — the `error` field covers that).
2438
+ */
2439
+ function responseFromSpans(blocks) {
2440
+ const reasoning = blocks.filter((block) => block.kind === "reasoning").map((block) => block.text ?? "").join("");
2441
+ const text = blocks.filter((block) => block.kind === "text").map((block) => block.text ?? "").join("");
2442
+ const toolCalls = blocks.filter((block) => block.kind === "tool-call" && block.toolName !== void 0).map((block) => ({
2443
+ ...block.callId === void 0 ? {} : { callId: block.callId },
2444
+ name: block.toolName,
2445
+ arguments: block.args ?? ""
2446
+ }));
2447
+ if (reasoning === "" && text === "" && toolCalls.length === 0) return void 0;
2448
+ return {
2449
+ ...reasoning === "" ? {} : { reasoning },
2450
+ ...text === "" ? {} : { text },
2451
+ toolCalls
2452
+ };
2453
+ }
2454
+ /** 明细头锚点(结构化):响应流里的工具调用 → `{kind:'tool', name, hint?, more?}`;纯文本 → `{kind:'text'}`;纯思考 → `{kind:'reasoning'}`;空响应 → `{kind:'none'}`。 */
2455
+ function summarizeResponse(request) {
2456
+ const calls = request.blocks.filter((block) => block.kind === "tool-call" && block.toolName !== void 0);
2457
+ if (calls.length > 0) {
2458
+ const first = calls[0];
2459
+ const hint = toolHint(first.args);
2460
+ return {
2461
+ kind: "tool",
2462
+ name: first.toolName,
2463
+ ...hint === void 0 ? {} : { hint },
2464
+ ...calls.length > 1 ? {
2465
+ more: calls.length - 1,
2466
+ toolCounts: countToolCalls(calls.map((call) => call.toolName))
2467
+ } : {}
2468
+ };
2469
+ }
2470
+ if (request.blocks.some((block) => block.kind === "text")) return { kind: "text" };
2471
+ return request.blocks.some((block) => block.kind === "reasoning") ? { kind: "reasoning" } : { kind: "none" };
2472
+ }
2473
+ /**
2474
+ * The one-line argument hint behind `tool:name` — the reference
2475
+ * reconstruction's key order (what → file basename → for → description),
2476
+ * truncated so the summary stays one line.
2477
+ */
2478
+ function toolHint(args) {
2479
+ if (args === void 0) return void 0;
2480
+ let parsed;
2481
+ try {
2482
+ parsed = JSON.parse(args);
2483
+ } catch {
2484
+ return;
2485
+ }
2486
+ if (typeof parsed !== "object" || parsed === null) return void 0;
2487
+ const value = parsed;
2488
+ const pick = (...keys) => {
2489
+ for (const key of keys) {
2490
+ const candidate = value[key];
2491
+ if (typeof candidate === "string" && candidate.length > 0) return candidate;
2492
+ }
2493
+ };
2494
+ const what = pick("what");
2495
+ if (what !== void 0) return clip(what);
2496
+ const file = pick("file_path", "path", "filePath");
2497
+ if (file !== void 0) return clip(file.split("/").pop() ?? file);
2498
+ const purpose = pick("for");
2499
+ if (purpose !== void 0) return clip(purpose);
2500
+ const description = pick("description");
2501
+ return description === void 0 ? void 0 : clip(description);
2502
+ }
2503
+ /** One-line clip for hint values. */
2504
+ function clip(text) {
2505
+ const collapsed = text.replace(/\s+/g, " ").trim();
2506
+ return collapsed.length <= 40 ? collapsed : `${collapsed.slice(0, 39)}…`;
2507
+ }
2508
+ /** The delegation tool family whose calls pair with `subagent/catalog` events. */
2509
+ function isDelegationTool$1(name) {
2510
+ return /^subagent[_-]/.test(name);
2511
+ }
2512
+ /** The delegation label out of the call arguments (`description` is the subagent_dsh convention). */
2513
+ function delegationLabel$1(args) {
2514
+ try {
2515
+ const parsed = JSON.parse(args);
2516
+ if (typeof parsed === "object" && parsed !== null) {
2517
+ const description = parsed.description;
2518
+ if (typeof description === "string" && description.length > 0) return { label: description };
2519
+ }
2520
+ } catch {}
2521
+ return {};
2522
+ }
2523
+ /** Content comparison is independent of durable message identity and provider cache behavior. */
2524
+ /** `provider/model`, or '' when the capture recorded neither. */
2525
+ function configKey(config) {
2526
+ return config.provider === void 0 && config.model === void 0 ? "" : `${config.provider ?? "?"}/${config.model ?? "?"}`;
2527
+ }
2528
+ function factLookup(session) {
2529
+ return {
2530
+ byNewId: (id) => session.replacements.get(id),
2531
+ byReplacedId: (id) => session.replacedBy.get(id)
2532
+ };
2533
+ }
2534
+ function comparePrefix(previous, current, facts) {
2535
+ const rows = (request) => [
2536
+ ...request.segments.system,
2537
+ ...request.segments.history,
2538
+ ...request.segments.added
2539
+ ].sort((a, b) => a.index - b.index);
2540
+ const a = rows(previous), b = rows(current);
2541
+ let i = 0;
2542
+ while (i < a.length && i < b.length && a[i]?.full === b[i]?.full && a[i]?.category === b[i]?.category) i++;
2543
+ const text = i < a.length ? prefixTextChange(a[i]?.full ?? "", b[i]?.full ?? "") : void 0;
2544
+ const changed = text === void 0 ? void 0 : {
2545
+ index: b[i]?.index ?? a[i]?.index ?? 0,
2546
+ before: `${text.leadingOmitted ? "…" : ""}${text.leading}${text.removed}${text.removedChars > 160 ? "…" : ""}${text.trailing}${text.trailingOmitted ? "…" : ""}`,
2547
+ after: `${text.leadingOmitted ? "…" : ""}${text.leading}${text.added}${text.addedChars > 160 ? "…" : ""}${text.trailing}${text.trailingOmitted ? "…" : ""}`,
2548
+ ...a[i] === void 0 ? {} : { beforeCategory: a[i].category },
2549
+ ...b[i] === void 0 ? {} : { afterCategory: b[i].category },
2550
+ text
2551
+ };
2552
+ const listed = diffRequests({
2553
+ rows: a,
2554
+ tools: previous.segments.tools
2555
+ }, {
2556
+ rows: b,
2557
+ tools: current.segments.tools
2558
+ }, facts, changed === void 0 ? void 0 : changed.index);
2559
+ return {
2560
+ previousSeq: previous.seq,
2561
+ ...previous.requestNumber === void 0 ? {} : { previousRequestNumber: previous.requestNumber },
2562
+ gapMs: Math.max(0, current.startedAt - (previous.settledAt ?? previous.startedAt)),
2563
+ changes: listed.changes,
2564
+ toolChanges: listed.tools,
2565
+ ...listed.tail === void 0 ? {} : { tail: listed.tail },
2566
+ ...configKey(previous.config) === configKey(current.config) || configKey(previous.config) === "" || configKey(current.config) === "" ? {} : { configChanged: {
2567
+ from: configKey(previous.config),
2568
+ to: configKey(current.config)
2569
+ } },
2570
+ toolsChanged: JSON.stringify(previous.segments.tools.map(({ added, ...tool }) => tool)) !== JSON.stringify(current.segments.tools.map(({ added, ...tool }) => tool)),
2571
+ ...changed === void 0 ? {} : { changed }
2572
+ };
2573
+ }
2574
+ /** Extra captured bodies consume retention budget, never input character totals. */
2575
+ function executionSize(request) {
2576
+ return request.execs.reduce((n, exec) => n + (exec.result?.length ?? 0), 0);
2577
+ }
2578
+ function responseSize(response) {
2579
+ return (response?.reasoning?.length ?? 0) + (response?.text?.length ?? 0) + (response?.toolCalls.reduce((n, call) => n + call.name.length + call.arguments.length, 0) ?? 0);
2580
+ }
2581
+ //#endregion
2582
+ //#region lib/types/log-lane.js
2583
+ /**
2584
+ * The log lane: reconstructs requests from an on-disk session log, read through
2585
+ * the host's `sessionPersistence` service (which owns the root layout, the
2586
+ * zstd framing, and the format-version chain). Two drivers share one fold:
2587
+ *
2588
+ * - **Child scopes** — a `subagent_dsh` one-shot child runs in a NEW process,
2589
+ * so the parent's `llm/stream` never sees its requests; the scope fills from
2590
+ * the child's own log, incrementally (the cursor resumes at the consumed
2591
+ * offset across panel queries).
2592
+ * - **Main-session backfill** — the panel otherwise shows only turns captured
2593
+ * live since mount; the session's own log reconstructs the pre-mount history
2594
+ * at log fidelity ({@link reconstructSession}), once per session.
2595
+ *
2596
+ * Honesty contract (one-shot child logs lack two things by design): no
2597
+ * `request/header` events means `config` stays `{}` until a header says
2598
+ * otherwise, and a missing system prompt never lands in the log, so the system
2599
+ * segment stays empty. Main-session logs carry both (`request/header` →
2600
+ * config + tools; `system/message` → the rendered prompt as ONE block — the
2601
+ * assemble-section attribution is the live lane's upgrade, the log keeps no
2602
+ * section names). The lane never invents either.
2603
+ *
2604
+ * Each `assistant/message` (a settled, usage-carrying response) or
2605
+ * `assistant/attempt` (a settled failure) completes one request row.
2606
+ *
2607
+ * @module @khorsheed/dsh-context-lens/log-lane
2608
+ */
2609
+ /** Structural check for the probed `sessionPersistence` service. */
2610
+ function isSessionLogReader(value) {
2611
+ return typeof value === "object" && value !== null && typeof value.open === "function";
2612
+ }
2613
+ /**
2614
+ * Subagent providers that run inside this host, so their requests pass
2615
+ * through the live capture. Every other provider (claude-local, codex-local,
2616
+ * kimi-cli, dsh-cli, …) is its own process, mirrored into its log after the
2617
+ * fact.
2618
+ */
2619
+ const IN_PROCESS_PROVIDERS = /* @__PURE__ */ new Set(["spawn", "fork"]);
2620
+ /** A step/start this close to its answer was written after the call, not when it began. */
2621
+ const MIRROR_LAG_MS = 50;
2622
+ /** A fresh cursor for one log. */
2623
+ function openCursor() {
2624
+ return {
2625
+ consumed: 0,
2626
+ numberedFromStart: false,
2627
+ messages: [],
2628
+ compactions: /* @__PURE__ */ new Map(),
2629
+ stepStartAt: void 0,
2630
+ config: void 0,
2631
+ tools: void 0,
2632
+ callAt: /* @__PURE__ */ new Map(),
2633
+ callNames: /* @__PURE__ */ new Map(),
2634
+ callOwners: /* @__PURE__ */ new Map(),
2635
+ messageDurations: /* @__PURE__ */ new Map(),
2636
+ previous: void 0,
2637
+ requests: [],
2638
+ turnHost: void 0,
2639
+ turnQuerySeen: false,
2640
+ turnTriggerSeen: false,
2641
+ pendingDelegations: /* @__PURE__ */ new Map(),
2642
+ broken: void 0
2643
+ };
2644
+ }
2645
+ /**
2646
+ * Fold new log rows into the cursor. Returns the requests completed by THIS
2647
+ * batch (the child-scope driver's whole output); the sink receives those plus
2648
+ * the turn/delegation structure (the backfill driver). Tool exec spans can
2649
+ * open and close in later batches, so pairing data lives on the cursor.
2650
+ */
2651
+ function foldLogRows(cursor, rows, sink = {}) {
2652
+ const built = [];
2653
+ for (const row of rows) {
2654
+ if (cursor.consumed === 0) cursor.numberedFromStart = row.type === "session" || row.seq === 0;
2655
+ cursor.consumed += 1;
2656
+ if (row.ignorable) continue;
2657
+ const data = typeof row.data === "object" && row.data !== null ? row.data : {};
2658
+ switch (row.type) {
2659
+ case "turn/start": {
2660
+ const hostTurn = typeof data["turn"] === "number" ? data["turn"] : void 0;
2661
+ cursor.turnHost = hostTurn;
2662
+ cursor.turnQuerySeen = false;
2663
+ cursor.turnTriggerSeen = false;
2664
+ if (hostTurn !== void 0) sink.turnStart?.(hostTurn, row.time, row.seq);
2665
+ break;
2666
+ }
2667
+ case "turn/end": {
2668
+ flushDeferred(cursor, built, sink);
2669
+ const hostTurn = typeof data["turn"] === "number" ? data["turn"] : cursor.turnHost;
2670
+ const reason = typeof data["reason"] === "object" && data["reason"] !== null ? data["reason"] : { kind: "unknown" };
2671
+ if (hostTurn !== void 0) sink.turnEnd?.(hostTurn, reason, row.time);
2672
+ cursor.turnHost = void 0;
2673
+ break;
2674
+ }
2675
+ case "step/start":
2676
+ cursor.stepStartAt = row.time;
2677
+ if (cursor.lastEventAt === void 0) delete cursor.stepPrevAt;
2678
+ else cursor.stepPrevAt = cursor.lastEventAt;
2679
+ break;
2680
+ case "subagent/descriptor": {
2681
+ const provider = data["provider"];
2682
+ if (typeof provider === "string" && !IN_PROCESS_PROVIDERS.has(provider)) cursor.external = true;
2683
+ break;
2684
+ }
2685
+ case "request/header": {
2686
+ const header = data["header"];
2687
+ const config = header?.config;
2688
+ if (config !== void 0) cursor.config = {
2689
+ ...typeof config["provider"] === "string" ? { provider: config["provider"] } : {},
2690
+ ...typeof config["model"] === "string" ? { model: config["model"] } : {},
2691
+ ...typeof config["reasoningEffort"] === "string" ? { reasoningEffort: config["reasoningEffort"] } : {},
2692
+ ...typeof config["temperature"] === "number" ? { temperature: config["temperature"] } : {},
2693
+ ...typeof config["maxTokens"] === "number" ? { maxTokens: config["maxTokens"] } : {}
2694
+ };
2695
+ if (Array.isArray(header?.tools)) cursor.tools = header.tools.filter((tool) => typeof tool === "object" && tool !== null && typeof tool.name === "string" && typeof tool.description === "string" && typeof tool.parameters === "object");
2696
+ break;
2697
+ }
2698
+ case "system/message":
2699
+ case "developer/message": {
2700
+ const message = fromLogMessage(data["message"]);
2701
+ if (message !== void 0) message.sourceTurn = typeof data["turn"] === "number" ? data["turn"] : cursor.turnHost;
2702
+ if (message !== void 0) placeMessage(cursor, row, message);
2703
+ break;
2704
+ }
2705
+ case "user/message": {
2706
+ const message = fromLogMessage(data);
2707
+ if (message !== void 0) message.sourceTurn = cursor.turnHost;
2708
+ if (message !== void 0) {
2709
+ placeMessage(cursor, row, message);
2710
+ if (!cursor.turnQuerySeen && message.sourceKind === "user") {
2711
+ cursor.turnQuerySeen = true;
2712
+ sink.queryText?.(cursor.turnHost, collapseQuery(serializeMessage(message)), row.time);
2713
+ } else if (!cursor.turnTriggerSeen && (row.surfaceOp === void 0 || row.surfaceOp === "append") && !AMBIENT_SOURCES.has(message.sourceKind)) {
2714
+ cursor.turnTriggerSeen = true;
2715
+ sink.triggerText?.(cursor.turnHost, message.sourceKind, lastParagraph(serializeMessage(message)));
2716
+ }
2717
+ }
2718
+ break;
2719
+ }
2720
+ case "assistant/message": {
2721
+ const hostTurn = typeof data["turn"] === "number" ? data["turn"] : cursor.turnHost;
2722
+ if (cursor.deferred !== void 0 && cursor.deferred.hostTurn !== hostTurn) flushDeferred(cursor, built, sink);
2723
+ const own = buildRequest(cursor, row, data, false);
2724
+ const message = fromLogMessage(data["message"]);
2725
+ let request = own;
2726
+ if (cursor.deferred !== void 0) {
2727
+ request = cursor.deferred.request;
2728
+ const blocks = [...cursor.deferred.blocks, ...message?.content ?? []];
2729
+ cursor.deferred.blocks = blocks;
2730
+ joinCall(request, own, blocks);
2731
+ }
2732
+ if (cursor.external === true && own.usage === void 0 && own.status !== "failed") cursor.deferred ??= {
2733
+ request,
2734
+ hostTurn,
2735
+ blocks: [...message?.content ?? []]
2736
+ };
2737
+ else {
2738
+ delete cursor.deferred;
2739
+ built.push(request);
2740
+ cursor.requests.push(request);
2741
+ sink.request?.(hostTurn, request);
2742
+ }
2743
+ if (message !== void 0) message.sourceTurn = typeof data["turn"] === "number" ? data["turn"] : cursor.turnHost;
2744
+ if (message !== void 0) {
2745
+ placeMessage(cursor, row, message);
2746
+ if (message.id !== void 0) {
2747
+ if (request.settledAt !== void 0) cursor.messageDurations.set(message.id, request.settledAt - request.startedAt);
2748
+ sink.closingMessage?.(hostTurn, message.id);
2749
+ }
2750
+ for (const block of message.content) if (block.type === "tool-call" && block.id !== void 0) cursor.callOwners.set(block.id, request);
2751
+ }
2752
+ break;
2753
+ }
2754
+ case "assistant/attempt": {
2755
+ const request = buildRequest(cursor, row, data, true);
2756
+ built.push(request);
2757
+ cursor.requests.push(request);
2758
+ sink.request?.(typeof data["turn"] === "number" ? data["turn"] : cursor.turnHost, request);
2759
+ break;
2760
+ }
2761
+ case "tool/call": {
2762
+ const callId = typeof data["callId"] === "string" ? data["callId"] : void 0;
2763
+ const name = typeof data["name"] === "string" ? data["name"] : void 0;
2764
+ if (callId !== void 0) {
2765
+ cursor.callAt.set(callId, row.time ?? 0);
2766
+ if (name !== void 0) cursor.callNames.set(callId, name);
2767
+ const owner = cursor.callOwners.get(callId);
2768
+ if (owner !== void 0 && name !== void 0) owner.execs.push({
2769
+ callId,
2770
+ name,
2771
+ start: row.time ?? owner.settledAt ?? owner.startedAt
2772
+ });
2773
+ if (name !== void 0 && isDelegationTool(name)) {
2774
+ const delegation = {
2775
+ ...typeof data["turn"] === "number" ? { hostTurn: data["turn"] } : {},
2776
+ callId,
2777
+ name,
2778
+ at: row.time ?? 0,
2779
+ ...delegationLabel(typeof data["arguments"] === "string" ? data["arguments"] : "")
2780
+ };
2781
+ cursor.pendingDelegations.set(callId, delegation);
2782
+ }
2783
+ }
2784
+ break;
2785
+ }
2786
+ case "subagent/catalog": {
2787
+ const childId = typeof data["childId"] === "string" ? data["childId"] : void 0;
2788
+ if (childId !== void 0 && childId.length > 0) {
2789
+ let bound = false;
2790
+ for (const pending of cursor.pendingDelegations.values()) if (pending.childId === void 0) {
2791
+ pending.childId = childId;
2792
+ if (typeof data["label"] === "string" && data["label"].length > 0) pending.catalogLabel = data["label"];
2793
+ bound = true;
2794
+ break;
2795
+ }
2796
+ if (!bound) sink.delegation?.({
2797
+ ...cursor.turnHost === void 0 ? {} : { hostTurn: cursor.turnHost },
2798
+ callId: "",
2799
+ name: "subagent/catalog",
2800
+ at: row.time ?? 0,
2801
+ childId,
2802
+ ...typeof data["label"] === "string" && data["label"].length > 0 ? { catalogLabel: data["label"] } : {}
2803
+ });
2804
+ }
2805
+ break;
2806
+ }
2807
+ case "compaction/start": {
2808
+ const id = data["compactionId"];
2809
+ if (typeof id === "string") cursor.compactions.set(id, {
2810
+ startedAt: row.time ?? 0,
2811
+ ...typeof data["turn"] === "number" ? { hostTurn: data["turn"] } : {}
2812
+ });
2813
+ break;
2814
+ }
2815
+ case "compaction/summary": {
2816
+ const request = buildCompaction(cursor, row, data);
2817
+ if (request) {
2818
+ const id = request.compaction.id;
2819
+ cursor.compactions.set(id, {
2820
+ ...cursor.compactions.get(id),
2821
+ startedAt: request.startedAt,
2822
+ request
2823
+ });
2824
+ built.push(request);
2825
+ sink.request?.(cursor.turnHost, request);
2826
+ }
2827
+ break;
2828
+ }
2829
+ case "compaction/end":
2830
+ if (typeof data["compactionId"] === "string") cursor.compactions.delete(data["compactionId"]);
2831
+ break;
2832
+ case "tool/result": {
2833
+ const message = fromLogMessage(data["message"]);
2834
+ if (message !== void 0) message.sourceTurn = typeof data["turn"] === "number" ? data["turn"] : cursor.turnHost;
2835
+ if (message !== void 0) placeMessage(cursor, row, message);
2836
+ if (row.surfaceOp && row.surfaceOp !== "append") break;
2837
+ const callId = message?.toolCallId ?? (typeof data["toolCallId"] === "string" ? data["toolCallId"] : void 0);
2838
+ if (callId !== void 0) {
2839
+ const span = cursor.callOwners.get(callId)?.execs.find((exec) => exec.callId === callId && exec.end === void 0);
2840
+ if (span !== void 0) {
2841
+ if (row.time !== void 0) span.end = row.time;
2842
+ span.isError = message?.isError === true;
2843
+ if (message !== void 0) span.result = serializeMessage(message);
2844
+ }
2845
+ const delegation = cursor.pendingDelegations.get(callId);
2846
+ if (delegation !== void 0) {
2847
+ cursor.pendingDelegations.delete(callId);
2848
+ if (row.time !== void 0) delegation.resultAt = row.time;
2849
+ delegation.isError = message?.isError === true;
2850
+ if (delegation.isError && message !== void 0) delegation.errorPreview = previewLine(serializeMessage(message));
2851
+ sink.delegation?.(delegation);
2852
+ }
2853
+ }
2854
+ break;
2855
+ }
2856
+ }
2857
+ if (row.time !== void 0 && row.type !== "local-agent/stream") cursor.lastEventAt = row.time;
2858
+ }
2859
+ return built;
2860
+ }
2861
+ /** Apply inclusive endpoints in CURRENT surface order (seq order may differ). */
2862
+ function placeMessage(cursor, row, message) {
2863
+ const node = {
2864
+ seq: row.seq ?? cursor.consumed,
2865
+ message
2866
+ };
2867
+ const op = row.surfaceOp;
2868
+ if (op === void 0 || op === "append") {
2869
+ cursor.messages.push(node);
2870
+ return;
2871
+ }
2872
+ const start = cursor.messages.findIndex((item) => item.seq === op.startSeq);
2873
+ const end = cursor.messages.findIndex((item) => item.seq === op.endSeq);
2874
+ if (op.op !== "replace" || start < 0 || end < start) throw new Error(`Invalid context surface replacement at event ${row.seq}`);
2875
+ if (row.type === "tool/result") {
2876
+ const original = cursor.messages[start].message;
2877
+ const beforeChars = original.pruning?.beforeChars ?? serializeMessage(original).length;
2878
+ const afterChars = serializeMessage(message).length;
2879
+ if (afterChars < beforeChars) message.pruning = {
2880
+ beforeChars,
2881
+ afterChars
2882
+ };
2883
+ }
2884
+ cursor.messages.splice(start, end - start + 1, node);
2885
+ const compression = message.compactionId === void 0 ? void 0 : cursor.compactions.get(message.compactionId)?.request?.compaction;
2886
+ if (compression) compression.applied = true;
2887
+ }
2888
+ /** Recover recorded model-backed summaries without inventing the missing instruction. */
2889
+ function buildCompaction(cursor, row, data) {
2890
+ const id = data["compactionId"];
2891
+ if (typeof id !== "string" || data["llmStreamCall"] !== true && data["usage"] === void 0) return void 0;
2892
+ const start = cursor.compactions.get(id);
2893
+ const seqs = Array.isArray(data["shadowedSeqs"]) ? data["shadowedSeqs"] : [];
2894
+ const selected = new Set(seqs);
2895
+ const region = cursor.messages.filter((item) => selected.has(item.seq));
2896
+ if (!fromLogMessage({
2897
+ role: "assistant",
2898
+ content: data["summary"]
2899
+ })) return void 0;
2900
+ const temp = openCursor();
2901
+ if (cursor.materialize !== void 0) temp.materialize = cursor.materialize;
2902
+ temp.turnHost = start?.hostTurn;
2903
+ const head = cursor.messages[0];
2904
+ temp.messages = head !== void 0 && head.message.role === "system" && !selected.has(head.seq) ? [head, ...region] : region;
2905
+ if (cursor.tools !== void 0) temp.tools = cursor.tools;
2906
+ temp.stepStartAt = start?.startedAt ?? row.time;
2907
+ temp.config = {
2908
+ ...typeof data["provider"] === "string" ? { provider: data["provider"] } : {},
2909
+ ...typeof data["model"] === "string" ? { model: data["model"] } : {},
2910
+ ...typeof data["maxTokens"] === "number" ? { maxTokens: data["maxTokens"] } : {}
2911
+ };
2912
+ return {
2913
+ ...buildRequest(temp, row, {
2914
+ message: {
2915
+ role: "assistant",
2916
+ content: data["summary"]
2917
+ },
2918
+ usage: data["usage"]
2919
+ }, false),
2920
+ responseSummary: { kind: "compaction" },
2921
+ purpose: "compaction",
2922
+ compaction: {
2923
+ id,
2924
+ applied: false,
2925
+ ...start?.hostTurn === void 0 ? {} : { hostTurn: start.hostTurn },
2926
+ ...Array.isArray(data["shadowedSeqs"]) && region.length === seqs.length ? {
2927
+ replacedCount: region.length,
2928
+ replacedChars: region.reduce((n, item) => n + serializeMessage(item.message).length, 0)
2929
+ } : {},
2930
+ inputPartial: true
2931
+ }
2932
+ };
2933
+ }
2934
+ /**
2935
+ * One-shot reconstruction of a whole session log into turns (the main-session
2936
+ * backfill). Requests group by their host turn number (step-scoped events
2937
+ * carry it; a torn bracket falls back to the fold's open-turn state). Turns
2938
+ * never closed by a turn/end (a crash) come out with `endReasonKind` unset.
2939
+ */
2940
+ function reconstructSession(rows, materialize) {
2941
+ const cursor = openCursor();
2942
+ if (materialize !== void 0) cursor.materialize = materialize;
2943
+ const turns = [];
2944
+ const byHostTurn = /* @__PURE__ */ new Map();
2945
+ const orphans = [];
2946
+ const place = (delegation) => {
2947
+ const turn = turnFor(delegation.hostTurn, delegation.at);
2948
+ if (turn !== void 0) turn.delegations.push(delegation);
2949
+ else if (delegation.childId !== void 0) orphans.push(delegation);
2950
+ };
2951
+ const turnFor = (hostTurn, at) => {
2952
+ if (hostTurn === void 0) return void 0;
2953
+ let turn = byHostTurn.get(hostTurn);
2954
+ if (turn === void 0) {
2955
+ turn = {
2956
+ hostTurn,
2957
+ query: "",
2958
+ ...at === void 0 ? { startedAt: 0 } : { startedAt: at },
2959
+ requests: [],
2960
+ delegations: []
2961
+ };
2962
+ byHostTurn.set(hostTurn, turn);
2963
+ turns.push(turn);
2964
+ } else if (turn.startedAt === 0 && at !== void 0) turn.startedAt = at;
2965
+ return turn;
2966
+ };
2967
+ const sink = {
2968
+ turnStart: (hostTurn, at, seq) => {
2969
+ const turn = turnFor(hostTurn, at);
2970
+ if (turn !== void 0 && seq !== void 0 && turn.startSeq === void 0) turn.startSeq = seq;
2971
+ },
2972
+ turnEnd: (hostTurn, reason, at) => {
2973
+ const turn = turnFor(hostTurn, at);
2974
+ if (turn === void 0) return;
2975
+ if (at !== void 0) turn.endedAt = at;
2976
+ turn.endReasonKind = reason.kind;
2977
+ if (reason.kind === "error" && reason.error?.message !== void 0) turn.endError = reason.error.message;
2978
+ },
2979
+ queryText: (hostTurn, text, at) => {
2980
+ const turn = turnFor(hostTurn, at);
2981
+ if (turn !== void 0 && turn.query === "") turn.query = text;
2982
+ },
2983
+ triggerText: (hostTurn, source, text) => {
2984
+ const turn = turnFor(hostTurn, void 0);
2985
+ if (turn !== void 0 && turn.trigger === void 0) turn.trigger = {
2986
+ source,
2987
+ text
2988
+ };
2989
+ },
2990
+ closingMessage: (hostTurn, messageId) => {
2991
+ const turn = turnFor(hostTurn, void 0);
2992
+ if (turn !== void 0) turn.closingMessageId = messageId;
2993
+ },
2994
+ request: (hostTurn, request) => {
2995
+ if (request.compaction) turns.push({
2996
+ hostTurn: -turns.length - 1,
2997
+ purpose: "compaction",
2998
+ query: "compaction",
2999
+ startedAt: request.startedAt,
3000
+ endedAt: request.settledAt ?? request.startedAt,
3001
+ endReasonKind: "completed",
3002
+ requests: [request],
3003
+ delegations: []
3004
+ });
3005
+ else turnFor(hostTurn, request.startedAt)?.requests.push(request);
3006
+ },
3007
+ delegation: (delegation) => {
3008
+ place(delegation);
3009
+ }
3010
+ };
3011
+ foldLogRows(cursor, rows, sink);
3012
+ flushDeferred(cursor, [], sink);
3013
+ for (const pending of cursor.pendingDelegations.values()) place(pending);
3014
+ if (orphans.length > 0) turns.push({
3015
+ hostTurn: -1,
3016
+ purpose: "room-members",
3017
+ query: "",
3018
+ startedAt: orphans[0].at,
3019
+ endedAt: orphans.at(-1).at,
3020
+ endReasonKind: "completed",
3021
+ requests: [],
3022
+ delegations: orphans
3023
+ });
3024
+ return turns;
3025
+ }
3026
+ /** The delegation tool family whose calls pair with `subagent/catalog` events. */
3027
+ function isDelegationTool(name) {
3028
+ return /^subagent[_-]/.test(name);
3029
+ }
3030
+ /** The delegation label out of the call arguments (`description` is the subagent_dsh convention). */
3031
+ function delegationLabel(args) {
3032
+ try {
3033
+ const parsed = JSON.parse(args);
3034
+ if (typeof parsed === "object" && parsed !== null) {
3035
+ const description = parsed.description;
3036
+ if (typeof description === "string" && description.length > 0) return { label: description };
3037
+ }
3038
+ } catch {}
3039
+ return {};
3040
+ }
3041
+ /** The turn-list query preview: one collapsed line, capped. */
3042
+ function collapseQuery(text) {
3043
+ return text.replace(/\s+/g, " ").trim().slice(0, 120);
3044
+ }
3045
+ /**
3046
+ * A log without request headers (an external agent's child: its CLI owns the
3047
+ * request) still names who answered on the assistant message's source; the
3048
+ * rest of the request config was never recorded and stays absent.
3049
+ */
3050
+ function configFromSource(raw) {
3051
+ const source = typeof raw === "object" && raw !== null ? raw["source"] : void 0;
3052
+ return {
3053
+ ...typeof source?.["provider"] === "string" ? { provider: source["provider"] } : {},
3054
+ ...typeof source?.["model"] === "string" ? { model: source["model"] } : {}
3055
+ };
3056
+ }
3057
+ /** One settled request out of its assistant event: stream timing, usage, and the segment split. */
3058
+ function buildRequest(cursor, row, data, failed) {
3059
+ const written = cursor.stepStartAt ?? row.time ?? 0;
3060
+ const startedAt = cursor.external === true && row.time !== void 0 && row.time - written < MIRROR_LAG_MS && cursor.stepPrevAt !== void 0 ? cursor.stepPrevAt : written;
3061
+ const settledAt = row.time ?? startedAt;
3062
+ const streamFacts = readStream(Array.isArray(data["stream"]) ? data["stream"] : []);
3063
+ const usage = readUsage(data["usage"]);
3064
+ const hostTurn = typeof data["turn"] === "number" ? data["turn"] : cursor.turnHost;
3065
+ const messages = cursor.messages.map((folded) => folded.message);
3066
+ let segments;
3067
+ if (cursor.materialize?.(hostTurn) === false) {
3068
+ cursor.previousMessages = messages;
3069
+ cursor.previousToolNames = (cursor.tools ?? []).map((tool) => tool.name);
3070
+ cursor.previous = void 0;
3071
+ segments = {
3072
+ system: [],
3073
+ tools: [],
3074
+ history: [],
3075
+ added: [],
3076
+ totalChars: 0,
3077
+ fingerprint: {
3078
+ messageKeys: [],
3079
+ toolNames: []
3080
+ }
3081
+ };
3082
+ } else {
3083
+ if (cursor.previous === void 0 && cursor.previousMessages !== void 0) cursor.previous = {
3084
+ messageKeys: cursor.previousMessages.map((message) => messageKey(message, contentHash)),
3085
+ toolNames: cursor.previousToolNames ?? []
3086
+ };
3087
+ delete cursor.previousMessages;
3088
+ segments = splitRequest({
3089
+ messages,
3090
+ ...cursor.tools === void 0 ? {} : { tools: cursor.tools },
3091
+ context: {
3092
+ ...cursor.previous === void 0 ? {} : { previous: cursor.previous },
3093
+ assemblies: [],
3094
+ resolveToolName: (callId) => cursor.callNames.get(callId),
3095
+ durationFor: (messageId) => cursor.messageDurations.get(messageId)
3096
+ }
3097
+ });
3098
+ cursor.previous = segments.fingerprint;
3099
+ }
3100
+ const message = failed ? void 0 : fromLogMessage(data["message"]);
3101
+ const blocks = message !== void 0 ? spansFromMessage(message, streamFacts, startedAt) : streamFacts.blocks;
3102
+ const response = message !== void 0 ? responseFromMessage(message) : responseFromSpans(streamFacts.blocks);
3103
+ const finish = streamFacts.finish;
3104
+ const error = failed ? finish?.failure ?? "attempt settled without a committed message" : void 0;
3105
+ const finishKind = finish?.kind;
3106
+ return {
3107
+ ...cursor.numberedFromStart ? { requestNumber: cursor.requests.length + 1 } : {},
3108
+ ...hostTurn === void 0 ? {} : { hostTurn },
3109
+ startedAt,
3110
+ status: failed ? "failed" : "ok",
3111
+ ...error === void 0 ? {} : { error },
3112
+ config: cursor.config ?? configFromSource(data["message"]),
3113
+ ...usage === void 0 ? {} : { usage },
3114
+ responseSummary: message !== void 0 ? summarizeMessage(message) : summarizeBlocks(streamFacts.blocks),
3115
+ ...response === void 0 ? {} : { response },
3116
+ segments: {
3117
+ system: segments.system,
3118
+ tools: segments.tools,
3119
+ history: segments.history,
3120
+ added: segments.added
3121
+ },
3122
+ totalChars: segments.totalChars,
3123
+ ...streamFacts.firstChunkAt === void 0 ? {} : { firstChunkAt: streamFacts.firstChunkAt },
3124
+ settledAt,
3125
+ ...finishKind === void 0 ? {} : { finishKind },
3126
+ blocks,
3127
+ execs: [],
3128
+ fingerprint: segments.fingerprint
3129
+ };
3130
+ }
3131
+ /** Block spans for a committed response: stream timing with the message's tool-call identity. */
3132
+ function spansFromMessage(message, facts, fallbackStart) {
3133
+ const calls = message.content.filter((block) => block.type === "tool-call");
3134
+ const spans = facts.blocks.map((span) => ({ ...span }));
3135
+ for (const span of spans) {
3136
+ if (span.kind !== "tool-call") continue;
3137
+ if (span.callId !== void 0) continue;
3138
+ const call = calls.find((block) => block.id !== void 0 && !spans.some((other) => other !== span && other.callId === block.id));
3139
+ if (call?.id !== void 0) {
3140
+ span.callId = call.id;
3141
+ if (call.name !== void 0) span.toolName = call.name;
3142
+ if (call.arguments !== void 0) span.args = call.arguments;
3143
+ }
3144
+ }
3145
+ if (spans.length === 0 && message.content.length > 0) return [{
3146
+ kind: "text",
3147
+ index: 0,
3148
+ start: facts.firstChunkAt ?? fallbackStart
3149
+ }];
3150
+ return spans;
3151
+ }
3152
+ /** A deferred call's step that never got its usage step still counts as a request. */
3153
+ function flushDeferred(cursor, built, sink) {
3154
+ const deferred = cursor.deferred;
3155
+ if (deferred === void 0) return;
3156
+ delete cursor.deferred;
3157
+ built.push(deferred.request);
3158
+ cursor.requests.push(deferred.request);
3159
+ sink.request?.(deferred.hostTurn, deferred.request);
3160
+ }
3161
+ /** Fold one more step of a call into the request the call began (its input stays the first step's). */
3162
+ function joinCall(request, step, blocks) {
3163
+ const message = {
3164
+ role: "assistant",
3165
+ content: blocks
3166
+ };
3167
+ const response = responseFromMessage(message);
3168
+ if (response !== void 0) request.response = response;
3169
+ request.responseSummary = summarizeMessage(message);
3170
+ request.status = step.status;
3171
+ if (step.error !== void 0) request.error = step.error;
3172
+ if (step.usage !== void 0) request.usage = step.usage;
3173
+ if (step.settledAt !== void 0) request.settledAt = step.settledAt;
3174
+ if (step.finishKind !== void 0) request.finishKind = step.finishKind;
3175
+ request.blocks.push(...step.blocks);
3176
+ request.execs.push(...step.execs);
3177
+ }
3178
+ /** The response's own content from a committed assistant message (authoritative). */
3179
+ function responseFromMessage(message) {
3180
+ const reasoning = message.content.filter((block) => block.type === "reasoning").map((block) => block.text ?? "").join("");
3181
+ const text = message.content.filter((block) => block.type === "text").map((block) => block.text ?? "").join("");
3182
+ const toolCalls = message.content.filter((block) => block.type === "tool-call" && block.name !== void 0).map((block) => ({
3183
+ ...block.id === void 0 ? {} : { callId: block.id },
3184
+ name: block.name,
3185
+ arguments: block.arguments ?? ""
3186
+ }));
3187
+ if (reasoning === "" && text === "" && toolCalls.length === 0) return void 0;
3188
+ return {
3189
+ ...reasoning === "" ? {} : { reasoning },
3190
+ ...text === "" ? {} : { text },
3191
+ toolCalls
3192
+ };
3193
+ }
3194
+ /** The response hint from a committed message's blocks (structured 明细头锚点). */
3195
+ function summarizeMessage(message) {
3196
+ const calls = message.content.filter((block) => block.type === "tool-call" && block.name !== void 0);
3197
+ if (calls.length > 0) {
3198
+ const first = calls[0];
3199
+ const hint = hintFromArgs(first.arguments);
3200
+ return {
3201
+ kind: "tool",
3202
+ name: first.name,
3203
+ ...hint === void 0 ? {} : { hint },
3204
+ ...calls.length > 1 ? {
3205
+ more: calls.length - 1,
3206
+ toolCounts: countToolCalls(calls.map((call) => call.name))
3207
+ } : {}
3208
+ };
3209
+ }
3210
+ if (message.content.some((block) => block.type === "text" && (block.text ?? "").trim().length > 0)) return { kind: "text" };
3211
+ return message.content.some((block) => block.type === "reasoning" && (block.text ?? "").trim().length > 0) ? { kind: "reasoning" } : { kind: "none" };
3212
+ }
3213
+ /** The response summary when only stream spans survived (an attempt). */
3214
+ function summarizeBlocks(blocks) {
3215
+ const calls = blocks.filter((block) => block.kind === "tool-call" && block.toolName !== void 0);
3216
+ if (calls.length === 0) {
3217
+ if (blocks.some((block) => block.kind === "text")) return { kind: "text" };
3218
+ return blocks.some((block) => block.kind === "reasoning") ? { kind: "reasoning" } : { kind: "none" };
3219
+ }
3220
+ const first = calls[0];
3221
+ const hint = hintFromArgs(first.args);
3222
+ return {
3223
+ kind: "tool",
3224
+ name: first.toolName,
3225
+ ...hint === void 0 ? {} : { hint },
3226
+ ...calls.length > 1 ? {
3227
+ more: calls.length - 1,
3228
+ toolCounts: countToolCalls(calls.map((call) => call.toolName))
3229
+ } : {}
3230
+ };
3231
+ }
3232
+ /** The one-line argument hint — the reference reconstruction's key order. */
3233
+ function hintFromArgs(args) {
3234
+ if (args === void 0) return void 0;
3235
+ let parsed;
3236
+ try {
3237
+ parsed = JSON.parse(args);
3238
+ } catch {
3239
+ return;
3240
+ }
3241
+ if (typeof parsed !== "object" || parsed === null) return void 0;
3242
+ const value = parsed;
3243
+ const what = value["what"];
3244
+ if (typeof what === "string" && what.length > 0) return previewLine(what, 40);
3245
+ const file = value["file_path"] ?? value["path"] ?? value["filePath"];
3246
+ if (typeof file === "string" && file.length > 0) return previewLine(file.split("/").pop() ?? file, 40);
3247
+ const purpose = value["for"];
3248
+ if (typeof purpose === "string" && purpose.length > 0) return previewLine(purpose, 40);
3249
+ const description = value["description"];
3250
+ if (typeof description === "string" && description.length > 0) return previewLine(description, 40);
3251
+ }
3252
+ /**
3253
+ * Block timing out of one compact stream record list (`AssistantStreamRecord`):
3254
+ * block-start/block-end ride as raw `chunk` records, deltas pack into runs
3255
+ * whose `time0` still counts toward the first-chunk time.
3256
+ */
3257
+ function readStream(records) {
3258
+ const blocks = [];
3259
+ let firstChunkAt;
3260
+ let finish;
3261
+ const note = (time) => {
3262
+ if (typeof time !== "number") return;
3263
+ if (firstChunkAt === void 0 || time < firstChunkAt) firstChunkAt = time;
3264
+ };
3265
+ for (const raw of records) {
3266
+ if (typeof raw !== "object" || raw === null) continue;
3267
+ const record = raw;
3268
+ if (record["type"] === "chunk") {
3269
+ const time = record["time"];
3270
+ const chunk = record["chunk"];
3271
+ if (typeof time !== "number" || chunk === void 0) continue;
3272
+ note(time);
3273
+ switch (chunk["type"]) {
3274
+ case "block-start":
3275
+ blocks.push({
3276
+ kind: typeof chunk["blockType"] === "string" ? chunk["blockType"] : "unknown",
3277
+ index: typeof chunk["index"] === "number" ? chunk["index"] : blocks.length,
3278
+ start: time
3279
+ });
3280
+ break;
3281
+ case "block-end": {
3282
+ const index = chunk["index"];
3283
+ const span = blocks.find((block) => block.index === index && block.end === void 0);
3284
+ if (span !== void 0) {
3285
+ span.end = time;
3286
+ const block = chunk["block"];
3287
+ if (typeof block?.["text"] === "string") span.text = block["text"];
3288
+ if (block?.["type"] === "tool-call") {
3289
+ if (typeof block["id"] === "string") span.callId = block["id"];
3290
+ if (typeof block["name"] === "string") span.toolName = block["name"];
3291
+ if (typeof block["arguments"] === "string") span.args = block["arguments"];
3292
+ }
3293
+ }
3294
+ break;
3295
+ }
3296
+ case "finish": {
3297
+ const reason = chunk["reason"];
3298
+ const kind = typeof reason?.["kind"] === "string" ? reason["kind"] : void 0;
3299
+ const failure = reason?.["failure"];
3300
+ finish = {
3301
+ kind: kind ?? "unknown",
3302
+ ...typeof failure?.["message"] === "string" ? { failure: failure["message"] } : {}
3303
+ };
3304
+ break;
3305
+ }
3306
+ }
3307
+ } else {
3308
+ note(record["time0"]);
3309
+ const span = blocks.find((block) => block.index === record["index"] && block.end === void 0);
3310
+ if (span !== void 0) {
3311
+ if (Array.isArray(record["texts"])) span.text = (span.text ?? "") + record["texts"].join("");
3312
+ else if (Array.isArray(record["args"])) span.args = (span.args ?? "") + record["args"].join("");
3313
+ }
3314
+ }
3315
+ }
3316
+ return {
3317
+ ...firstChunkAt === void 0 ? {} : { firstChunkAt },
3318
+ blocks,
3319
+ ...finish === void 0 ? {} : { finish }
3320
+ };
3321
+ }
3322
+ /** Structural read of one usage object (provider figures, cache fields optional). */
3323
+ function readUsage(raw) {
3324
+ if (typeof raw !== "object" || raw === null) return void 0;
3325
+ const value = raw;
3326
+ if (typeof value["inputTokens"] !== "number" || typeof value["outputTokens"] !== "number") return void 0;
3327
+ return {
3328
+ inputTokens: value["inputTokens"],
3329
+ outputTokens: value["outputTokens"],
3330
+ ...typeof value["totalTokens"] === "number" ? { totalTokens: value["totalTokens"] } : {},
3331
+ ...typeof value["cacheReadTokens"] === "number" ? { cacheReadTokens: value["cacheReadTokens"] } : {},
3332
+ ...typeof value["cacheWriteTokens"] === "number" ? { cacheWriteTokens: value["cacheWriteTokens"] } : {}
3333
+ };
3334
+ }
3335
+ //#endregion
3336
+ //#region lib/types/service.js
3337
+ /** The capture store's query face (Remote delegates here). */
3338
+ var InspectorService = class {
3339
+ ctx;
3340
+ config;
3341
+ store;
3342
+ disposers;
3343
+ /** childId → open read handle (persistent: reads are offset-incremental). */
3344
+ handles = /* @__PURE__ */ new Map();
3345
+ cursors = /* @__PURE__ */ new Map();
3346
+ /** In-flight refresh per childId — concurrent panel queries share one read. */
3347
+ refreshing = /* @__PURE__ */ new Map();
3348
+ /** sessionId → the metering event (`compaction/summary` | `compaction/prune`) just seen; the host appends its replacement next. */
3349
+ meters = /* @__PURE__ */ new Map();
3350
+ now;
3351
+ logWarn;
3352
+ persistenceOverride;
3353
+ constructor(ctx, config = {}, deps = {}) {
3354
+ this.ctx = ctx;
3355
+ this.config = config;
3356
+ this.now = deps.now ?? Date.now;
3357
+ this.logWarn = deps.logger?.warn.bind(deps.logger) ?? ((message) => ctx.logger.warn(message));
3358
+ this.persistenceOverride = deps.persistence;
3359
+ const storeConfig = {
3360
+ ...config.maxTurnsPerSession === void 0 ? {} : { maxTurnsPerSession: config.maxTurnsPerSession },
3361
+ ...config.maxBackfillRequestsPerSession === void 0 ? {} : { maxBackfillRequestsPerSession: config.maxBackfillRequestsPerSession },
3362
+ ...config.maxStoredCharsTotal === void 0 ? {} : { maxStoredCharsTotal: config.maxStoredCharsTotal }
3363
+ };
3364
+ this.store = new InspectorStore(storeConfig, {
3365
+ now: this.now,
3366
+ onTurnEvicted: (_sessionId, turn) => this.dropEvictedChildState(turn),
3367
+ ...deps.generation === void 0 ? {} : { generation: deps.generation }
3368
+ });
3369
+ this.disposers = [
3370
+ ctx.on("llm/stream", (options, next) => this.onStream(options, next)),
3371
+ ctx.on("system-prompt/assemble", (_assembly, _context, next) => this.onAssemble(next)),
3372
+ ctx.on("session/event", (session, event) => this.onSessionEvent(session, event))
3373
+ ];
3374
+ }
3375
+ /**
3376
+ * The waterfall tee: snapshot the (possibly deep-frozen) request at dispatch,
3377
+ * then forward every chunk unchanged while observing. A capture-side failure
3378
+ * at setup degrades to the bare downstream stream; mid-stream failures only
3379
+ * lose the remaining observations.
3380
+ */
3381
+ onStream(options, next) {
3382
+ let request;
3383
+ try {
3384
+ request = this.store.beginRequest(snapshotOptions(options), this.now());
3385
+ } catch (error) {
3386
+ this.logWarn(`context-lens: capture setup failed, forwarding uncaptured (${String(error)})`);
3387
+ return next();
3388
+ }
3389
+ if (request === void 0) return next();
3390
+ return this.tee(request, next);
3391
+ }
3392
+ tee(request, next) {
3393
+ const { store, now } = this;
3394
+ const warn = this.logWarn;
3395
+ return (async function* () {
3396
+ let thrown;
3397
+ try {
3398
+ for await (const chunk of next()) {
3399
+ try {
3400
+ store.observeChunk(request, chunk, now());
3401
+ } catch (error) {
3402
+ warn(`context-lens: chunk observation dropped (${String(error)})`);
3403
+ }
3404
+ yield chunk;
3405
+ }
3406
+ } catch (error) {
3407
+ thrown = error;
3408
+ throw error;
3409
+ } finally {
3410
+ try {
3411
+ store.settleRequest(request, now(), thrown);
3412
+ } catch (error) {
3413
+ warn(`context-lens: request settlement dropped (${String(error)})`);
3414
+ }
3415
+ }
3416
+ })();
3417
+ }
3418
+ /**
3419
+ * Record the post-waterfall assembly for section attribution. The returned
3420
+ * value passes through unchanged — this listener observes, it never rewrites
3421
+ * a prompt.
3422
+ */
3423
+ async onAssemble(next) {
3424
+ const result = await next();
3425
+ try {
3426
+ recordAssembly(this.store.assemblyRing, result, this.now());
3427
+ } catch (error) {
3428
+ this.logWarn(`context-lens: assembly record dropped (${String(error)})`);
3429
+ }
3430
+ return result;
3431
+ }
3432
+ onSessionEvent(session, event) {
3433
+ try {
3434
+ this.dispatchSessionEvent(session, event);
3435
+ } catch (error) {
3436
+ this.logWarn(`context-lens: session event "${String(event.type)}" dropped (${String(error)})`);
3437
+ }
3438
+ }
3439
+ dispatchSessionEvent(session, event) {
3440
+ const sessionId = session.id;
3441
+ const at = event.time;
3442
+ const eventType = event.type;
3443
+ this.noteReplacement(session, event);
3444
+ this.store.noteLogActivity(sessionId);
3445
+ if (eventType.startsWith("compaction/")) this.store.noteCompaction(sessionId, eventType, event.data);
3446
+ if (eventType.startsWith("room/")) {
3447
+ this.store.noteRoomEvent(sessionId, eventType, event.data, event.seq, at);
3448
+ return;
3449
+ }
3450
+ if (event.type === "tool/result" && event.surfaceOp !== "append") {
3451
+ const original = session.eventAt(event.surfaceOp.startSeq);
3452
+ if (original?.type === "tool/result") this.store.notePruning(sessionId, event.data.message.id, serializeMessage(fromRequestMessage(original.data.message)).length, serializeMessage(fromRequestMessage(event.data.message)).length);
3453
+ return;
3454
+ }
3455
+ switch (event.type) {
3456
+ case "turn/start":
3457
+ this.store.turnStart(sessionId, event.data.turn, at, event.seq);
3458
+ return;
3459
+ case "turn/end":
3460
+ this.store.turnEnd(sessionId, event.data.turn, event.data.reason, at);
3461
+ return;
3462
+ case "user/message": {
3463
+ const message = fromRequestMessage(event.data);
3464
+ if (message.compactionId !== void 0 && event.surfaceOp !== "append") this.store.noteCompaction(sessionId, "compaction/applied", { compactionId: message.compactionId });
3465
+ if (message.id) this.store.noteMessageTurn(sessionId, message.id);
3466
+ this.store.noteUserMessage(sessionId, message.sourceKind, serializeMessage(message));
3467
+ return;
3468
+ }
3469
+ case "assistant/message":
3470
+ this.store.noteMessageTurn(sessionId, event.data.message.id, event.data.turn);
3471
+ this.store.noteAssistantMessage(sessionId, event.data.message.id);
3472
+ return;
3473
+ case "tool/call":
3474
+ this.store.toolCall(sessionId, event.data.callId, event.data.name, event.data.arguments, at);
3475
+ return;
3476
+ case "tool/result": {
3477
+ this.store.noteMessageTurn(sessionId, event.data.message.id, event.data.turn);
3478
+ const message = fromRequestMessage(event.data.message);
3479
+ this.store.toolResult(sessionId, event.data.message.toolCallId, event.data.message.isError === true, serializeMessage(message), at);
3480
+ return;
3481
+ }
3482
+ }
3483
+ const type = event.type;
3484
+ if (type === "subagent/descriptor") {
3485
+ const provider = event.data["provider"];
3486
+ if (typeof provider === "string") {
3487
+ this.store.noteChildProvider(sessionId, provider);
3488
+ if (!IN_PROCESS_PROVIDERS.has(provider)) this.store.markLogDriven(sessionId);
3489
+ }
3490
+ return;
3491
+ }
3492
+ if (type === "subagent/catalog") {
3493
+ const data = event.data;
3494
+ const childId = data["childId"];
3495
+ if (typeof childId === "string" && childId.length > 0) {
3496
+ const label = data["label"];
3497
+ this.store.subagentCatalog(sessionId, childId, typeof label === "string" && label.length > 0 ? label : void 0, at);
3498
+ }
3499
+ }
3500
+ }
3501
+ /** The probed `sessionPersistence` service, when the composition mounts one. */
3502
+ reader() {
3503
+ if (this.persistenceOverride !== void 0) return this.persistenceOverride;
3504
+ try {
3505
+ const candidate = this.ctx.get("sessionPersistence");
3506
+ return isSessionLogReader(candidate) ? candidate : void 0;
3507
+ } catch {
3508
+ return;
3509
+ }
3510
+ }
3511
+ /**
3512
+ * Refresh the log-sourced child scopes of one session (query-time,
3513
+ * incremental). A scope the query's {@link ScopeFilter} excludes is skipped
3514
+ * outright — its log is not opened on an unauthorized query's behalf.
3515
+ */
3516
+ async refreshChildren(rootSessionId, filter) {
3517
+ const reader = this.reader();
3518
+ if (reader === void 0) return;
3519
+ const scopes = this.store.childScopes(rootSessionId);
3520
+ await Promise.all(scopes.map((scope) => scopeAuthorized(scope.scopeId, filter) ? this.refreshChild(rootSessionId, scope.scopeId, reader, scope.live) : Promise.resolve()));
3521
+ }
3522
+ refreshChild(rootSessionId, childId, reader, live) {
3523
+ if (live) return Promise.resolve();
3524
+ const pending = this.refreshing.get(childId);
3525
+ if (pending !== void 0) return pending;
3526
+ const job = this.doRefreshChild(rootSessionId, childId, reader).finally(() => {
3527
+ this.refreshing.delete(childId);
3528
+ });
3529
+ this.refreshing.set(childId, job);
3530
+ return job;
3531
+ }
3532
+ async doRefreshChild(rootSessionId, childId, reader) {
3533
+ let cursor = this.cursors.get(childId);
3534
+ if (cursor?.broken !== void 0) return;
3535
+ if (cursor === void 0) {
3536
+ cursor = openCursor();
3537
+ this.cursors.set(childId, cursor);
3538
+ }
3539
+ try {
3540
+ let handle = this.handles.get(childId);
3541
+ if (handle === void 0) {
3542
+ handle = await reader.open(childId, "read");
3543
+ this.handles.set(childId, handle);
3544
+ }
3545
+ const { events } = await handle.read(cursor.consumed);
3546
+ const provider = events.find((row) => row.type === "subagent/descriptor")?.data?.["provider"];
3547
+ if (typeof provider === "string") this.store.noteChildProvider(childId, provider);
3548
+ for (const request of foldLogRows(cursor, events)) this.store.appendLogRequest(rootSessionId, childId, request);
3549
+ this.store.refreshExecutionResults(rootSessionId, childId);
3550
+ } catch (error) {
3551
+ if (error instanceof Error && error.name === "SessionPersistenceNotFoundError") return;
3552
+ cursor.broken = error instanceof Error ? error.message : String(error);
3553
+ this.logWarn(`context-lens: child log "${childId}" unreadable (${cursor.broken})`);
3554
+ }
3555
+ }
3556
+ /** Turn eviction drops the child cursors/handles whose scopes lived on it. */
3557
+ dropEvictedChildState(turn) {
3558
+ for (const scopeId of turn.scopes.keys()) {
3559
+ if (scopeId === "main") continue;
3560
+ this.cursors.delete(scopeId);
3561
+ const handle = this.handles.get(scopeId);
3562
+ if (handle !== void 0) {
3563
+ this.handles.delete(scopeId);
3564
+ handle.close().catch(() => void 0);
3565
+ }
3566
+ }
3567
+ }
3568
+ /**
3569
+ * Reconstruct the session's pre-mount history from its own log, once per
3570
+ * session (the store's watermark holds). The read is one full pass at first
3571
+ * query; the store keeps the newest turns that fit the budget behind the
3572
+ * live ones. A not-yet-materialized log is transient (retry next query); any
3573
+ * other read failure marks the session broken and stops hammering it.
3574
+ */
3575
+ /** Pair a surface replacement with what it shadowed, typed by the metering event right before it. */
3576
+ noteReplacement(session, row) {
3577
+ const meter = this.meters.get(session.id);
3578
+ this.meters.delete(session.id);
3579
+ const next = meterOf(row);
3580
+ if (next !== void 0) this.meters.set(session.id, next);
3581
+ const fact = replacementFact(row, (seq) => {
3582
+ const shadowed = session.eventAt(seq);
3583
+ return shadowed === void 0 ? void 0 : messageOfRow(shadowed)?.id;
3584
+ }, meter);
3585
+ if (fact !== void 0) this.store.noteReplacement(session.id, fact);
3586
+ }
3587
+ async backfillMainSession(sessionId) {
3588
+ const reader = this.reader();
3589
+ if (reader === void 0) return;
3590
+ const state = this.store.backfillState(sessionId);
3591
+ if (state === "broken" || state === "complete" && !this.store.needsRequestNumbers(sessionId) && !this.store.needsLogRead(sessionId)) return;
3592
+ const jobKey = `main:${sessionId}`;
3593
+ const pending = this.refreshing.get(jobKey);
3594
+ if (pending !== void 0) return pending;
3595
+ const job = this.doBackfillMainSession(sessionId, reader).finally(() => {
3596
+ this.refreshing.delete(jobKey);
3597
+ });
3598
+ this.refreshing.set(jobKey, job);
3599
+ return job;
3600
+ }
3601
+ async doBackfillMainSession(sessionId, reader) {
3602
+ try {
3603
+ const handle = await reader.open(sessionId, "read");
3604
+ try {
3605
+ const { events } = await handle.read(0);
3606
+ const hosts = events.filter((row) => row.type === "turn/start").map((row) => row.data?.turn).filter((turn) => typeof turn === "number");
3607
+ const provider = events.find((row) => row.type === "subagent/descriptor")?.data?.["provider"];
3608
+ if (typeof provider === "string" && !IN_PROCESS_PROVIDERS.has(provider)) this.store.markLogDriven(sessionId);
3609
+ const keep = this.store.windowHosts(sessionId, hosts);
3610
+ this.store.insertLogTurns(sessionId, reconstructSession(events, (host) => host === void 0 || keep.has(host)));
3611
+ for (const fact of replacementFactsOf(events)) this.store.noteReplacement(sessionId, fact);
3612
+ for (const row of events) if (row.type.startsWith("room/")) this.store.noteRoomEvent(sessionId, row.type, row.data, row.seq, row.time);
3613
+ await this.resolveParent(sessionId, events, reader);
3614
+ } finally {
3615
+ handle.close().catch(() => void 0);
3616
+ }
3617
+ } catch (error) {
3618
+ if (error instanceof Error && error.name === "SessionPersistenceNotFoundError") return;
3619
+ const reason = error instanceof Error ? error.message : String(error);
3620
+ this.store.markBackfillBroken(sessionId, reason);
3621
+ this.logWarn(`context-lens: session "${sessionId}" log unreadable, history backfill skipped (${reason})`);
3622
+ }
3623
+ }
3624
+ /**
3625
+ * A session opened on its own whose log header names a `parentSession`:
3626
+ * read the parent's log once for its title, the turn whose catalog row
3627
+ * started this child, and that row's label. Best effort — an unreadable
3628
+ * parent leaves just its id.
3629
+ */
3630
+ async resolveParent(sessionId, events, reader) {
3631
+ const header = events[0];
3632
+ const parentId = header?.type === "session" ? header.parentSession : void 0;
3633
+ if (typeof parentId !== "string" || parentId.length === 0 || this.store.hasParent(sessionId)) return;
3634
+ const parent = { sessionId: parentId };
3635
+ try {
3636
+ const handle = await reader.open(parentId, "read");
3637
+ try {
3638
+ const { events: rows } = await handle.read(0);
3639
+ let turn;
3640
+ for (const row of rows) {
3641
+ const data = row.data ?? {};
3642
+ if (row.type === "turn/start" && typeof data["turn"] === "number") turn = data["turn"];
3643
+ if (row.type === "session/title" && typeof data["title"] === "string") parent.title = data["title"];
3644
+ if (row.type === "subagent/catalog" && data["childId"] === sessionId) {
3645
+ if (turn !== void 0) parent.hostTurn = turn;
3646
+ if (typeof data["label"] === "string") parent.label = data["label"];
3647
+ }
3648
+ }
3649
+ } finally {
3650
+ handle.close().catch(() => void 0);
3651
+ }
3652
+ } catch {}
3653
+ this.store.noteParent(sessionId, parent);
3654
+ }
3655
+ /**
3656
+ * Light turn list of one session plus its backfill watermark. The optional
3657
+ * {@link ScopeFilter} restricts rows and aggregates to the authorized
3658
+ * scopes; absent is the unfiltered local semantics.
3659
+ */
3660
+ async listTurns(sessionId, filter) {
3661
+ await this.backfillMainSession(sessionId);
3662
+ await this.refreshChildren(sessionId, filter);
3663
+ return this.store.listTurns(sessionId, filter);
3664
+ }
3665
+ /**
3666
+ * Move the session's backfill window to the turns ending at `end` (the
3667
+ * latest turns when null) and answer the refreshed turn list. Live turns
3668
+ * stay; the window refills from the log in the same call.
3669
+ */
3670
+ async loadTurns(sessionId, end, filter) {
3671
+ const pending = this.refreshing.get(`main:${sessionId}`);
3672
+ if (pending !== void 0) await pending;
3673
+ this.store.setWindow(sessionId, end ?? void 0);
3674
+ return this.listTurns(sessionId, filter);
3675
+ }
3676
+ /** One turn's scope/request overview, without segments. */
3677
+ async getTurn(sessionId, turnSeq, filter) {
3678
+ await this.backfillMainSession(sessionId);
3679
+ await this.refreshChildren(sessionId, filter);
3680
+ return this.store.getTurn(sessionId, turnSeq, filter);
3681
+ }
3682
+ /** One request's full four-segment breakdown (a {@link ScopedRefusal} when the filter excludes its scope). */
3683
+ async getRequest(sessionId, seq, filter) {
3684
+ await this.backfillMainSession(sessionId);
3685
+ await this.refreshChildren(sessionId, filter);
3686
+ const request = this.store.getRequest(sessionId, seq, filter);
3687
+ const tools = request !== void 0 && "prefix" in request ? request.prefix?.toolChanges : void 0;
3688
+ if (tools === void 0 || tools.length === 0) return request;
3689
+ return {
3690
+ ...request,
3691
+ prefix: {
3692
+ ...request.prefix,
3693
+ toolChanges: tools.map((tool) => {
3694
+ const owner = this.toolOwner(tool.name);
3695
+ return owner === void 0 ? tool : {
3696
+ ...tool,
3697
+ owner
3698
+ };
3699
+ })
3700
+ }
3701
+ };
3702
+ }
3703
+ /** The declaring package from a registered tool's host-side origin tag (capability-catalog convention); absent when untagged. */
3704
+ toolOwner(name) {
3705
+ try {
3706
+ const definition = this.ctx.get("tools")?.get?.(name);
3707
+ if (typeof definition !== "object" || definition === null) return void 0;
3708
+ const origin = definition[Symbol.for("dsh.tool.origin")];
3709
+ return typeof origin?.owner === "string" ? origin.owner : void 0;
3710
+ } catch {
3711
+ return;
3712
+ }
3713
+ }
3714
+ /** Read the session map through the same backfill and scope-filtered path. */
3715
+ async getSessionTrend(sessionId, filter) {
3716
+ await this.listTurns(sessionId, filter);
3717
+ return this.store.getSessionTrend(sessionId, filter);
3718
+ }
3719
+ /** One turn's whole-turn timing aggregation. */
3720
+ async getTurnTiming(sessionId, turnSeq, filter) {
3721
+ await this.backfillMainSession(sessionId);
3722
+ await this.refreshChildren(sessionId, filter);
3723
+ return this.store.getTurnTiming(sessionId, turnSeq, filter);
3724
+ }
3725
+ /** Release every session buffer, listener, and open child-log handle (plugin dispose). */
3726
+ dispose() {
3727
+ for (const dispose of this.disposers.splice(0)) dispose();
3728
+ this.store.dispose();
3729
+ for (const handle of this.handles.values()) handle.close().catch(() => void 0);
3730
+ this.handles.clear();
3731
+ this.cursors.clear();
3732
+ }
3733
+ };
3734
+ /** The dispatch-time snapshot of one GenerateOptions (never mutates the frozen input). */
3735
+ function snapshotOptions(options) {
3736
+ return {
3737
+ ...options.sessionId === void 0 ? {} : { sessionId: options.sessionId },
3738
+ ...options.purpose === void 0 ? {} : { purpose: options.purpose },
3739
+ config: {
3740
+ provider: options.provider,
3741
+ model: options.model,
3742
+ ...options.reasoningEffort === void 0 ? {} : { reasoningEffort: options.reasoningEffort },
3743
+ ...options.temperature === void 0 ? {} : { temperature: options.temperature },
3744
+ ...options.maxTokens === void 0 ? {} : { maxTokens: options.maxTokens }
3745
+ },
3746
+ messages: options.messages.map(fromRequestMessage),
3747
+ ...options.system === void 0 ? {} : { systemSlot: options.system },
3748
+ ...options.tools === void 0 ? {} : { tools: options.tools }
3749
+ };
3750
+ }
3751
+ //#endregion
3752
+ //#region lib/types/remote.js
3753
+ var __runInitializers = function(thisArg, initializers, value) {
3754
+ var useValue = arguments.length > 2;
3755
+ for (var i = 0; i < initializers.length; i++) value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
3756
+ return useValue ? value : void 0;
3757
+ };
3758
+ var __esDecorate = function(ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
3759
+ function accept(f) {
3760
+ if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected");
3761
+ return f;
3762
+ }
3763
+ var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
3764
+ var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
3765
+ var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
3766
+ var _, done = false;
3767
+ for (var i = decorators.length - 1; i >= 0; i--) {
3768
+ var context = {};
3769
+ for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
3770
+ for (var p in contextIn.access) context.access[p] = contextIn.access[p];
3771
+ context.addInitializer = function(f) {
3772
+ if (done) throw new TypeError("Cannot add initializers after decoration has completed");
3773
+ extraInitializers.push(accept(f || null));
3774
+ };
3775
+ var result = (0, decorators[i])(kind === "accessor" ? {
3776
+ get: descriptor.get,
3777
+ set: descriptor.set
3778
+ } : descriptor[key], context);
3779
+ if (kind === "accessor") {
3780
+ if (result === void 0) continue;
3781
+ if (result === null || typeof result !== "object") throw new TypeError("Object expected");
3782
+ if (_ = accept(result.get)) descriptor.get = _;
3783
+ if (_ = accept(result.set)) descriptor.set = _;
3784
+ if (_ = accept(result.init)) initializers.unshift(_);
3785
+ } else if (_ = accept(result)) {
3786
+ if (kind === "field") initializers.unshift(_);
3787
+ else descriptor[key] = _;
3788
+ }
3789
+ }
3790
+ if (target) Object.defineProperty(target, contextIn.name, descriptor);
3791
+ done = true;
3792
+ };
3793
+ /** The wire namespace is `requestInspector`. */
3794
+ let RequestInspectorRemoteService = (() => {
3795
+ let _classSuper = TypertRemoteService;
3796
+ let _instanceExtraInitializers = [];
3797
+ let _listTurns_decorators;
3798
+ let _loadTurns_decorators;
3799
+ let _getTurn_decorators;
3800
+ let _getRequest_decorators;
3801
+ let _getSessionTrend_decorators;
3802
+ let _getTurnTiming_decorators;
3803
+ return class RequestInspectorRemoteService extends _classSuper {
3804
+ static {
3805
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
3806
+ _listTurns_decorators = [Remote("listTurns")];
3807
+ _loadTurns_decorators = [Remote("loadTurns")];
3808
+ _getTurn_decorators = [Remote("getTurn")];
3809
+ _getRequest_decorators = [Remote("getRequest")];
3810
+ _getSessionTrend_decorators = [Remote("getSessionTrend")];
3811
+ _getTurnTiming_decorators = [Remote("getTurnTiming")];
3812
+ __esDecorate(this, null, _listTurns_decorators, {
3813
+ kind: "method",
3814
+ name: "listTurns",
3815
+ static: false,
3816
+ private: false,
3817
+ access: {
3818
+ has: (obj) => "listTurns" in obj,
3819
+ get: (obj) => obj.listTurns
3820
+ },
3821
+ metadata: _metadata
3822
+ }, null, _instanceExtraInitializers);
3823
+ __esDecorate(this, null, _loadTurns_decorators, {
3824
+ kind: "method",
3825
+ name: "loadTurns",
3826
+ static: false,
3827
+ private: false,
3828
+ access: {
3829
+ has: (obj) => "loadTurns" in obj,
3830
+ get: (obj) => obj.loadTurns
3831
+ },
3832
+ metadata: _metadata
3833
+ }, null, _instanceExtraInitializers);
3834
+ __esDecorate(this, null, _getTurn_decorators, {
3835
+ kind: "method",
3836
+ name: "getTurn",
3837
+ static: false,
3838
+ private: false,
3839
+ access: {
3840
+ has: (obj) => "getTurn" in obj,
3841
+ get: (obj) => obj.getTurn
3842
+ },
3843
+ metadata: _metadata
3844
+ }, null, _instanceExtraInitializers);
3845
+ __esDecorate(this, null, _getRequest_decorators, {
3846
+ kind: "method",
3847
+ name: "getRequest",
3848
+ static: false,
3849
+ private: false,
3850
+ access: {
3851
+ has: (obj) => "getRequest" in obj,
3852
+ get: (obj) => obj.getRequest
3853
+ },
3854
+ metadata: _metadata
3855
+ }, null, _instanceExtraInitializers);
3856
+ __esDecorate(this, null, _getSessionTrend_decorators, {
3857
+ kind: "method",
3858
+ name: "getSessionTrend",
3859
+ static: false,
3860
+ private: false,
3861
+ access: {
3862
+ has: (obj) => "getSessionTrend" in obj,
3863
+ get: (obj) => obj.getSessionTrend
3864
+ },
3865
+ metadata: _metadata
3866
+ }, null, _instanceExtraInitializers);
3867
+ __esDecorate(this, null, _getTurnTiming_decorators, {
3868
+ kind: "method",
3869
+ name: "getTurnTiming",
3870
+ static: false,
3871
+ private: false,
3872
+ access: {
3873
+ has: (obj) => "getTurnTiming" in obj,
3874
+ get: (obj) => obj.getTurnTiming
3875
+ },
3876
+ metadata: _metadata
3877
+ }, null, _instanceExtraInitializers);
3878
+ if (_metadata) Object.defineProperty(this, Symbol.metadata, {
3879
+ enumerable: true,
3880
+ configurable: true,
3881
+ writable: true,
3882
+ value: _metadata
3883
+ });
3884
+ }
3885
+ static inject = ["requestInspector"];
3886
+ constructor(ctx, _config = {}) {
3887
+ super(ctx, "requestInspectorRemote", { namespace: "requestInspector" });
3888
+ __runInitializers(this, _instanceExtraInitializers);
3889
+ }
3890
+ /** The service core this face delegates to. */
3891
+ get core() {
3892
+ return this.ctx.requestInspector;
3893
+ }
3894
+ /** Light turn list of one session plus its backfill watermark, newest last. */
3895
+ listTurns(sessionId, filter) {
3896
+ return this.core.listTurns(sessionId, filter);
3897
+ }
3898
+ /** Move the backfill window to the turns ending at `end` (latest when null); answers the new turn list. */
3899
+ loadTurns(sessionId, end, filter) {
3900
+ return this.core.loadTurns(sessionId, end, filter);
3901
+ }
3902
+ /** One turn's scope/request overview — the left column, no segments. */
3903
+ getTurn(sessionId, turnSeq, filter) {
3904
+ return this.core.getTurn(sessionId, turnSeq, filter);
3905
+ }
3906
+ /** One request's full four-segment breakdown — the right column. */
3907
+ getRequest(sessionId, seq, filter) {
3908
+ return this.core.getRequest(sessionId, seq, filter);
3909
+ }
3910
+ /** Lightweight session map, with no message bodies. */
3911
+ getSessionTrend(sessionId, filter) {
3912
+ return this.core.getSessionTrend(sessionId, filter);
3913
+ }
3914
+ /** One turn's whole-turn timing aggregation — the 耗时分布 tab. */
3915
+ getTurnTiming(sessionId, turnSeq, filter) {
3916
+ return this.core.getTurnTiming(sessionId, turnSeq, filter);
3917
+ }
3918
+ };
3919
+ })();
3920
+ //#endregion
3921
+ //#region lib/types/index.js
3922
+ /** Cordis plugin name used by loader diagnostics. */
3923
+ const name = "context-lens";
3924
+ /**
3925
+ * Plugin body: provide the service core, then mount the Remote data face.
3926
+ *
3927
+ * No `static inject`: every seam the capture touches is an event name or a
3928
+ * probed service, so a composition missing one (no persistence backend, a host
3929
+ * without these events) simply yields an empty panel instead of a failed boot.
3930
+ *
3931
+ * @param ctx - owning Cordis context.
3932
+ * @param config - optional plugin config.
3933
+ */
3934
+ function apply(ctx, config = {}) {
3935
+ const service = new InspectorService(ctx, config);
3936
+ ctx.provide("requestInspector", service);
3937
+ ctx.plugin(RequestInspectorRemoteService, {});
3938
+ ctx.effect(() => () => {
3939
+ service.dispose();
3940
+ }, "context-lens: capture shutdown");
3941
+ }
3942
+ //#endregion
3943
+ export { InspectorService, RequestInspectorRemoteService, apply, name };