@caupulican/pi-agent-core 0.93.18 → 0.94.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.
@@ -0,0 +1,648 @@
1
+ import { collectSessionBranch } from "./session-tree.js";
2
+ /** Maximum serialized size for one lifecycle payload. Lifecycle records are a ledger, not a payload store. */
3
+ export const MAX_LIFECYCLE_PAYLOAD_CHARS = 32 * 1024;
4
+ const MAX_LIFECYCLE_STRING_CHARS = 4 * 1024;
5
+ const MAX_LIFECYCLE_ARRAY_ITEMS = 256;
6
+ const MAX_EXTERNAL_ID_CHARS = 512;
7
+ export const TOOL_NOT_STARTED = "TOOL_NOT_STARTED";
8
+ export const TOOL_OUTCOME_UNKNOWN = "TOOL_OUTCOME_UNKNOWN";
9
+ function lifecycleValidationError(path, detail) {
10
+ return new TypeError(`Invalid session lifecycle field ${path}: ${detail}`);
11
+ }
12
+ function assertBoundedString(value, path, max = MAX_LIFECYCLE_STRING_CHARS) {
13
+ if (typeof value !== "string" || value.length === 0 || value.length > max) {
14
+ throw lifecycleValidationError(path, `expected a non-empty string of at most ${max} characters`);
15
+ }
16
+ if (/[\u0000-\u001f\u007f]/u.test(value) || value.trim() !== value) {
17
+ throw lifecycleValidationError(path, "control characters and surrounding whitespace are not allowed");
18
+ }
19
+ }
20
+ function assertSessionEntryId(value, path) {
21
+ assertBoundedString(value, path);
22
+ if (!/^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/u.test(value)) {
23
+ throw lifecycleValidationError(path, "expected a session-safe identifier");
24
+ }
25
+ }
26
+ /** External provider/tool identifiers may contain ':' and '|'; only bounded text is assumed. */
27
+ function assertExternalId(value, path) {
28
+ assertBoundedString(value, path, MAX_EXTERNAL_ID_CHARS);
29
+ }
30
+ function assertCanonicalIsoTimestamp(value, path) {
31
+ assertBoundedString(value, path, 128);
32
+ const timestamp = new Date(value);
33
+ if (Number.isNaN(timestamp.getTime()) || timestamp.toISOString() !== value) {
34
+ throw lifecycleValidationError(path, "expected a canonical ISO timestamp with milliseconds and Z");
35
+ }
36
+ }
37
+ function assertNonNegativeSafeInteger(value, path) {
38
+ if (typeof value !== "number" || Object.is(value, -0) || !Number.isSafeInteger(value) || value < 0) {
39
+ throw lifecycleValidationError(path, "expected a non-negative safe integer other than -0");
40
+ }
41
+ }
42
+ function assertRecordKeys(value, expected) {
43
+ const allowed = new Set(expected);
44
+ for (const key of Object.keys(value)) {
45
+ if (!allowed.has(key))
46
+ throw lifecycleValidationError(key, "unknown field");
47
+ }
48
+ }
49
+ function fieldsForType(type) {
50
+ switch (type) {
51
+ case "request_snapshot":
52
+ return [
53
+ "requestId",
54
+ "reason",
55
+ "api",
56
+ "provider",
57
+ "modelId",
58
+ "effectiveConfigFingerprint",
59
+ "systemFingerprint",
60
+ "toolsFingerprint",
61
+ "historyFingerprint",
62
+ "messageEntryIds",
63
+ ];
64
+ case "foreground_tool_start":
65
+ return ["requestId", "assistantMessageEntryId", "callId", "toolName"];
66
+ case "foreground_tool_terminal":
67
+ return [
68
+ "requestId",
69
+ "assistantMessageEntryId",
70
+ "callId",
71
+ "toolName",
72
+ "outcome",
73
+ "resultMessageEntryId",
74
+ "errorKind",
75
+ ];
76
+ case "compaction_start":
77
+ return ["compactionId", "firstKeptEntryId", "tokensBefore"];
78
+ case "compaction_end":
79
+ return ["compactionId", "outcome", "compactionEntryId", "error"];
80
+ default:
81
+ return [];
82
+ }
83
+ }
84
+ function assertStringArray(value, path) {
85
+ if (!Array.isArray(value) || value.length > MAX_LIFECYCLE_ARRAY_ITEMS) {
86
+ throw lifecycleValidationError(path, `expected at most ${MAX_LIFECYCLE_ARRAY_ITEMS} identifiers`);
87
+ }
88
+ const seen = new Set();
89
+ for (let index = 0; index < value.length; index += 1) {
90
+ assertSessionEntryId(value[index], `${path}[${index}]`);
91
+ if (seen.has(value[index]))
92
+ throw lifecycleValidationError(`${path}[${index}]`, "duplicate identifier");
93
+ seen.add(value[index]);
94
+ }
95
+ }
96
+ /** Runtime validation for all persisted lifecycle records. */
97
+ export function validateSessionLifecycleEntry(value) {
98
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
99
+ throw lifecycleValidationError("entry", "expected an object");
100
+ }
101
+ const record = value;
102
+ if (typeof record.type !== "string" || fieldsForType(record.type).length === 0) {
103
+ throw lifecycleValidationError("type", "unknown lifecycle entry type");
104
+ }
105
+ assertRecordKeys(record, ["type", "id", "parentId", "timestamp", ...fieldsForType(record.type)]);
106
+ assertSessionEntryId(record.id, "id");
107
+ if (record.parentId !== null)
108
+ assertSessionEntryId(record.parentId, "parentId");
109
+ assertCanonicalIsoTimestamp(record.timestamp, "timestamp");
110
+ switch (record.type) {
111
+ case "request_snapshot":
112
+ assertExternalId(record.requestId, "requestId");
113
+ if (record.reason !== "initial" && record.reason !== "resume" && record.reason !== "change") {
114
+ throw lifecycleValidationError("reason", "expected initial, resume, or change");
115
+ }
116
+ assertBoundedString(record.api, "api", 256);
117
+ assertBoundedString(record.provider, "provider", 256);
118
+ assertBoundedString(record.modelId, "modelId", 256);
119
+ assertBoundedString(record.effectiveConfigFingerprint, "effectiveConfigFingerprint");
120
+ assertBoundedString(record.systemFingerprint, "systemFingerprint");
121
+ assertBoundedString(record.toolsFingerprint, "toolsFingerprint");
122
+ assertBoundedString(record.historyFingerprint, "historyFingerprint");
123
+ assertStringArray(record.messageEntryIds, "messageEntryIds");
124
+ break;
125
+ case "foreground_tool_start":
126
+ assertExternalId(record.requestId, "requestId");
127
+ assertSessionEntryId(record.assistantMessageEntryId, "assistantMessageEntryId");
128
+ assertExternalId(record.callId, "callId");
129
+ assertBoundedString(record.toolName, "toolName", 256);
130
+ break;
131
+ case "foreground_tool_terminal":
132
+ assertExternalId(record.requestId, "requestId");
133
+ assertSessionEntryId(record.assistantMessageEntryId, "assistantMessageEntryId");
134
+ assertExternalId(record.callId, "callId");
135
+ assertBoundedString(record.toolName, "toolName", 256);
136
+ if (record.outcome !== "success" && record.outcome !== "error" && record.outcome !== "cancelled") {
137
+ throw lifecycleValidationError("outcome", "expected success, error, or cancelled");
138
+ }
139
+ assertSessionEntryId(record.resultMessageEntryId, "resultMessageEntryId");
140
+ if (record.errorKind !== undefined &&
141
+ record.errorKind !== "tool_failure" &&
142
+ record.errorKind !== "operation_outcome") {
143
+ throw lifecycleValidationError("errorKind", "expected tool_failure or operation_outcome");
144
+ }
145
+ if (record.outcome === "error" && record.errorKind === undefined) {
146
+ throw lifecycleValidationError("errorKind", "error outcomes require an error kind");
147
+ }
148
+ if (record.outcome !== "error" && record.errorKind !== undefined) {
149
+ throw lifecycleValidationError("errorKind", "only error outcomes may carry an error kind");
150
+ }
151
+ break;
152
+ case "compaction_start":
153
+ assertExternalId(record.compactionId, "compactionId");
154
+ assertSessionEntryId(record.firstKeptEntryId, "firstKeptEntryId");
155
+ assertNonNegativeSafeInteger(record.tokensBefore, "tokensBefore");
156
+ break;
157
+ case "compaction_end":
158
+ assertExternalId(record.compactionId, "compactionId");
159
+ if (record.outcome !== "success" &&
160
+ record.outcome !== "failure" &&
161
+ record.outcome !== "cancelled" &&
162
+ record.outcome !== "interrupted") {
163
+ throw lifecycleValidationError("outcome", "expected success, failure, cancelled, or interrupted");
164
+ }
165
+ if (record.outcome === "success") {
166
+ assertSessionEntryId(record.compactionEntryId, "compactionEntryId");
167
+ if ("error" in record)
168
+ throw lifecycleValidationError("error", "successful compactions cannot carry an error");
169
+ }
170
+ else {
171
+ if ("compactionEntryId" in record) {
172
+ throw lifecycleValidationError("compactionEntryId", "only successful compactions may carry an entry id");
173
+ }
174
+ if (record.outcome === "failure" && !record.error) {
175
+ throw lifecycleValidationError("error", "failure outcomes require a bounded error");
176
+ }
177
+ if ("error" in record)
178
+ assertBoundedString(record.error, "error", MAX_LIFECYCLE_PAYLOAD_CHARS);
179
+ }
180
+ break;
181
+ }
182
+ }
183
+ function assertLifecycleEntries(entries) {
184
+ for (const entry of entries) {
185
+ if (entry.type !== "session" && isSessionLifecycleEntry(entry))
186
+ encodeSessionLifecycleEntry(entry);
187
+ }
188
+ }
189
+ export function isSessionLifecycleEntry(entry) {
190
+ return fieldsForType(entry.type).length > 0;
191
+ }
192
+ export function validateLoadedLifecycleEntries(entries) {
193
+ assertLifecycleEntries(entries);
194
+ }
195
+ export function encodeSessionLifecycleEntry(entry) {
196
+ validateSessionLifecycleEntry(entry);
197
+ const encoded = JSON.stringify(entry);
198
+ if (encoded.length > MAX_LIFECYCLE_PAYLOAD_CHARS) {
199
+ throw lifecycleValidationError("entry", `serialized record exceeds ${MAX_LIFECYCLE_PAYLOAD_CHARS} characters`);
200
+ }
201
+ return encoded;
202
+ }
203
+ export function decodeSessionLifecycleEntry(value) {
204
+ validateSessionLifecycleEntry(value);
205
+ encodeSessionLifecycleEntry(value);
206
+ return value;
207
+ }
208
+ export function encodeSessionEntry(entry) {
209
+ return isSessionLifecycleEntry(entry) ? encodeSessionLifecycleEntry(entry) : JSON.stringify(entry);
210
+ }
211
+ function createToolRecord() {
212
+ return { starts: [], terminals: [], assistantCalls: [], results: [] };
213
+ }
214
+ function createCompactionRecord() {
215
+ return { starts: [], ends: [] };
216
+ }
217
+ export function sessionLifecycleToolIdentityKey(requestId, assistantMessageEntryId, callId) {
218
+ return JSON.stringify([requestId ?? null, assistantMessageEntryId, callId]);
219
+ }
220
+ function assistantToolKey(assistantMessageEntryId, callId) {
221
+ return JSON.stringify([assistantMessageEntryId, callId]);
222
+ }
223
+ function requestCallKey(requestId, callId) {
224
+ return JSON.stringify([requestId ?? null, callId]);
225
+ }
226
+ export function indexSessionLifecycle(entries, leafId) {
227
+ const branch = collectSessionBranch(entries, leafId);
228
+ const entryPositions = new Map();
229
+ const entryTypes = new Map();
230
+ const entriesById = new Map();
231
+ for (let position = 0; position < branch.length; position += 1) {
232
+ entryPositions.set(branch[position].id, position);
233
+ entryTypes.set(branch[position].id, branch[position].type);
234
+ entriesById.set(branch[position].id, branch[position]);
235
+ }
236
+ const requestSnapshots = branch.filter((entry) => entry.type === "request_snapshot");
237
+ const requestAtPosition = [];
238
+ let currentRequest;
239
+ for (let position = 0; position < branch.length; position += 1) {
240
+ const entry = branch[position];
241
+ if (entry.type === "request_snapshot")
242
+ currentRequest = entry;
243
+ requestAtPosition[position] = currentRequest;
244
+ }
245
+ const assistantToolCalls = [];
246
+ const assistantCallsByMessageCall = new Map();
247
+ const pendingCallsByRequestAndCall = new Map();
248
+ const completedCallsByRequestAndCall = new Map();
249
+ const ambiguousResultEntryIds = [];
250
+ const unmatchedResultEntryIds = [];
251
+ const toolResults = [];
252
+ const createToolResult = (call, entry, position) => {
253
+ if (entry.message.role !== "toolResult")
254
+ throw new TypeError("Expected a tool result message entry.");
255
+ return {
256
+ requestId: call.requestId,
257
+ assistantMessageEntryId: call.assistantMessageEntryId,
258
+ callId: entry.message.toolCallId,
259
+ resultMessageEntryId: entry.id,
260
+ position,
261
+ toolName: entry.message.toolName,
262
+ isError: entry.message.isError,
263
+ ...(entry.message.isError
264
+ ? { errorKind: entry.message.errorKind ?? "tool_failure" }
265
+ : entry.message.errorKind === undefined
266
+ ? {}
267
+ : { errorKind: entry.message.errorKind }),
268
+ };
269
+ };
270
+ for (let position = 0; position < branch.length; position += 1) {
271
+ const entry = branch[position];
272
+ if (entry.type !== "message")
273
+ continue;
274
+ if (entry.message.role === "assistant") {
275
+ const request = requestAtPosition[position];
276
+ for (const block of entry.message.content) {
277
+ if (block.type !== "toolCall")
278
+ continue;
279
+ const call = {
280
+ requestId: request?.requestId,
281
+ assistantMessageEntryId: entry.id,
282
+ callId: block.id,
283
+ modelOrder: assistantToolCalls.length,
284
+ position,
285
+ toolName: block.name,
286
+ };
287
+ assistantToolCalls.push(call);
288
+ const messageCallKey = assistantToolKey(call.assistantMessageEntryId, call.callId);
289
+ const messageCalls = assistantCallsByMessageCall.get(messageCallKey) ?? [];
290
+ messageCalls.push(call);
291
+ assistantCallsByMessageCall.set(messageCallKey, messageCalls);
292
+ const requestKey = requestCallKey(call.requestId, call.callId);
293
+ const pendingCalls = pendingCallsByRequestAndCall.get(requestKey) ?? [];
294
+ pendingCalls.push(call);
295
+ pendingCallsByRequestAndCall.set(requestKey, pendingCalls);
296
+ }
297
+ }
298
+ else if (entry.message.role === "toolResult") {
299
+ const request = requestAtPosition[position];
300
+ const requestKey = requestCallKey(request?.requestId, entry.message.toolCallId);
301
+ const pendingCalls = pendingCallsByRequestAndCall.get(requestKey);
302
+ if (!pendingCalls || pendingCalls.length !== 1) {
303
+ if (pendingCalls && pendingCalls.length > 1)
304
+ ambiguousResultEntryIds.push(entry.id);
305
+ else {
306
+ const completedCall = completedCallsByRequestAndCall.get(requestKey);
307
+ if (completedCall)
308
+ toolResults.push(createToolResult(completedCall, entry, position));
309
+ else
310
+ unmatchedResultEntryIds.push(entry.id);
311
+ }
312
+ continue;
313
+ }
314
+ const call = pendingCalls.pop();
315
+ completedCallsByRequestAndCall.set(requestKey, call);
316
+ toolResults.push(createToolResult(call, entry, position));
317
+ }
318
+ }
319
+ const toolsByIdentity = new Map();
320
+ const resultByIdentity = new Map();
321
+ for (const result of toolResults) {
322
+ const key = sessionLifecycleToolIdentityKey(result.requestId, result.assistantMessageEntryId, result.callId);
323
+ const results = resultByIdentity.get(key) ?? [];
324
+ results.push(result);
325
+ resultByIdentity.set(key, results);
326
+ }
327
+ for (const call of assistantToolCalls) {
328
+ const key = sessionLifecycleToolIdentityKey(call.requestId, call.assistantMessageEntryId, call.callId);
329
+ const record = toolsByIdentity.get(key) ?? createToolRecord();
330
+ record.assistantCalls.push(call);
331
+ const results = resultByIdentity.get(key);
332
+ if (results) {
333
+ record.results.push(...results);
334
+ if (!record.result)
335
+ record.result = results[0];
336
+ }
337
+ toolsByIdentity.set(key, record);
338
+ }
339
+ const legacyAliases = new Map();
340
+ const lifecycleRecord = (requestId, assistantMessageEntryId, callId) => {
341
+ const key = sessionLifecycleToolIdentityKey(requestId, assistantMessageEntryId, callId);
342
+ const direct = toolsByIdentity.get(key);
343
+ if (direct)
344
+ return direct;
345
+ const messageCallKey = assistantToolKey(assistantMessageEntryId, callId);
346
+ const candidates = assistantCallsByMessageCall.get(messageCallKey) ?? [];
347
+ if (candidates.length === 1 && candidates[0].requestId === undefined) {
348
+ const previousKey = legacyAliases.get(messageCallKey);
349
+ if (previousKey !== undefined && previousKey !== key) {
350
+ const unmatched = createToolRecord();
351
+ toolsByIdentity.set(key, unmatched);
352
+ return unmatched;
353
+ }
354
+ const legacyKey = sessionLifecycleToolIdentityKey(undefined, assistantMessageEntryId, callId);
355
+ const legacyRecord = toolsByIdentity.get(legacyKey);
356
+ if (legacyRecord) {
357
+ toolsByIdentity.delete(legacyKey);
358
+ toolsByIdentity.set(key, legacyRecord);
359
+ legacyAliases.set(messageCallKey, key);
360
+ return legacyRecord;
361
+ }
362
+ }
363
+ const record = createToolRecord();
364
+ toolsByIdentity.set(key, record);
365
+ return record;
366
+ };
367
+ const compactionsById = new Map();
368
+ for (const entry of branch) {
369
+ if (entry.type === "foreground_tool_start") {
370
+ const record = lifecycleRecord(entry.requestId, entry.assistantMessageEntryId, entry.callId);
371
+ record.starts.push(entry);
372
+ if (!record.start)
373
+ record.start = entry;
374
+ }
375
+ else if (entry.type === "foreground_tool_terminal") {
376
+ const record = lifecycleRecord(entry.requestId, entry.assistantMessageEntryId, entry.callId);
377
+ record.terminals.push(entry);
378
+ if (!record.terminal)
379
+ record.terminal = entry;
380
+ }
381
+ else if (entry.type === "compaction_start") {
382
+ const record = compactionsById.get(entry.compactionId) ?? createCompactionRecord();
383
+ record.starts.push(entry);
384
+ if (!record.start)
385
+ record.start = entry;
386
+ compactionsById.set(entry.compactionId, record);
387
+ }
388
+ else if (entry.type === "compaction_end") {
389
+ const record = compactionsById.get(entry.compactionId) ?? createCompactionRecord();
390
+ record.ends.push(entry);
391
+ if (!record.end)
392
+ record.end = entry;
393
+ compactionsById.set(entry.compactionId, record);
394
+ }
395
+ }
396
+ return {
397
+ branchEntryIds: branch.map((entry) => entry.id),
398
+ entryPositions,
399
+ entryTypes,
400
+ entriesById,
401
+ requestSnapshots,
402
+ assistantToolCalls,
403
+ toolResults,
404
+ toolsByIdentity,
405
+ compactionsById,
406
+ ambiguousResultEntryIds,
407
+ unmatchedResultEntryIds,
408
+ };
409
+ }
410
+ function resultForRecord(record) {
411
+ return record.result ?? record.results[0];
412
+ }
413
+ export function inspectSessionLifecycle(entries, leafId) {
414
+ const index = indexSessionLifecycle(entries, leafId);
415
+ const duplicateRequestSnapshots = [];
416
+ const requestIds = new Set();
417
+ for (const request of index.requestSnapshots) {
418
+ if (requestIds.has(request.requestId))
419
+ duplicateRequestSnapshots.push(request);
420
+ requestIds.add(request.requestId);
421
+ }
422
+ const unmatchedToolStarts = [];
423
+ const unmatchedToolTerminals = [];
424
+ const duplicateToolStarts = [];
425
+ const duplicateToolTerminals = [];
426
+ const duplicateAssistantToolCalls = [];
427
+ const duplicateToolResults = [];
428
+ const mismatchedToolEntries = [];
429
+ const outOfOrderToolEntries = [];
430
+ const unstartedTools = [];
431
+ const unknownToolOutcomes = [];
432
+ const terminalPromotions = [];
433
+ for (const record of index.toolsByIdentity.values()) {
434
+ if (record.starts.length > 1)
435
+ duplicateToolStarts.push(...record.starts.slice(1));
436
+ if (record.terminals.length > 1)
437
+ duplicateToolTerminals.push(...record.terminals.slice(1));
438
+ if (record.assistantCalls.length > 1)
439
+ duplicateAssistantToolCalls.push(...record.assistantCalls.slice(1));
440
+ if (record.results.length > 1)
441
+ duplicateToolResults.push(...record.results.slice(1));
442
+ if (record.assistantCalls.length === 0) {
443
+ unmatchedToolStarts.push(...record.starts);
444
+ unmatchedToolTerminals.push(...record.terminals);
445
+ continue;
446
+ }
447
+ const call = record.assistantCalls[0];
448
+ const start = record.start;
449
+ const terminal = record.terminal;
450
+ const result = resultForRecord(record);
451
+ if (start && start.toolName !== call.toolName)
452
+ mismatchedToolEntries.push(start.id);
453
+ if (terminal && terminal.toolName !== call.toolName)
454
+ mismatchedToolEntries.push(terminal.id);
455
+ if (result && result.toolName !== call.toolName)
456
+ mismatchedToolEntries.push(result.resultMessageEntryId);
457
+ if (start && index.entryPositions.get(start.id) < call.position)
458
+ outOfOrderToolEntries.push(start.id);
459
+ if (terminal && start && index.entryPositions.get(terminal.id) < index.entryPositions.get(start.id))
460
+ outOfOrderToolEntries.push(terminal.id);
461
+ if (result && result.position < call.position)
462
+ outOfOrderToolEntries.push(result.resultMessageEntryId);
463
+ if (terminal && result && (index.entryPositions.get(terminal.id) ?? -1) < result.position) {
464
+ outOfOrderToolEntries.push(terminal.id);
465
+ }
466
+ if (terminal?.resultMessageEntryId !== undefined && !result)
467
+ outOfOrderToolEntries.push(terminal.resultMessageEntryId);
468
+ if (terminal && !start)
469
+ mismatchedToolEntries.push(terminal.id);
470
+ if (result) {
471
+ if (!result.isError && result.errorKind !== undefined) {
472
+ mismatchedToolEntries.push(result.resultMessageEntryId);
473
+ }
474
+ const terminalMatches = terminal?.resultMessageEntryId === result.resultMessageEntryId &&
475
+ terminal.outcome === (result.isError ? "error" : "success") &&
476
+ terminal.errorKind === result.errorKind;
477
+ if (terminal && !terminalMatches) {
478
+ mismatchedToolEntries.push(terminal.id);
479
+ }
480
+ if (start && !terminal) {
481
+ terminalPromotions.push({
482
+ ...(start?.requestId === undefined && call.requestId === undefined
483
+ ? {}
484
+ : { requestId: start?.requestId ?? call.requestId }),
485
+ assistantMessageEntryId: call.assistantMessageEntryId,
486
+ toolName: call.toolName,
487
+ callId: call.callId,
488
+ resultMessageEntryId: result.resultMessageEntryId,
489
+ outcome: result.isError ? "error" : "success",
490
+ ...(result.errorKind === undefined ? {} : { errorKind: result.errorKind }),
491
+ });
492
+ }
493
+ }
494
+ else if (!start) {
495
+ unstartedTools.push({
496
+ requestId: call.requestId,
497
+ assistantMessageEntryId: call.assistantMessageEntryId,
498
+ toolName: call.toolName,
499
+ callId: call.callId,
500
+ });
501
+ }
502
+ else {
503
+ if (terminal)
504
+ mismatchedToolEntries.push(terminal.id);
505
+ unknownToolOutcomes.push({
506
+ requestId: call.requestId ?? start.requestId,
507
+ assistantMessageEntryId: call.assistantMessageEntryId,
508
+ toolName: call.toolName,
509
+ callId: call.callId,
510
+ startEntryId: start.id,
511
+ });
512
+ }
513
+ }
514
+ const unmatchedCompactionEnds = [];
515
+ const duplicateCompactionStarts = [];
516
+ const duplicateCompactionEnds = [];
517
+ const outOfOrderCompactionEntries = [];
518
+ const invalidCompactionReferences = [];
519
+ const orphanedCompactions = [];
520
+ for (const [compactionId, record] of index.compactionsById) {
521
+ if (record.starts.length > 1)
522
+ duplicateCompactionStarts.push(...record.starts.slice(1));
523
+ if (record.ends.length > 1)
524
+ duplicateCompactionEnds.push(...record.ends.slice(1));
525
+ if (!record.start && record.end)
526
+ unmatchedCompactionEnds.push(record.end);
527
+ if (record.start) {
528
+ const startPosition = index.entryPositions.get(record.start.id);
529
+ const firstKeptPosition = index.entryPositions.get(record.start.firstKeptEntryId);
530
+ if (firstKeptPosition === undefined || startPosition === undefined || firstKeptPosition >= startPosition) {
531
+ invalidCompactionReferences.push(compactionId);
532
+ }
533
+ if (!record.end)
534
+ orphanedCompactions.push(record.start);
535
+ if (record.end?.outcome === "success") {
536
+ const compactionPosition = record.end.compactionEntryId
537
+ ? index.entryPositions.get(record.end.compactionEntryId)
538
+ : undefined;
539
+ const compactionEntry = record.end.compactionEntryId
540
+ ? index.entriesById.get(record.end.compactionEntryId)
541
+ : undefined;
542
+ const finalFirstKeptPosition = compactionEntry?.type === "compaction"
543
+ ? index.entryPositions.get(compactionEntry.firstKeptEntryId)
544
+ : undefined;
545
+ if (!record.end.compactionEntryId ||
546
+ index.entryTypes.get(record.end.compactionEntryId) !== "compaction" ||
547
+ compactionPosition === undefined ||
548
+ startPosition === undefined ||
549
+ compactionPosition <= startPosition ||
550
+ (index.entryPositions.get(record.end.id) ?? -1) <= compactionPosition ||
551
+ compactionEntry?.type !== "compaction" ||
552
+ finalFirstKeptPosition === undefined ||
553
+ finalFirstKeptPosition >= compactionPosition) {
554
+ invalidCompactionReferences.push(compactionId);
555
+ }
556
+ }
557
+ if (record.end &&
558
+ startPosition !== undefined &&
559
+ (index.entryPositions.get(record.end.id) ?? -1) < startPosition) {
560
+ outOfOrderCompactionEntries.push(record.end.id);
561
+ }
562
+ }
563
+ }
564
+ const refusalReasons = [
565
+ ...(index.ambiguousResultEntryIds.length > 0 ? ["ambiguous tool-result association"] : []),
566
+ ...(index.unmatchedResultEntryIds.length > 0 ? ["unmatched tool-result association"] : []),
567
+ ...(duplicateRequestSnapshots.length > 0 ? ["duplicate request snapshots"] : []),
568
+ ...(unmatchedToolStarts.length > 0 ? ["unmatched lifecycle tool starts"] : []),
569
+ ...(unmatchedToolTerminals.length > 0 ? ["unmatched lifecycle tool terminals"] : []),
570
+ ...(duplicateToolStarts.length > 0 ? ["duplicate lifecycle tool starts"] : []),
571
+ ...(duplicateToolTerminals.length > 0 ? ["duplicate lifecycle tool terminals"] : []),
572
+ ...(duplicateAssistantToolCalls.length > 0 ? ["duplicate assistant tool calls"] : []),
573
+ ...(duplicateToolResults.length > 0 ? ["duplicate tool results"] : []),
574
+ ...(mismatchedToolEntries.length > 0 ? ["mismatched tool metadata"] : []),
575
+ ...(outOfOrderToolEntries.length > 0 ? ["out-of-order tool lifecycle"] : []),
576
+ ...(duplicateCompactionStarts.length > 0 ? ["duplicate compaction starts"] : []),
577
+ ...(duplicateCompactionEnds.length > 0 ? ["duplicate compaction ends"] : []),
578
+ ...(unmatchedCompactionEnds.length > 0 ? ["unmatched compaction ends"] : []),
579
+ ...(outOfOrderCompactionEntries.length > 0 ? ["out-of-order compaction lifecycle"] : []),
580
+ ...(invalidCompactionReferences.length > 0 ? ["invalid compaction references"] : []),
581
+ ];
582
+ return {
583
+ ...index,
584
+ refusalReasons,
585
+ duplicateRequestSnapshots,
586
+ unmatchedToolStarts,
587
+ unmatchedToolTerminals,
588
+ duplicateToolStarts,
589
+ duplicateToolTerminals,
590
+ duplicateAssistantToolCalls,
591
+ duplicateToolResults,
592
+ mismatchedToolEntries,
593
+ outOfOrderToolEntries,
594
+ unstartedTools,
595
+ unknownToolOutcomes,
596
+ unmatchedCompactionEnds,
597
+ duplicateCompactionStarts,
598
+ duplicateCompactionEnds,
599
+ outOfOrderCompactionEntries,
600
+ invalidCompactionReferences,
601
+ orphanedCompactions,
602
+ terminalPromotions,
603
+ balanced: refusalReasons.length === 0 &&
604
+ unstartedTools.length === 0 &&
605
+ unknownToolOutcomes.length === 0 &&
606
+ terminalPromotions.length === 0 &&
607
+ orphanedCompactions.length === 0,
608
+ };
609
+ }
610
+ export function planSessionLifecycleRepair(entries, leafId) {
611
+ const inspection = inspectSessionLifecycle(entries, leafId);
612
+ const refused = inspection.refusalReasons.length > 0;
613
+ if (refused) {
614
+ return {
615
+ refused: true,
616
+ refusalReasons: inspection.refusalReasons,
617
+ toolClosers: [],
618
+ terminalPromotions: [],
619
+ compactionClosers: [],
620
+ };
621
+ }
622
+ const toolClosers = [
623
+ ...inspection.unstartedTools.map((tool) => ({
624
+ ...tool,
625
+ code: (tool.requestId === undefined ? TOOL_OUTCOME_UNKNOWN : TOOL_NOT_STARTED),
626
+ })),
627
+ ...inspection.unknownToolOutcomes.map((tool) => ({
628
+ requestId: tool.requestId,
629
+ assistantMessageEntryId: tool.assistantMessageEntryId,
630
+ toolName: tool.toolName,
631
+ callId: tool.callId,
632
+ code: TOOL_OUTCOME_UNKNOWN,
633
+ sourceEntryId: tool.startEntryId,
634
+ })),
635
+ ];
636
+ return {
637
+ refused: false,
638
+ refusalReasons: [],
639
+ toolClosers,
640
+ terminalPromotions: inspection.terminalPromotions,
641
+ compactionClosers: inspection.orphanedCompactions.map((start) => ({
642
+ compactionId: start.compactionId,
643
+ sourceEntryId: start.id,
644
+ outcome: "interrupted",
645
+ })),
646
+ };
647
+ }
648
+ //# sourceMappingURL=lifecycle-ledger.js.map