dsh-thinking-guard 0.1.19 → 0.1.20

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 (73) hide show
  1. package/README.md +80 -80
  2. package/cordis.patch.yml +5 -0
  3. package/lib/client.d.ts +8 -2
  4. package/lib/client.d.ts.map +1 -1
  5. package/lib/client.js +4625 -6730
  6. package/lib/guard-core.js +72 -1
  7. package/lib/guard-core.js.map +1 -1
  8. package/lib/guard-loop-core.js +36 -5
  9. package/lib/guard-loop-core.js.map +1 -1
  10. package/lib/guard.js +20 -4
  11. package/lib/guard.js.map +1 -1
  12. package/lib/host-face.js +1 -0
  13. package/lib/host-face.js.map +1 -1
  14. package/lib/index.js +11 -1
  15. package/lib/index.js.map +1 -1
  16. package/lib/policy-state.js +29 -1
  17. package/lib/policy-state.js.map +1 -1
  18. package/lib/policy.js +24 -2
  19. package/lib/policy.js.map +1 -1
  20. package/lib/server/call-index-remote.js +110 -0
  21. package/lib/server/call-index-remote.js.map +1 -0
  22. package/lib/server/remote.js +13 -0
  23. package/lib/server/remote.js.map +1 -1
  24. package/lib/types/guard-core.d.ts +69 -1
  25. package/lib/types/guard-core.d.ts.map +1 -1
  26. package/lib/types/guard-loop-core.d.ts +15 -0
  27. package/lib/types/guard-loop-core.d.ts.map +1 -1
  28. package/lib/types/guard.d.ts +7 -1
  29. package/lib/types/guard.d.ts.map +1 -1
  30. package/lib/types/host-face.d.ts.map +1 -1
  31. package/lib/types/index.d.ts +5 -0
  32. package/lib/types/index.d.ts.map +1 -1
  33. package/lib/types/policy-state.d.ts +6 -0
  34. package/lib/types/policy-state.d.ts.map +1 -1
  35. package/lib/types/policy.d.ts +22 -3
  36. package/lib/types/policy.d.ts.map +1 -1
  37. package/lib/types/server/call-index-remote.d.ts +17 -0
  38. package/lib/types/server/call-index-remote.d.ts.map +1 -0
  39. package/lib/types/server/remote.d.ts +9 -0
  40. package/lib/types/server/remote.d.ts.map +1 -1
  41. package/lib/types/usage/call-index-core.d.ts +152 -0
  42. package/lib/types/usage/call-index-core.d.ts.map +1 -0
  43. package/lib/types/usage/call-index-host-face.d.ts +44 -0
  44. package/lib/types/usage/call-index-host-face.d.ts.map +1 -0
  45. package/lib/types/usage/call-index-host.d.ts +7 -0
  46. package/lib/types/usage/call-index-host.d.ts.map +1 -0
  47. package/lib/types/usage/call-index-query.d.ts +97 -0
  48. package/lib/types/usage/call-index-query.d.ts.map +1 -0
  49. package/lib/types/usage/call-index-runtime.d.ts +46 -0
  50. package/lib/types/usage/call-index-runtime.d.ts.map +1 -0
  51. package/lib/types/usage/call-index-scope.d.ts +59 -0
  52. package/lib/types/usage/call-index-scope.d.ts.map +1 -0
  53. package/lib/types/usage/call-index-wire.d.ts +286 -0
  54. package/lib/types/usage/call-index-wire.d.ts.map +1 -0
  55. package/lib/types/usage/session-index.d.ts +149 -0
  56. package/lib/types/usage/session-index.d.ts.map +1 -0
  57. package/lib/usage/call-index-core.js +761 -0
  58. package/lib/usage/call-index-core.js.map +1 -0
  59. package/lib/usage/call-index-host-face.js +77 -0
  60. package/lib/usage/call-index-host-face.js.map +1 -0
  61. package/lib/usage/call-index-host.js +13 -0
  62. package/lib/usage/call-index-host.js.map +1 -0
  63. package/lib/usage/call-index-query.js +669 -0
  64. package/lib/usage/call-index-query.js.map +1 -0
  65. package/lib/usage/call-index-runtime.js +376 -0
  66. package/lib/usage/call-index-runtime.js.map +1 -0
  67. package/lib/usage/call-index-scope.js +79 -0
  68. package/lib/usage/call-index-scope.js.map +1 -0
  69. package/lib/usage/call-index-wire.js +490 -0
  70. package/lib/usage/call-index-wire.js.map +1 -0
  71. package/lib/usage/session-index.js +600 -0
  72. package/lib/usage/session-index.js.map +1 -0
  73. package/package.json +144 -144
@@ -0,0 +1,761 @@
1
+ /**
2
+ * Metadata-only call index core: projector + bounded single-session partition.
3
+ *
4
+ * This module is the pure, dependency-light half of the new call index (D-026).
5
+ * It never reads tool arguments, tool result content, model streams, reasoning,
6
+ * `error.reason`/`error.name`, or raw `source` text; it only extracts the stable
7
+ * lifecycle coordinates a call index needs. All sequence numbers are
8
+ * non-negative safe integers and all timestamps are finite valid epoch values.
9
+ *
10
+ * The partition is a bounded, per-session accumulation:
11
+ * - events dedupe by `seq`; a conflicting payload for an already seen `seq` is a
12
+ * coverage gap and never overwrites the first value;
13
+ * - results may arrive before their call (same batch or a later batch) and are
14
+ * paired by `callId` plus turn/step, never by tool name alone;
15
+ * - multiple calls sharing one `callId` stay as separate rows and are not
16
+ * guessed onto one another;
17
+ * - a result must follow its call (`resultSeq > callSeq`) and a turn/step
18
+ * mismatch is not paired;
19
+ * - the first real result (lowest `seq`) wins; surface replacements and
20
+ * synthetic crash-recovery results never override a real result;
21
+ * - every observed `seq` (tool and non-tool control alike) participates in the
22
+ * continuity check, so a snapshot is `complete` only for a contiguous,
23
+ * gap-free `[0, cut)` range;
24
+ * - capacity (events / calls / bytes / external reserve) and orphan bounds make
25
+ * the index degrade to `partial` instead of pretending to be complete.
26
+ *
27
+ * Raw events are never retained: a compact per-seq signature is kept only to
28
+ * detect seq conflicts, and only bounded metadata rows survive.
29
+ */
30
+ import { Buffer } from 'node:buffer';
31
+ import { KNOWN_SESSION_EVENT_TYPES, TOOL_NOT_STARTED, TOOL_OUTCOME_UNKNOWN, } from '@deepseek-ai/dsh-session';
32
+ import { TOOL_ABORTED, TOOL_ABORTED_BEFORE_DISPATCH } from '@deepseek-ai/dsh-tools';
33
+ const CALL_INDEX_REASONS = new Set([
34
+ 'malformed-event',
35
+ 'invalid-seq',
36
+ 'invalid-time',
37
+ 'unreadable-field',
38
+ 'unknown-required-event',
39
+ 'malformed-tool-call',
40
+ 'malformed-tool-result',
41
+ 'field-too-long',
42
+ 'seq-conflict',
43
+ 'duplicate-call-id',
44
+ 'result-before-call',
45
+ 'turn-step-mismatch',
46
+ 'clock-anomaly',
47
+ 'orphan-capacity',
48
+ 'orphan-result',
49
+ 'index-capacity',
50
+ 'reserve-denied',
51
+ 'projection-gap',
52
+ 'event-count-underreported',
53
+ ]);
54
+ const MAX_ID_LENGTH = 512;
55
+ const MAX_TOOL_NAME_LENGTH = 512;
56
+ const MAX_ERROR_CODE_LENGTH = 80;
57
+ const ERROR_CODE_PATTERN = /^[A-Za-z0-9_.:-]+$/;
58
+ const UNREADABLE = Symbol('unreadable');
59
+ function isRecord(value) {
60
+ return typeof value === 'object' && value !== null;
61
+ }
62
+ /** Read exactly one whitelisted property; a throwing getter becomes {@link UNREADABLE}. */
63
+ function readField(value, key) {
64
+ if (!isRecord(value))
65
+ return undefined;
66
+ try {
67
+ return value[key];
68
+ }
69
+ catch {
70
+ return UNREADABLE;
71
+ }
72
+ }
73
+ function readOptionalField(value, key) {
74
+ const field = readField(value, key);
75
+ return field === UNREADABLE ? undefined : field;
76
+ }
77
+ function safeSeq(value) {
78
+ return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : undefined;
79
+ }
80
+ const safeCount = safeSeq;
81
+ function safeTime(value) {
82
+ return typeof value === 'number'
83
+ && Number.isFinite(value)
84
+ && Number.isFinite(new Date(value).getTime())
85
+ ? value
86
+ : undefined;
87
+ }
88
+ function boundedId(value, max) {
89
+ if (typeof value !== 'string')
90
+ return { tooLong: false };
91
+ if (value.trim().length === 0)
92
+ return { tooLong: false };
93
+ if (value.length > max)
94
+ return { tooLong: true };
95
+ return { value, tooLong: false };
96
+ }
97
+ function isReplaceSurfaceOp(value) {
98
+ if (value === UNREADABLE || !isRecord(value))
99
+ return false;
100
+ return readOptionalField(value, 'op') === 'replace';
101
+ }
102
+ function projectToolCall(raw, seq, time, reasons) {
103
+ const data = readField(raw, 'data');
104
+ if (data === UNREADABLE) {
105
+ reasons.push('unreadable-field');
106
+ return { reasons };
107
+ }
108
+ if (!isRecord(data)) {
109
+ reasons.push('malformed-tool-call');
110
+ return { reasons };
111
+ }
112
+ const callIdRaw = readField(data, 'callId');
113
+ const nameRaw = readField(data, 'name');
114
+ if (callIdRaw === UNREADABLE || nameRaw === UNREADABLE) {
115
+ reasons.push('unreadable-field');
116
+ return { reasons };
117
+ }
118
+ const callId = boundedId(callIdRaw, MAX_ID_LENGTH);
119
+ const name = boundedId(nameRaw, MAX_TOOL_NAME_LENGTH);
120
+ if (callId.tooLong || name.tooLong) {
121
+ reasons.push('field-too-long');
122
+ return { reasons };
123
+ }
124
+ if (callId.value === undefined || name.value === undefined) {
125
+ reasons.push('malformed-tool-call');
126
+ return { reasons };
127
+ }
128
+ const turn = safeCount(readOptionalField(data, 'turn'));
129
+ const step = safeCount(readOptionalField(data, 'step'));
130
+ const event = {
131
+ seq,
132
+ time,
133
+ kind: 'call',
134
+ callId: callId.value,
135
+ toolName: name.value,
136
+ ...(turn === undefined ? {} : { turn }),
137
+ ...(step === undefined ? {} : { step }),
138
+ };
139
+ return { event, reasons };
140
+ }
141
+ function projectToolResult(raw, seq, time, reasons) {
142
+ const data = readField(raw, 'data');
143
+ if (data === UNREADABLE) {
144
+ reasons.push('unreadable-field');
145
+ return { reasons };
146
+ }
147
+ if (!isRecord(data)) {
148
+ reasons.push('malformed-tool-result');
149
+ return { reasons };
150
+ }
151
+ const message = readField(data, 'message');
152
+ if (message === UNREADABLE) {
153
+ reasons.push('unreadable-field');
154
+ return { reasons };
155
+ }
156
+ if (!isRecord(message)) {
157
+ reasons.push('malformed-tool-result');
158
+ return { reasons };
159
+ }
160
+ const callIdRaw = readField(message, 'toolCallId');
161
+ const isErrorRaw = readField(message, 'isError');
162
+ if (callIdRaw === UNREADABLE || isErrorRaw === UNREADABLE) {
163
+ reasons.push('unreadable-field');
164
+ return { reasons };
165
+ }
166
+ const callId = boundedId(callIdRaw, MAX_ID_LENGTH);
167
+ if (callId.tooLong) {
168
+ reasons.push('field-too-long');
169
+ return { reasons };
170
+ }
171
+ if (callId.value === undefined) {
172
+ reasons.push('malformed-tool-result');
173
+ return { reasons };
174
+ }
175
+ if (isErrorRaw !== undefined && typeof isErrorRaw !== 'boolean') {
176
+ reasons.push('malformed-tool-result');
177
+ return { reasons };
178
+ }
179
+ const replacement = isReplaceSurfaceOp(readField(raw, 'surfaceOp'));
180
+ const turn = safeCount(readOptionalField(data, 'turn'));
181
+ const step = safeCount(readOptionalField(data, 'step'));
182
+ const coords = {
183
+ ...(turn === undefined ? {} : { turn }),
184
+ ...(step === undefined ? {} : { step }),
185
+ };
186
+ if (isErrorRaw !== true) {
187
+ return {
188
+ event: {
189
+ seq,
190
+ time,
191
+ kind: 'result',
192
+ callId: callId.value,
193
+ outcome: 'success',
194
+ ...coords,
195
+ ...(replacement ? { replacement: true } : {}),
196
+ },
197
+ reasons,
198
+ };
199
+ }
200
+ const errorRaw = readField(data, 'error');
201
+ if (errorRaw === UNREADABLE) {
202
+ reasons.push('unreadable-field');
203
+ return { reasons };
204
+ }
205
+ let code;
206
+ if (isRecord(errorRaw)) {
207
+ const codeRaw = readField(errorRaw, 'code');
208
+ if (codeRaw === UNREADABLE) {
209
+ reasons.push('unreadable-field');
210
+ return { reasons };
211
+ }
212
+ // `error.name` and `error.reason` are deliberately never read.
213
+ if (typeof codeRaw === 'string' && codeRaw.length <= MAX_ERROR_CODE_LENGTH && ERROR_CODE_PATTERN.test(codeRaw)) {
214
+ code = codeRaw;
215
+ }
216
+ }
217
+ const recovery = code === TOOL_NOT_STARTED || code === TOOL_OUTCOME_UNKNOWN;
218
+ if (recovery) {
219
+ // Synthetic crash-recovery results are not failures; keep the call unresolved.
220
+ return {
221
+ event: {
222
+ seq,
223
+ time,
224
+ kind: 'result',
225
+ callId: callId.value,
226
+ outcome: 'unresolved',
227
+ replacement: true,
228
+ ...(code === undefined ? {} : { errorCode: code }),
229
+ ...coords,
230
+ },
231
+ reasons,
232
+ };
233
+ }
234
+ const cancelled = code === TOOL_ABORTED || code === TOOL_ABORTED_BEFORE_DISPATCH;
235
+ const outcome = cancelled ? 'cancelled' : 'error';
236
+ return {
237
+ event: {
238
+ seq,
239
+ time,
240
+ kind: 'result',
241
+ callId: callId.value,
242
+ outcome,
243
+ ...(code === undefined ? {} : { errorCode: code }),
244
+ ...coords,
245
+ ...(replacement ? { replacement: true } : {}),
246
+ },
247
+ reasons,
248
+ };
249
+ }
250
+ /**
251
+ * Project one raw session event to a metadata-only {@link CallIndexEvent}.
252
+ *
253
+ * Unknown *required* event types are a coverage gap; unknown *ignorable* types
254
+ * project as `other`. Control events expose only `seq`/`time`/`type` plus the
255
+ * closing `turn` of `turn/end`. A throwing getter on any read field is reported
256
+ * as `unreadable-field` and never propagated.
257
+ */
258
+ export function projectCallIndexEvent(raw) {
259
+ const reasons = [];
260
+ const push = (reason) => {
261
+ if (!reasons.includes(reason))
262
+ reasons.push(reason);
263
+ };
264
+ if (!isRecord(raw))
265
+ return { reasons: ['malformed-event'] };
266
+ const typeRaw = readField(raw, 'type');
267
+ if (typeRaw === UNREADABLE)
268
+ return { reasons: ['unreadable-field'] };
269
+ if (typeof typeRaw !== 'string' || typeRaw.length === 0)
270
+ return { reasons: ['malformed-event'] };
271
+ const type = typeRaw;
272
+ const seqRaw = readField(raw, 'seq');
273
+ const timeRaw = readField(raw, 'time');
274
+ if (seqRaw === UNREADABLE || timeRaw === UNREADABLE)
275
+ return { reasons: ['unreadable-field'] };
276
+ const seq = safeSeq(seqRaw);
277
+ const time = safeTime(timeRaw);
278
+ if (seq === undefined)
279
+ push('invalid-seq');
280
+ if (time === undefined)
281
+ push('invalid-time');
282
+ if (seq === undefined || time === undefined)
283
+ return { reasons };
284
+ if (!KNOWN_SESSION_EVENT_TYPES.has(type) && readOptionalField(raw, 'ignorable') !== true) {
285
+ push('unknown-required-event');
286
+ return { reasons };
287
+ }
288
+ if (type === 'tool/call')
289
+ return projectToolCall(raw, seq, time, reasons);
290
+ if (type === 'tool/result')
291
+ return projectToolResult(raw, seq, time, reasons);
292
+ if (type === 'turn/end') {
293
+ const data = readField(raw, 'data');
294
+ if (data === UNREADABLE) {
295
+ push('unreadable-field');
296
+ return { event: { seq, time, kind: 'other', turnClosed: true }, reasons };
297
+ }
298
+ const turnRaw = readField(data, 'turn');
299
+ if (turnRaw === UNREADABLE) {
300
+ push('unreadable-field');
301
+ return { event: { seq, time, kind: 'other', turnClosed: true }, reasons };
302
+ }
303
+ const turn = safeCount(turnRaw);
304
+ return {
305
+ event: { seq, time, kind: 'other', turnClosed: true, ...(turn === undefined ? {} : { turn }) },
306
+ reasons,
307
+ };
308
+ }
309
+ return { event: { seq, time, kind: 'other' }, reasons };
310
+ }
311
+ const DEFAULT_MAX_EVENTS = 250_000;
312
+ const DEFAULT_MAX_CALLS = 100_000;
313
+ const DEFAULT_MAX_BYTES = 8_000_000;
314
+ const DEFAULT_MAX_ORPHANS = 2_000;
315
+ const MAX_REASONS = 32;
316
+ function resolveLimit(value, fallback, name) {
317
+ if (value === undefined)
318
+ return fallback;
319
+ if (!Number.isSafeInteger(value) || value < 0) {
320
+ throw new RangeError(`${name} must be a non-negative safe integer`);
321
+ }
322
+ return value;
323
+ }
324
+ function utf8Length(text) {
325
+ return Buffer.byteLength(text, 'utf8');
326
+ }
327
+ /** Conservative per-entry byte estimate: payload copy plus projection/row overhead. */
328
+ function estimateBytes(value) {
329
+ let json = '';
330
+ try {
331
+ json = JSON.stringify(value) ?? '';
332
+ }
333
+ catch {
334
+ json = '';
335
+ }
336
+ return 2 * utf8Length(json) + 1024;
337
+ }
338
+ function signatureOf(event) {
339
+ return JSON.stringify([
340
+ event.kind,
341
+ event.seq,
342
+ event.time,
343
+ event.turn ?? null,
344
+ event.step ?? null,
345
+ event.callId ?? null,
346
+ event.toolName ?? null,
347
+ event.outcome ?? null,
348
+ event.errorCode ?? null,
349
+ event.replacement === true,
350
+ event.turnClosed === true,
351
+ ]);
352
+ }
353
+ function pickLowestSeq(results) {
354
+ let best;
355
+ for (const result of results) {
356
+ if (best === undefined || result.seq < best.seq)
357
+ best = result;
358
+ }
359
+ return best;
360
+ }
361
+ /** Bounded, single-session call index partition. */
362
+ export class CallIndexPartition {
363
+ sessionId;
364
+ revision;
365
+ inheritedEventCount;
366
+ maxEvents;
367
+ maxCalls;
368
+ maxBytes;
369
+ maxOrphans;
370
+ reserveCallback;
371
+ resolveSource;
372
+ /** Resolved provenance per tool name; a catalog read must happen once per name. */
373
+ sourceByTool = new Map();
374
+ /** seq -> compact signature, used only for dedupe/conflict detection. */
375
+ seqMeta = new Map();
376
+ calls = [];
377
+ callsByCallId = new Map();
378
+ orphans = new Map();
379
+ reasonCodes = new Set();
380
+ turnClosedSeq = new Map();
381
+ stopped = false;
382
+ bytes = 0;
383
+ orphanCount = 0;
384
+ constructor(options) {
385
+ if (typeof options !== 'object' || options === null)
386
+ throw new TypeError('options are required');
387
+ if (typeof options.sessionId !== 'string' || options.sessionId.trim().length === 0) {
388
+ throw new TypeError('sessionId must be a non-empty string');
389
+ }
390
+ if (typeof options.revision !== 'string')
391
+ throw new TypeError('revision must be a string');
392
+ if (!Number.isSafeInteger(options.inheritedEventCount) || options.inheritedEventCount < 0) {
393
+ throw new RangeError('inheritedEventCount must be a non-negative safe integer');
394
+ }
395
+ this.sessionId = options.sessionId;
396
+ this.revision = options.revision;
397
+ this.inheritedEventCount = options.inheritedEventCount;
398
+ this.maxEvents = resolveLimit(options.maxEvents, DEFAULT_MAX_EVENTS, 'maxEvents');
399
+ this.maxCalls = resolveLimit(options.maxCalls, DEFAULT_MAX_CALLS, 'maxCalls');
400
+ this.maxBytes = resolveLimit(options.maxBytes, DEFAULT_MAX_BYTES, 'maxBytes');
401
+ this.maxOrphans = resolveLimit(options.maxOrphans, DEFAULT_MAX_ORPHANS, 'maxOrphans');
402
+ this.reserveCallback = options.reserve;
403
+ this.resolveSource = options.resolveSource;
404
+ }
405
+ /** Ingest a batch of already-projected events. Ingestion stops at the first capacity breach. */
406
+ ingest(events) {
407
+ for (const event of events) {
408
+ if (this.stopped)
409
+ break;
410
+ this.ingestOne(event);
411
+ }
412
+ }
413
+ /** Record a coverage gap identified outside the projector (fixed code or generic fallback). */
414
+ addGap(reason) {
415
+ this.addReason(reason);
416
+ }
417
+ usage() {
418
+ return { events: this.seqMeta.size, calls: this.calls.length, bytes: this.bytes };
419
+ }
420
+ snapshot(cut) {
421
+ const limit = Number.isSafeInteger(cut) && cut >= 0 ? cut : 0;
422
+ const reasons = new Set(this.reasonCodes);
423
+ const rows = [];
424
+ for (const record of this.calls) {
425
+ if (record.seq >= limit)
426
+ continue;
427
+ rows.push(this.buildRow(record, limit, reasons));
428
+ }
429
+ rows.sort((a, b) => a.callSeq - b.callSeq);
430
+ const orphanWithinCut = [...this.orphans.values()].some(results => results.some(result => result.seq < limit));
431
+ if (orphanWithinCut)
432
+ reasons.add('orphan-result');
433
+ return {
434
+ sessionId: this.sessionId,
435
+ revision: this.revision,
436
+ cut: limit,
437
+ rows,
438
+ coverage: this.isComplete(limit) && reasons.size === 0 ? 'complete' : 'partial',
439
+ reasons: [...reasons],
440
+ estimatedBytes: this.bytes,
441
+ };
442
+ }
443
+ addReason(reason) {
444
+ if (this.reasonCodes.size >= MAX_REASONS)
445
+ return;
446
+ this.reasonCodes.add(CALL_INDEX_REASONS.has(reason) ? reason : 'projection-gap');
447
+ }
448
+ reachCapacity(reason) {
449
+ this.stopped = true;
450
+ this.addReason(reason);
451
+ }
452
+ tryReserve(bytes, denial) {
453
+ if (this.stopped)
454
+ return false;
455
+ if (this.bytes + bytes > this.maxBytes) {
456
+ this.reachCapacity('index-capacity');
457
+ return false;
458
+ }
459
+ if (this.reserveCallback !== undefined) {
460
+ let accepted = false;
461
+ try {
462
+ accepted = this.reserveCallback(bytes) === true;
463
+ }
464
+ catch {
465
+ accepted = false;
466
+ }
467
+ if (!accepted) {
468
+ this.reachCapacity(denial);
469
+ return false;
470
+ }
471
+ }
472
+ this.bytes += bytes;
473
+ return true;
474
+ }
475
+ ingestOne(event) {
476
+ if (safeSeq(event.seq) === undefined || safeTime(event.time) === undefined) {
477
+ this.addReason('malformed-event');
478
+ return;
479
+ }
480
+ const signature = signatureOf(event);
481
+ const existing = this.seqMeta.get(event.seq);
482
+ if (existing !== undefined) {
483
+ if (existing !== signature)
484
+ this.addReason('seq-conflict');
485
+ return;
486
+ }
487
+ if (this.seqMeta.size >= this.maxEvents) {
488
+ this.reachCapacity('index-capacity');
489
+ return;
490
+ }
491
+ const seqBytes = estimateBytes({
492
+ kind: event.kind,
493
+ seq: event.seq,
494
+ time: event.time,
495
+ callId: event.callId ?? null,
496
+ toolName: event.toolName ?? null,
497
+ outcome: event.outcome ?? null,
498
+ });
499
+ if (!this.tryReserve(seqBytes, 'reserve-denied'))
500
+ return;
501
+ this.seqMeta.set(event.seq, signature);
502
+ if (event.kind === 'other') {
503
+ if (event.turnClosed === true && event.turn !== undefined && safeCount(event.turn) !== undefined) {
504
+ const previous = this.turnClosedSeq.get(event.turn);
505
+ if (previous === undefined || event.seq < previous)
506
+ this.turnClosedSeq.set(event.turn, event.seq);
507
+ }
508
+ return;
509
+ }
510
+ if (event.kind === 'call') {
511
+ this.ingestCall(event);
512
+ return;
513
+ }
514
+ if (event.kind === 'result') {
515
+ this.ingestResult(event);
516
+ }
517
+ }
518
+ ingestCall(event) {
519
+ const callId = event.callId;
520
+ const toolName = event.toolName;
521
+ if (typeof callId !== 'string' || callId.trim().length === 0
522
+ || typeof toolName !== 'string' || toolName.trim().length === 0) {
523
+ this.addReason('malformed-tool-call');
524
+ return;
525
+ }
526
+ if (this.calls.length >= this.maxCalls) {
527
+ this.reachCapacity('index-capacity');
528
+ return;
529
+ }
530
+ const record = {
531
+ seq: event.seq,
532
+ callId,
533
+ toolName,
534
+ ...(event.turn === undefined ? {} : { turn: event.turn }),
535
+ ...(event.step === undefined ? {} : { step: event.step }),
536
+ startedAt: event.time,
537
+ inherited: event.seq < this.inheritedEventCount,
538
+ results: [],
539
+ ambiguous: false,
540
+ };
541
+ const bytes = estimateBytes({
542
+ kind: 'call',
543
+ seq: event.seq,
544
+ callId,
545
+ toolName,
546
+ turn: event.turn ?? null,
547
+ step: event.step ?? null,
548
+ time: event.time,
549
+ });
550
+ if (!this.tryReserve(bytes, 'reserve-denied'))
551
+ return;
552
+ this.calls.push(record);
553
+ const bucket = this.callsByCallId.get(callId);
554
+ if (bucket === undefined)
555
+ this.callsByCallId.set(callId, [record]);
556
+ else {
557
+ bucket.push(record);
558
+ for (const call of bucket)
559
+ call.ambiguous = true;
560
+ this.addReason('duplicate-call-id');
561
+ }
562
+ this.retryOrphans(callId);
563
+ }
564
+ ingestResult(event) {
565
+ const callId = event.callId;
566
+ const outcome = event.outcome;
567
+ if (typeof callId !== 'string' || callId.trim().length === 0) {
568
+ this.addReason('malformed-tool-result');
569
+ return;
570
+ }
571
+ if (outcome !== 'success' && outcome !== 'error' && outcome !== 'cancelled' && outcome !== 'unresolved') {
572
+ this.addReason('malformed-tool-result');
573
+ return;
574
+ }
575
+ const replacement = event.replacement === true || outcome === 'unresolved';
576
+ // A surface replacement that is not a recovery marker is a replay, not a fresh result.
577
+ if (replacement && outcome !== 'unresolved')
578
+ return;
579
+ const result = {
580
+ seq: event.seq,
581
+ time: event.time,
582
+ callId,
583
+ outcome,
584
+ replacement,
585
+ ...(event.errorCode === undefined ? {} : { errorCode: event.errorCode }),
586
+ ...(event.turn === undefined ? {} : { turn: event.turn }),
587
+ ...(event.step === undefined ? {} : { step: event.step }),
588
+ };
589
+ const status = this.tryAttachResult(result);
590
+ if (status === 'orphan')
591
+ this.bufferOrphan(result);
592
+ }
593
+ isCompatible(record, result) {
594
+ if (result.seq <= record.seq)
595
+ return false;
596
+ if (result.turn !== undefined && record.turn !== undefined && result.turn !== record.turn)
597
+ return false;
598
+ if (result.step !== undefined && record.step !== undefined && result.step !== record.step)
599
+ return false;
600
+ return true;
601
+ }
602
+ tryAttachResult(result) {
603
+ const bucket = this.callsByCallId.get(result.callId);
604
+ if (bucket === undefined || bucket.length === 0)
605
+ return 'orphan';
606
+ if (bucket.length > 1) {
607
+ this.addReason('duplicate-call-id');
608
+ return 'skipped';
609
+ }
610
+ const candidates = bucket.filter(record => this.isCompatible(record, result));
611
+ if (candidates.length === 0) {
612
+ if (bucket.every(record => result.seq <= record.seq)) {
613
+ this.addReason('result-before-call');
614
+ return 'skipped';
615
+ }
616
+ const mismatch = bucket.some(record => ((result.turn !== undefined && record.turn !== undefined && result.turn !== record.turn)
617
+ || (result.step !== undefined && record.step !== undefined && result.step !== record.step)));
618
+ if (mismatch)
619
+ this.addReason('turn-step-mismatch');
620
+ return 'orphan';
621
+ }
622
+ if (candidates.length > 1) {
623
+ for (const record of candidates)
624
+ record.ambiguous = true;
625
+ this.addReason('duplicate-call-id');
626
+ return 'skipped';
627
+ }
628
+ const record = candidates[0];
629
+ const bytes = estimateBytes({
630
+ kind: 'result',
631
+ seq: result.seq,
632
+ callId: result.callId,
633
+ outcome: result.outcome,
634
+ errorCode: result.errorCode ?? null,
635
+ replacement: result.replacement,
636
+ time: result.time,
637
+ });
638
+ if (!this.tryReserve(bytes, 'reserve-denied'))
639
+ return 'skipped';
640
+ record.results.push(result);
641
+ if (result.time < record.startedAt)
642
+ this.addReason('clock-anomaly');
643
+ return 'attached';
644
+ }
645
+ bufferOrphan(result) {
646
+ if (this.orphanCount >= this.maxOrphans) {
647
+ this.reachCapacity('orphan-capacity');
648
+ return;
649
+ }
650
+ const bytes = estimateBytes({ kind: 'result-orphan', seq: result.seq, callId: result.callId });
651
+ if (!this.tryReserve(bytes, 'reserve-denied'))
652
+ return;
653
+ const list = this.orphans.get(result.callId);
654
+ if (list === undefined)
655
+ this.orphans.set(result.callId, [result]);
656
+ else
657
+ list.push(result);
658
+ this.orphanCount += 1;
659
+ }
660
+ retryOrphans(callId) {
661
+ const list = this.orphans.get(callId);
662
+ if (list === undefined)
663
+ return;
664
+ this.orphans.delete(callId);
665
+ this.orphanCount -= list.length;
666
+ const remaining = [];
667
+ const ordered = [...list].sort((a, b) => a.seq - b.seq);
668
+ for (const result of ordered) {
669
+ if (this.stopped) {
670
+ remaining.push(result);
671
+ continue;
672
+ }
673
+ if (this.tryAttachResult(result) === 'orphan')
674
+ remaining.push(result);
675
+ }
676
+ if (remaining.length > 0) {
677
+ this.orphans.set(callId, remaining);
678
+ this.orphanCount += remaining.length;
679
+ }
680
+ }
681
+ /**
682
+ * Resolve one call's provenance from the caller's trusted evidence.
683
+ *
684
+ * Cached per tool name because a Session can make thousands of calls to the
685
+ * same tool and the catalog lookup is a synchronous map read. A throwing
686
+ * resolver degrades that call to `unknown`; it never propagates and never
687
+ * guesses a kind from the name.
688
+ */
689
+ sourceKindFor(toolName) {
690
+ const cached = this.sourceByTool.get(toolName);
691
+ if (cached !== undefined)
692
+ return cached;
693
+ let resolved = 'unknown';
694
+ if (this.resolveSource !== undefined) {
695
+ try {
696
+ const value = this.resolveSource(toolName);
697
+ if (value === 'mcp' || value === 'plugin-tool' || value === 'native' || value === 'skill' || value === 'unknown') {
698
+ resolved = value;
699
+ }
700
+ }
701
+ catch {
702
+ resolved = 'unknown';
703
+ }
704
+ }
705
+ this.sourceByTool.set(toolName, resolved);
706
+ return resolved;
707
+ }
708
+ buildRow(record, limit, reasons) {
709
+ const results = record.results.filter(result => result.seq < limit);
710
+ const real = results.filter(result => result.replacement !== true);
711
+ const recovered = results.filter(result => result.replacement === true && result.outcome === 'unresolved');
712
+ const chosen = pickLowestSeq(real) ?? pickLowestSeq(recovered);
713
+ const base = {
714
+ sessionId: this.sessionId,
715
+ callSeq: record.seq,
716
+ callId: record.callId,
717
+ toolName: record.toolName,
718
+ ...(record.turn === undefined ? {} : { turn: record.turn }),
719
+ ...(record.step === undefined ? {} : { step: record.step }),
720
+ startedAt: record.startedAt,
721
+ inherited: record.inherited,
722
+ sourceKind: this.sourceKindFor(record.toolName),
723
+ layer: 'unknown',
724
+ };
725
+ if (record.ambiguous)
726
+ return { ...base, outcome: 'unknown' };
727
+ if (chosen !== undefined) {
728
+ const duration = chosen.time - record.startedAt;
729
+ if (duration < 0)
730
+ reasons.add('clock-anomaly');
731
+ return {
732
+ ...base,
733
+ endedAt: chosen.time,
734
+ resultSeq: chosen.seq,
735
+ ...(duration >= 0 ? { durationMs: duration } : {}),
736
+ outcome: chosen.outcome,
737
+ };
738
+ }
739
+ if (record.ambiguous)
740
+ return { ...base, outcome: 'unknown' };
741
+ if (record.turn !== undefined) {
742
+ const closed = this.turnClosedSeq.get(record.turn);
743
+ if (closed !== undefined && closed < limit)
744
+ return { ...base, outcome: 'unresolved' };
745
+ }
746
+ // An unclosed historic log is not evidence that an operation is running now.
747
+ return { ...base, outcome: 'unresolved' };
748
+ }
749
+ isComplete(limit) {
750
+ if (this.stopped)
751
+ return false;
752
+ if (this.reasonCodes.size > 0)
753
+ return false;
754
+ for (let seq = 0; seq < limit; seq += 1) {
755
+ if (!this.seqMeta.has(seq))
756
+ return false;
757
+ }
758
+ return true;
759
+ }
760
+ }
761
+ //# sourceMappingURL=call-index-core.js.map