session-steward 0.4.0 → 0.5.1

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