cogpit-memory 0.1.8 → 0.1.10

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.
package/dist/index.js CHANGED
@@ -57,7 +57,7 @@ var Database = import_better_sqlite3.default;
57
57
  var import_node_fs = require("node:fs");
58
58
  var import_node_path = require("node:path");
59
59
 
60
- // src/lib/turnBuilder.ts
60
+ // src/lib/messageTypeGuards.ts
61
61
  function isUserMessage(msg) {
62
62
  return msg.type === "user";
63
63
  }
@@ -73,9 +73,14 @@ function isSystemMessage(msg) {
73
73
  function isSummaryMessage(msg) {
74
74
  return msg.type === "summary";
75
75
  }
76
+ function isQueueOperationMessage(msg) {
77
+ return msg.type === "queue-operation";
78
+ }
76
79
  function isCompactBoundary(msg) {
77
80
  return msg.type === "system" && msg.subtype === "compact_boundary";
78
81
  }
82
+
83
+ // src/lib/turnBuilder.ts
79
84
  function extractTextFromContent(content) {
80
85
  if (typeof content === "string") return content;
81
86
  return content.filter((b) => b.type === "text").map((b) => b.text).join("\n");
@@ -88,6 +93,11 @@ function extractToolResultText(content) {
88
93
  return "";
89
94
  }).filter(Boolean).join("\n");
90
95
  }
96
+ function isVisibleQueuedPrompt(content) {
97
+ if (!content?.trim()) return false;
98
+ const trimmed = content.trimStart();
99
+ return !trimmed.startsWith("<task-notification>") && !trimmed.startsWith("<local-command-");
100
+ }
91
101
  function mergeTokenUsage(existing, incoming) {
92
102
  if (!existing) {
93
103
  return { ...incoming };
@@ -96,7 +106,8 @@ function mergeTokenUsage(existing, incoming) {
96
106
  input_tokens: existing.input_tokens + incoming.input_tokens,
97
107
  output_tokens: existing.output_tokens + incoming.output_tokens,
98
108
  cache_creation_input_tokens: (existing.cache_creation_input_tokens ?? 0) + (incoming.cache_creation_input_tokens ?? 0),
99
- cache_read_input_tokens: (existing.cache_read_input_tokens ?? 0) + (incoming.cache_read_input_tokens ?? 0)
109
+ cache_read_input_tokens: (existing.cache_read_input_tokens ?? 0) + (incoming.cache_read_input_tokens ?? 0),
110
+ speed: incoming.speed ?? existing.speed
100
111
  };
101
112
  }
102
113
  function buildCompactionSummary(turns, title) {
@@ -129,10 +140,95 @@ function buildCompactionSummary(turns, title) {
129
140
  }
130
141
  return parts.join("\n");
131
142
  }
143
+ function groupPlanModeBlocks(blocks) {
144
+ const result = [];
145
+ let i = 0;
146
+ while (i < blocks.length) {
147
+ const block = blocks[i];
148
+ if (block.kind === "tool_calls") {
149
+ const enterIdx = block.toolCalls.findIndex((tc) => tc.name === "EnterPlanMode");
150
+ if (enterIdx !== -1) {
151
+ const enterCall = block.toolCalls[enterIdx];
152
+ const plan = String(enterCall.input.plan ?? "");
153
+ const timestamp = block.timestamp;
154
+ const before = block.toolCalls.slice(0, enterIdx);
155
+ if (before.length > 0) {
156
+ result.push({ kind: "tool_calls", toolCalls: before, timestamp: block.timestamp });
157
+ }
158
+ const embedded = [];
159
+ const afterEnterInSameBlock = block.toolCalls.slice(enterIdx + 1);
160
+ const exitInSameBlock = afterEnterInSameBlock.findIndex((tc) => tc.name === "ExitPlanMode");
161
+ if (exitInSameBlock !== -1) {
162
+ embedded.push(...afterEnterInSameBlock.slice(0, exitInSameBlock));
163
+ const exitCall = afterEnterInSameBlock[exitInSameBlock];
164
+ const planFilePath = exitCall.input.path ? String(exitCall.input.path) : void 0;
165
+ let status = "pending";
166
+ if (exitCall.result !== null) {
167
+ status = exitCall.isError ? "rejected" : "approved";
168
+ }
169
+ result.push({ kind: "plan_mode", plan, planFilePath, status, toolCalls: embedded, timestamp });
170
+ const after = afterEnterInSameBlock.slice(exitInSameBlock + 1);
171
+ if (after.length > 0) {
172
+ result.push({ kind: "tool_calls", toolCalls: after, timestamp: block.timestamp });
173
+ }
174
+ i++;
175
+ continue;
176
+ } else {
177
+ embedded.push(...afterEnterInSameBlock);
178
+ let exitCall;
179
+ let planFilePath;
180
+ let status = "pending";
181
+ let j = i + 1;
182
+ const passthroughBlocks = [];
183
+ while (j < blocks.length) {
184
+ const next = blocks[j];
185
+ if (next.kind === "tool_calls") {
186
+ const exitIdx = next.toolCalls.findIndex((tc) => tc.name === "ExitPlanMode");
187
+ if (exitIdx !== -1) {
188
+ embedded.push(...next.toolCalls.slice(0, exitIdx));
189
+ exitCall = next.toolCalls[exitIdx];
190
+ planFilePath = exitCall.input.path ? String(exitCall.input.path) : void 0;
191
+ if (exitCall.result !== null) {
192
+ status = exitCall.isError ? "rejected" : "approved";
193
+ }
194
+ const tail = next.toolCalls.slice(exitIdx + 1);
195
+ result.push({ kind: "plan_mode", plan, planFilePath, status, toolCalls: embedded, timestamp });
196
+ result.push(...passthroughBlocks);
197
+ if (tail.length > 0) {
198
+ result.push({ kind: "tool_calls", toolCalls: tail, timestamp: next.timestamp });
199
+ }
200
+ i = j + 1;
201
+ break;
202
+ } else {
203
+ embedded.push(...next.toolCalls);
204
+ j++;
205
+ }
206
+ } else if (next.kind === "hook_event" || next.kind === "text" || next.kind === "queued_prompt") {
207
+ passthroughBlocks.push(next);
208
+ j++;
209
+ } else {
210
+ break;
211
+ }
212
+ }
213
+ if (!exitCall) {
214
+ result.push({ kind: "plan_mode", plan, planFilePath: void 0, status: "pending", toolCalls: embedded, timestamp });
215
+ result.push(...passthroughBlocks);
216
+ i = j;
217
+ }
218
+ continue;
219
+ }
220
+ }
221
+ }
222
+ result.push(block);
223
+ i++;
224
+ }
225
+ return result;
226
+ }
132
227
  function buildTurns(messages) {
133
228
  const turns = [];
134
229
  let current = null;
135
230
  let pendingCompaction = null;
231
+ let pendingRecap = null;
136
232
  const pendingToolUses = /* @__PURE__ */ new Map();
137
233
  const seenMessageIds = /* @__PURE__ */ new Set();
138
234
  const subAgentMap = /* @__PURE__ */ new Map();
@@ -163,6 +259,22 @@ function buildTurns(messages) {
163
259
  for (const [parentId] of subAgentMap) {
164
260
  flushSubAgentMessages(parentId);
165
261
  }
262
+ for (const block of current.contentBlocks) {
263
+ if (block.kind !== "hook_event") continue;
264
+ for (const ev of block.events) {
265
+ const isPostToolUse = ev.eventName === "PostToolUse" || ev.eventName.startsWith("PostToolUse:");
266
+ if (!isPostToolUse || !ev.toolUseId) continue;
267
+ const toolCall = current.toolCalls.find((tc) => tc.id === ev.toolUseId);
268
+ if (!toolCall) continue;
269
+ if (ev.updatedToolOutput) {
270
+ toolCall.outputReplacedByHook = true;
271
+ }
272
+ if (ev.durationMs !== void 0) {
273
+ toolCall.hookDurationMs = (toolCall.hookDurationMs ?? 0) + ev.durationMs;
274
+ }
275
+ }
276
+ }
277
+ current.contentBlocks = groupPlanModeBlocks(current.contentBlocks);
166
278
  turns.push(current);
167
279
  current = null;
168
280
  agentBlockMap.clear();
@@ -184,6 +296,20 @@ function buildTurns(messages) {
184
296
  );
185
297
  continue;
186
298
  }
299
+ if (isSystemMessage(msg) && msg.subtype === "away_summary" && msg.content) {
300
+ pendingRecap = { content: msg.content, timestamp: msg.timestamp };
301
+ continue;
302
+ }
303
+ if (isQueueOperationMessage(msg)) {
304
+ if (current && msg.operation === "enqueue" && isVisibleQueuedPrompt(msg.content)) {
305
+ current.contentBlocks.push({
306
+ kind: "queued_prompt",
307
+ content: msg.content,
308
+ timestamp: msg.timestamp
309
+ });
310
+ }
311
+ continue;
312
+ }
187
313
  if (isUserMessage(msg) && !msg.isMeta) {
188
314
  const content = msg.message.content;
189
315
  if (typeof content !== "string" && Array.isArray(content)) {
@@ -194,7 +320,7 @@ function buildTurns(messages) {
194
320
  const pending = pendingToolUses.get(block.tool_use_id);
195
321
  if (pending) {
196
322
  pending.turn.toolCalls[pending.index].result = extractToolResultText(block.content);
197
- pending.turn.toolCalls[pending.index].isError = block.is_error;
323
+ pending.turn.toolCalls[pending.index].isError = block.is_error === true;
198
324
  pendingToolUses.delete(block.tool_use_id);
199
325
  }
200
326
  }
@@ -212,6 +338,7 @@ function buildTurns(messages) {
212
338
  }
213
339
  const agentMsg = {
214
340
  agentId: toolUseResult.agentId,
341
+ parentToolUseId: toolUseId || void 0,
215
342
  agentName: taskMeta?.name ?? null,
216
343
  subagentType: taskMeta?.subagentType ?? null,
217
344
  type: "assistant",
@@ -263,6 +390,10 @@ function buildTurns(messages) {
263
390
  current.compactionSummary = pendingCompaction;
264
391
  pendingCompaction = null;
265
392
  }
393
+ if (pendingRecap) {
394
+ current.contentBlocks.push({ kind: "recap", content: pendingRecap.content, timestamp: pendingRecap.timestamp });
395
+ pendingRecap = null;
396
+ }
266
397
  continue;
267
398
  }
268
399
  if (isAssistantMessage(msg)) {
@@ -312,6 +443,7 @@ function buildTurns(messages) {
312
443
  if (block.type === "thinking") {
313
444
  flushToolCalls2();
314
445
  const tb = block;
446
+ if (!tb.thinking) continue;
315
447
  current.thinking.push(tb);
316
448
  msgThinking.push(tb);
317
449
  } else if (block.type === "text") {
@@ -391,6 +523,7 @@ function buildTurns(messages) {
391
523
  const taskMeta = taskMetaMap.get(parentId);
392
524
  const agentMsg = {
393
525
  agentId: data.agentId,
526
+ parentToolUseId: parentId || void 0,
394
527
  agentName: taskMeta?.name ?? null,
395
528
  subagentType: taskMeta?.subagentType ?? null,
396
529
  type: data.message.type,
@@ -436,7 +569,7 @@ function buildTurns(messages) {
436
569
  );
437
570
  if (match) {
438
571
  match.result = extractToolResultText(block.content);
439
- match.isError = block.is_error;
572
+ match.isError = block.is_error === true;
440
573
  }
441
574
  }
442
575
  }
@@ -455,6 +588,36 @@ function buildTurns(messages) {
455
588
  }
456
589
  continue;
457
590
  }
591
+ if (isProgressMessage(msg) && msg.data.type === "hook_progress") {
592
+ const data = msg.data;
593
+ const eventName = String(data.hook_event_name ?? data.hookEvent ?? "unknown");
594
+ const hookSpecific = data.hookSpecificOutput;
595
+ const ev = {
596
+ eventName,
597
+ source: data.source !== void 0 ? String(data.source) : void 0,
598
+ toolName: data.tool_name !== void 0 ? String(data.tool_name) : void 0,
599
+ toolUseId: data.tool_use_id !== void 0 ? String(data.tool_use_id) : void 0,
600
+ command: data.command !== void 0 ? String(data.command) : void 0,
601
+ output: data.output !== void 0 ? String(data.output) : void 0,
602
+ stderr: data.stderr !== void 0 ? String(data.stderr) : void 0,
603
+ exitCode: data.exit_code !== void 0 ? Number(data.exit_code) : void 0,
604
+ decision: data.decision !== void 0 ? String(data.decision) : void 0,
605
+ durationMs: data.duration_ms !== void 0 ? Number(data.duration_ms) : void 0,
606
+ updatedToolOutput: hookSpecific?.updatedToolOutput !== void 0 ? String(hookSpecific.updatedToolOutput) : void 0,
607
+ sessionTitle: hookSpecific?.sessionTitle !== void 0 ? String(hookSpecific.sessionTitle) : void 0,
608
+ worktreePath: hookSpecific?.worktreePath !== void 0 ? String(hookSpecific.worktreePath) : void 0,
609
+ timestamp: msg.timestamp ?? ""
610
+ };
611
+ if (current) {
612
+ const last = current.contentBlocks[current.contentBlocks.length - 1];
613
+ if (last && last.kind === "hook_event") {
614
+ last.events.push(ev);
615
+ } else {
616
+ current.contentBlocks.push({ kind: "hook_event", events: [ev], timestamp: msg.timestamp });
617
+ }
618
+ }
619
+ continue;
620
+ }
458
621
  if (isSystemMessage(msg) && msg.subtype === "turn_duration" && current) {
459
622
  current.durationMs = msg.durationMs ?? null;
460
623
  continue;
@@ -465,58 +628,84 @@ function buildTurns(messages) {
465
628
  }
466
629
 
467
630
  // src/lib/pricingTiers.ts
468
- var EXTENDED_CONTEXT_THRESHOLD = 2e5;
469
631
  var TIER_HAIKU_35 = { input: 0.8, output: 4, cacheWrite: 1, cacheRead: 0.08, webSearch: 0.01 };
470
632
  var TIER_HAIKU_45 = { input: 1, output: 5, cacheWrite: 1.25, cacheRead: 0.1, webSearch: 0.01 };
471
- var TIER_SONNET_LEGACY = { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.3, webSearch: 0.01 };
472
- var TIER_SONNET_LATEST = { input: 5, output: 25, cacheWrite: 6.25, cacheRead: 0.5, webSearch: 0.01 };
633
+ var TIER_SONNET = { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.3, webSearch: 0.01 };
634
+ var TIER_OPUS_MID = { input: 5, output: 25, cacheWrite: 6.25, cacheRead: 0.5, webSearch: 0.01 };
635
+ var TIER_FRONTIER = { input: 10, output: 50, cacheWrite: 12.5, cacheRead: 1, webSearch: 0.01 };
473
636
  var TIER_OPUS_LEGACY = { input: 15, output: 75, cacheWrite: 18.75, cacheRead: 1.5, webSearch: 0.01 };
474
- var TIER_SONNET_LEGACY_EXT = { input: 6, output: 22.5, cacheWrite: 7.5, cacheRead: 0.6, webSearch: 0.01 };
475
- var TIER_EXTENDED = { input: 10, output: 37.5, cacheWrite: 12.5, cacheRead: 1, webSearch: 0.01 };
637
+ var TIER_GPT_56_SOL = { input: 5, output: 30, cacheWrite: 6.25, cacheRead: 0.5, webSearch: 0.01 };
638
+ var TIER_GPT_56_TERRA = { input: 2.5, output: 15, cacheWrite: 3.125, cacheRead: 0.25, webSearch: 0.01 };
639
+ var TIER_GPT_56_LUNA = { input: 1, output: 6, cacheWrite: 1.25, cacheRead: 0.1, webSearch: 0.01 };
640
+ var TIER_GPT_55 = { input: 5, output: 30, cacheWrite: 5, cacheRead: 0.5, webSearch: 0.01 };
641
+ var TIER_GPT_54 = { input: 2.5, output: 15, cacheWrite: 2.5, cacheRead: 0.25, webSearch: 0.01 };
642
+ var TIER_GPT_54_MINI = { input: 0.75, output: 4.5, cacheWrite: 0.75, cacheRead: 0.075, webSearch: 0.01 };
643
+ var TIER_FAST = { input: 30, output: 150, cacheWrite: 37.5, cacheRead: 3, webSearch: 0.01 };
476
644
  var MODEL_TIERS = [
645
+ // OpenAI GPT models. Keep variants before their shared family prefixes.
646
+ { match: "gpt-5.6-sol", tier: TIER_GPT_56_SOL },
647
+ { match: "gpt-5.6-terra", tier: TIER_GPT_56_TERRA },
648
+ { match: "gpt-5.6-luna", tier: TIER_GPT_56_LUNA },
649
+ { match: "gpt-5.6", tier: TIER_GPT_56_SOL },
650
+ { match: "gpt-5.4-mini", tier: TIER_GPT_54_MINI },
651
+ { match: "gpt-5.5", tier: TIER_GPT_55 },
652
+ { match: "gpt-5.4", tier: TIER_GPT_54 },
653
+ // Frontier (Fable 5 / Mythos 5 / Opus 4.8)
654
+ { match: "fable-5", tier: TIER_FRONTIER },
655
+ { match: "mythos-5", tier: TIER_FRONTIER },
656
+ { match: "opus-4-8", tier: TIER_FRONTIER },
477
657
  // Haiku
478
658
  { match: "haiku-4-5", tier: TIER_HAIKU_45 },
479
659
  { match: "haiku-4-0", tier: TIER_HAIKU_35 },
480
660
  { match: "3-5-haiku", tier: TIER_HAIKU_35 },
481
- // Sonnet latest (4.5+)
482
- { match: "sonnet-4-6", tier: TIER_SONNET_LATEST, extendedTier: TIER_EXTENDED },
483
- { match: "sonnet-4-5", tier: TIER_SONNET_LATEST, extendedTier: TIER_EXTENDED },
484
- // Sonnet legacy (3.5, 3.7, 4.0)
485
- { match: "sonnet-4-0", tier: TIER_SONNET_LEGACY, extendedTier: TIER_SONNET_LEGACY_EXT },
486
- { match: "3-7-sonnet", tier: TIER_SONNET_LEGACY, extendedTier: TIER_SONNET_LEGACY_EXT },
487
- { match: "3-5-sonnet", tier: TIER_SONNET_LEGACY, extendedTier: TIER_SONNET_LEGACY_EXT },
488
- // Opus latest (4.5+) — same tier as sonnet latest
489
- { match: "opus-4-6", tier: TIER_SONNET_LATEST, extendedTier: TIER_EXTENDED },
490
- { match: "opus-4-5", tier: TIER_SONNET_LATEST, extendedTier: TIER_EXTENDED },
661
+ // Sonnet (3.5 through 4.6 — all one tier in current CC)
662
+ { match: "sonnet-4-7", tier: TIER_SONNET },
663
+ { match: "sonnet-4-6", tier: TIER_SONNET },
664
+ { match: "sonnet-4-5", tier: TIER_SONNET },
665
+ { match: "sonnet-4-0", tier: TIER_SONNET },
666
+ { match: "3-7-sonnet", tier: TIER_SONNET },
667
+ { match: "3-5-sonnet", tier: TIER_SONNET },
668
+ // Opus 4.5–4.7
669
+ { match: "opus-4-7", tier: TIER_OPUS_MID },
670
+ { match: "opus-4-6", tier: TIER_OPUS_MID },
671
+ { match: "opus-4-5", tier: TIER_OPUS_MID },
491
672
  // Opus legacy (4.0, 4.1)
492
673
  { match: "opus-4-1", tier: TIER_OPUS_LEGACY },
493
674
  { match: "opus-4-0", tier: TIER_OPUS_LEGACY }
494
675
  ];
495
- var DEFAULT_TIER = TIER_SONNET_LATEST;
496
- var DEFAULT_EXTENDED_TIER = TIER_EXTENDED;
676
+ var FAST_MODE_MODELS = ["opus-4-6", "opus-4-7"];
677
+ var DEFAULT_TIER = TIER_OPUS_MID;
497
678
  var FAMILY_FALLBACKS = [
679
+ { match: "fable", tier: TIER_FRONTIER },
680
+ { match: "mythos", tier: TIER_FRONTIER },
498
681
  { match: "haiku", tier: TIER_HAIKU_45 },
499
- { match: "sonnet", tier: TIER_SONNET_LATEST },
500
- { match: "opus", tier: TIER_SONNET_LATEST }
682
+ { match: "sonnet", tier: TIER_SONNET },
683
+ { match: "opus", tier: TIER_OPUS_MID }
501
684
  ];
502
- function resolveTier(model, totalInputTokens) {
503
- const isExtended = (totalInputTokens ?? 0) > EXTENDED_CONTEXT_THRESHOLD;
685
+ function resolveTier(model, speed) {
686
+ if (speed === "fast" && FAST_MODE_MODELS.some((m) => model.includes(m))) {
687
+ return TIER_FAST;
688
+ }
504
689
  for (const entry of MODEL_TIERS) {
505
- if (model.includes(entry.match)) {
506
- return isExtended && entry.extendedTier ? entry.extendedTier : entry.tier;
507
- }
690
+ if (model.includes(entry.match)) return entry.tier;
508
691
  }
509
692
  for (const entry of FAMILY_FALLBACKS) {
510
693
  if (model.includes(entry.match)) return entry.tier;
511
694
  }
512
- return isExtended ? DEFAULT_EXTENDED_TIER : DEFAULT_TIER;
695
+ return DEFAULT_TIER;
696
+ }
697
+ function hasKnownPricing(model) {
698
+ if (!model) return true;
699
+ const normalized = model.toLowerCase();
700
+ if (!normalized.startsWith("gpt-")) return true;
701
+ return MODEL_TIERS.some((entry) => normalized.includes(entry.match));
513
702
  }
514
703
 
515
704
  // src/lib/token-costs.ts
516
705
  var CHARS_PER_TOKEN = 4;
517
706
  function calculateCost(c) {
518
- const totalInput = c.inputTokens + c.cacheWriteTokens + c.cacheReadTokens;
519
- const p = resolveTier(c.model ?? "", totalInput);
707
+ if (!hasKnownPricing(c.model)) return Number.NaN;
708
+ const p = resolveTier(c.model ?? "", c.speed);
520
709
  return c.inputTokens / 1e6 * p.input + c.outputTokens / 1e6 * p.output + c.cacheWriteTokens / 1e6 * p.cacheWrite + c.cacheReadTokens / 1e6 * p.cacheRead + (c.webSearchRequests ?? 0) * p.webSearch;
521
710
  }
522
711
  function charsToTokens(chars) {
@@ -554,7 +743,8 @@ function calculateTurnCostEstimated(turn) {
554
743
  inputTokens: u.input_tokens,
555
744
  outputTokens: estimateTotalOutputTokens(turn),
556
745
  cacheWriteTokens: u.cache_creation_input_tokens ?? 0,
557
- cacheReadTokens: u.cache_read_input_tokens ?? 0
746
+ cacheReadTokens: u.cache_read_input_tokens ?? 0,
747
+ speed: u.speed
558
748
  });
559
749
  }
560
750
  function calculateSubAgentCostEstimated(sa) {
@@ -565,7 +755,8 @@ function calculateSubAgentCostEstimated(sa) {
565
755
  inputTokens: u.input_tokens,
566
756
  outputTokens: estimateSubAgentOutput(sa),
567
757
  cacheWriteTokens: u.cache_creation_input_tokens ?? 0,
568
- cacheReadTokens: u.cache_read_input_tokens ?? 0
758
+ cacheReadTokens: u.cache_read_input_tokens ?? 0,
759
+ speed: u.speed
569
760
  });
570
761
  }
571
762
 
@@ -585,6 +776,19 @@ function addUsageToStats(stats, usage, estimatedOutput, cost) {
585
776
  stats.totalCacheReadTokens += usage.cache_read_input_tokens ?? 0;
586
777
  stats.totalCostUSD += cost;
587
778
  }
779
+ function createEmptySessionStats(turnCount) {
780
+ return {
781
+ totalInputTokens: 0,
782
+ totalOutputTokens: 0,
783
+ totalCacheCreationTokens: 0,
784
+ totalCacheReadTokens: 0,
785
+ totalCostUSD: 0,
786
+ toolCallCounts: {},
787
+ errorCount: 0,
788
+ totalDurationMs: 0,
789
+ turnCount
790
+ };
791
+ }
588
792
  function computeStats(turns) {
589
793
  const stats = {
590
794
  totalInputTokens: 0,
@@ -613,8 +817,271 @@ function computeStats(turns) {
613
817
  return stats;
614
818
  }
615
819
 
820
+ // src/lib/codex-patches.ts
821
+ var JS_SIMPLE_ESCAPES = {
822
+ "0": "\0",
823
+ b: "\b",
824
+ f: "\f",
825
+ n: "\n",
826
+ r: "\r",
827
+ t: " ",
828
+ v: "\v"
829
+ };
830
+ function readJsStringLiteral(source, startIndex) {
831
+ const quote = source[startIndex];
832
+ if (quote !== '"' && quote !== "'" && quote !== "`") return null;
833
+ let value = "";
834
+ let index = startIndex + 1;
835
+ while (index < source.length) {
836
+ const char = source[index];
837
+ if (char === quote) return { endIndex: index + 1, value };
838
+ if (char !== "\\") {
839
+ value += char;
840
+ index++;
841
+ continue;
842
+ }
843
+ index++;
844
+ if (index >= source.length) return null;
845
+ const escaped = source[index];
846
+ if (escaped in JS_SIMPLE_ESCAPES) {
847
+ value += JS_SIMPLE_ESCAPES[escaped];
848
+ index++;
849
+ continue;
850
+ }
851
+ if (escaped === "\n") {
852
+ index++;
853
+ continue;
854
+ }
855
+ if (escaped === "\r") {
856
+ index += source[index + 1] === "\n" ? 2 : 1;
857
+ continue;
858
+ }
859
+ if (escaped === "x" && /^[0-9a-fA-F]{2}$/.test(source.slice(index + 1, index + 3))) {
860
+ value += String.fromCharCode(Number.parseInt(source.slice(index + 1, index + 3), 16));
861
+ index += 3;
862
+ continue;
863
+ }
864
+ if (escaped === "u") {
865
+ const braced = source.slice(index + 1).match(/^\{([0-9a-fA-F]+)\}/);
866
+ if (braced) {
867
+ const codePoint = Number.parseInt(braced[1], 16);
868
+ if (Number.isFinite(codePoint) && codePoint <= 1114111) {
869
+ value += String.fromCodePoint(codePoint);
870
+ index += braced[0].length + 1;
871
+ continue;
872
+ }
873
+ }
874
+ const hex = source.slice(index + 1, index + 5);
875
+ if (/^[0-9a-fA-F]{4}$/.test(hex)) {
876
+ value += String.fromCharCode(Number.parseInt(hex, 16));
877
+ index += 5;
878
+ continue;
879
+ }
880
+ }
881
+ value += escaped;
882
+ index++;
883
+ }
884
+ return null;
885
+ }
886
+ function isApplyPatchText(value) {
887
+ return value.includes("*** Begin Patch") && value.includes("*** End Patch");
888
+ }
889
+ function extractApplyPatchInputs(execSource) {
890
+ if (!execSource.includes("apply_patch") || !execSource.includes("*** Begin Patch")) return [];
891
+ const assignedPatches = /* @__PURE__ */ new Map();
892
+ for (let index = 0; index < execSource.length; index++) {
893
+ const literal = readJsStringLiteral(execSource, index);
894
+ if (!literal) continue;
895
+ if (isApplyPatchText(literal.value)) {
896
+ const assignment = execSource.slice(0, index).match(/(?:^|[;\n])\s*(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*$/);
897
+ if (assignment) assignedPatches.set(assignment[1], literal.value);
898
+ }
899
+ index = literal.endIndex - 1;
900
+ }
901
+ const patches = [];
902
+ const seen = /* @__PURE__ */ new Set();
903
+ const applyCall = /\btools\s*\.\s*apply_patch\s*\(/g;
904
+ let match;
905
+ while ((match = applyCall.exec(execSource)) !== null) {
906
+ let argumentIndex = match.index + match[0].length;
907
+ while (/\s/.test(execSource[argumentIndex] ?? "")) argumentIndex++;
908
+ const literal = readJsStringLiteral(execSource, argumentIndex);
909
+ const identifier = literal ? null : execSource.slice(argumentIndex).match(/^([A-Za-z_$][\w$]*)/);
910
+ const patch = literal?.value ?? (identifier ? assignedPatches.get(identifier[1]) : void 0);
911
+ if (!patch || !isApplyPatchText(patch) || seen.has(patch)) continue;
912
+ seen.add(patch);
913
+ patches.push(patch);
914
+ }
915
+ return patches;
916
+ }
917
+ function resolveCodexPatchPath(filePath, cwd) {
918
+ if (!cwd || filePath.startsWith("/") || /^[A-Za-z]:[\\/]/.test(filePath)) return filePath;
919
+ return `${cwd.replace(/[\\/]+$/, "")}/${filePath.replace(/^\.\//, "")}`;
920
+ }
921
+ function parseApplyPatch(patchText, callId, timestamp, cwd = "") {
922
+ const toolCalls = [];
923
+ const filePattern = /^\*\*\*\s+(Update File|Add File|Delete File):\s*(.+)$/gm;
924
+ const sections = [];
925
+ let match;
926
+ while ((match = filePattern.exec(patchText)) !== null) {
927
+ sections.push({
928
+ action: match[1],
929
+ filePath: match[2].trim(),
930
+ headerIdx: match.index,
931
+ startIdx: match.index + match[0].length
932
+ });
933
+ }
934
+ for (let i = 0; i < sections.length; i++) {
935
+ const section = sections[i];
936
+ const endIdx = i + 1 < sections.length ? sections[i + 1].headerIdx : patchText.length;
937
+ const body = patchText.slice(section.startIdx, endIdx);
938
+ const movedTo = body.match(/^\*\*\*\s+Move to:\s*(.+)$/m)?.[1]?.trim();
939
+ const sourcePath = resolveCodexPatchPath(section.filePath, cwd);
940
+ const filePath = resolveCodexPatchPath(movedTo || section.filePath, cwd);
941
+ const hunkBodies = body.split(/^@@.*$/m).filter((s) => s.trim());
942
+ const oldLines = [];
943
+ const newLines = [];
944
+ for (const hunk of hunkBodies) {
945
+ for (const line of hunk.split("\n")) {
946
+ if (line.startsWith("-")) {
947
+ oldLines.push(line.slice(1));
948
+ } else if (line.startsWith("+")) {
949
+ newLines.push(line.slice(1));
950
+ } else if (line.startsWith(" ")) {
951
+ oldLines.push(line.slice(1));
952
+ newLines.push(line.slice(1));
953
+ }
954
+ }
955
+ }
956
+ const fileId = sections.length > 1 ? `${callId}:file-${i}` : callId;
957
+ if (section.action === "Add File") {
958
+ toolCalls.push({
959
+ id: fileId,
960
+ name: "Write",
961
+ input: { file_path: filePath, content: newLines.join("\n") },
962
+ result: null,
963
+ isError: false,
964
+ timestamp
965
+ });
966
+ } else if (section.action === "Delete File") {
967
+ toolCalls.push({
968
+ id: fileId,
969
+ name: "Edit",
970
+ input: { file_path: filePath, old_string: oldLines.join("\n"), new_string: "" },
971
+ result: null,
972
+ isError: false,
973
+ timestamp
974
+ });
975
+ } else {
976
+ toolCalls.push({
977
+ id: fileId,
978
+ name: "Edit",
979
+ input: {
980
+ file_path: filePath,
981
+ ...movedTo ? { old_path: sourcePath } : {},
982
+ old_string: oldLines.join("\n"),
983
+ new_string: newLines.join("\n")
984
+ },
985
+ result: null,
986
+ isError: false,
987
+ timestamp
988
+ });
989
+ }
990
+ }
991
+ return toolCalls;
992
+ }
993
+ function parseCodexToolPatches(toolName, rawInput, callId, timestamp, cwd = "") {
994
+ if (toolName === "apply_patch") {
995
+ return parseApplyPatch(rawInput, callId, timestamp, cwd);
996
+ }
997
+ return extractApplyPatchInputs(rawInput).flatMap(
998
+ (patch, patchIndex) => parseApplyPatch(patch, `${callId}:patch-${patchIndex}`, timestamp, cwd)
999
+ );
1000
+ }
1001
+ function findFailedNestedPatchCallIds(output, wrapperIsError, calls) {
1002
+ if (!wrapperIsError || !/(?:apply[_ ]patch|invalid patch|invalid context|failed to (?:apply|find)|could not apply)/i.test(output)) {
1003
+ return /* @__PURE__ */ new Set();
1004
+ }
1005
+ const groups = /* @__PURE__ */ new Map();
1006
+ for (const call of calls) {
1007
+ const patchIndex = call.id.match(/:patch-(\d+)(?::file-\d+)?$/)?.[1];
1008
+ if (patchIndex === void 0) continue;
1009
+ const index = Number(patchIndex);
1010
+ const group = groups.get(index) ?? [];
1011
+ group.push(call);
1012
+ groups.set(index, group);
1013
+ }
1014
+ if (groups.size === 0) return new Set(calls.map((call) => call.id));
1015
+ const normalizedOutput = output.toLowerCase().replaceAll("\\", "/");
1016
+ const mentionedGroups = /* @__PURE__ */ new Set();
1017
+ for (const [index, group] of groups) {
1018
+ if (group.some((call) => {
1019
+ const filePath = String(call.input.file_path ?? "").toLowerCase().replaceAll("\\", "/");
1020
+ if (!filePath) return false;
1021
+ const segments = filePath.split("/").filter(Boolean);
1022
+ const suffix = segments.slice(-2).join("/");
1023
+ return normalizedOutput.includes(filePath) || suffix.includes("/") && normalizedOutput.includes(suffix);
1024
+ })) {
1025
+ mentionedGroups.add(index);
1026
+ }
1027
+ }
1028
+ for (const match of normalizedOutput.matchAll(/\bpatch\s*(?:#|-)?\s*(\d+)\b/g)) {
1029
+ const index = Number(match[1]);
1030
+ if (groups.has(index)) mentionedGroups.add(index);
1031
+ }
1032
+ if (mentionedGroups.size === 0) mentionedGroups.add(Math.max(...groups.keys()));
1033
+ const firstFailedIndex = Math.min(...mentionedGroups);
1034
+ for (const index of groups.keys()) {
1035
+ if (index >= firstFailedIndex) mentionedGroups.add(index);
1036
+ }
1037
+ return new Set(
1038
+ [...mentionedGroups].flatMap((index) => groups.get(index)?.map((call) => call.id) ?? [])
1039
+ );
1040
+ }
1041
+
1042
+ // src/lib/codex-tool-normalization.ts
1043
+ function isObject(value) {
1044
+ return typeof value === "object" && value !== null;
1045
+ }
1046
+ function inferToolError(output) {
1047
+ if (!output) return false;
1048
+ const exitMatch = output.match(/Process exited with code (\d+)/);
1049
+ if (exitMatch) return exitMatch[1] !== "0";
1050
+ const withoutSuccessSummaries = output.replace(/\b0\s+(?:fail(?:ed|ures?)?|errors?)\b/gi, "").replace(/\bno\s+(?:failures?|errors?)\b/gi, "");
1051
+ return /\b(error|failed|exception)\b/i.test(withoutSuccessSummaries);
1052
+ }
1053
+ function normalizePlanToTodos(input) {
1054
+ const plan = Array.isArray(input.plan) ? input.plan : [];
1055
+ const todos = plan.filter((item) => isObject(item)).map((item) => ({
1056
+ content: typeof item.step === "string" ? item.step : "",
1057
+ status: typeof item.status === "string" ? item.status : "pending",
1058
+ activeForm: typeof item.step === "string" ? item.step : ""
1059
+ }));
1060
+ return { todos };
1061
+ }
1062
+ function extractCustomToolOutputText(output) {
1063
+ if (typeof output === "string") return output;
1064
+ if (!Array.isArray(output)) return "";
1065
+ return output.filter((block) => isObject(block) && typeof block.text === "string").map((block) => block.text).join("");
1066
+ }
1067
+ function parseCustomToolOutput(output) {
1068
+ const rawOutput = extractCustomToolOutputText(output);
1069
+ if (!rawOutput) return { text: "", isError: false };
1070
+ try {
1071
+ const parsed = JSON.parse(rawOutput);
1072
+ const text = typeof parsed.output === "string" ? parsed.output : rawOutput;
1073
+ const meta = isObject(parsed.metadata) ? parsed.metadata : null;
1074
+ const isError = meta ? meta.exit_code !== 0 && meta.exit_code !== void 0 : inferToolError(text);
1075
+ return { text, isError };
1076
+ } catch {
1077
+ return { text: rawOutput, isError: inferToolError(rawOutput) };
1078
+ }
1079
+ }
1080
+
616
1081
  // src/lib/codex.ts
617
1082
  var SKIP_PROMPT_PREFIXES = [
1083
+ "# AGENTS.md instructions for ",
1084
+ "<recommended_plugins>",
618
1085
  "<environment_context>",
619
1086
  "<permissions instructions>",
620
1087
  "<collaboration_mode>",
@@ -630,17 +1097,120 @@ function safeParseLine(line) {
630
1097
  return null;
631
1098
  }
632
1099
  }
633
- function isObject(value) {
1100
+ function isObject2(value) {
634
1101
  return typeof value === "object" && value !== null;
635
1102
  }
636
1103
  function isCodexRecord(record) {
637
1104
  if (!record || typeof record.type !== "string") return false;
638
- return record.type === "session_meta" || record.type === "turn_context" || record.type === "event_msg" || record.type === "response_item";
1105
+ return record.type === "session_meta" || record.type === "turn_context" || record.type === "event_msg" || record.type === "response_item" || record.type === "inter_agent_communication_metadata" || record.type === "compacted" || record.type === "world_state";
639
1106
  }
640
1107
  function extractMessageText(payload, blockType) {
641
1108
  const content = payload?.content;
642
1109
  if (!Array.isArray(content)) return "";
643
- return content.filter((block) => isObject(block) && block.type === blockType && typeof block.text === "string").map((block) => block.text).join("\n").trim();
1110
+ return content.filter((block) => isObject2(block) && block.type === blockType && typeof block.text === "string").map((block) => block.text).join("\n").trim();
1111
+ }
1112
+ var CODEX_IMAGE_DATA_URL = /^data:(image\/(?:png|jpeg|gif|webp));base64,(.+)$/is;
1113
+ var CODEX_AUDIO_DATA_URL = /^data:(audio\/(?:wav|x-wav|mpeg|mp4|webm|ogg));base64,(.+)$/is;
1114
+ function parseImageDataUrl(value) {
1115
+ if (typeof value !== "string") return null;
1116
+ const match = CODEX_IMAGE_DATA_URL.exec(value);
1117
+ if (!match?.[1] || !match[2]) return null;
1118
+ return {
1119
+ type: "image",
1120
+ source: {
1121
+ type: "base64",
1122
+ media_type: match[1].toLowerCase(),
1123
+ data: match[2]
1124
+ }
1125
+ };
1126
+ }
1127
+ function parseAudioDataUrl(value) {
1128
+ if (typeof value !== "string") return null;
1129
+ const match = CODEX_AUDIO_DATA_URL.exec(value);
1130
+ if (!match?.[1] || !match[2]) return null;
1131
+ return {
1132
+ type: "audio",
1133
+ source: {
1134
+ type: "base64",
1135
+ media_type: match[1].toLowerCase(),
1136
+ data: match[2]
1137
+ }
1138
+ };
1139
+ }
1140
+ function extractEventMessageImages(payload) {
1141
+ if (!Array.isArray(payload.images)) return [];
1142
+ return payload.images.map(parseImageDataUrl).filter((image) => image !== null);
1143
+ }
1144
+ function extractEventMessageAudio(payload) {
1145
+ if (!Array.isArray(payload.audio)) return [];
1146
+ return payload.audio.map(parseAudioDataUrl).filter((audio) => audio !== null);
1147
+ }
1148
+ function extractResponseMessageImages(payload) {
1149
+ if (!Array.isArray(payload.content)) return [];
1150
+ return payload.content.filter((block) => isObject2(block) && block.type === "input_image").map((block) => parseImageDataUrl(block.image_url)).filter((image) => image !== null);
1151
+ }
1152
+ function extractResponseMessageAudio(payload) {
1153
+ if (!Array.isArray(payload.content)) return [];
1154
+ return payload.content.filter((block) => isObject2(block) && block.type === "input_audio").map((block) => parseAudioDataUrl(block.audio_url)).filter((audio) => audio !== null);
1155
+ }
1156
+ function buildCodexUserContent(message, images, audio = [], localImages = [], localAudio = []) {
1157
+ const attachmentSuffix = [
1158
+ ...localImages.map((path) => `
1159
+ ![image](<${path}>)`),
1160
+ ...localAudio.map((path) => `
1161
+ [audio attachment](<${path}>)`)
1162
+ ].join("");
1163
+ const text = message + attachmentSuffix;
1164
+ if (images.length === 0 && audio.length === 0) return text;
1165
+ return [
1166
+ ...images,
1167
+ ...audio,
1168
+ ...text ? [{ type: "text", text }] : []
1169
+ ];
1170
+ }
1171
+ function normalizeFunctionName(rawName) {
1172
+ const leaf = rawName.split(/(?:__|[.:/])+/).filter(Boolean).at(-1) ?? rawName;
1173
+ const aliases = {
1174
+ spawnAgent: "spawn_agent",
1175
+ waitAgent: "wait_agent",
1176
+ sendMessage: "send_message",
1177
+ followupTask: "followup_task",
1178
+ listAgents: "list_agents",
1179
+ interruptAgent: "interrupt_agent"
1180
+ };
1181
+ const canonicalCollaborationNames = /* @__PURE__ */ new Set([
1182
+ "spawn_agent",
1183
+ "wait_agent",
1184
+ "send_message",
1185
+ "followup_task",
1186
+ "list_agents",
1187
+ "interrupt_agent"
1188
+ ]);
1189
+ if (aliases[leaf]) return aliases[leaf];
1190
+ if (canonicalCollaborationNames.has(leaf)) return leaf;
1191
+ return rawName;
1192
+ }
1193
+ function extractInterAgentMessage(payload) {
1194
+ const rawText = extractMessageText(payload, "input_text");
1195
+ if (!rawText) return { messageType: null, text: "" };
1196
+ const headerEnd = rawText.match(/\r?\nPayload:\s*(?:\r?\n|$)/);
1197
+ if (!rawText.startsWith("Message Type:") || !headerEnd || headerEnd.index === void 0) {
1198
+ return { messageType: null, text: rawText.trim() };
1199
+ }
1200
+ const header = rawText.slice(0, headerEnd.index);
1201
+ const messageType = header.match(/^Message Type:\s*([^\r\n]+)/)?.[1]?.trim().toUpperCase() ?? null;
1202
+ const text = rawText.slice(headerEnd.index + headerEnd[0].length).trim();
1203
+ return { messageType, text };
1204
+ }
1205
+ function agentNameFromPath(agentPath) {
1206
+ const segments = agentPath.split("/").filter(Boolean);
1207
+ return segments.at(-1) ?? null;
1208
+ }
1209
+ function readableAgentPrompt(value) {
1210
+ if (typeof value !== "string") return "";
1211
+ const prompt = value.trim();
1212
+ if (/^gAAAAA[A-Za-z0-9_-]+$/.test(prompt)) return "";
1213
+ return prompt;
644
1214
  }
645
1215
  function normalizePromptText(text) {
646
1216
  const trimmed = text.trim();
@@ -658,11 +1228,13 @@ function mergeTokenUsage2(existing, incoming) {
658
1228
  };
659
1229
  }
660
1230
  function parseTokenUsage(value) {
661
- if (!isObject(value)) return null;
662
- const inputTokens = typeof value.input_tokens === "number" ? value.input_tokens : 0;
1231
+ if (!isObject2(value)) return null;
1232
+ const reportedInputTokens = typeof value.input_tokens === "number" ? value.input_tokens : 0;
663
1233
  const outputTokens = typeof value.output_tokens === "number" ? value.output_tokens : 0;
664
- const cacheCreation = typeof value.cache_creation_input_tokens === "number" ? value.cache_creation_input_tokens : 0;
665
- const cacheRead = typeof value.cache_read_input_tokens === "number" ? value.cache_read_input_tokens : 0;
1234
+ const cacheCreation = typeof value.cache_write_input_tokens === "number" ? value.cache_write_input_tokens : typeof value.cache_creation_input_tokens === "number" ? value.cache_creation_input_tokens : 0;
1235
+ const nativeCachedInput = typeof value.cached_input_tokens === "number" ? Math.max(0, value.cached_input_tokens) : null;
1236
+ const cacheRead = nativeCachedInput ?? (typeof value.cache_read_input_tokens === "number" ? value.cache_read_input_tokens : 0);
1237
+ const inputTokens = nativeCachedInput === null ? reportedInputTokens : Math.max(0, reportedInputTokens - nativeCachedInput);
666
1238
  if (inputTokens === 0 && outputTokens === 0 && cacheCreation === 0 && cacheRead === 0) return null;
667
1239
  return {
668
1240
  input_tokens: inputTokens,
@@ -703,7 +1275,7 @@ function appendToolCall(turn, toolCall, timestamp) {
703
1275
  }
704
1276
  function finalizeTurn(turns, current, lastTimestamp) {
705
1277
  if (!current) return null;
706
- const hasContent = current.userMessage !== null || current.assistantText.length > 0 || current.toolCalls.length > 0 || current.thinking.length > 0;
1278
+ const hasContent = current.userMessage !== null || current.assistantText.length > 0 || current.toolCalls.length > 0 || current.thinking.length > 0 || current.subAgentActivity.length > 0;
707
1279
  if (!hasContent) return null;
708
1280
  if (current.timestamp && lastTimestamp) {
709
1281
  const start = new Date(current.timestamp).getTime();
@@ -719,17 +1291,11 @@ function parseToolInput(argumentsText) {
719
1291
  if (typeof argumentsText !== "string" || !argumentsText.trim()) return {};
720
1292
  try {
721
1293
  const parsed = JSON.parse(argumentsText);
722
- return isObject(parsed) ? parsed : { value: parsed };
1294
+ return isObject2(parsed) ? parsed : { value: parsed };
723
1295
  } catch {
724
1296
  return { raw: argumentsText };
725
1297
  }
726
1298
  }
727
- function inferToolError(output) {
728
- if (!output) return false;
729
- const exitMatch = output.match(/Process exited with code (\d+)/);
730
- if (exitMatch) return exitMatch[1] !== "0";
731
- return /\b(error|failed|exception)\b/i.test(output);
732
- }
733
1299
  function createTurn(turnId, timestamp, model) {
734
1300
  return {
735
1301
  id: turnId || randomTurnId("codex-turn"),
@@ -746,10 +1312,10 @@ function createTurn(turnId, timestamp, model) {
746
1312
  };
747
1313
  }
748
1314
  function extractPromptFromRecord(record) {
749
- if (record.type === "event_msg" && isObject(record.payload) && record.payload.type === "user_message" && typeof record.payload.message === "string") {
1315
+ if (record.type === "event_msg" && isObject2(record.payload) && record.payload.type === "user_message" && typeof record.payload.message === "string") {
750
1316
  return normalizePromptText(record.payload.message);
751
1317
  }
752
- if (record.type === "response_item" && isObject(record.payload) && record.payload.type === "message" && record.payload.role === "user") {
1318
+ if (record.type === "response_item" && isObject2(record.payload) && record.payload.type === "message" && record.payload.role === "user") {
753
1319
  return normalizePromptText(extractMessageText(record.payload, "input_text"));
754
1320
  }
755
1321
  return "";
@@ -766,14 +1332,17 @@ function extractMetadataFromRecords(records) {
766
1332
  let timestamp = "";
767
1333
  let lastTimestamp = "";
768
1334
  let turnCount = 0;
1335
+ let isSubagent = false;
1336
+ let parentSessionId = null;
1337
+ let agentPath = "/root";
769
1338
  let previousPrompt = "";
770
1339
  for (const record of records) {
771
- if (!isObject(record.payload)) continue;
1340
+ if (!isObject2(record.payload)) continue;
772
1341
  if (record.type === "session_meta") {
773
1342
  sessionId ||= typeof record.payload.id === "string" ? record.payload.id : "";
774
1343
  version ||= typeof record.payload.cli_version === "string" ? record.payload.cli_version : "";
775
1344
  cwd ||= typeof record.payload.cwd === "string" ? record.payload.cwd : "";
776
- if (!branchedFrom && isObject(record.payload.branchedFrom)) {
1345
+ if (!branchedFrom && isObject2(record.payload.branchedFrom)) {
777
1346
  const sourceId = typeof record.payload.branchedFrom.sessionId === "string" ? record.payload.branchedFrom.sessionId : "";
778
1347
  if (sourceId) {
779
1348
  branchedFrom = {
@@ -782,7 +1351,18 @@ function extractMetadataFromRecords(records) {
782
1351
  };
783
1352
  }
784
1353
  }
785
- const git = isObject(record.payload.git) ? record.payload.git : null;
1354
+ const source = isObject2(record.payload.source) ? record.payload.source : null;
1355
+ if (source && isObject2(source.subagent)) {
1356
+ isSubagent = true;
1357
+ const threadSpawn = isObject2(source.subagent.thread_spawn) ? source.subagent.thread_spawn : null;
1358
+ if (threadSpawn && typeof threadSpawn.agent_path === "string") {
1359
+ agentPath = threadSpawn.agent_path;
1360
+ }
1361
+ }
1362
+ if (typeof record.payload.forked_from_id === "string" && record.payload.forked_from_id) {
1363
+ parentSessionId = record.payload.forked_from_id;
1364
+ }
1365
+ const git = isObject2(record.payload.git) ? record.payload.git : null;
786
1366
  gitBranch ||= git && typeof git.branch === "string" ? git.branch : "";
787
1367
  }
788
1368
  if (record.type === "turn_context") {
@@ -807,12 +1387,16 @@ function extractMetadataFromRecords(records) {
807
1387
  cwd,
808
1388
  model,
809
1389
  slug: "",
1390
+ name: "",
810
1391
  branchedFrom,
811
1392
  firstUserMessage,
812
1393
  lastUserMessage,
813
1394
  timestamp,
814
1395
  lastTimestamp,
815
- turnCount
1396
+ turnCount,
1397
+ isSubagent,
1398
+ parentSessionId,
1399
+ agentPath
816
1400
  };
817
1401
  }
818
1402
  function isCodexSessionText(jsonlText) {
@@ -827,18 +1411,99 @@ function extractCodexMetadataFromLines(lines) {
827
1411
  const records = lines.map(safeParseLine).filter(isCodexRecord);
828
1412
  return extractMetadataFromRecords(records);
829
1413
  }
830
- function parseCodexSession(jsonlText) {
1414
+ function parseCodexSession(jsonlText, options) {
831
1415
  const records = jsonlText.split("\n").map((line) => line.trim()).filter(Boolean).map(safeParseLine).filter(isCodexRecord);
832
1416
  const metadata = extractMetadataFromRecords(records);
833
1417
  const turns = [];
834
1418
  const pendingToolCalls = /* @__PURE__ */ new Map();
1419
+ const webSearchCalls = /* @__PURE__ */ new Map();
1420
+ const patchCallIds = /* @__PURE__ */ new Map();
1421
+ const spawnAgentCalls = /* @__PURE__ */ new Map();
1422
+ const agentRegistry = /* @__PURE__ */ new Map();
1423
+ const agentIdByPath = /* @__PURE__ */ new Map();
1424
+ const agentIdBySpawnCall = /* @__PURE__ */ new Map();
1425
+ const agentMessages = /* @__PURE__ */ new Map();
1426
+ function upsertAgentMessage(agentId, targetTurn, eventTimestamp, update = {}) {
1427
+ const info = agentRegistry.get(agentId);
1428
+ let entry = agentMessages.get(agentId);
1429
+ if (!entry) {
1430
+ const agentMessage2 = {
1431
+ agentId,
1432
+ parentToolUseId: info?.parentToolCallId || void 0,
1433
+ agentName: info?.nickname ?? (info?.agentPath ? agentNameFromPath(info.agentPath) : null),
1434
+ subagentType: info?.agentType ?? null,
1435
+ type: "assistant",
1436
+ content: null,
1437
+ toolCalls: [],
1438
+ thinking: [],
1439
+ text: [],
1440
+ timestamp: info?.timestamp ?? eventTimestamp,
1441
+ tokenUsage: null,
1442
+ model: info?.model ?? null,
1443
+ isBackground: false,
1444
+ prompt: info?.message || void 0,
1445
+ status: update.status ?? "running"
1446
+ };
1447
+ targetTurn.subAgentActivity.push(agentMessage2);
1448
+ const lastBlock = targetTurn.contentBlocks[targetTurn.contentBlocks.length - 1];
1449
+ if (lastBlock?.kind === "sub_agent") {
1450
+ lastBlock.messages.push(agentMessage2);
1451
+ } else {
1452
+ targetTurn.contentBlocks.push({ kind: "sub_agent", messages: [agentMessage2], timestamp: eventTimestamp });
1453
+ }
1454
+ entry = { message: agentMessage2, turn: targetTurn };
1455
+ agentMessages.set(agentId, entry);
1456
+ }
1457
+ const agentMessage = entry.message;
1458
+ if (info) {
1459
+ agentMessage.parentToolUseId = info.parentToolCallId || agentMessage.parentToolUseId;
1460
+ agentMessage.agentName = info.nickname ?? (info.agentPath ? agentNameFromPath(info.agentPath) : agentMessage.agentName);
1461
+ agentMessage.subagentType = info.agentType;
1462
+ agentMessage.model = info.model;
1463
+ agentMessage.prompt = info.message || agentMessage.prompt;
1464
+ }
1465
+ if (update.text && !agentMessage.text.includes(update.text)) {
1466
+ agentMessage.text.push(update.text);
1467
+ agentMessage.content = agentMessage.text.join("\n\n");
1468
+ }
1469
+ if (update.status) {
1470
+ const terminalStatuses = /* @__PURE__ */ new Set(["completed", "failed", "interrupted"]);
1471
+ if (!(update.status === "running" && agentMessage.status && terminalStatuses.has(agentMessage.status))) {
1472
+ agentMessage.status = update.status;
1473
+ }
1474
+ if (terminalStatuses.has(update.status) && agentMessage.timestamp && eventTimestamp) {
1475
+ const startedAt = new Date(agentMessage.timestamp).getTime();
1476
+ const endedAt = new Date(eventTimestamp).getTime();
1477
+ if (Number.isFinite(startedAt) && Number.isFinite(endedAt) && endedAt >= startedAt) {
1478
+ agentMessage.durationMs = endedAt - startedAt;
1479
+ }
1480
+ }
1481
+ }
1482
+ return agentMessage;
1483
+ }
835
1484
  let current = null;
836
1485
  let currentTurnId = null;
837
1486
  let currentModel = metadata.model || null;
838
1487
  let lastTurnTimestamp = "";
1488
+ let pendingCompaction = null;
1489
+ function createActiveTurn(turnId, timestamp, model) {
1490
+ const turn = createTurn(turnId, timestamp, model);
1491
+ if (pendingCompaction) {
1492
+ turn.compactionSummary = pendingCompaction;
1493
+ pendingCompaction = null;
1494
+ }
1495
+ return turn;
1496
+ }
839
1497
  for (const record of records) {
840
- const payload = isObject(record.payload) ? record.payload : void 0;
1498
+ const payload = isObject2(record.payload) ? record.payload : void 0;
841
1499
  const timestamp = record.timestamp ?? "";
1500
+ if (record.type === "compacted") {
1501
+ current = finalizeTurn(turns, current, lastTurnTimestamp);
1502
+ pendingCompaction = "Conversation compacted";
1503
+ lastTurnTimestamp = timestamp;
1504
+ continue;
1505
+ }
1506
+ if (record.type === "world_state") continue;
842
1507
  if (record.type === "turn_context") {
843
1508
  current = finalizeTurn(turns, current, lastTurnTimestamp);
844
1509
  currentTurnId = typeof payload?.turn_id === "string" ? payload.turn_id : null;
@@ -850,24 +1515,93 @@ function parseCodexSession(jsonlText) {
850
1515
  if (current && (current.assistantText.length > 0 || current.toolCalls.length > 0 || current.thinking.length > 0)) {
851
1516
  current = finalizeTurn(turns, current, lastTurnTimestamp);
852
1517
  }
853
- current ??= createTurn(currentTurnId, timestamp, currentModel);
854
- current.userMessage = payload.message;
1518
+ current ??= createActiveTurn(currentTurnId, timestamp, currentModel);
1519
+ const localImages = (Array.isArray(payload.local_images) ? payload.local_images : []).filter(
1520
+ (p) => typeof p === "string" && p.length > 0
1521
+ );
1522
+ const localAudio = (Array.isArray(payload.local_audio) ? payload.local_audio : []).filter(
1523
+ (p) => typeof p === "string" && p.length > 0
1524
+ );
1525
+ current.userMessage = buildCodexUserContent(
1526
+ payload.message,
1527
+ extractEventMessageImages(payload),
1528
+ extractEventMessageAudio(payload),
1529
+ localImages,
1530
+ localAudio
1531
+ );
855
1532
  current.timestamp = current.timestamp || timestamp;
856
1533
  lastTurnTimestamp = timestamp;
857
1534
  continue;
858
1535
  }
859
- current ??= createTurn(currentTurnId, timestamp, currentModel);
1536
+ current ??= createActiveTurn(currentTurnId, timestamp, currentModel);
860
1537
  if (!current.model && currentModel) current.model = currentModel;
861
1538
  if (!current.timestamp) current.timestamp = timestamp;
862
1539
  if (timestamp) lastTurnTimestamp = timestamp;
863
1540
  if (record.type === "event_msg" && payload?.type === "token_count") {
864
- const info = isObject(payload.info) ? payload.info : null;
1541
+ const info = isObject2(payload.info) ? payload.info : null;
865
1542
  const lastUsage = info ? parseTokenUsage(info.last_token_usage) : null;
866
1543
  if (lastUsage) {
867
1544
  current.tokenUsage = mergeTokenUsage2(current.tokenUsage, lastUsage);
868
1545
  }
869
1546
  continue;
870
1547
  }
1548
+ if (record.type === "event_msg" && payload?.type === "web_search_end" && typeof payload.call_id === "string") {
1549
+ const input = {};
1550
+ if (typeof payload.query === "string" && payload.query) input.query = payload.query;
1551
+ if (isObject2(payload.action)) input.action = payload.action;
1552
+ const existing = webSearchCalls.get(payload.call_id);
1553
+ if (existing) {
1554
+ existing.input = { ...existing.input, ...input };
1555
+ existing.result = "completed";
1556
+ existing.isError = false;
1557
+ } else {
1558
+ const toolCall = {
1559
+ id: payload.call_id,
1560
+ name: "WebSearch",
1561
+ input,
1562
+ result: "completed",
1563
+ isError: false,
1564
+ timestamp
1565
+ };
1566
+ webSearchCalls.set(toolCall.id, toolCall);
1567
+ appendToolCall(current, toolCall, timestamp);
1568
+ }
1569
+ continue;
1570
+ }
1571
+ if (record.type === "event_msg" && payload?.type === "sub_agent_activity" && typeof payload.agent_thread_id === "string" && typeof payload.agent_path === "string") {
1572
+ const agentId = payload.agent_thread_id;
1573
+ const eventId = typeof payload.event_id === "string" ? payload.event_id : "";
1574
+ const kind = typeof payload.kind === "string" ? payload.kind : "started";
1575
+ const spawnInfo = eventId ? spawnAgentCalls.get(eventId) : void 0;
1576
+ const existingId = agentIdByPath.get(payload.agent_path);
1577
+ const existing = agentRegistry.get(agentId) ?? (existingId ? agentRegistry.get(existingId) : void 0);
1578
+ if (kind !== "started" && !existing && !spawnInfo) continue;
1579
+ if (existingId && existingId !== agentId && !agentMessages.has(agentId)) {
1580
+ const provisionalEntry = agentMessages.get(existingId);
1581
+ if (provisionalEntry) {
1582
+ provisionalEntry.message.agentId = agentId;
1583
+ agentMessages.delete(existingId);
1584
+ agentMessages.set(agentId, provisionalEntry);
1585
+ }
1586
+ agentRegistry.delete(existingId);
1587
+ }
1588
+ const info = {
1589
+ agentId,
1590
+ nickname: existing?.nickname ?? agentNameFromPath(payload.agent_path) ?? spawnInfo?.taskName ?? null,
1591
+ agentPath: payload.agent_path,
1592
+ message: existing?.message ?? spawnInfo?.message ?? "",
1593
+ model: existing?.model ?? spawnInfo?.model ?? null,
1594
+ agentType: existing?.agentType ?? spawnInfo?.agentType ?? null,
1595
+ timestamp: existing?.timestamp ?? (timestamp || spawnInfo?.timestamp || ""),
1596
+ parentToolCallId: existing?.parentToolCallId ?? eventId
1597
+ };
1598
+ agentRegistry.set(agentId, info);
1599
+ agentIdByPath.set(payload.agent_path, agentId);
1600
+ if (eventId && (kind === "started" || spawnInfo)) agentIdBySpawnCall.set(eventId, agentId);
1601
+ const status = kind === "interrupted" ? "interrupted" : "running";
1602
+ upsertAgentMessage(agentId, current, timestamp, { status });
1603
+ continue;
1604
+ }
871
1605
  if (record.type !== "response_item" || !payload) continue;
872
1606
  if (payload.type === "reasoning") {
873
1607
  const summary = Array.isArray(payload.summary) ? payload.summary.filter((item) => typeof item === "string").join("\n") : "";
@@ -880,14 +1614,95 @@ function parseCodexSession(jsonlText) {
880
1614
  }
881
1615
  if (payload.type === "message" && payload.role === "user" && current.userMessage === null) {
882
1616
  const text = normalizePromptText(extractMessageText(payload, "input_text"));
883
- if (text) current.userMessage = text;
1617
+ const images = extractResponseMessageImages(payload);
1618
+ const audio = extractResponseMessageAudio(payload);
1619
+ if (text || images.length > 0 || audio.length > 0) {
1620
+ current.userMessage = buildCodexUserContent(text, images, audio);
1621
+ }
1622
+ continue;
1623
+ }
1624
+ if (payload.type === "web_search_call" && typeof payload.id === "string") {
1625
+ const input = {};
1626
+ if (isObject2(payload.action)) input.action = payload.action;
1627
+ const status = typeof payload.status === "string" ? payload.status : "completed";
1628
+ const existing = webSearchCalls.get(payload.id);
1629
+ if (existing) {
1630
+ existing.input = { ...existing.input, ...input };
1631
+ existing.result = status;
1632
+ existing.isError = status === "failed";
1633
+ } else {
1634
+ const toolCall = {
1635
+ id: payload.id,
1636
+ name: "WebSearch",
1637
+ input,
1638
+ result: status,
1639
+ isError: status === "failed",
1640
+ timestamp
1641
+ };
1642
+ webSearchCalls.set(toolCall.id, toolCall);
1643
+ appendToolCall(current, toolCall, timestamp);
1644
+ }
1645
+ continue;
1646
+ }
1647
+ if (payload.type === "agent_message" && typeof payload.author === "string") {
1648
+ const author = payload.author;
1649
+ const recipient = typeof payload.recipient === "string" ? payload.recipient : null;
1650
+ const interAgentMessage = extractInterAgentMessage(payload);
1651
+ if (interAgentMessage.messageType === "NEW_TASK") continue;
1652
+ const knownAgentId = agentIdByPath.get(author);
1653
+ const isPotentialChild = author.startsWith(`${metadata.agentPath}/`) && (recipient === null || recipient === metadata.agentPath);
1654
+ if (!knownAgentId && !isPotentialChild) continue;
1655
+ const agentId = knownAgentId ?? author;
1656
+ if (!knownAgentId) {
1657
+ agentRegistry.set(agentId, {
1658
+ agentId,
1659
+ nickname: agentNameFromPath(author),
1660
+ agentPath: author,
1661
+ message: "",
1662
+ model: null,
1663
+ agentType: null,
1664
+ timestamp,
1665
+ parentToolCallId: ""
1666
+ });
1667
+ agentIdByPath.set(author, agentId);
1668
+ }
1669
+ let status;
1670
+ if (interAgentMessage.messageType === "FINAL_ANSWER") status = "completed";
1671
+ else if (interAgentMessage.messageType === "ERROR" || interAgentMessage.messageType === "FAILED") status = "failed";
1672
+ else if (interAgentMessage.messageType === "INTERRUPTED") status = "interrupted";
1673
+ else if (interAgentMessage.messageType === "MESSAGE") status = "running";
1674
+ upsertAgentMessage(agentId, current, timestamp, {
1675
+ status,
1676
+ text: interAgentMessage.text || void 0
1677
+ });
884
1678
  continue;
885
1679
  }
886
1680
  if (payload.type === "function_call" && typeof payload.call_id === "string") {
1681
+ const rawName = typeof payload.name === "string" ? payload.name : "tool";
1682
+ const functionName = normalizeFunctionName(rawName);
1683
+ const parsedInput = parseToolInput(payload.arguments);
1684
+ let name = functionName;
1685
+ let input = parsedInput;
1686
+ if (functionName === "exec_command") {
1687
+ name = "Bash";
1688
+ } else if (functionName === "update_plan") {
1689
+ name = "TodoWrite";
1690
+ input = normalizePlanToTodos(parsedInput);
1691
+ }
1692
+ if (functionName === "spawn_agent") {
1693
+ spawnAgentCalls.set(payload.call_id, {
1694
+ callId: payload.call_id,
1695
+ message: readableAgentPrompt(parsedInput.message),
1696
+ taskName: typeof parsedInput.task_name === "string" ? parsedInput.task_name : null,
1697
+ model: typeof parsedInput.model === "string" ? parsedInput.model : null,
1698
+ agentType: typeof parsedInput.agent_type === "string" ? parsedInput.agent_type : null,
1699
+ timestamp
1700
+ });
1701
+ }
887
1702
  const toolCall = {
888
1703
  id: payload.call_id,
889
- name: typeof payload.name === "string" ? payload.name : "tool",
890
- input: parseToolInput(payload.arguments),
1704
+ name,
1705
+ input,
891
1706
  result: null,
892
1707
  isError: false,
893
1708
  timestamp
@@ -903,6 +1718,116 @@ function parseCodexSession(jsonlText) {
903
1718
  toolCall.result = output;
904
1719
  toolCall.isError = inferToolError(output);
905
1720
  pendingToolCalls.delete(payload.call_id);
1721
+ if (toolCall.name === "spawn_agent" && output) {
1722
+ try {
1723
+ const result = JSON.parse(output);
1724
+ const agentId = typeof result.agent_id === "string" ? result.agent_id : agentIdBySpawnCall.get(toolCall.id) ?? "";
1725
+ const agentPath = typeof result.task_name === "string" ? result.task_name : null;
1726
+ const nickname = typeof result.nickname === "string" ? result.nickname : agentPath ? agentNameFromPath(agentPath) : null;
1727
+ if (agentId) {
1728
+ const spawnInfo = spawnAgentCalls.get(toolCall.id);
1729
+ const existing = agentRegistry.get(agentId);
1730
+ const info = {
1731
+ agentId,
1732
+ nickname: nickname ?? existing?.nickname ?? spawnInfo?.taskName ?? null,
1733
+ agentPath: agentPath ?? existing?.agentPath ?? null,
1734
+ message: existing?.message ?? spawnInfo?.message ?? "",
1735
+ model: existing?.model ?? spawnInfo?.model ?? null,
1736
+ agentType: existing?.agentType ?? spawnInfo?.agentType ?? null,
1737
+ timestamp: existing?.timestamp ?? spawnInfo?.timestamp ?? timestamp,
1738
+ parentToolCallId: existing?.parentToolCallId ?? toolCall.id
1739
+ };
1740
+ agentRegistry.set(agentId, info);
1741
+ agentIdBySpawnCall.set(toolCall.id, agentId);
1742
+ if (info.agentPath) agentIdByPath.set(info.agentPath, agentId);
1743
+ upsertAgentMessage(agentId, current, timestamp, { status: "running" });
1744
+ }
1745
+ } catch {
1746
+ }
1747
+ }
1748
+ if (toolCall.name === "wait_agent" && output && current) {
1749
+ try {
1750
+ const result = JSON.parse(output);
1751
+ const statusMap = isObject2(result.status) ? result.status : {};
1752
+ for (const [agentId, status] of Object.entries(statusMap)) {
1753
+ const statusValue = isObject2(status) ? status : {};
1754
+ const completedText = typeof statusValue.completed === "string" ? statusValue.completed : "";
1755
+ const failedText = typeof statusValue.failed === "string" ? statusValue.failed : typeof statusValue.error === "string" ? statusValue.error : "";
1756
+ const interruptedText = typeof statusValue.interrupted === "string" ? statusValue.interrupted : "";
1757
+ const lifecycle = completedText ? "completed" : failedText ? "failed" : interruptedText ? "interrupted" : typeof statusValue.status === "string" ? statusValue.status : "running";
1758
+ if (!agentRegistry.has(agentId)) {
1759
+ agentRegistry.set(agentId, {
1760
+ agentId,
1761
+ nickname: null,
1762
+ agentPath: null,
1763
+ message: "",
1764
+ model: null,
1765
+ agentType: null,
1766
+ timestamp,
1767
+ parentToolCallId: ""
1768
+ });
1769
+ }
1770
+ upsertAgentMessage(agentId, current, timestamp, {
1771
+ status: lifecycle,
1772
+ text: completedText || failedText || interruptedText || void 0
1773
+ });
1774
+ }
1775
+ } catch {
1776
+ }
1777
+ }
1778
+ continue;
1779
+ }
1780
+ if (payload.type === "custom_tool_call" && typeof payload.call_id === "string") {
1781
+ const name = typeof payload.name === "string" ? payload.name : "tool";
1782
+ const rawInput = typeof payload.input === "string" ? payload.input : "";
1783
+ const callId = payload.call_id;
1784
+ const perFileCalls = rawInput ? parseCodexToolPatches(name, rawInput, callId, timestamp, metadata.cwd) : [];
1785
+ if (name !== "apply_patch") {
1786
+ const toolCall = {
1787
+ id: callId,
1788
+ name: name === "exec_command" ? "Bash" : name,
1789
+ input: parseToolInput(rawInput),
1790
+ result: null,
1791
+ isError: false,
1792
+ timestamp
1793
+ };
1794
+ pendingToolCalls.set(toolCall.id, toolCall);
1795
+ appendToolCall(current, toolCall, timestamp);
1796
+ }
1797
+ for (const tc of perFileCalls) {
1798
+ pendingToolCalls.set(tc.id, tc);
1799
+ appendToolCall(current, tc, timestamp);
1800
+ }
1801
+ if (perFileCalls.length > 0) {
1802
+ patchCallIds.set(callId, {
1803
+ calls: perFileCalls,
1804
+ direct: name === "apply_patch"
1805
+ });
1806
+ }
1807
+ continue;
1808
+ }
1809
+ if (payload.type === "custom_tool_call_output" && typeof payload.call_id === "string") {
1810
+ const callId = payload.call_id;
1811
+ const { text, isError } = parseCustomToolOutput(payload.output);
1812
+ const patchCalls = patchCallIds.get(callId);
1813
+ if (patchCalls) {
1814
+ const failedIds = patchCalls.direct && isError ? new Set(patchCalls.calls.map((call) => call.id)) : findFailedNestedPatchCallIds(text, isError, patchCalls.calls);
1815
+ for (const patchCall of patchCalls.calls) {
1816
+ const tc = pendingToolCalls.get(patchCall.id);
1817
+ if (tc) {
1818
+ tc.result = text;
1819
+ tc.isError = failedIds.has(tc.id);
1820
+ pendingToolCalls.delete(tc.id);
1821
+ }
1822
+ }
1823
+ patchCallIds.delete(callId);
1824
+ }
1825
+ const toolCall = pendingToolCalls.get(callId);
1826
+ if (toolCall) {
1827
+ toolCall.result = text;
1828
+ toolCall.isError = isError;
1829
+ pendingToolCalls.delete(callId);
1830
+ }
906
1831
  continue;
907
1832
  }
908
1833
  }
@@ -913,19 +1838,17 @@ function parseCodexSession(jsonlText) {
913
1838
  gitBranch: metadata.gitBranch,
914
1839
  cwd: metadata.cwd,
915
1840
  slug: metadata.slug,
1841
+ name: "",
916
1842
  model: metadata.model,
917
1843
  turns,
918
- stats: computeStats(turns),
919
- rawMessages: records,
1844
+ stats: options?.skipStats ? createEmptySessionStats(turns.length) : computeStats(turns),
1845
+ rawMessages: options?.skipStats ? [] : records,
920
1846
  branchedFrom: metadata.branchedFrom,
921
1847
  agentKind: "codex"
922
1848
  };
923
1849
  }
924
1850
 
925
1851
  // src/lib/parser.ts
926
- function isAssistantMessage2(msg) {
927
- return msg.type === "assistant";
928
- }
929
1852
  function extractTextFromContent2(content) {
930
1853
  if (typeof content === "string") return content;
931
1854
  return content.filter((b) => b.type === "text").map((b) => b.text).join("\n");
@@ -944,42 +1867,43 @@ function parseLines(jsonlText) {
944
1867
  }
945
1868
  function isCodexRawMessages(rawMessages) {
946
1869
  const firstType = rawMessages[0]?.type;
947
- return firstType === "session_meta" || firstType === "turn_context" || firstType === "event_msg" || firstType === "response_item";
1870
+ return firstType === "session_meta" || firstType === "turn_context" || firstType === "event_msg" || firstType === "response_item" || firstType === "compacted" || firstType === "world_state" || firstType === "inter_agent_communication_metadata";
948
1871
  }
949
1872
  function serializeRawMessages(rawMessages) {
950
1873
  return rawMessages.map((msg) => JSON.stringify(msg)).join("\n");
951
1874
  }
952
1875
  function extractSessionMetadata(messages) {
953
- const meta = { sessionId: "", version: "", gitBranch: "", cwd: "", slug: "", model: "", branchedFrom: void 0 };
1876
+ const meta = { sessionId: "", version: "", gitBranch: "", cwd: "", slug: "", name: "", model: "", branchedFrom: void 0 };
954
1877
  for (const msg of messages) {
955
1878
  if (msg.sessionId && !meta.sessionId) meta.sessionId = msg.sessionId;
956
1879
  if (msg.version && !meta.version) meta.version = msg.version;
957
1880
  if (msg.gitBranch && !meta.gitBranch) meta.gitBranch = msg.gitBranch;
958
1881
  if (msg.cwd && !meta.cwd) meta.cwd = msg.cwd;
959
1882
  if (msg.slug && !meta.slug) meta.slug = msg.slug;
1883
+ if (msg.name && !meta.name) meta.name = msg.name;
960
1884
  if (msg.branchedFrom && !meta.branchedFrom) {
961
1885
  meta.branchedFrom = msg.branchedFrom;
962
1886
  }
963
- if (isAssistantMessage2(msg) && msg.message.model && !meta.model) {
1887
+ if (isAssistantMessage(msg) && msg.message.model && !meta.model) {
964
1888
  meta.model = msg.message.model;
965
1889
  }
966
1890
  if (meta.sessionId && meta.version && meta.gitBranch && meta.cwd && meta.model) break;
967
1891
  }
968
1892
  return meta;
969
1893
  }
970
- function parseSession(jsonlText, opts) {
1894
+ function parseSession(jsonlText, options) {
971
1895
  if (isCodexSessionText(jsonlText)) {
972
- return parseCodexSession(jsonlText);
1896
+ return parseCodexSession(jsonlText, options);
973
1897
  }
974
1898
  const rawMessages = parseLines(jsonlText);
975
1899
  const metadata = extractSessionMetadata(rawMessages);
976
1900
  const turns = buildTurns(rawMessages);
977
- const stats = opts?.skipStats ? { totalInputTokens: 0, totalOutputTokens: 0, totalCacheCreationTokens: 0, totalCacheReadTokens: 0, totalCostUSD: 0, toolCallCounts: {}, errorCount: 0, totalDurationMs: 0, turnCount: turns.length } : computeStats(turns);
1901
+ const stats = options?.skipStats ? createEmptySessionStats(turns.length) : computeStats(turns);
978
1902
  return {
979
1903
  ...metadata,
980
1904
  turns,
981
1905
  stats,
982
- rawMessages: opts?.skipStats ? [] : rawMessages,
1906
+ rawMessages: options?.skipStats ? [] : rawMessages,
983
1907
  agentKind: "claude"
984
1908
  };
985
1909
  }
@@ -991,13 +1915,14 @@ ${newJsonlText}` : newJsonlText);
991
1915
  }
992
1916
  const newMessages = parseLines(newJsonlText);
993
1917
  if (newMessages.length === 0) return existing;
994
- const allRawMessages = [...existing.rawMessages, ...newMessages];
995
- let lastTurnStartIdx = existing.rawMessages.length;
1918
+ const existingRawMessages = existing.rawMessages;
1919
+ const allRawMessages = [...existingRawMessages, ...newMessages];
1920
+ let lastTurnStartIdx = existingRawMessages.length;
996
1921
  let turnsToKeep = existing.turns.length > 0 ? existing.turns.length - 1 : 0;
997
1922
  if (existing.turns.length > 0) {
998
1923
  let userMsgCount = 0;
999
- for (let i = existing.rawMessages.length - 1; i >= 0; i--) {
1000
- const msg = existing.rawMessages[i];
1924
+ for (let i = existingRawMessages.length - 1; i >= 0; i--) {
1925
+ const msg = existingRawMessages[i];
1001
1926
  if (msg.type === "user" && !msg.isMeta) {
1002
1927
  const content = msg.message?.content;
1003
1928
  if (Array.isArray(content) && content.some((b) => b.type === "tool_result")) {
@@ -1024,8 +1949,8 @@ ${newJsonlText}` : newJsonlText);
1024
1949
  if (ownsProgressParent) {
1025
1950
  turnsToKeep = t;
1026
1951
  let found = 0;
1027
- for (let i = 0; i < existing.rawMessages.length; i++) {
1028
- const msg = existing.rawMessages[i];
1952
+ for (let i = 0; i < existingRawMessages.length; i++) {
1953
+ const msg = existingRawMessages[i];
1029
1954
  if (msg.type === "user" && !msg.isMeta) {
1030
1955
  const content = msg.message?.content;
1031
1956
  if (Array.isArray(content) && content.some((b) => b.type === "tool_result")) {
@@ -1054,11 +1979,13 @@ ${newJsonlText}` : newJsonlText);
1054
1979
  gitBranch: existing.gitBranch,
1055
1980
  cwd: existing.cwd,
1056
1981
  slug: existing.slug,
1982
+ name: existing.name,
1057
1983
  model: existing.model || (allTurns.length > 0 ? allTurns[allTurns.length - 1].model || "" : ""),
1058
1984
  turns: allTurns,
1059
1985
  stats,
1060
1986
  rawMessages: allRawMessages,
1061
- branchedFrom: existing.branchedFrom
1987
+ branchedFrom: existing.branchedFrom,
1988
+ agentKind: existing.agentKind
1062
1989
  };
1063
1990
  }
1064
1991
  function getUserMessageText(content) {
@@ -1136,6 +2063,18 @@ var SearchIndex = class {
1136
2063
  lastUpdate: this._lastUpdate
1137
2064
  };
1138
2065
  }
2066
+ /** Index tool call inputs and results under the given location prefix. */
2067
+ insertToolCalls(insert, sessionId, filePath, prefix, toolCalls) {
2068
+ for (const tc of toolCalls) {
2069
+ const inputStr = JSON.stringify(tc.input);
2070
+ if (inputStr && inputStr !== "{}") {
2071
+ insert.run(sessionId, filePath, `${prefix}/toolCall/${tc.id}/input`, truncContent(inputStr));
2072
+ }
2073
+ if (tc.result) {
2074
+ insert.run(sessionId, filePath, `${prefix}/toolCall/${tc.id}/result`, truncContent(tc.result));
2075
+ }
2076
+ }
2077
+ }
1139
2078
  /**
1140
2079
  * Insert all searchable content from a parsed session into the FTS5 index.
1141
2080
  * Shared by both `indexFile` (single-file) and `buildFull` (batch).
@@ -1156,15 +2095,7 @@ var SearchIndex = class {
1156
2095
  if (thinkingText) {
1157
2096
  insert.run(sessionId, filePath, `${prefix}/thinking`, truncContent(thinkingText));
1158
2097
  }
1159
- for (const tc of turn.toolCalls) {
1160
- const inputStr = JSON.stringify(tc.input);
1161
- if (inputStr && inputStr !== "{}") {
1162
- insert.run(sessionId, filePath, `${prefix}/toolCall/${tc.id}/input`, truncContent(inputStr));
1163
- }
1164
- if (tc.result) {
1165
- insert.run(sessionId, filePath, `${prefix}/toolCall/${tc.id}/result`, truncContent(tc.result));
1166
- }
1167
- }
2098
+ this.insertToolCalls(insert, sessionId, filePath, prefix, turn.toolCalls);
1168
2099
  for (const sa of turn.subAgentActivity) {
1169
2100
  const saPrefix = `agent/${sa.agentId}`;
1170
2101
  const saText = sa.text.join("\n\n").trim();
@@ -1175,15 +2106,7 @@ var SearchIndex = class {
1175
2106
  if (saThinking) {
1176
2107
  insert.run(sessionId, filePath, `${saPrefix}/thinking`, truncContent(saThinking));
1177
2108
  }
1178
- for (const tc of sa.toolCalls) {
1179
- const inputStr = JSON.stringify(tc.input);
1180
- if (inputStr && inputStr !== "{}") {
1181
- insert.run(sessionId, filePath, `${saPrefix}/toolCall/${tc.id}/input`, truncContent(inputStr));
1182
- }
1183
- if (tc.result) {
1184
- insert.run(sessionId, filePath, `${saPrefix}/toolCall/${tc.id}/result`, truncContent(tc.result));
1185
- }
1186
- }
2109
+ this.insertToolCalls(insert, sessionId, filePath, saPrefix, sa.toolCalls);
1187
2110
  }
1188
2111
  if (turn.compactionSummary) {
1189
2112
  insert.run(sessionId, filePath, `${prefix}/compactionSummary`, truncContent(turn.compactionSummary));
@@ -1230,7 +2153,7 @@ var SearchIndex = class {
1230
2153
  /**
1231
2154
  * Query the FTS5 index and return structured search results.
1232
2155
  *
1233
- * - FTS5 trigram tokenizer is case-insensitive by default.
2156
+ * - FTS5 unicode61 tokenizer is case-insensitive by default.
1234
2157
  * - When `caseSensitive` is true, a post-filter checks the original query
1235
2158
  * against the snippet text (exact case match).
1236
2159
  * - When `maxAgeMs` is provided, only files whose mtime in `indexed_files`
@@ -1320,17 +2243,11 @@ var SearchIndex = class {
1320
2243
  buildFull(projectsDir) {
1321
2244
  this.projectsDir = projectsDir;
1322
2245
  this.db.close();
1323
- try {
1324
- (0, import_node_fs.unlinkSync)(this.dbPath);
1325
- } catch {
1326
- }
1327
- try {
1328
- (0, import_node_fs.unlinkSync)(this.dbPath + "-wal");
1329
- } catch {
1330
- }
1331
- try {
1332
- (0, import_node_fs.unlinkSync)(this.dbPath + "-shm");
1333
- } catch {
2246
+ for (const suffix of ["", "-wal", "-shm"]) {
2247
+ try {
2248
+ (0, import_node_fs.unlinkSync)(this.dbPath + suffix);
2249
+ } catch {
2250
+ }
1334
2251
  }
1335
2252
  this.db = new Database(this.dbPath);
1336
2253
  this.db.exec("PRAGMA journal_mode = WAL");
@@ -1401,9 +2318,10 @@ var SearchIndex = class {
1401
2318
  /**
1402
2319
  * Lightweight incremental update for CLI search paths.
1403
2320
  *
1404
- * Instead of stat-ing every file, only discovers files modified after the
1405
- * most recent mtime in the index. Caps re-indexing to `maxFiles` to prevent
1406
- * blocking on large backlogs (run `index rebuild` for a full catch-up).
2321
+ * Still walks and stats all files via `discoverFiles`, but skips DB lookups
2322
+ * for files with mtime <= the high-water mark that are already indexed.
2323
+ * Caps re-indexing to `maxFiles` to prevent blocking on large backlogs
2324
+ * (run `index rebuild` for a full catch-up).
1407
2325
  */
1408
2326
  updateRecent(projectsDir, maxFiles = 50) {
1409
2327
  this.projectsDir = projectsDir;
@@ -1601,7 +2519,8 @@ var import_node_os = require("node:os");
1601
2519
  var dirs = {
1602
2520
  PROJECTS_DIR: (0, import_node_path2.join)((0, import_node_os.homedir)(), ".claude", "projects"),
1603
2521
  TEAMS_DIR: (0, import_node_path2.join)((0, import_node_os.homedir)(), ".claude", "teams"),
1604
- TASKS_DIR: (0, import_node_path2.join)((0, import_node_os.homedir)(), ".claude", "tasks")
2522
+ TASKS_DIR: (0, import_node_path2.join)((0, import_node_os.homedir)(), ".claude", "tasks"),
2523
+ CODEX_SESSIONS_DIR: (0, import_node_path2.join)((0, import_node_os.homedir)(), ".codex", "sessions")
1605
2524
  };
1606
2525
  var DEFAULT_DB_PATH = (0, import_node_path2.join)((0, import_node_os.homedir)(), ".claude", "cogpit-memory", "search-index.db");
1607
2526
 
@@ -2073,6 +2992,8 @@ function extractUserMessageText(userMessage) {
2073
2992
  for (const block of userMessage) {
2074
2993
  if (block.type === "text") parts.push(block.text);
2075
2994
  else if (block.type === "image") parts.push("[image attached]");
2995
+ else if (block.type === "document") parts.push("[document attached]");
2996
+ else if (block.type === "audio") parts.push("[audio attached]");
2076
2997
  }
2077
2998
  return parts.length > 0 ? parts.join("\n") : null;
2078
2999
  }
@@ -2171,6 +3092,10 @@ function mapTurnToDetail(session, turnIndex) {
2171
3092
  durationMs: turn.durationMs
2172
3093
  };
2173
3094
  }
3095
+ function mapToolCall(tc) {
3096
+ const { result, resultTruncated } = truncateResult(tc.result);
3097
+ return { id: tc.id, name: tc.name, input: tc.input, result, resultTruncated, isError: tc.isError };
3098
+ }
2174
3099
  function mapContentBlock(block) {
2175
3100
  switch (block.kind) {
2176
3101
  case "thinking": {
@@ -2182,10 +3107,7 @@ function mapContentBlock(block) {
2182
3107
  case "tool_calls":
2183
3108
  return {
2184
3109
  kind: "tool_calls",
2185
- toolCalls: block.toolCalls.map((tc) => {
2186
- const { result, resultTruncated } = truncateResult(tc.result);
2187
- return { id: tc.id, name: tc.name, input: tc.input, result, resultTruncated, isError: tc.isError };
2188
- }),
3110
+ toolCalls: block.toolCalls.map(mapToolCall),
2189
3111
  timestamp: block.timestamp ?? null
2190
3112
  };
2191
3113
  case "sub_agent":
@@ -2195,6 +3117,21 @@ function mapContentBlock(block) {
2195
3117
  agents: block.messages.map(mapSubAgentDetail),
2196
3118
  timestamp: block.timestamp ?? null
2197
3119
  };
3120
+ case "queued_prompt":
3121
+ return { kind: "queued_prompt", content: block.content, timestamp: block.timestamp ?? null };
3122
+ case "hook_event":
3123
+ return { kind: "hook_event", events: block.events, timestamp: block.timestamp ?? null };
3124
+ case "plan_mode":
3125
+ return {
3126
+ kind: "plan_mode",
3127
+ plan: block.plan,
3128
+ planFilePath: block.planFilePath ?? null,
3129
+ status: block.status,
3130
+ toolCalls: block.toolCalls.map(mapToolCall),
3131
+ timestamp: block.timestamp ?? null
3132
+ };
3133
+ case "recap":
3134
+ return { kind: "recap", content: block.content, timestamp: block.timestamp ?? null };
2198
3135
  }
2199
3136
  }
2200
3137
  async function findSubagentFile(parentJsonlPath, agentId) {
@@ -2317,7 +3254,6 @@ async function getAgentOverview(sessionId, agentId) {
2317
3254
  async function getAgentTurnDetail(sessionId, agentId, turnIndex) {
2318
3255
  const jsonlPath = await findJsonlPath(sessionId);
2319
3256
  if (!jsonlPath) return { error: "Session not found" };
2320
- const content = await (0, import_promises3.readFile)(jsonlPath, "utf-8");
2321
3257
  const subagentFile = await findSubagentFile(jsonlPath, agentId);
2322
3258
  if (!subagentFile) return { error: "Agent not found" };
2323
3259
  const subagentContent = await (0, import_promises3.readFile)(subagentFile.filePath, "utf-8");
@@ -2329,7 +3265,6 @@ async function getAgentTurnDetail(sessionId, agentId, turnIndex) {
2329
3265
  // src/commands/sessions.ts
2330
3266
  var import_promises5 = require("node:fs/promises");
2331
3267
  var import_node_path6 = require("node:path");
2332
- var import_node_os3 = require("node:os");
2333
3268
 
2334
3269
  // src/lib/metadata.ts
2335
3270
  var import_promises4 = require("node:fs/promises");
@@ -2346,6 +3281,19 @@ function deriveSessionStatus(rawMessages) {
2346
3281
  if (toolName) info.toolName = toolName;
2347
3282
  return info;
2348
3283
  }
3284
+ for (let i = rawMessages.length - 1; i >= 0; i--) {
3285
+ const msg = rawMessages[i];
3286
+ if (msg.type === "queue-operation") continue;
3287
+ if (msg.type === "progress") {
3288
+ const data = msg.data;
3289
+ if (data?.type === "hook_progress") {
3290
+ const decision = data.decision ?? data.hookSpecificOutput?.permissionDecision;
3291
+ if (decision === "defer") return result("deferred");
3292
+ }
3293
+ break;
3294
+ }
3295
+ break;
3296
+ }
2349
3297
  for (let i = rawMessages.length - 1; i >= 0; i--) {
2350
3298
  const msg = rawMessages[i];
2351
3299
  if (msg.type === "queue-operation") {
@@ -2380,6 +3328,13 @@ function deriveSessionStatus(rawMessages) {
2380
3328
  if (isMeta) continue;
2381
3329
  return result("processing");
2382
3330
  }
3331
+ if (msg.type === "system" && msg.subtype === "terminal_reason") {
3332
+ const reason = msg.reason;
3333
+ if (reason) {
3334
+ return { status: "completed", terminalReason: reason, pendingQueue: Math.max(0, pendingEnqueues) };
3335
+ }
3336
+ continue;
3337
+ }
2383
3338
  if (msg.type === "summary") continue;
2384
3339
  if (msg.type === "system" && msg.subtype === "compact_boundary") continue;
2385
3340
  }
@@ -2605,7 +3560,23 @@ async function getSessionStatus(filePath) {
2605
3560
  }
2606
3561
 
2607
3562
  // src/commands/sessions.ts
2608
- var CODEX_SESSIONS_DIR = (0, import_node_path6.join)((0, import_node_os3.homedir)(), ".codex", "sessions");
3563
+ function toSessionSummary(file, meta, status) {
3564
+ return {
3565
+ sessionId: meta.sessionId,
3566
+ filePath: file.path,
3567
+ timestamp: meta.timestamp,
3568
+ model: meta.model,
3569
+ cwd: meta.cwd,
3570
+ gitBranch: meta.gitBranch,
3571
+ slug: meta.slug,
3572
+ firstMessage: meta.firstUserMessage,
3573
+ lastMessage: meta.lastUserMessage,
3574
+ turnCount: meta.turnCount,
3575
+ status,
3576
+ mtime: file.mtimeMs,
3577
+ source: file.path.startsWith(dirs.CODEX_SESSIONS_DIR + "/") ? "codex" : "claude"
3578
+ };
3579
+ }
2609
3580
  async function listCodexSessionFiles(cutoff) {
2610
3581
  const walk = async (dir, depth) => {
2611
3582
  if (depth > 4) return [];
@@ -2632,7 +3603,7 @@ async function listCodexSessionFiles(cutoff) {
2632
3603
  }
2633
3604
  return results;
2634
3605
  };
2635
- return walk(CODEX_SESSIONS_DIR, 0);
3606
+ return walk(dirs.CODEX_SESSIONS_DIR, 0);
2636
3607
  }
2637
3608
  async function listSessions(opts = {}) {
2638
3609
  const limit = Math.min(Math.max(1, opts.limit ?? 20), 100);
@@ -2645,29 +3616,31 @@ async function listSessions(opts = {}) {
2645
3616
  return [];
2646
3617
  }
2647
3618
  const projectDirs = entries.filter((e) => e.isDirectory() && e.name !== "memory").map((e) => (0, import_node_path6.join)(dirs.PROJECTS_DIR, e.name));
2648
- const nested = await Promise.all(
2649
- projectDirs.map(async (projectDir) => {
2650
- try {
2651
- const files = await (0, import_promises5.readdir)(projectDir);
2652
- const jsonlFiles = files.filter((f) => f.endsWith(".jsonl"));
2653
- const statResults = await Promise.all(
2654
- jsonlFiles.map(async (f) => {
2655
- const filePath = (0, import_node_path6.join)(projectDir, f);
2656
- try {
2657
- const s = await (0, import_promises5.stat)(filePath);
2658
- return s.mtimeMs >= cutoff ? { path: filePath, mtimeMs: s.mtimeMs } : null;
2659
- } catch {
2660
- return null;
2661
- }
2662
- })
2663
- );
2664
- return statResults.filter((r) => r !== null);
2665
- } catch {
2666
- return [];
2667
- }
2668
- })
2669
- );
2670
- const codexFiles = await listCodexSessionFiles(cutoff);
3619
+ const [nested, codexFiles] = await Promise.all([
3620
+ Promise.all(
3621
+ projectDirs.map(async (projectDir) => {
3622
+ try {
3623
+ const files = await (0, import_promises5.readdir)(projectDir);
3624
+ const jsonlFiles = files.filter((f) => f.endsWith(".jsonl"));
3625
+ const statResults = await Promise.all(
3626
+ jsonlFiles.map(async (f) => {
3627
+ const filePath = (0, import_node_path6.join)(projectDir, f);
3628
+ try {
3629
+ const s = await (0, import_promises5.stat)(filePath);
3630
+ return s.mtimeMs >= cutoff ? { path: filePath, mtimeMs: s.mtimeMs } : null;
3631
+ } catch {
3632
+ return null;
3633
+ }
3634
+ })
3635
+ );
3636
+ return statResults.filter((r) => r !== null);
3637
+ } catch {
3638
+ return [];
3639
+ }
3640
+ })
3641
+ ),
3642
+ listCodexSessionFiles(cutoff)
3643
+ ]);
2671
3644
  const allFiles = [...nested.flat(), ...codexFiles];
2672
3645
  allFiles.sort((a, b) => b.mtimeMs - a.mtimeMs);
2673
3646
  const results = [];
@@ -2679,21 +3652,7 @@ async function listSessions(opts = {}) {
2679
3652
  getSessionStatus(file.path)
2680
3653
  ]);
2681
3654
  if (opts.cwd && meta.cwd !== opts.cwd) continue;
2682
- results.push({
2683
- sessionId: meta.sessionId,
2684
- filePath: file.path,
2685
- timestamp: meta.timestamp,
2686
- model: meta.model,
2687
- cwd: meta.cwd,
2688
- gitBranch: meta.gitBranch,
2689
- slug: meta.slug,
2690
- firstMessage: meta.firstUserMessage,
2691
- lastMessage: meta.lastUserMessage,
2692
- turnCount: meta.turnCount,
2693
- status: statusInfo.status,
2694
- mtime: file.mtimeMs,
2695
- source: file.path.startsWith(CODEX_SESSIONS_DIR + "/") ? "codex" : "claude"
2696
- });
3655
+ results.push(toSessionSummary(file, meta, statusInfo.status));
2697
3656
  } catch {
2698
3657
  }
2699
3658
  }
@@ -2709,18 +3668,20 @@ async function currentSession(cwd) {
2709
3668
  files = [];
2710
3669
  }
2711
3670
  const jsonlFiles = files.filter((f) => f.endsWith(".jsonl"));
2712
- const statResults = await Promise.all(
2713
- jsonlFiles.map(async (f) => {
2714
- const filePath = (0, import_node_path6.join)(projectDir, f);
2715
- try {
2716
- const s = await (0, import_promises5.stat)(filePath);
2717
- return { path: filePath, mtimeMs: s.mtimeMs };
2718
- } catch {
2719
- return null;
2720
- }
2721
- })
2722
- );
2723
- const codexCandidates = await listCodexSessionFiles(0);
3671
+ const [statResults, codexCandidates] = await Promise.all([
3672
+ Promise.all(
3673
+ jsonlFiles.map(async (f) => {
3674
+ const filePath = (0, import_node_path6.join)(projectDir, f);
3675
+ try {
3676
+ const s = await (0, import_promises5.stat)(filePath);
3677
+ return { path: filePath, mtimeMs: s.mtimeMs };
3678
+ } catch {
3679
+ return null;
3680
+ }
3681
+ })
3682
+ ),
3683
+ listCodexSessionFiles(0)
3684
+ ]);
2724
3685
  const codexMatches = [];
2725
3686
  for (const file of codexCandidates) {
2726
3687
  try {
@@ -2742,21 +3703,7 @@ async function currentSession(cwd) {
2742
3703
  getSessionMeta(latest.path),
2743
3704
  getSessionStatus(latest.path)
2744
3705
  ]);
2745
- return {
2746
- sessionId: meta.sessionId,
2747
- filePath: latest.path,
2748
- timestamp: meta.timestamp,
2749
- model: meta.model,
2750
- cwd: meta.cwd,
2751
- gitBranch: meta.gitBranch,
2752
- slug: meta.slug,
2753
- firstMessage: meta.firstUserMessage,
2754
- lastMessage: meta.lastUserMessage,
2755
- turnCount: meta.turnCount,
2756
- status: statusInfo.status,
2757
- mtime: latest.mtimeMs,
2758
- source: latest.path.startsWith(CODEX_SESSIONS_DIR + "/") ? "codex" : "claude"
2759
- };
3706
+ return toSessionSummary(latest, meta, statusInfo.status);
2760
3707
  }
2761
3708
 
2762
3709
  // src/commands/index-cmd.ts