session-steward 0.3.0 → 0.5.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,731 @@
1
+ import {
2
+ createSessionEvent,
3
+ createSessionEventCoverage,
4
+ createSessionEventHeader,
5
+ createSessionEventsResult,
6
+ SESSION_EVENT_KIND,
7
+ SESSION_EVENT_REASON,
8
+ } from "../../session-events.mjs";
9
+ import {
10
+ createSessionEventReadState,
11
+ createUnmappedSessionEventTracker,
12
+ isInjectedSessionAsk,
13
+ } from "../../session-event-reader.mjs";
14
+ import { visitJsonlSnapshotEntries } from "../../storage/jsonl.mjs";
15
+ import { getSessionRecord, loadSessionStore } from "./store.mjs";
16
+
17
+ const PROVIDER_ID = "codex";
18
+ const RECORD_CLASSIFICATION = Object.freeze({
19
+ RECOGNIZED: "recognized",
20
+ SKIPPED: "skipped",
21
+ UNMAPPED: "unmapped",
22
+ UNPARSEABLE: "unparseable",
23
+ });
24
+ const SKIPPED_PAYLOAD_TYPES = new Set([
25
+ "agent_reasoning",
26
+ "reasoning",
27
+ "token_count",
28
+ ]);
29
+ const SKIPPED_RECORD_TYPES = new Set([
30
+ "inter_agent_communication_metadata",
31
+ "world_state",
32
+ ]);
33
+ const DUPLICATE_TEXT_WINDOW = 8;
34
+ const DUPLICATE_TEXT_TRACKED = 64;
35
+
36
+ function createDuplicateTextTracker() {
37
+ const seen = new Map();
38
+
39
+ return {
40
+ isDuplicate(kind, text, sequence) {
41
+ const key = `${kind}\u0000${text.replace(/\s+/gu, " ").trim().slice(0, 400)}`;
42
+ const previous = seen.get(key);
43
+ seen.set(key, sequence);
44
+ if (seen.size > DUPLICATE_TEXT_TRACKED) {
45
+ seen.delete(seen.keys().next().value);
46
+ }
47
+ return previous !== undefined && sequence - previous <= DUPLICATE_TEXT_WINDOW;
48
+ },
49
+ };
50
+ }
51
+
52
+ function asTimestamp(value) {
53
+ if (typeof value === "number" && Number.isFinite(value)) return value;
54
+ if (typeof value !== "string") return null;
55
+ const timestamp = Date.parse(value);
56
+ return Number.isFinite(timestamp) ? timestamp : null;
57
+ }
58
+
59
+ function contentText(value) {
60
+ if (typeof value === "string") return value;
61
+
62
+ if (Array.isArray(value)) {
63
+ return value
64
+ .map((part) => contentText(part))
65
+ .filter((part) => part !== "")
66
+ .join("\n");
67
+ }
68
+
69
+ if (!value || typeof value !== "object") return "";
70
+ if (typeof value.text === "string") return value.text;
71
+ if (typeof value.content === "string" || Array.isArray(value.content)) {
72
+ return contentText(value.content);
73
+ }
74
+ if (typeof value.message === "string" || Array.isArray(value.message)) {
75
+ return contentText(value.message);
76
+ }
77
+ if (typeof value.summary === "string") return value.summary;
78
+ return "";
79
+ }
80
+
81
+ function parseArguments(value) {
82
+ if (value && typeof value === "object" && !Array.isArray(value)) return value;
83
+ if (typeof value !== "string") return null;
84
+
85
+ try {
86
+ const parsed = JSON.parse(value);
87
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
88
+ } catch {
89
+ return null;
90
+ }
91
+ }
92
+
93
+ function readJavaScriptString(source, start) {
94
+ const quote = source[start];
95
+ if (!['"', "'", "`"].includes(quote)) return null;
96
+ let result = "";
97
+
98
+ for (let index = start + 1; index < source.length; index += 1) {
99
+ const character = source[index];
100
+
101
+ if (character === quote) return result;
102
+
103
+ if (character !== "\\") {
104
+ result += character;
105
+ continue;
106
+ }
107
+
108
+ index += 1;
109
+ if (index >= source.length) return null;
110
+ const escaped = source[index];
111
+ result += {
112
+ n: "\n",
113
+ r: "\r",
114
+ t: "\t",
115
+ }[escaped] ?? escaped;
116
+ }
117
+
118
+ return null;
119
+ }
120
+
121
+ function javascriptProperty(source, property) {
122
+ if (typeof source !== "string") return null;
123
+ const match = new RegExp(`\\b${property}\\s*:\\s*`, "u").exec(source);
124
+ if (!match) return null;
125
+ return readJavaScriptString(source, match.index + match[0].length);
126
+ }
127
+
128
+ function embeddedJsonProperty(source, property) {
129
+ if (typeof source !== "string") return null;
130
+ const match = new RegExp(`"${property}"\\s*:\\s*"((?:[^"\\\\]|\\\\.)*)"`, "u").exec(source);
131
+ if (!match) return null;
132
+
133
+ try {
134
+ const value = JSON.parse(`"${match[1]}"`);
135
+ return typeof value === "string" ? value : null;
136
+ } catch {
137
+ return null;
138
+ }
139
+ }
140
+
141
+ function toolArguments(payload) {
142
+ const parsed = parseArguments(payload.arguments ?? payload.input);
143
+ if (parsed) return parsed;
144
+
145
+ return {
146
+ cmd: embeddedJsonProperty(payload.input, "cmd")
147
+ ?? javascriptProperty(payload.input, "cmd"),
148
+ workdir: embeddedJsonProperty(payload.input, "workdir")
149
+ ?? javascriptProperty(payload.input, "workdir"),
150
+ };
151
+ }
152
+
153
+ function nestedToolName(source) {
154
+ if (typeof source !== "string") return null;
155
+ const direct = /\btools\.([A-Za-z][\w]*)\s*\(/u.exec(source)?.[1];
156
+ if (direct) return direct;
157
+ return /\bx\.name\s*===\s*"([A-Za-z][\w]*)"/u.exec(source)?.[1] ?? null;
158
+ }
159
+
160
+ function nestedToolLabel(name) {
161
+ if (!name) return null;
162
+ if (name === "exec_command") return null;
163
+ if (name.startsWith("mcp__")) {
164
+ const [server, ...toolParts] = name.slice(5).split("__");
165
+ return `mcp: ${server}/${toolParts.join("/") || "unknown tool"}`;
166
+ }
167
+ if (name.includes("__")) return `tool: ${name.replaceAll("__", "/")}`;
168
+ return `tool: ${name}`;
169
+ }
170
+
171
+ function changePaths(changes) {
172
+ if (!changes || typeof changes !== "object" || Array.isArray(changes)) return [];
173
+ return Object.keys(changes).filter(Boolean);
174
+ }
175
+
176
+ function patchResultFiles(payload) {
177
+ const fromChanges = changePaths(payload.changes);
178
+ if (fromChanges.length > 0) return fromChanges;
179
+ if (typeof payload.stdout !== "string") return [];
180
+ return [...payload.stdout.matchAll(/^[MAD]\s+(.+)$/gmu)]
181
+ .map((match) => match[1].trim())
182
+ .filter(Boolean);
183
+ }
184
+
185
+ function patchResultDetails(payload) {
186
+ return {
187
+ added: null,
188
+ applied: typeof payload.success === "boolean"
189
+ ? payload.success
190
+ : typeof payload.stdout === "string"
191
+ ? payload.stdout.startsWith("Success.")
192
+ : null,
193
+ files: patchResultFiles(payload),
194
+ removed: null,
195
+ };
196
+ }
197
+
198
+ function patchDetails(input) {
199
+ if (typeof input !== "string") {
200
+ return { added: null, files: [], removed: null };
201
+ }
202
+
203
+ const files = [];
204
+ const seen = new Set();
205
+ const pattern = /^\*\*\* (?:Update|Add|Delete) File: (.+)$/gmu;
206
+ let match;
207
+
208
+ while ((match = pattern.exec(input))) {
209
+ const file = match[1].trim();
210
+ if (!file || seen.has(file)) continue;
211
+ seen.add(file);
212
+ files.push(file);
213
+ }
214
+
215
+ return {
216
+ added: (input.match(/^\+/gmu) ?? []).length,
217
+ files,
218
+ removed: (input.match(/^-/gmu) ?? []).length,
219
+ };
220
+ }
221
+
222
+ function commandFailure(payload) {
223
+ if (typeof payload.is_error === "boolean") return payload.is_error;
224
+ if (Number.isInteger(payload.exit_code)) return payload.exit_code !== 0;
225
+ const output = contentText(payload.output ?? payload.content ?? payload.stdout);
226
+ if (/^Script completed(?:\n|$)/u.test(output)) return false;
227
+ if (/^Script failed(?:\n|$)/u.test(output)) return true;
228
+ const match = /(?:Process exited with code|exit[_ ]code["']?\s*[:=]?|Exit code)\s*(-?\d+)/iu.exec(output);
229
+ return match ? Number(match[1]) !== 0 : null;
230
+ }
231
+
232
+ function mcpOutcome(payload) {
233
+ const result = payload.result?.Ok ?? payload.result?.Err ?? payload.result;
234
+ const failed = payload.result?.Err !== undefined
235
+ ? true
236
+ : typeof result?.isError === "boolean"
237
+ ? result.isError
238
+ : typeof result?.is_error === "boolean"
239
+ ? result.is_error
240
+ : null;
241
+ return {
242
+ error: failed === true
243
+ ? contentText(result?.content ?? result?.error ?? result).trim() || "Tool failed."
244
+ : null,
245
+ failed,
246
+ };
247
+ }
248
+
249
+ function mcpToolEvent(payload, atMs, sequence) {
250
+ const invocation = payload.invocation && typeof payload.invocation === "object"
251
+ ? payload.invocation
252
+ : {};
253
+ const input = invocation.arguments && typeof invocation.arguments === "object"
254
+ ? invocation.arguments
255
+ : {};
256
+ const server = typeof invocation.server === "string" ? invocation.server : "unknown server";
257
+ const tool = typeof invocation.tool === "string" ? invocation.tool : "unknown tool";
258
+ const outcome = mcpOutcome(payload);
259
+ const filePath = typeof input.file_path === "string" ? input.file_path : null;
260
+
261
+ if (filePath) {
262
+ return createSessionEvent({
263
+ applied: outcome.failed === null ? null : !outcome.failed,
264
+ atMs,
265
+ files: [filePath],
266
+ kind: SESSION_EVENT_KIND.EDIT,
267
+ sequence,
268
+ });
269
+ }
270
+
271
+ return createSessionEvent({
272
+ atMs,
273
+ command: typeof input.command === "string"
274
+ ? input.command
275
+ : `mcp: ${server}/${tool}`,
276
+ error: outcome.error,
277
+ failed: outcome.failed,
278
+ kind: SESSION_EVENT_KIND.RAN,
279
+ sequence,
280
+ unclassified: false,
281
+ workdir: typeof input.workdir === "string" ? input.workdir : null,
282
+ });
283
+ }
284
+
285
+ function unmappedRecordType(recordValue) {
286
+ const outerType = typeof recordValue.type === "string" ? recordValue.type : "missing record type";
287
+ const payloadType = typeof recordValue.payload?.type === "string"
288
+ ? recordValue.payload.type
289
+ : "missing payload type";
290
+ return `${outerType}:${payloadType}`;
291
+ }
292
+
293
+ function outputText(payload) {
294
+ return contentText(payload.output ?? payload.content ?? payload.stderr ?? payload.stdout).trim() || null;
295
+ }
296
+
297
+ function emptyResult({ cwd = null, origin = null, reason }) {
298
+ return createSessionEventsResult({
299
+ coverage: createSessionEventCoverage(),
300
+ events: [],
301
+ header: createSessionEventHeader({ cwd, origin, provider: PROVIDER_ID }),
302
+ reason,
303
+ window: {
304
+ complete: true,
305
+ end: null,
306
+ outcomesMayBeUnresolved: false,
307
+ },
308
+ });
309
+ }
310
+
311
+ async function findSessionRecord({ codexHome, id }) {
312
+ const record = await getSessionRecord({ codexHome, id });
313
+ if (record) return record;
314
+ return (await loadSessionStore({ codexHome })).recordsById.get(id) ?? null;
315
+ }
316
+
317
+ export async function readSessionEvents({
318
+ codexHome,
319
+ id,
320
+ limit,
321
+ maxLineBytes,
322
+ mode,
323
+ signal,
324
+ }) {
325
+ const record = await findSessionRecord({ codexHome, id });
326
+ if (!record) return null;
327
+
328
+ const origin = record.recordSource ?? null;
329
+ if (!record.rolloutPath) {
330
+ return emptyResult({
331
+ cwd: record.cwd || null,
332
+ origin,
333
+ reason: SESSION_EVENT_REASON.NO_TRANSCRIPT_PATH,
334
+ });
335
+ }
336
+
337
+ const coverage = createSessionEventCoverage();
338
+ const readState = createSessionEventReadState({ limit, mode });
339
+ const unmappedTypes = createUnmappedSessionEventTracker();
340
+ const duplicateTexts = createDuplicateTextTracker();
341
+ let acceptingInjectedAsks = true;
342
+ let header = createSessionEventHeader({
343
+ cwd: record.cwd || null,
344
+ origin,
345
+ provider: PROVIDER_ID,
346
+ });
347
+
348
+ function addEvent(event, pendingId = null) {
349
+ return readState.add(event, { pendingId });
350
+ }
351
+
352
+ function toolEvent(payload, atMs, sequence) {
353
+ const callId = payload.call_id ?? payload.id ?? null;
354
+ const name = typeof payload.name === "string" ? payload.name : "unknown tool";
355
+
356
+ if (name === "apply_patch") {
357
+ const details = patchDetails(payload.input);
358
+ return {
359
+ event: createSessionEvent({
360
+ ...details,
361
+ atMs,
362
+ kind: SESSION_EVENT_KIND.EDIT,
363
+ sequence,
364
+ }),
365
+ pendingId: callId,
366
+ };
367
+ }
368
+
369
+ const argumentsValue = toolArguments(payload);
370
+
371
+ if (name === "request_user_input") {
372
+ const question = Array.isArray(argumentsValue?.questions)
373
+ ? argumentsValue.questions
374
+ .map((item) => item?.question)
375
+ .filter((item) => typeof item === "string")
376
+ .join("\n")
377
+ : "";
378
+ return {
379
+ event: createSessionEvent({
380
+ atMs,
381
+ kind: SESSION_EVENT_KIND.DECIDED,
382
+ question: question || "Question",
383
+ sequence,
384
+ }),
385
+ pendingId: callId,
386
+ };
387
+ }
388
+
389
+ if (name === "update_plan") {
390
+ const steps = Array.isArray(argumentsValue?.plan)
391
+ ? argumentsValue.plan
392
+ .filter((step) => typeof step?.step === "string" && typeof step?.status === "string")
393
+ .map((step) => ({ status: step.status, text: step.step }))
394
+ : [];
395
+ return {
396
+ event: createSessionEvent({
397
+ atMs,
398
+ kind: SESSION_EVENT_KIND.PLAN,
399
+ sequence,
400
+ steps,
401
+ }),
402
+ pendingId: null,
403
+ };
404
+ }
405
+
406
+ const filePath = typeof argumentsValue?.file_path === "string"
407
+ ? argumentsValue.file_path
408
+ : null;
409
+ if (filePath) {
410
+ return {
411
+ event: createSessionEvent({
412
+ atMs,
413
+ files: [filePath],
414
+ kind: SESSION_EVENT_KIND.EDIT,
415
+ sequence,
416
+ }),
417
+ pendingId: callId,
418
+ };
419
+ }
420
+
421
+ const command = typeof argumentsValue?.cmd === "string"
422
+ ? argumentsValue.cmd
423
+ : typeof argumentsValue?.command === "string"
424
+ ? argumentsValue.command
425
+ : null;
426
+ const execTool = name === "exec" || name === "exec_command";
427
+ const nestedLabel = execTool ? nestedToolLabel(nestedToolName(payload.input)) : null;
428
+ return {
429
+ event: createSessionEvent({
430
+ atMs,
431
+ command: command ?? nestedLabel ?? (execTool ? null : name),
432
+ kind: SESSION_EVENT_KIND.RAN,
433
+ sequence,
434
+ unclassified: command === null && (nestedLabel !== null || !execTool),
435
+ unextracted: execTool && command === null && nestedLabel === null,
436
+ workdir: typeof argumentsValue?.workdir === "string" ? argumentsValue.workdir : null,
437
+ }),
438
+ pendingId: callId,
439
+ };
440
+ }
441
+
442
+ function handleRecord(recordValue, sequence) {
443
+ const payload = recordValue.payload;
444
+ const atMs = asTimestamp(recordValue.timestamp ?? payload?.timestamp);
445
+
446
+ if (recordValue.type === "session_meta" && payload?.id) {
447
+ const git = payload.git && typeof payload.git === "object" ? {
448
+ branch: payload.git.branch ?? null,
449
+ commit: payload.git.commit ?? payload.git.commit_hash ?? null,
450
+ repository: payload.git.repository ?? payload.git.repository_url ?? null,
451
+ } : null;
452
+ header = createSessionEventHeader({
453
+ cwd: payload.cwd ?? header.cwd,
454
+ git,
455
+ model: payload.model ?? header.model,
456
+ origin: payload.originator ?? header.origin,
457
+ provider: PROVIDER_ID,
458
+ version: payload.cli_version ?? payload.version ?? null,
459
+ });
460
+ return { classification: RECORD_CLASSIFICATION.SKIPPED, stop: false };
461
+ }
462
+
463
+ if (recordValue.type === "turn_context") {
464
+ header = createSessionEventHeader({
465
+ cwd: payload?.cwd ?? header.cwd,
466
+ git: header.git,
467
+ model: payload?.model ?? header.model,
468
+ origin: header.origin,
469
+ provider: PROVIDER_ID,
470
+ version: header.version,
471
+ });
472
+ return { classification: RECORD_CLASSIFICATION.SKIPPED, stop: false };
473
+ }
474
+
475
+ if (payload?.type === "message") {
476
+ const text = contentText(payload.content);
477
+ if (!["assistant", "user"].includes(payload.role)) {
478
+ return { classification: RECORD_CLASSIFICATION.SKIPPED, stop: false };
479
+ }
480
+ if (!text) {
481
+ return { classification: RECORD_CLASSIFICATION.UNPARSEABLE, stop: false };
482
+ }
483
+
484
+ const injected = payload.role === "user"
485
+ && acceptingInjectedAsks
486
+ && isInjectedSessionAsk(text);
487
+ if (payload.role === "assistant" || !injected) acceptingInjectedAsks = false;
488
+
489
+ const kind = payload.role === "user" ? SESSION_EVENT_KIND.ASK : SESSION_EVENT_KIND.SAID;
490
+ if (duplicateTexts.isDuplicate(kind, text, sequence)) {
491
+ return { classification: RECORD_CLASSIFICATION.SKIPPED, duplicate: true, stop: false };
492
+ }
493
+
494
+ return {
495
+ classification: RECORD_CLASSIFICATION.RECOGNIZED,
496
+ stop: addEvent(createSessionEvent({ atMs, injected, kind, sequence, text })),
497
+ };
498
+ }
499
+
500
+ if (payload?.type === "thread_settings_applied") {
501
+ header = createSessionEventHeader({
502
+ cwd: header.cwd,
503
+ git: header.git,
504
+ model: payload.thread_settings?.model ?? header.model,
505
+ origin: header.origin,
506
+ provider: PROVIDER_ID,
507
+ version: header.version,
508
+ });
509
+ return { classification: RECORD_CLASSIFICATION.SKIPPED, stop: false };
510
+ }
511
+
512
+ if (payload?.type === "web_search_end") {
513
+ const query = typeof payload.query === "string" ? payload.query : null;
514
+ return {
515
+ classification: RECORD_CLASSIFICATION.RECOGNIZED,
516
+ stop: addEvent(createSessionEvent({
517
+ atMs,
518
+ command: query ? `web search: ${query}` : "web search",
519
+ kind: SESSION_EVENT_KIND.RAN,
520
+ sequence,
521
+ })),
522
+ };
523
+ }
524
+
525
+ if (payload?.type === "task_complete" || payload?.type === "agent_message" || payload?.type === "user_message") {
526
+ const text = contentText(
527
+ payload.last_agent_message ?? payload.message ?? payload.text ?? payload.content,
528
+ );
529
+ if (!text) {
530
+ return { classification: RECORD_CLASSIFICATION.SKIPPED, stop: false };
531
+ }
532
+
533
+ const kind = payload.type === "user_message"
534
+ ? SESSION_EVENT_KIND.ASK
535
+ : SESSION_EVENT_KIND.SAID;
536
+ if (duplicateTexts.isDuplicate(kind, text, sequence)) {
537
+ return { classification: RECORD_CLASSIFICATION.SKIPPED, duplicate: true, stop: false };
538
+ }
539
+
540
+ const injected = kind === SESSION_EVENT_KIND.ASK
541
+ && acceptingInjectedAsks
542
+ && isInjectedSessionAsk(text);
543
+ if (kind === SESSION_EVENT_KIND.SAID || !injected) acceptingInjectedAsks = false;
544
+
545
+ return {
546
+ classification: RECORD_CLASSIFICATION.RECOGNIZED,
547
+ stop: addEvent(createSessionEvent({ atMs, injected, kind, sequence, text })),
548
+ };
549
+ }
550
+
551
+ if (recordValue.type === "compacted" || payload?.type === "compacted") {
552
+ acceptingInjectedAsks = false;
553
+ const text = contentText(
554
+ payload.content
555
+ ?? payload.message
556
+ ?? payload.summary
557
+ ?? payload.replacement_history,
558
+ );
559
+ if (!text) {
560
+ return { classification: RECORD_CLASSIFICATION.UNPARSEABLE, stop: false };
561
+ }
562
+ return {
563
+ classification: RECORD_CLASSIFICATION.RECOGNIZED,
564
+ stop: addEvent(createSessionEvent({
565
+ atMs,
566
+ kind: SESSION_EVENT_KIND.SUMMARY,
567
+ sequence,
568
+ text,
569
+ })),
570
+ };
571
+ }
572
+
573
+ if (payload?.type === "custom_tool_call" || payload?.type === "function_call") {
574
+ acceptingInjectedAsks = false;
575
+ const nestedName = ["exec", "exec_command"].includes(payload.name)
576
+ ? nestedToolName(payload.input)
577
+ : null;
578
+ if (nestedName === "apply_patch" && toolArguments(payload).cmd === null) {
579
+ return { classification: RECORD_CLASSIFICATION.RECOGNIZED, stop: false };
580
+ }
581
+ const { event, pendingId } = toolEvent(payload, atMs, sequence);
582
+ return {
583
+ classification: RECORD_CLASSIFICATION.RECOGNIZED,
584
+ stop: addEvent(event, pendingId),
585
+ };
586
+ }
587
+
588
+ if (payload?.type === "patch_apply_begin") {
589
+ acceptingInjectedAsks = false;
590
+ const files = changePaths(payload.changes);
591
+ const resolved = readState.resolve(payload.call_id, (event) => {
592
+ if (event.kind === SESSION_EVENT_KIND.EDIT && files.length > 0) {
593
+ event.files = files;
594
+ }
595
+ }, { consume: false });
596
+
597
+ return {
598
+ classification: RECORD_CLASSIFICATION.RECOGNIZED,
599
+ stop: resolved
600
+ ? false
601
+ : addEvent(createSessionEvent({
602
+ added: null,
603
+ applied: null,
604
+ atMs,
605
+ files,
606
+ kind: SESSION_EVENT_KIND.EDIT,
607
+ removed: null,
608
+ sequence,
609
+ }), payload.call_id),
610
+ };
611
+ }
612
+
613
+ if (payload?.type === "patch_apply_end") {
614
+ acceptingInjectedAsks = false;
615
+ const details = patchResultDetails(payload);
616
+ const resolved = readState.resolve(payload.call_id, (event) => {
617
+ if (event.kind !== SESSION_EVENT_KIND.EDIT) return;
618
+ event.applied = details.applied;
619
+ if (details.files.length > 0) event.files = details.files;
620
+ });
621
+ return {
622
+ classification: RECORD_CLASSIFICATION.RECOGNIZED,
623
+ stop: resolved
624
+ ? false
625
+ : addEvent(createSessionEvent({
626
+ ...details,
627
+ atMs,
628
+ kind: SESSION_EVENT_KIND.EDIT,
629
+ sequence,
630
+ })),
631
+ };
632
+ }
633
+
634
+ if (payload?.type === "mcp_tool_call_end") {
635
+ acceptingInjectedAsks = false;
636
+ return {
637
+ classification: RECORD_CLASSIFICATION.RECOGNIZED,
638
+ stop: addEvent(mcpToolEvent(payload, atMs, sequence)),
639
+ };
640
+ }
641
+
642
+ if (
643
+ payload?.type === "custom_tool_call_output"
644
+ || payload?.type === "function_call_output"
645
+ ) {
646
+ const failed = commandFailure(payload);
647
+ const error = failed ? outputText(payload) : null;
648
+ readState.resolve(payload.call_id, (event) => {
649
+ if (event.kind === SESSION_EVENT_KIND.DECIDED) {
650
+ event.answer = outputText(payload);
651
+ } else if (event.kind === SESSION_EVENT_KIND.EDIT && failed !== null) {
652
+ event.applied = !failed;
653
+ } else if (event.kind === SESSION_EVENT_KIND.RAN && failed !== null) {
654
+ event.error = error;
655
+ event.failed = failed;
656
+ }
657
+ });
658
+ return { classification: RECORD_CLASSIFICATION.RECOGNIZED, stop: false };
659
+ }
660
+
661
+ if (
662
+ SKIPPED_RECORD_TYPES.has(recordValue.type)
663
+ || SKIPPED_PAYLOAD_TYPES.has(payload?.type)
664
+ ) {
665
+ return { classification: RECORD_CLASSIFICATION.SKIPPED, stop: false };
666
+ }
667
+
668
+ if (typeof recordValue.type !== "string" && typeof payload?.type !== "string") {
669
+ return { classification: RECORD_CLASSIFICATION.UNPARSEABLE, stop: false };
670
+ }
671
+
672
+ return {
673
+ classification: RECORD_CLASSIFICATION.UNMAPPED,
674
+ stop: false,
675
+ unmappedType: unmappedRecordType(recordValue),
676
+ };
677
+ }
678
+
679
+ let read;
680
+
681
+ try {
682
+ read = await visitJsonlSnapshotEntries(
683
+ record.rolloutPath,
684
+ (entry) => {
685
+ if (signal?.aborted) return false;
686
+ coverage.total += 1;
687
+
688
+ if (entry.oversized) {
689
+ coverage.oversized += 1;
690
+ return true;
691
+ }
692
+
693
+ if (!entry.parsed || typeof entry.parsed !== "object") {
694
+ coverage.unparseable += 1;
695
+ return true;
696
+ }
697
+
698
+ const result = handleRecord(entry.parsed, entry.index);
699
+ coverage[result.classification] += 1;
700
+ if (result.duplicate) coverage.duplicates += 1;
701
+ if (result.classification === RECORD_CLASSIFICATION.UNMAPPED) {
702
+ unmappedTypes.add(result.unmappedType);
703
+ }
704
+ return !result.stop;
705
+ },
706
+ { maxLineBytes },
707
+ );
708
+ } catch (error) {
709
+ if (error?.code === "ENOENT") {
710
+ return emptyResult({
711
+ cwd: record.cwd || null,
712
+ origin,
713
+ reason: SESSION_EVENT_REASON.TRANSCRIPT_MISSING,
714
+ });
715
+ }
716
+
717
+ throw error;
718
+ }
719
+
720
+ const events = readState.values();
721
+ coverage.unmappedTypes = unmappedTypes.values();
722
+ return createSessionEventsResult({
723
+ coverage,
724
+ events,
725
+ header,
726
+ reason: events.length === 0 && read.complete
727
+ ? SESSION_EVENT_REASON.NO_RECOGNIZED_EVENTS
728
+ : null,
729
+ window: readState.window(read),
730
+ });
731
+ }