cogpit-memory 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -54,8 +54,526 @@ var import_better_sqlite3 = __toESM(require("better-sqlite3"));
54
54
  var Database = import_better_sqlite3.default;
55
55
 
56
56
  // src/lib/search-index.ts
57
- var import_node_fs = require("node:fs");
58
- var import_node_path = require("node:path");
57
+ var import_node_fs2 = require("node:fs");
58
+
59
+ // src/lib/types.ts
60
+ var AGENT_KINDS = ["codex", "copilot", "claude"];
61
+
62
+ // src/lib/agent-descriptors.ts
63
+ var SESSION_UUID = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
64
+ var SESSION_UUID_RE = new RegExp(`^${SESSION_UUID}$`, "i");
65
+ function encodeBase64DirName(prefix, cwd) {
66
+ const bytes = new TextEncoder().encode(cwd);
67
+ let binary = "";
68
+ for (const byte of bytes) binary += String.fromCharCode(byte);
69
+ const encoded = btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
70
+ return `${prefix}${encoded}`;
71
+ }
72
+ function decodeBase64DirName(prefix, dirName) {
73
+ if (!dirName.startsWith(prefix)) return null;
74
+ try {
75
+ const encoded = dirName.slice(prefix.length).replace(/-/g, "+").replace(/_/g, "/");
76
+ const binary = atob(encoded);
77
+ const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
78
+ return new TextDecoder().decode(bytes);
79
+ } catch {
80
+ return null;
81
+ }
82
+ }
83
+ function base64DirNameCodec(prefix) {
84
+ return {
85
+ lossy: false,
86
+ owns: (dirName) => typeof dirName === "string" && dirName.startsWith(prefix),
87
+ encode: (cwd) => encodeBase64DirName(prefix, cwd),
88
+ decode: (dirName) => decodeBase64DirName(prefix, dirName)
89
+ };
90
+ }
91
+ function sessionIdFromJsonlName(fileName) {
92
+ const match = new RegExp(`(${SESSION_UUID})\\.jsonl$`, "i").exec(fileName);
93
+ return match ? match[1] : null;
94
+ }
95
+ var jsonlPathUrlCodec = {
96
+ urlId: (fileName) => fileName.replace(/\.jsonl$/, ""),
97
+ fileNameFromUrlId: (urlId) => urlId.endsWith(".jsonl") ? urlId : `${urlId}.jsonl`
98
+ };
99
+ function shellQuote(value) {
100
+ return `'${value.replace(/'/g, `'\\''`)}'`;
101
+ }
102
+ function flag(name, value) {
103
+ return value ? [name, value] : [];
104
+ }
105
+ function commandCapture(command, pattern) {
106
+ return pattern.exec(command)?.[1] ?? null;
107
+ }
108
+ var ARGV_SESSION_ID = "[0-9a-f-]{36}";
109
+ var CODEX_DIR_PREFIX = "codex__";
110
+ var COPILOT_DIR_PREFIX = "copilot__";
111
+ var CLAUDE_PERMISSION_MODES = /* @__PURE__ */ new Set(["default", "plan", "acceptEdits", "dontAsk", "auto"]);
112
+ var claude = {
113
+ kind: "claude",
114
+ displayName: "Claude Code",
115
+ binName: "claude",
116
+ dirName: {
117
+ lossy: true,
118
+ // Claude has no prefix: it owns every dirName no other agent claims. The
119
+ // registry checks the prefixed agents first, so this is only ever reached
120
+ // as the terminal arm.
121
+ owns: (dirName) => typeof dirName === "string" && !dirName.startsWith(CODEX_DIR_PREFIX) && !dirName.startsWith(COPILOT_DIR_PREFIX),
122
+ /**
123
+ * Claude Code's own convention: every character outside `[A-Za-z0-9]`
124
+ * becomes `-`, so `/Users/x/proj` and `C:\Users\x\proj` both collapse into a
125
+ * flat name under `~/.claude/projects`.
126
+ */
127
+ encode: (cwd) => (cwd.replace(/[\\/]+$/, "") || cwd).replace(/[^a-zA-Z0-9]/g, "-"),
128
+ /**
129
+ * Best-effort inverse. The encoding is lossy — a literal `-` is
130
+ * indistinguishable from a separator — so a caller that can read the
131
+ * session's recorded `cwd` should always prefer that.
132
+ */
133
+ decode: (dirName) => {
134
+ const windowsDrive = /^([A-Za-z])--(.*)$/.exec(dirName);
135
+ if (windowsDrive) return `${windowsDrive[1]}:\\${windowsDrive[2].replace(/-/g, "\\")}`;
136
+ return "/" + dirName.replace(/^-/, "").replace(/-/g, "/");
137
+ }
138
+ },
139
+ sessionFile: {
140
+ name: (sessionId) => `${sessionId}.jsonl`,
141
+ sessionId: sessionIdFromJsonlName,
142
+ ...jsonlPathUrlCodec
143
+ },
144
+ metadataFromHead: true,
145
+ resume: {
146
+ command: (sessionId) => `claude --resume ${sessionId}`,
147
+ args: (sessionId) => ["--resume", sessionId]
148
+ },
149
+ launchArgs: {
150
+ permissions: (permissions) => {
151
+ if (!permissions?.mode) return ["--permission-mode", "default"];
152
+ if (permissions.mode === "bypassPermissions") return ["--dangerously-skip-permissions"];
153
+ return [
154
+ ...CLAUDE_PERMISSION_MODES.has(permissions.mode) ? ["--permission-mode", permissions.mode] : [],
155
+ ...(permissions.allowedTools ?? []).flatMap((tool) => ["--allowedTools", tool]),
156
+ ...(permissions.disallowedTools ?? []).flatMap((tool) => ["--disallowedTools", tool])
157
+ ];
158
+ },
159
+ model: (model) => flag("--model", model),
160
+ effort: (effort) => flag("--effort", effort),
161
+ // Claude's faster tier is a setting on the live Agent SDK query, not a flag.
162
+ fastTier: () => []
163
+ },
164
+ serviceTier: null,
165
+ cli: {
166
+ packageName: "@anthropic-ai/claude-code",
167
+ homebrew: { name: "claude-code", cask: false },
168
+ wingetId: null,
169
+ versionArgs: ["--version"],
170
+ selfUpdate: {
171
+ args: ["update"],
172
+ matches: (path) => path.endsWith("/.local/bin/claude") || path.endsWith("/.local/bin/claude.exe") || path.includes("/.local/share/claude/")
173
+ },
174
+ bundledBySdk: true,
175
+ homeDirName: ".claude",
176
+ homeEnvVar: null,
177
+ installMarker: "projects",
178
+ homeIsDiscoverable: false,
179
+ process: {
180
+ by: "command",
181
+ sessionIdFromCommand: (command) => commandCapture(command, new RegExp(`--resume(?:=|\\s+)(${ARGV_SESSION_ID})`)) ?? commandCapture(command, new RegExp(`--session-id(?:=|\\s+)(${ARGV_SESSION_ID})`))
182
+ }
183
+ },
184
+ config: {
185
+ rootDirName: ".claude",
186
+ instructions: [
187
+ { in: "scope", path: "CLAUDE.md", scopes: ["global", "project"] },
188
+ { in: "root", path: "CLAUDE.md", label: ".claude/CLAUDE.md", scopes: ["project"] }
189
+ ],
190
+ settings: [
191
+ { in: "root", path: "settings.json", scopes: ["global"] },
192
+ { in: "root", path: "settings.local.json", scopes: ["project"] }
193
+ ],
194
+ skillsDir: "skills",
195
+ agentsDir: "agents",
196
+ commandsDir: "commands",
197
+ themesDir: "themes",
198
+ pluginsDir: "plugins",
199
+ pluginManifestDir: ".claude-plugin"
200
+ },
201
+ contextWindow: {
202
+ // Anything not listed is treated as current-generation, so a model released
203
+ // after this table was written reports the larger window rather than a
204
+ // stale small one. Limits are LiteLLM `max_input_tokens`, the same table the
205
+ // cost code prices against. Deliberately not family-based: sonnet-4-5 is
206
+ // 200k while sonnet-4-6 is 1M.
207
+ defaultLimit: 1e6,
208
+ limits: [
209
+ { match: "claude-haiku-4-5", limit: 2e5 },
210
+ { match: "claude-haiku-4-1", limit: 2e5 },
211
+ { match: "claude-sonnet-4-5", limit: 2e5 },
212
+ { match: "claude-opus-4-5", limit: 2e5 },
213
+ { match: "claude-opus-4-1", limit: 2e5 }
214
+ ],
215
+ compactBuffer: 33e3,
216
+ extendedContext: { marker: "[1m]", limit: 1e6 }
217
+ },
218
+ capabilities: {
219
+ worktrees: true,
220
+ mcp: true,
221
+ slashCommands: true,
222
+ workflows: true,
223
+ agentTeams: true,
224
+ subagentTranscripts: true,
225
+ nestedSubagents: false,
226
+ sharing: true,
227
+ goals: "transcript",
228
+ undo: true,
229
+ redo: true,
230
+ nativeRewind: false,
231
+ fileCheckpoints: true,
232
+ reasoningEffort: true,
233
+ fastTier: true,
234
+ imageInput: true,
235
+ midTurnSteering: false,
236
+ stopSession: true,
237
+ ultracode: true,
238
+ contextWindow: true,
239
+ externalProcesses: false,
240
+ accountSwitching: true,
241
+ modelFallbackNotices: true,
242
+ autoPermissionMode: "per-model",
243
+ settingsApply: "live",
244
+ turnLiveness: "transcript",
245
+ tokenStreaming: true,
246
+ nativeFork: false,
247
+ namedSessions: true
248
+ }
249
+ };
250
+ var CODEX_SERVICE_TIER = {
251
+ appServerValue: "priority",
252
+ cliConfigValue: "fast"
253
+ };
254
+ var codex = {
255
+ kind: "codex",
256
+ displayName: "Codex",
257
+ binName: "codex",
258
+ dirName: base64DirNameCodec(CODEX_DIR_PREFIX),
259
+ sessionFile: {
260
+ /**
261
+ * Codex nests rollouts by local date:
262
+ * `YYYY/MM/DD/rollout-YYYY-MM-DDTHH-MM-SS-<sessionId>.jsonl`.
263
+ */
264
+ name: (sessionId, at = /* @__PURE__ */ new Date()) => {
265
+ const year = String(at.getFullYear());
266
+ const month = String(at.getMonth() + 1).padStart(2, "0");
267
+ const day = String(at.getDate()).padStart(2, "0");
268
+ const hour = String(at.getHours()).padStart(2, "0");
269
+ const minute = String(at.getMinutes()).padStart(2, "0");
270
+ const second = String(at.getSeconds()).padStart(2, "0");
271
+ return `${year}/${month}/${day}/rollout-${year}-${month}-${day}T${hour}-${minute}-${second}-${sessionId}.jsonl`;
272
+ },
273
+ sessionId: sessionIdFromJsonlName,
274
+ // The date nesting is unrecoverable from the id, so the URL keeps the path.
275
+ ...jsonlPathUrlCodec
276
+ },
277
+ // A rollout's header carries no turn count and no last user message; both
278
+ // only settle at the end of the file.
279
+ metadataFromHead: false,
280
+ resume: {
281
+ command: (sessionId, cwd) => cwd ? `codex -C ${shellQuote(cwd)} resume ${sessionId}` : `codex resume ${sessionId}`,
282
+ args: (sessionId) => ["resume", sessionId]
283
+ },
284
+ launchArgs: {
285
+ permissions: (permissions) => {
286
+ const mode = permissions?.mode || "default";
287
+ if (mode === "bypassPermissions") return ["--dangerously-bypass-approvals-and-sandbox"];
288
+ return [
289
+ "--sandbox",
290
+ mode === "plan" ? "read-only" : "workspace-write",
291
+ "-c",
292
+ 'approval_policy="never"'
293
+ ];
294
+ },
295
+ model: (model) => flag("-m", model),
296
+ effort: (effort) => effort ? ["-c", `model_reasoning_effort=${JSON.stringify(effort)}`] : [],
297
+ fastTier: (enabled) => enabled ? [
298
+ "-c",
299
+ `service_tier=${JSON.stringify(CODEX_SERVICE_TIER.cliConfigValue)}`,
300
+ "--enable",
301
+ "fast_mode"
302
+ ] : []
303
+ },
304
+ serviceTier: CODEX_SERVICE_TIER,
305
+ cli: {
306
+ packageName: "@openai/codex",
307
+ homebrew: { name: "codex", cask: false },
308
+ wingetId: null,
309
+ versionArgs: ["--version"],
310
+ // Codex ships no self-updater; a hand-placed binary has to be replaced by
311
+ // whatever put it there.
312
+ selfUpdate: null,
313
+ bundledBySdk: false,
314
+ homeDirName: ".codex",
315
+ homeEnvVar: "CODEX_HOME",
316
+ installMarker: "",
317
+ homeIsDiscoverable: true,
318
+ process: {
319
+ by: "command",
320
+ sessionIdFromCommand: (command) => commandCapture(
321
+ command,
322
+ new RegExp(`codex(?:\\s+\\S+)*\\s+exec\\s+resume\\s+(${ARGV_SESSION_ID})`)
323
+ )
324
+ }
325
+ },
326
+ config: {
327
+ rootDirName: ".codex",
328
+ instructions: [{ in: "scope", path: "AGENTS.md", scopes: ["global", "project"] }],
329
+ settings: [{ in: "root", path: "config.toml", scopes: ["global", "project"] }],
330
+ skillsDir: "skills",
331
+ agentsDir: "agents",
332
+ commandsDir: "prompts",
333
+ themesDir: null,
334
+ pluginsDir: null,
335
+ pluginManifestDir: null
336
+ },
337
+ contextWindow: {
338
+ // The GPT-5 family window. A rollout that reports `model_context_window`
339
+ // overrides this; it is only the fallback for a model Cogpit has not seen.
340
+ defaultLimit: 272e3,
341
+ limits: [],
342
+ // Codex decides its own compaction point and never publishes a reserve.
343
+ compactBuffer: 0,
344
+ extendedContext: null
345
+ },
346
+ capabilities: {
347
+ worktrees: false,
348
+ mcp: false,
349
+ slashCommands: false,
350
+ workflows: false,
351
+ agentTeams: false,
352
+ subagentTranscripts: true,
353
+ nestedSubagents: true,
354
+ sharing: false,
355
+ goals: "thread-api",
356
+ undo: true,
357
+ redo: true,
358
+ nativeRewind: false,
359
+ fileCheckpoints: false,
360
+ reasoningEffort: true,
361
+ fastTier: true,
362
+ imageInput: true,
363
+ midTurnSteering: true,
364
+ stopSession: false,
365
+ ultracode: false,
366
+ contextWindow: false,
367
+ externalProcesses: false,
368
+ accountSwitching: false,
369
+ modelFallbackNotices: false,
370
+ autoPermissionMode: "never",
371
+ settingsApply: "next-turn",
372
+ turnLiveness: "runtime",
373
+ tokenStreaming: true,
374
+ nativeFork: false,
375
+ // Codex derives its own thread name.
376
+ namedSessions: false
377
+ }
378
+ };
379
+ var copilot = {
380
+ kind: "copilot",
381
+ displayName: "GitHub Copilot CLI",
382
+ binName: "copilot",
383
+ dirName: base64DirNameCodec(COPILOT_DIR_PREFIX),
384
+ sessionFile: {
385
+ /** Copilot keeps one directory per session: `<uuid>/events.jsonl`. */
386
+ name: (sessionId) => `${sessionId}/events.jsonl`,
387
+ sessionId: (fileName) => {
388
+ const match = new RegExp(`^(${SESSION_UUID})/events\\.jsonl$`, "i").exec(fileName);
389
+ return match ? match[1] : null;
390
+ },
391
+ // `<id>/events.jsonl` rebuilds from the id, so URLs stay short.
392
+ urlId: (fileName) => {
393
+ const match = new RegExp(`^(${SESSION_UUID})/events\\.jsonl$`, "i").exec(fileName);
394
+ return match ? match[1] : fileName.replace(/\.jsonl$/, "");
395
+ },
396
+ fileNameFromUrlId: (urlId) => urlId.endsWith(".jsonl") ? urlId : `${urlId}/events.jsonl`
397
+ },
398
+ metadataFromHead: true,
399
+ resume: {
400
+ command: (sessionId, cwd) => cwd ? `copilot -C ${shellQuote(cwd)} --resume ${sessionId}` : `copilot --resume ${sessionId}`,
401
+ args: (sessionId) => [`--resume=${sessionId}`]
402
+ },
403
+ // Cogpit launches Copilot as a stdio RPC server and configures each turn
404
+ // afterwards, over `session.create` and `session.setPermissionMode`. These are
405
+ // the flags its CLI accepts for the same settings when it is driven straight
406
+ // from a shell instead.
407
+ launchArgs: {
408
+ permissions: (permissions) => permissions?.mode === "bypassPermissions" || permissions?.mode === "auto" ? ["--allow-all"] : [],
409
+ model: (model) => flag("--model", model),
410
+ effort: (effort) => flag("--reasoning-effort", effort),
411
+ fastTier: () => []
412
+ },
413
+ serviceTier: null,
414
+ cli: {
415
+ packageName: "@github/copilot",
416
+ homebrew: { name: "copilot-cli", cask: true },
417
+ wingetId: "GitHub.Copilot",
418
+ versionArgs: ["--version"],
419
+ selfUpdate: {
420
+ args: ["update"],
421
+ // `/usr/local/bin/copilot` is also an npm prefix, so this is only ever
422
+ // reached after the package-manager checks have declined — which they can
423
+ // only do because the caller passes the realpath in as well.
424
+ matches: (path) => path.endsWith("/.local/bin/copilot") || path === "/usr/local/bin/copilot"
425
+ },
426
+ bundledBySdk: false,
427
+ homeDirName: ".copilot",
428
+ homeEnvVar: "COPILOT_HOME",
429
+ installMarker: "session-state",
430
+ homeIsDiscoverable: true,
431
+ // `copilot` is too common a word to match loosely: editor extensions and
432
+ // unrelated tools name it in their argv.
433
+ process: {
434
+ by: "executable",
435
+ sessionIdFromCommand: (command) => commandCapture(command, new RegExp(`--resume(?:=|\\s+)(${ARGV_SESSION_ID})`))
436
+ }
437
+ },
438
+ config: {
439
+ rootDirName: ".copilot",
440
+ // Copilot reads the same cross-CLI instruction file Codex does, so the two
441
+ // entries merge into one row carrying both badges.
442
+ instructions: [{ in: "scope", path: "AGENTS.md", scopes: ["global", "project"] }],
443
+ settings: [{ in: "root", path: "config.json", scopes: ["global"] }],
444
+ skillsDir: "skills",
445
+ agentsDir: null,
446
+ commandsDir: null,
447
+ themesDir: null,
448
+ pluginsDir: null,
449
+ pluginManifestDir: null
450
+ },
451
+ contextWindow: {
452
+ // Copilot brokers models from several vendors, so this is the smallest
453
+ // window any of them offers rather than a per-model table Cogpit cannot
454
+ // keep current.
455
+ defaultLimit: 2e5,
456
+ limits: [],
457
+ compactBuffer: 0,
458
+ extendedContext: null
459
+ },
460
+ capabilities: {
461
+ worktrees: false,
462
+ mcp: false,
463
+ slashCommands: false,
464
+ workflows: false,
465
+ agentTeams: false,
466
+ subagentTranscripts: false,
467
+ nestedSubagents: false,
468
+ sharing: false,
469
+ goals: false,
470
+ undo: true,
471
+ // Copilot persists no undo state of its own, so there is nothing to redo
472
+ // from and no branch history to show.
473
+ redo: false,
474
+ nativeRewind: true,
475
+ fileCheckpoints: false,
476
+ // Effort is only offered for models whose catalog entry advertises it.
477
+ reasoningEffort: false,
478
+ fastTier: false,
479
+ imageInput: false,
480
+ midTurnSteering: true,
481
+ stopSession: true,
482
+ ultracode: false,
483
+ contextWindow: false,
484
+ externalProcesses: true,
485
+ accountSwitching: false,
486
+ modelFallbackNotices: false,
487
+ autoPermissionMode: "always",
488
+ settingsApply: "next-turn",
489
+ turnLiveness: "runtime",
490
+ tokenStreaming: false,
491
+ nativeFork: true,
492
+ namedSessions: true
493
+ }
494
+ };
495
+ var DESCRIPTORS = Object.freeze({
496
+ claude,
497
+ codex,
498
+ copilot
499
+ });
500
+ function descriptorFor(kind) {
501
+ return DESCRIPTORS[kind];
502
+ }
503
+ function descriptorForDirName(dirName) {
504
+ for (const kind of AGENT_KINDS) {
505
+ if (kind === "claude") continue;
506
+ if (DESCRIPTORS[kind].dirName.owns(dirName)) return DESCRIPTORS[kind];
507
+ }
508
+ return claude;
509
+ }
510
+
511
+ // src/lib/codex-exec.ts
512
+ var JS_SIMPLE_ESCAPES = {
513
+ "0": "\0",
514
+ b: "\b",
515
+ f: "\f",
516
+ n: "\n",
517
+ r: "\r",
518
+ t: " ",
519
+ v: "\v"
520
+ };
521
+ function readJsStringLiteral(source, startIndex) {
522
+ const quote = source[startIndex];
523
+ if (quote !== '"' && quote !== "'" && quote !== "`") return null;
524
+ let value = "";
525
+ let index = startIndex + 1;
526
+ while (index < source.length) {
527
+ const char = source[index];
528
+ if (char === quote) return { endIndex: index + 1, value };
529
+ if (char !== "\\") {
530
+ value += char;
531
+ index++;
532
+ continue;
533
+ }
534
+ index++;
535
+ if (index >= source.length) return null;
536
+ const escaped = source[index];
537
+ if (escaped in JS_SIMPLE_ESCAPES) {
538
+ value += JS_SIMPLE_ESCAPES[escaped];
539
+ index++;
540
+ continue;
541
+ }
542
+ if (escaped === "\n") {
543
+ index++;
544
+ continue;
545
+ }
546
+ if (escaped === "\r") {
547
+ index += source[index + 1] === "\n" ? 2 : 1;
548
+ continue;
549
+ }
550
+ if (escaped === "x" && /^[0-9a-fA-F]{2}$/.test(source.slice(index + 1, index + 3))) {
551
+ value += String.fromCharCode(Number.parseInt(source.slice(index + 1, index + 3), 16));
552
+ index += 3;
553
+ continue;
554
+ }
555
+ if (escaped === "u") {
556
+ const braced = source.slice(index + 1).match(/^\{([0-9a-fA-F]+)\}/);
557
+ if (braced) {
558
+ const codePoint = Number.parseInt(braced[1], 16);
559
+ if (Number.isFinite(codePoint) && codePoint <= 1114111) {
560
+ value += String.fromCodePoint(codePoint);
561
+ index += braced[0].length + 1;
562
+ continue;
563
+ }
564
+ }
565
+ const hex = source.slice(index + 1, index + 5);
566
+ if (/^[0-9a-fA-F]{4}$/.test(hex)) {
567
+ value += String.fromCharCode(Number.parseInt(hex, 16));
568
+ index += 5;
569
+ continue;
570
+ }
571
+ }
572
+ value += escaped;
573
+ index++;
574
+ }
575
+ return null;
576
+ }
59
577
 
60
578
  // src/lib/messageTypeGuards.ts
61
579
  function isUserMessage(msg) {
@@ -86,6 +604,59 @@ function isCompactBoundary(msg) {
86
604
  return msg.type === "system" && msg.subtype === "compact_boundary";
87
605
  }
88
606
 
607
+ // src/lib/sessionStats.ts
608
+ function countToolCalls(toolCalls, counts) {
609
+ let errors = 0;
610
+ for (const tc of toolCalls) {
611
+ counts[tc.name] = (counts[tc.name] ?? 0) + 1;
612
+ if (tc.isError) errors++;
613
+ }
614
+ return errors;
615
+ }
616
+ function addUsageToStats(stats, usage) {
617
+ stats.totalInputTokens += usage.input_tokens;
618
+ stats.totalOutputTokens += usage.output_tokens;
619
+ stats.totalCacheCreationTokens += usage.cache_creation_input_tokens ?? 0;
620
+ stats.totalCacheReadTokens += usage.cache_read_input_tokens ?? 0;
621
+ }
622
+ function createEmptySessionStats(turnCount) {
623
+ return {
624
+ totalInputTokens: 0,
625
+ totalOutputTokens: 0,
626
+ totalCacheCreationTokens: 0,
627
+ totalCacheReadTokens: 0,
628
+ toolCallCounts: {},
629
+ errorCount: 0,
630
+ totalDurationMs: 0,
631
+ turnCount
632
+ };
633
+ }
634
+ function computeStats(turns) {
635
+ const stats = createEmptySessionStats(turns.length);
636
+ for (const turn of turns) {
637
+ if (turn.tokenUsage) addUsageToStats(stats, turn.tokenUsage);
638
+ if (turn.durationMs) stats.totalDurationMs += turn.durationMs;
639
+ stats.errorCount += countToolCalls(turn.toolCalls, stats.toolCallCounts);
640
+ for (const sa of turn.subAgentActivity) {
641
+ stats.errorCount += countToolCalls(sa.toolCalls, stats.toolCallCounts);
642
+ if (sa.tokenUsage) addUsageToStats(stats, sa.tokenUsage);
643
+ }
644
+ }
645
+ return stats;
646
+ }
647
+ function mergeTokenUsage(existing, incoming) {
648
+ if (!existing) return { ...incoming };
649
+ const thinking = (existing.output_tokens_details?.thinking_tokens ?? 0) + (incoming.output_tokens_details?.thinking_tokens ?? 0);
650
+ return {
651
+ input_tokens: existing.input_tokens + incoming.input_tokens,
652
+ output_tokens: existing.output_tokens + incoming.output_tokens,
653
+ cache_creation_input_tokens: (existing.cache_creation_input_tokens ?? 0) + (incoming.cache_creation_input_tokens ?? 0),
654
+ cache_read_input_tokens: (existing.cache_read_input_tokens ?? 0) + (incoming.cache_read_input_tokens ?? 0),
655
+ speed: incoming.speed ?? existing.speed,
656
+ ...existing.output_tokens_details || incoming.output_tokens_details ? { output_tokens_details: { thinking_tokens: thinking } } : {}
657
+ };
658
+ }
659
+
89
660
  // src/lib/agentEnvelope.ts
90
661
  var ENVELOPE_RE = /<(agent-message|teammate-message)([^>]*)>([\s\S]*?)<\/\1>/g;
91
662
  var SENDER_PATTERNS = [
@@ -180,18 +751,6 @@ function mergeAttribution(current, msg) {
180
751
  }
181
752
  return merged;
182
753
  }
183
- function mergeTokenUsage(existing, incoming) {
184
- if (!existing) {
185
- return { ...incoming };
186
- }
187
- return {
188
- input_tokens: existing.input_tokens + incoming.input_tokens,
189
- output_tokens: existing.output_tokens + incoming.output_tokens,
190
- cache_creation_input_tokens: (existing.cache_creation_input_tokens ?? 0) + (incoming.cache_creation_input_tokens ?? 0),
191
- cache_read_input_tokens: (existing.cache_read_input_tokens ?? 0) + (incoming.cache_read_input_tokens ?? 0),
192
- speed: incoming.speed ?? existing.speed
193
- };
194
- }
195
754
  var RESUME_PREAMBLE_RE = /^This session is being continued from a previous conversation[\s\S]*?\n\nSummary:\s*\n/;
196
755
  var RESUME_INSTRUCTIONS_RE = /\n+(?:If you need specific details from before compaction|(?:Please c|C)ontinue the conversation from where it left off)[\s\S]*$/;
197
756
  function extractCompactionSummary(content) {
@@ -364,8 +923,11 @@ function findTurnStartIndices(messages) {
364
923
  }
365
924
  return starts;
366
925
  }
367
- function buildTurns(messages) {
926
+ function buildTurnsWithStarts(messages) {
368
927
  const turns = [];
928
+ const turnStartIndices = [];
929
+ let pendingTurnStart = null;
930
+ let recordIndex = 0;
369
931
  let current = null;
370
932
  let pendingCompaction = null;
371
933
  function attachPendingCompaction(turn) {
@@ -457,10 +1019,13 @@ function buildTurns(messages) {
457
1019
  }
458
1020
  current.contentBlocks = groupPlanModeBlocks(current.contentBlocks);
459
1021
  turns.push(current);
1022
+ if (pendingTurnStart !== null) turnStartIndices.push(pendingTurnStart);
1023
+ pendingTurnStart = null;
460
1024
  current = null;
461
1025
  agentBlockMap.clear();
462
1026
  }
463
- for (const msg of messages) {
1027
+ for (recordIndex = 0; recordIndex < messages.length; recordIndex++) {
1028
+ const msg = messages[recordIndex];
464
1029
  if (isSummaryMessage(msg)) {
465
1030
  finalizeTurn2();
466
1031
  pendingCompaction = { summary: msg.summary };
@@ -473,6 +1038,7 @@ function buildTurns(messages) {
473
1038
  }
474
1039
  if (isCompactSummaryMessage(msg)) {
475
1040
  finalizeTurn2();
1041
+ pendingTurnStart = recordIndex;
476
1042
  current = createTurn(msg, null);
477
1043
  current.compactionSummary = extractCompactionSummary(msg.message.content);
478
1044
  current.compactionMeta = pendingCompaction?.meta;
@@ -595,12 +1161,14 @@ function buildTurns(messages) {
595
1161
  current.contentBlocks.push(notification);
596
1162
  continue;
597
1163
  }
1164
+ pendingTurnStart = recordIndex;
598
1165
  current = createTurn(msg, null);
599
1166
  current.contentBlocks.push(notification);
600
1167
  attachPendingCompaction(current);
601
1168
  continue;
602
1169
  }
603
1170
  finalizeTurn2();
1171
+ pendingTurnStart = recordIndex;
604
1172
  current = createTurn(msg, msg.message.content);
605
1173
  attachPendingCompaction(current);
606
1174
  if (pendingRecap) {
@@ -628,6 +1196,7 @@ function buildTurns(messages) {
628
1196
  };
629
1197
  var flushToolCalls = flushToolCalls2, flushThinking = flushThinking2;
630
1198
  if (!current) {
1199
+ pendingTurnStart = recordIndex;
631
1200
  current = createTurn(msg, null);
632
1201
  attachPendingCompaction(current);
633
1202
  }
@@ -831,115 +1400,290 @@ function buildTurns(messages) {
831
1400
  }
832
1401
  flushPendingQueuedPrompts();
833
1402
  finalizeTurn2();
834
- return pairAgentMessageReplies(turns);
1403
+ return { turns: pairAgentMessageReplies(turns), turnStartIndices };
1404
+ }
1405
+ function buildTurns(messages) {
1406
+ return buildTurnsWithStarts(messages).turns;
835
1407
  }
836
1408
 
837
- // src/lib/sessionStats.ts
838
- function countToolCalls(toolCalls, counts) {
839
- let errors = 0;
840
- for (const tc of toolCalls) {
841
- counts[tc.name] = (counts[tc.name] ?? 0) + 1;
842
- if (tc.isError) errors++;
1409
+ // src/lib/claude.ts
1410
+ function isClaudeSessionText() {
1411
+ return true;
1412
+ }
1413
+ function parseRecords(lines) {
1414
+ const messages = [];
1415
+ for (const line of lines) {
1416
+ const trimmed = line.trim();
1417
+ if (!trimmed) continue;
1418
+ try {
1419
+ messages.push(JSON.parse(trimmed));
1420
+ } catch {
1421
+ }
843
1422
  }
844
- return errors;
1423
+ return messages;
845
1424
  }
846
- function addUsageToStats(stats, usage) {
847
- stats.totalInputTokens += usage.input_tokens;
848
- stats.totalOutputTokens += usage.output_tokens;
849
- stats.totalCacheCreationTokens += usage.cache_creation_input_tokens ?? 0;
850
- stats.totalCacheReadTokens += usage.cache_read_input_tokens ?? 0;
1425
+ function parseLines(jsonlText) {
1426
+ return parseRecords(jsonlText.split("\n"));
851
1427
  }
852
- function createEmptySessionStats(turnCount) {
1428
+ function extractSessionMetadata(messages) {
1429
+ const meta = { sessionId: "", version: "", gitBranch: "", cwd: "", slug: "", name: "", model: "", branchedFrom: void 0 };
1430
+ for (const msg of messages) {
1431
+ if (msg.sessionId && !meta.sessionId) meta.sessionId = msg.sessionId;
1432
+ if (msg.version && !meta.version) meta.version = msg.version;
1433
+ if (msg.gitBranch && !meta.gitBranch) meta.gitBranch = msg.gitBranch;
1434
+ if (msg.cwd && !meta.cwd) meta.cwd = msg.cwd;
1435
+ if (msg.slug && !meta.slug) meta.slug = msg.slug;
1436
+ if (msg.name && !meta.name) meta.name = msg.name;
1437
+ if (msg.branchedFrom && !meta.branchedFrom) {
1438
+ meta.branchedFrom = msg.branchedFrom;
1439
+ }
1440
+ if (isAssistantMessage(msg) && msg.message.model && !meta.model) {
1441
+ meta.model = msg.message.model;
1442
+ }
1443
+ if (meta.sessionId && meta.version && meta.gitBranch && meta.cwd && meta.model) break;
1444
+ }
1445
+ return meta;
1446
+ }
1447
+ function extractClaudeMetadataFromLines(lines) {
1448
+ return extractSessionMetadata(parseRecords(lines));
1449
+ }
1450
+ function parseClaudeSession(jsonlText, options) {
1451
+ const rawMessages = parseLines(jsonlText);
1452
+ const metadata = extractSessionMetadata(rawMessages);
1453
+ const turns = buildTurns(rawMessages);
1454
+ const stats = options?.skipStats ? createEmptySessionStats(turns.length) : computeStats(turns);
853
1455
  return {
854
- totalInputTokens: 0,
855
- totalOutputTokens: 0,
856
- totalCacheCreationTokens: 0,
857
- totalCacheReadTokens: 0,
858
- toolCallCounts: {},
859
- errorCount: 0,
860
- totalDurationMs: 0,
861
- turnCount
1456
+ ...metadata,
1457
+ turns,
1458
+ stats,
1459
+ rawMessages: options?.skipStats ? [] : rawMessages,
1460
+ agentKind: "claude"
862
1461
  };
863
1462
  }
864
- function computeStats(turns) {
865
- const stats = createEmptySessionStats(turns.length);
866
- for (const turn of turns) {
867
- if (turn.tokenUsage) addUsageToStats(stats, turn.tokenUsage);
868
- if (turn.durationMs) stats.totalDurationMs += turn.durationMs;
869
- stats.errorCount += countToolCalls(turn.toolCalls, stats.toolCallCounts);
870
- for (const sa of turn.subAgentActivity) {
871
- stats.errorCount += countToolCalls(sa.toolCalls, stats.toolCallCounts);
872
- if (sa.tokenUsage) addUsageToStats(stats, sa.tokenUsage);
1463
+ function appendClaudeSession(existing, newJsonlText) {
1464
+ const newMessages = parseLines(newJsonlText);
1465
+ if (newMessages.length === 0) return existing;
1466
+ const existingRawMessages = existing.rawMessages;
1467
+ const allRawMessages = [...existingRawMessages, ...newMessages];
1468
+ const turnStarts = findTurnStartIndices(existingRawMessages);
1469
+ const historyTurnCount = Math.max(0, existing.turns.length - turnStarts.length);
1470
+ let rebuildFromTurn = Math.max(0, turnStarts.length - 1);
1471
+ const progressParentIds = /* @__PURE__ */ new Set();
1472
+ for (const msg of newMessages) {
1473
+ if (msg.type === "progress") {
1474
+ const parentId = msg.parentToolUseID;
1475
+ if (parentId) progressParentIds.add(parentId);
873
1476
  }
874
1477
  }
875
- return stats;
876
- }
877
-
878
- // src/lib/codex-exec.ts
879
- var JS_SIMPLE_ESCAPES = {
880
- "0": "\0",
881
- b: "\b",
882
- f: "\f",
883
- n: "\n",
884
- r: "\r",
885
- t: " ",
886
- v: "\v"
887
- };
888
- function readJsStringLiteral(source, startIndex) {
889
- const quote = source[startIndex];
890
- if (quote !== '"' && quote !== "'" && quote !== "`") return null;
891
- let value = "";
892
- let index = startIndex + 1;
893
- while (index < source.length) {
894
- const char = source[index];
895
- if (char === quote) return { endIndex: index + 1, value };
896
- if (char !== "\\") {
897
- value += char;
898
- index++;
1478
+ if (progressParentIds.size > 0) {
1479
+ for (let t = rebuildFromTurn - 1; t >= 0; t--) {
1480
+ const turn = existing.turns[historyTurnCount + t];
1481
+ if (turn?.toolCalls.some((tc) => progressParentIds.has(tc.id))) {
1482
+ rebuildFromTurn = t;
1483
+ break;
1484
+ }
1485
+ }
1486
+ }
1487
+ const rebuildFrom = turnStarts[rebuildFromTurn] ?? existingRawMessages.length;
1488
+ const keptTurns = existing.turns.slice(0, historyTurnCount + rebuildFromTurn);
1489
+ const tailTurns = buildTurns(allRawMessages.slice(rebuildFrom));
1490
+ const allTurns = pairAgentMessageReplies([...keptTurns, ...tailTurns]);
1491
+ const stats = computeStats(allTurns);
1492
+ return {
1493
+ sessionId: existing.sessionId,
1494
+ version: existing.version,
1495
+ gitBranch: existing.gitBranch,
1496
+ cwd: existing.cwd,
1497
+ slug: existing.slug,
1498
+ name: existing.name,
1499
+ model: existing.model || (allTurns.length > 0 ? allTurns[allTurns.length - 1].model || "" : ""),
1500
+ turns: allTurns,
1501
+ stats,
1502
+ rawMessages: allRawMessages,
1503
+ branchedFrom: existing.branchedFrom,
1504
+ agentKind: existing.agentKind
1505
+ };
1506
+ }
1507
+ var TASK_NOTIFICATION_MARKER = "<task-notification>";
1508
+ var TASK_ID_RE = /<task-id>([^<]+)<\/task-id>/g;
1509
+ var TOOL_USE_ID_RE = /<tool-use-id>([^<]+)<\/tool-use-id>/g;
1510
+ function isObject(value) {
1511
+ return typeof value === "object" && value !== null;
1512
+ }
1513
+ function userMessageTexts(msg) {
1514
+ const message = isObject(msg.message) ? msg.message : null;
1515
+ const content = message?.content;
1516
+ if (typeof content === "string") return [content];
1517
+ if (!Array.isArray(content)) return [];
1518
+ return content.filter((block) => isObject(block) && typeof block.text === "string").map((block) => block.text);
1519
+ }
1520
+ function collectClaudeBackgroundWork(rawMessages) {
1521
+ const launches = [];
1522
+ const resolved = /* @__PURE__ */ new Set();
1523
+ const queuedIds = /* @__PURE__ */ new Set();
1524
+ const deliveredIds = /* @__PURE__ */ new Set();
1525
+ function noteNotifications(text, sink) {
1526
+ if (!text.includes(TASK_NOTIFICATION_MARKER)) return;
1527
+ for (const match of text.matchAll(TASK_ID_RE)) {
1528
+ resolved.add(match[1]);
1529
+ sink?.add(match[1]);
1530
+ }
1531
+ for (const match of text.matchAll(TOOL_USE_ID_RE)) {
1532
+ resolved.add(match[1]);
1533
+ sink?.add(match[1]);
1534
+ }
1535
+ }
1536
+ for (const msg of rawMessages) {
1537
+ if (msg.type === "user") {
1538
+ const toolUseResult = isObject(msg.toolUseResult) ? msg.toolUseResult : null;
1539
+ if (toolUseResult?.status === "async_launched") {
1540
+ const message = isObject(msg.message) ? msg.message : null;
1541
+ const blocks = Array.isArray(message?.content) ? message.content : [];
1542
+ const toolUseIds = blocks.filter((block) => isObject(block) && block.type === "tool_result").map((block) => block.tool_use_id);
1543
+ const ids = [toolUseResult.agentId, toolUseResult.taskId, ...toolUseIds].filter((id) => typeof id === "string" && id.length > 0);
1544
+ const description = [toolUseResult.description, toolUseResult.workflowName, toolUseResult.summary].find((value) => typeof value === "string" && value.length > 0) ?? "";
1545
+ if (ids.length > 0) launches.push({ ids, description });
1546
+ }
1547
+ for (const text of userMessageTexts(msg)) noteNotifications(text, deliveredIds);
899
1548
  continue;
900
1549
  }
901
- index++;
902
- if (index >= source.length) return null;
903
- const escaped = source[index];
904
- if (escaped in JS_SIMPLE_ESCAPES) {
905
- value += JS_SIMPLE_ESCAPES[escaped];
906
- index++;
1550
+ if (msg.type === "assistant") {
1551
+ const message = isObject(msg.message) ? msg.message : null;
1552
+ const blocks = Array.isArray(message?.content) ? message.content : [];
1553
+ for (const block of blocks) {
1554
+ if (!isObject(block) || block.type !== "tool_use" || block.name !== "TaskStop") continue;
1555
+ const input = isObject(block.input) ? block.input : null;
1556
+ const taskId = input?.task_id ?? input?.shell_id;
1557
+ if (typeof taskId === "string") resolved.add(taskId);
1558
+ }
907
1559
  continue;
908
1560
  }
909
- if (escaped === "\n") {
910
- index++;
1561
+ if (msg.type === "attachment") {
1562
+ const attachment = isObject(msg.attachment) ? msg.attachment : null;
1563
+ if (typeof attachment?.prompt === "string") noteNotifications(attachment.prompt, deliveredIds);
911
1564
  continue;
912
1565
  }
913
- if (escaped === "\r") {
914
- index += source[index + 1] === "\n" ? 2 : 1;
1566
+ if (msg.type === "queue-operation" && typeof msg.content === "string") {
1567
+ const op = msg.operation;
1568
+ noteNotifications(msg.content, op === "enqueue" ? queuedIds : deliveredIds);
1569
+ }
1570
+ }
1571
+ return {
1572
+ pendingDescriptions: launches.filter((launch) => !launch.ids.some((id) => resolved.has(id))).map((launch) => launch.description),
1573
+ undeliveredNotifications: [...queuedIds].filter((id) => !deliveredIds.has(id)).length
1574
+ };
1575
+ }
1576
+ function awaitingAgentsResult(pendingDescriptions, pendingQueue) {
1577
+ return {
1578
+ status: "awaiting_agents",
1579
+ pendingQueue,
1580
+ pendingAgents: pendingDescriptions.length,
1581
+ pendingAgentDescriptions: pendingDescriptions.filter((description) => description.length > 0)
1582
+ };
1583
+ }
1584
+ function deriveClaudeSessionStatus(rawMessages) {
1585
+ let pendingEnqueues = 0;
1586
+ let backgroundWork = null;
1587
+ let deliveringNotification = false;
1588
+ function result(status, toolName) {
1589
+ const info = { status, pendingQueue: Math.max(0, pendingEnqueues) };
1590
+ if (toolName) info.toolName = toolName;
1591
+ return info;
1592
+ }
1593
+ function getBackgroundWork() {
1594
+ return backgroundWork ??= collectClaudeBackgroundWork(rawMessages);
1595
+ }
1596
+ for (let i = rawMessages.length - 1; i >= 0; i--) {
1597
+ const msg = rawMessages[i];
1598
+ if (msg.type === "queue-operation") continue;
1599
+ if (msg.type === "progress") {
1600
+ const data = msg.data;
1601
+ if (data?.type === "hook_progress") {
1602
+ const decision = data.decision ?? data.hookSpecificOutput?.permissionDecision;
1603
+ if (decision === "defer") return result("deferred");
1604
+ }
1605
+ break;
1606
+ }
1607
+ break;
1608
+ }
1609
+ for (let i = rawMessages.length - 1; i >= 0; i--) {
1610
+ const msg = rawMessages[i];
1611
+ if (msg.type === "queue-operation") {
1612
+ if (typeof msg.content === "string" && msg.content.includes(TASK_NOTIFICATION_MARKER)) continue;
1613
+ const op = msg.operation;
1614
+ if (op === "enqueue") pendingEnqueues++;
1615
+ else if (op === "dequeue" || op === "remove") pendingEnqueues--;
915
1616
  continue;
916
1617
  }
917
- if (escaped === "x" && /^[0-9a-fA-F]{2}$/.test(source.slice(index + 1, index + 3))) {
918
- value += String.fromCharCode(Number.parseInt(source.slice(index + 1, index + 3), 16));
919
- index += 3;
1618
+ if (msg.type === "attachment") {
1619
+ const attachment = isObject(msg.attachment) ? msg.attachment : null;
1620
+ if (attachment?.commandMode === "task-notification") deliveringNotification = true;
920
1621
  continue;
921
1622
  }
922
- if (escaped === "u") {
923
- const braced = source.slice(index + 1).match(/^\{([0-9a-fA-F]+)\}/);
924
- if (braced) {
925
- const codePoint = Number.parseInt(braced[1], 16);
926
- if (Number.isFinite(codePoint) && codePoint <= 1114111) {
927
- value += String.fromCodePoint(codePoint);
928
- index += braced[0].length + 1;
929
- continue;
1623
+ if (msg.type === "assistant") {
1624
+ const message = msg.message;
1625
+ const stopReason = message?.stop_reason;
1626
+ if (stopReason === "end_turn") {
1627
+ const work = getBackgroundWork();
1628
+ if (deliveringNotification || work.undeliveredNotifications > 0) return result("processing");
1629
+ if (work.pendingDescriptions.length > 0) {
1630
+ return awaitingAgentsResult(work.pendingDescriptions, Math.max(0, pendingEnqueues));
930
1631
  }
1632
+ let hasActivity = false;
1633
+ for (let j = i - 1; j >= 0; j--) {
1634
+ const m = rawMessages[j];
1635
+ if (m.type === "user" && !m.isMeta) {
1636
+ hasActivity = true;
1637
+ break;
1638
+ }
1639
+ }
1640
+ return result(hasActivity ? "completed" : "idle");
931
1641
  }
932
- const hex = source.slice(index + 1, index + 5);
933
- if (/^[0-9a-fA-F]{4}$/.test(hex)) {
934
- value += String.fromCharCode(Number.parseInt(hex, 16));
935
- index += 5;
936
- continue;
1642
+ if (stopReason === "tool_use") {
1643
+ const content = message?.content;
1644
+ const toolUseBlock = content?.findLast?.((b) => b.type === "tool_use");
1645
+ return result("tool_use", toolUseBlock?.name);
937
1646
  }
1647
+ return result("thinking");
938
1648
  }
939
- value += escaped;
940
- index++;
1649
+ if (msg.type === "user") {
1650
+ const isMeta = msg.isMeta;
1651
+ if (isMeta) continue;
1652
+ return result("processing");
1653
+ }
1654
+ if (msg.type === "system" && msg.subtype === "terminal_reason") {
1655
+ const reason = msg.reason;
1656
+ if (reason) {
1657
+ return { status: "completed", terminalReason: reason, pendingQueue: Math.max(0, pendingEnqueues) };
1658
+ }
1659
+ continue;
1660
+ }
1661
+ if (msg.type === "summary") continue;
1662
+ if (msg.type === "system" && msg.subtype === "compact_boundary") continue;
941
1663
  }
942
- return null;
1664
+ return { status: "idle" };
1665
+ }
1666
+ function brandClaudeBranch(firstRecord, sessionId, turnIndex) {
1667
+ const originalId = typeof firstRecord.sessionId === "string" ? firstRecord.sessionId : "";
1668
+ return {
1669
+ record: { ...firstRecord, sessionId, branchedFrom: { sessionId: originalId, turnIndex } },
1670
+ originalId
1671
+ };
1672
+ }
1673
+ function claudeTurnBoundaries(records) {
1674
+ return buildTurnsWithStarts(records).turnStartIndices;
1675
+ }
1676
+
1677
+ // src/lib/turnContent.ts
1678
+ function appendAssistantText(turn, text, timestamp) {
1679
+ if (!text) return;
1680
+ turn.assistantText.push(text);
1681
+ const last = turn.contentBlocks[turn.contentBlocks.length - 1];
1682
+ if (last?.kind === "text") {
1683
+ last.text.push(text);
1684
+ return;
1685
+ }
1686
+ turn.contentBlocks.push({ kind: "text", text: [text], timestamp });
943
1687
  }
944
1688
 
945
1689
  // src/lib/codex-patches.ts
@@ -977,8 +1721,8 @@ function extractApplyPatchInputs(execSource) {
977
1721
  function resolveCodexPatchPath(filePath, cwd) {
978
1722
  if (!cwd || filePath.startsWith("/") || /^[A-Za-z]:[\\/]/.test(filePath)) return filePath;
979
1723
  const base = cwd.replace(/[\\/]+$/, "");
980
- const relative = filePath.replace(/^\.[\\/]/, "");
981
- return /^[A-Za-z]:[\\/]/.test(cwd) ? `${base}\\${relative.replace(/\//g, "\\")}` : `${base}/${relative}`;
1724
+ const relative2 = filePath.replace(/^\.[\\/]/, "");
1725
+ return /^[A-Za-z]:[\\/]/.test(cwd) ? `${base}\\${relative2.replace(/\//g, "\\")}` : `${base}/${relative2}`;
982
1726
  }
983
1727
  function parseApplyPatch(patchText, callId, timestamp, cwd = "") {
984
1728
  const toolCalls = [];
@@ -1102,7 +1846,7 @@ function findFailedNestedPatchCallIds(output, wrapperIsError, calls) {
1102
1846
  }
1103
1847
 
1104
1848
  // src/lib/codex-tool-normalization.ts
1105
- function isObject(value) {
1849
+ function isObject2(value) {
1106
1850
  return typeof value === "object" && value !== null;
1107
1851
  }
1108
1852
  var COLLABORATION_TOOL_NAMES = /* @__PURE__ */ new Map([
@@ -1119,43 +1863,135 @@ var COLLABORATION_TOOL_NAMES = /* @__PURE__ */ new Map([
1119
1863
  ["interrupt_agent", "interrupt_agent"],
1120
1864
  ["interruptAgent", "interrupt_agent"]
1121
1865
  ]);
1866
+ var NATIVE_TOOL_NAMES = /* @__PURE__ */ new Set([
1867
+ "exec",
1868
+ "exec_command",
1869
+ "write_stdin",
1870
+ "apply_patch",
1871
+ "update_plan",
1872
+ "view_image",
1873
+ "request_user_input",
1874
+ "request_user_input_async",
1875
+ "tool_search",
1876
+ "list_mcp_resources",
1877
+ "list_mcp_resource_templates",
1878
+ "read_mcp_resource",
1879
+ "request_plugin_install",
1880
+ "create_goal",
1881
+ "get_goal",
1882
+ "update_goal",
1883
+ "wait"
1884
+ ]);
1122
1885
  function normalizeFunctionName(rawName) {
1886
+ if (rawName.startsWith("mcp__")) return rawName;
1123
1887
  const leaf = rawName.split(/(?:__|[.:/])+/).filter(Boolean).at(-1) ?? rawName;
1124
- return COLLABORATION_TOOL_NAMES.get(leaf) ?? rawName;
1888
+ return COLLABORATION_TOOL_NAMES.get(leaf) ?? (NATIVE_TOOL_NAMES.has(leaf) ? leaf : rawName);
1889
+ }
1890
+ function hasFailedExit(output) {
1891
+ const exitMatch = output?.match(/Process exited with code (\d+)/);
1892
+ return exitMatch ? exitMatch[1] !== "0" : false;
1893
+ }
1894
+ function isCodexQuestionTool(name) {
1895
+ return name === "request_user_input" || name === "request_user_input_async";
1125
1896
  }
1126
- function inferToolError(output) {
1127
- if (!output) return false;
1128
- const exitMatch = output.match(/Process exited with code (\d+)/);
1129
- if (exitMatch) return exitMatch[1] !== "0";
1130
- const withoutSuccessSummaries = output.replace(/\b0\s+(?:fail(?:ed|ures?)?|errors?)\b/gi, "").replace(/\bno\s+(?:failures?|errors?)\b/gi, "");
1131
- return /\b(error|failed|exception)\b/i.test(withoutSuccessSummaries);
1897
+ function normalizeQuestions(input) {
1898
+ const source = Array.isArray(input.questions) ? input.questions : [input];
1899
+ const questions = source.filter((question) => isObject2(question)).map((question) => {
1900
+ const text = typeof question.question === "string" ? question.question : typeof question.title === "string" ? question.title : "";
1901
+ const options = Array.isArray(question.options) ? question.options : [];
1902
+ return {
1903
+ question: text,
1904
+ ...typeof question.header === "string" && question.header ? { header: question.header } : {},
1905
+ options: options.map((option) => {
1906
+ if (typeof option === "string") return { label: option };
1907
+ if (!isObject2(option) || typeof option.label !== "string") return null;
1908
+ return {
1909
+ label: option.label,
1910
+ ...typeof option.description === "string" && option.description ? { description: option.description } : {}
1911
+ };
1912
+ }).filter((option) => option !== null)
1913
+ };
1914
+ }).filter((question) => question.question);
1915
+ return questions.length > 0 ? { ...input, questions } : input;
1132
1916
  }
1133
1917
  function normalizePlanToTodos(input) {
1134
1918
  const plan = Array.isArray(input.plan) ? input.plan : [];
1135
- const todos = plan.filter((item) => isObject(item)).map((item) => ({
1919
+ const todos = plan.filter((item) => isObject2(item)).map((item) => ({
1136
1920
  content: typeof item.step === "string" ? item.step : "",
1137
1921
  status: typeof item.status === "string" ? item.status : "pending",
1138
1922
  activeForm: typeof item.step === "string" ? item.step : ""
1139
1923
  }));
1140
1924
  return { todos };
1141
1925
  }
1142
- function extractCustomToolOutputText(output) {
1143
- if (typeof output === "string") return output;
1144
- if (!Array.isArray(output)) return "";
1145
- return output.filter((block) => isObject(block) && typeof block.text === "string").map((block) => block.text).join("");
1926
+ function outputImage(block) {
1927
+ if (block.type !== "image" && block.type !== "input_image" && block.type !== "image_url") return null;
1928
+ const source = isObject2(block.source) ? block.source : block;
1929
+ const mediaType = source.media_type ?? source.mimeType;
1930
+ if (typeof mediaType === "string" && mediaType.startsWith("image/") && typeof source.data === "string" && source.data) {
1931
+ return { type: "image", source: { type: "base64", media_type: mediaType, data: source.data } };
1932
+ }
1933
+ const url = isObject2(block.image_url) ? block.image_url.url : block.image_url;
1934
+ const match = typeof url === "string" ? /^data:(image\/[^;,]+);base64,(.+)$/is.exec(url) : null;
1935
+ return match ? { type: "image", source: { type: "base64", media_type: match[1].toLowerCase(), data: match[2] } } : null;
1936
+ }
1937
+ function outputContent(output) {
1938
+ if (typeof output === "string") return { parts: [output], text: output, images: [] };
1939
+ if (!Array.isArray(output)) {
1940
+ const text = output == null ? "" : JSON.stringify(output);
1941
+ return { parts: [text], text, images: [] };
1942
+ }
1943
+ const parts = [];
1944
+ const images = [];
1945
+ for (const block of output) {
1946
+ if (!isObject2(block)) continue;
1947
+ if (typeof block.text === "string") parts.push(block.text);
1948
+ const image = outputImage(block);
1949
+ if (image) images.push(image);
1950
+ }
1951
+ return { parts, text: parts.join(""), images };
1952
+ }
1953
+ var SCRIPT_STATUS = /^Script (?:completed|failed|running)\b/;
1954
+ function collectExitCodes(chunk, codes) {
1955
+ if (!isObject2(chunk)) return;
1956
+ if (chunk.status === "rejected") codes.push(1);
1957
+ else if (chunk.status === "fulfilled") collectExitCodes(chunk.value, codes);
1958
+ else if (typeof chunk.exit_code === "number") codes.push(chunk.exit_code);
1959
+ }
1960
+ function scriptFailed(parts) {
1961
+ const status = parts[0] ?? "";
1962
+ if (!SCRIPT_STATUS.test(status)) return void 0;
1963
+ if (status.startsWith("Script failed")) return true;
1964
+ const codes = [];
1965
+ for (const part of parts.slice(1)) {
1966
+ let chunk;
1967
+ try {
1968
+ chunk = JSON.parse(part);
1969
+ } catch {
1970
+ continue;
1971
+ }
1972
+ for (const entry of Array.isArray(chunk) ? chunk : [chunk]) collectExitCodes(entry, codes);
1973
+ }
1974
+ return codes.length > 0 ? codes.some((code) => code !== 0) : void 0;
1146
1975
  }
1147
1976
  function parseCustomToolOutput(output) {
1148
- const rawOutput = extractCustomToolOutputText(output);
1149
- if (!rawOutput) return { text: "", isError: false };
1977
+ const original = outputContent(output);
1978
+ let envelope = isObject2(output) && !Array.isArray(output) ? output : null;
1150
1979
  try {
1151
- const parsed = JSON.parse(rawOutput);
1152
- const text = typeof parsed.output === "string" ? parsed.output : rawOutput;
1153
- const meta = isObject(parsed.metadata) ? parsed.metadata : null;
1154
- const isError = meta ? meta.exit_code !== 0 && meta.exit_code !== void 0 : inferToolError(text);
1155
- return { text, isError };
1980
+ const parsed = envelope ?? JSON.parse(original.text);
1981
+ if (isObject2(parsed) && !Array.isArray(parsed)) envelope = parsed;
1156
1982
  } catch {
1157
- return { text: rawOutput, isError: inferToolError(rawOutput) };
1158
1983
  }
1984
+ const content = envelope && ("output" in envelope || "content" in envelope) ? outputContent(envelope.output ?? envelope.content) : original;
1985
+ const images = content === original ? original.images : [...original.images, ...content.images];
1986
+ const metadata = isObject2(envelope?.metadata) ? envelope.metadata : null;
1987
+ const exitCode = metadata?.exit_code ?? envelope?.exit_code;
1988
+ const explicitError = typeof envelope?.isError === "boolean" ? envelope.isError : typeof envelope?.is_error === "boolean" ? envelope.is_error : void 0;
1989
+ const isError = explicitError === true || (typeof exitCode === "number" ? exitCode !== 0 : explicitError ?? scriptFailed(original.parts) ?? hasFailedExit(content.text));
1990
+ return {
1991
+ text: content.text,
1992
+ isError,
1993
+ ...images.length > 0 ? { images } : {}
1994
+ };
1159
1995
  }
1160
1996
 
1161
1997
  // src/lib/codex.ts
@@ -1177,7 +2013,7 @@ function safeParseLine(line) {
1177
2013
  return null;
1178
2014
  }
1179
2015
  }
1180
- function isObject2(value) {
2016
+ function isObject3(value) {
1181
2017
  return typeof value === "object" && value !== null;
1182
2018
  }
1183
2019
  function isCodexRecord(record) {
@@ -1187,7 +2023,7 @@ function isCodexRecord(record) {
1187
2023
  function extractMessageText(payload, blockType) {
1188
2024
  const content = payload?.content;
1189
2025
  if (!Array.isArray(content)) return "";
1190
- return content.filter((block) => isObject2(block) && block.type === blockType && typeof block.text === "string").map((block) => block.text).join("\n").trim();
2026
+ return content.filter((block) => isObject3(block) && block.type === blockType && typeof block.text === "string").map((block) => block.text).join("\n").trim();
1191
2027
  }
1192
2028
  var CODEX_IMAGE_DATA_URL = /^data:(image\/(?:png|jpeg|gif|webp));base64,(.+)$/is;
1193
2029
  var CODEX_AUDIO_DATA_URL = /^data:(audio\/(?:wav|x-wav|mpeg|mp4|webm|ogg));base64,(.+)$/is;
@@ -1227,11 +2063,11 @@ function extractEventMessageAudio(payload) {
1227
2063
  }
1228
2064
  function extractResponseMessageImages(payload) {
1229
2065
  if (!Array.isArray(payload.content)) return [];
1230
- return payload.content.filter((block) => isObject2(block) && block.type === "input_image").map((block) => parseImageDataUrl(block.image_url)).filter((image) => image !== null);
2066
+ return payload.content.filter((block) => isObject3(block) && block.type === "input_image").map((block) => parseImageDataUrl(block.image_url)).filter((image) => image !== null);
1231
2067
  }
1232
2068
  function extractResponseMessageAudio(payload) {
1233
2069
  if (!Array.isArray(payload.content)) return [];
1234
- return payload.content.filter((block) => isObject2(block) && block.type === "input_audio").map((block) => parseAudioDataUrl(block.audio_url)).filter((audio) => audio !== null);
2070
+ return payload.content.filter((block) => isObject3(block) && block.type === "input_audio").map((block) => parseAudioDataUrl(block.audio_url)).filter((audio) => audio !== null);
1235
2071
  }
1236
2072
  function buildCodexUserContent(message, images, audio = [], localImages = [], localAudio = []) {
1237
2073
  const attachmentSuffix = [
@@ -1276,17 +2112,8 @@ function normalizePromptText(text) {
1276
2112
  if (SKIP_PROMPT_PREFIXES.some((prefix) => trimmed.startsWith(prefix))) return "";
1277
2113
  return trimmed;
1278
2114
  }
1279
- function mergeTokenUsage2(existing, incoming) {
1280
- if (!existing) return { ...incoming };
1281
- return {
1282
- input_tokens: existing.input_tokens + incoming.input_tokens,
1283
- output_tokens: existing.output_tokens + incoming.output_tokens,
1284
- cache_creation_input_tokens: (existing.cache_creation_input_tokens ?? 0) + (incoming.cache_creation_input_tokens ?? 0),
1285
- cache_read_input_tokens: (existing.cache_read_input_tokens ?? 0) + (incoming.cache_read_input_tokens ?? 0)
1286
- };
1287
- }
1288
2115
  function parseTokenUsage(value) {
1289
- if (!isObject2(value)) return null;
2116
+ if (!isObject3(value)) return null;
1290
2117
  const reportedInputTokens = typeof value.input_tokens === "number" ? value.input_tokens : 0;
1291
2118
  const outputTokens = typeof value.output_tokens === "number" ? value.output_tokens : 0;
1292
2119
  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;
@@ -1301,16 +2128,6 @@ function parseTokenUsage(value) {
1301
2128
  cache_read_input_tokens: cacheRead
1302
2129
  };
1303
2130
  }
1304
- function appendAssistantText(turn, text, timestamp) {
1305
- if (!text) return;
1306
- turn.assistantText.push(text);
1307
- const last = turn.contentBlocks[turn.contentBlocks.length - 1];
1308
- if (last && last.kind === "text") {
1309
- last.text.push(text);
1310
- return;
1311
- }
1312
- turn.contentBlocks.push({ kind: "text", text: [text], timestamp });
1313
- }
1314
2131
  function appendThinking(turn, text, timestamp) {
1315
2132
  if (!text) return;
1316
2133
  const block = { type: "thinking", thinking: text, signature: "" };
@@ -1349,7 +2166,7 @@ function parseToolInput(argumentsText) {
1349
2166
  if (typeof argumentsText !== "string" || !argumentsText.trim()) return {};
1350
2167
  try {
1351
2168
  const parsed = JSON.parse(argumentsText);
1352
- return isObject2(parsed) ? parsed : { value: parsed };
2169
+ return isObject3(parsed) ? parsed : { value: parsed };
1353
2170
  } catch {
1354
2171
  return { raw: argumentsText };
1355
2172
  }
@@ -1369,15 +2186,16 @@ function isCodeModeChild(callId) {
1369
2186
  return callId.startsWith("exec-");
1370
2187
  }
1371
2188
  function parseMcpEventResult(value) {
1372
- if (!isObject2(value)) return { text: safeStringify(value), isError: false };
2189
+ if (!isObject3(value)) return { text: safeStringify(value), isError: false };
1373
2190
  if ("Err" in value) return { text: safeStringify(value.Err), isError: true };
1374
- const ok = isObject2(value.Ok) ? value.Ok : null;
2191
+ const ok = isObject3(value.Ok) ? value.Ok : null;
1375
2192
  if (!ok) return { text: safeStringify(value), isError: false };
1376
- const parsedContent = parseCustomToolOutput(ok.content);
1377
- const text = parsedContent.text || (ok.structuredContent !== void 0 ? safeStringify(ok.structuredContent) : safeStringify(ok));
2193
+ const parsedContent = parseCustomToolOutput({ ...ok, content: ok.content });
2194
+ const text = parsedContent.text || (parsedContent.images?.length ? "" : ok.structuredContent !== void 0 ? safeStringify(ok.structuredContent) : safeStringify(ok));
1378
2195
  return {
2196
+ ...parsedContent,
1379
2197
  text,
1380
- isError: ok.isError === true || parsedContent.isError
2198
+ isError: parsedContent.isError
1381
2199
  };
1382
2200
  }
1383
2201
  function createTurn2(turnId, timestamp, model) {
@@ -1396,10 +2214,10 @@ function createTurn2(turnId, timestamp, model) {
1396
2214
  };
1397
2215
  }
1398
2216
  function extractPromptFromRecord(record) {
1399
- if (record.type === "event_msg" && isObject2(record.payload) && record.payload.type === "user_message" && typeof record.payload.message === "string") {
2217
+ if (record.type === "event_msg" && isObject3(record.payload) && record.payload.type === "user_message" && typeof record.payload.message === "string") {
1400
2218
  return normalizePromptText(record.payload.message);
1401
2219
  }
1402
- if (record.type === "response_item" && isObject2(record.payload) && record.payload.type === "message" && record.payload.role === "user") {
2220
+ if (record.type === "response_item" && isObject3(record.payload) && record.payload.type === "message" && record.payload.role === "user") {
1403
2221
  return normalizePromptText(extractMessageText(record.payload, "input_text"));
1404
2222
  }
1405
2223
  return "";
@@ -1432,12 +2250,12 @@ function extractMetadataFromRecords(records) {
1432
2250
  let agentPath = "/root";
1433
2251
  let previousPrompt = "";
1434
2252
  for (const record of records) {
1435
- if (!isObject2(record.payload)) continue;
2253
+ if (!isObject3(record.payload)) continue;
1436
2254
  if (record.type === "session_meta") {
1437
2255
  sessionId ||= typeof record.payload.id === "string" ? record.payload.id : "";
1438
2256
  version ||= typeof record.payload.cli_version === "string" ? record.payload.cli_version : "";
1439
2257
  cwd ||= typeof record.payload.cwd === "string" ? record.payload.cwd : "";
1440
- if (!branchedFrom && isObject2(record.payload.branchedFrom)) {
2258
+ if (!branchedFrom && isObject3(record.payload.branchedFrom)) {
1441
2259
  const sourceId = typeof record.payload.branchedFrom.sessionId === "string" ? record.payload.branchedFrom.sessionId : "";
1442
2260
  if (sourceId) {
1443
2261
  branchedFrom = {
@@ -1446,10 +2264,10 @@ function extractMetadataFromRecords(records) {
1446
2264
  };
1447
2265
  }
1448
2266
  }
1449
- const source = isObject2(record.payload.source) ? record.payload.source : null;
1450
- if (source && isObject2(source.subagent)) {
2267
+ const source = isObject3(record.payload.source) ? record.payload.source : null;
2268
+ if (source && isObject3(source.subagent)) {
1451
2269
  isSubagent = true;
1452
- const threadSpawn = isObject2(source.subagent.thread_spawn) ? source.subagent.thread_spawn : null;
2270
+ const threadSpawn = isObject3(source.subagent.thread_spawn) ? source.subagent.thread_spawn : null;
1453
2271
  if (threadSpawn && typeof threadSpawn.agent_path === "string") {
1454
2272
  agentPath = threadSpawn.agent_path;
1455
2273
  }
@@ -1457,7 +2275,7 @@ function extractMetadataFromRecords(records) {
1457
2275
  if (typeof record.payload.forked_from_id === "string" && record.payload.forked_from_id) {
1458
2276
  parentSessionId = record.payload.forked_from_id;
1459
2277
  }
1460
- const git = isObject2(record.payload.git) ? record.payload.git : null;
2278
+ const git = isObject3(record.payload.git) ? record.payload.git : null;
1461
2279
  gitBranch ||= git && typeof git.branch === "string" ? git.branch : "";
1462
2280
  }
1463
2281
  if (record.type === "turn_context") {
@@ -1502,13 +2320,17 @@ function isCodexSessionText(jsonlText) {
1502
2320
  }
1503
2321
  return false;
1504
2322
  }
2323
+ function isCodexRawRecords(records) {
2324
+ return isCodexRecord(records[0] ?? null);
2325
+ }
1505
2326
  function extractCodexMetadataFromLines(lines) {
1506
2327
  const records = lines.map(safeParseLine).filter(isCodexRecord);
1507
2328
  return extractMetadataFromRecords(records);
1508
2329
  }
1509
- function parseCodexSession(jsonlText, options) {
1510
- const records = jsonlText.split("\n").map((line) => line.trim()).filter(Boolean).map(safeParseLine).filter(isCodexRecord);
1511
- const metadata = extractMetadataFromRecords(records);
2330
+ function walkCodexRecords(records, options) {
2331
+ const metadata = extractMetadataFromRecords(
2332
+ records.filter(isCodexRecord)
2333
+ );
1512
2334
  const turns = [];
1513
2335
  const pendingToolCalls = /* @__PURE__ */ new Map();
1514
2336
  const webSearchCalls = /* @__PURE__ */ new Map();
@@ -1518,7 +2340,7 @@ function parseCodexSession(jsonlText, options) {
1518
2340
  const agentIdByPath = /* @__PURE__ */ new Map();
1519
2341
  const agentIdBySpawnCall = /* @__PURE__ */ new Map();
1520
2342
  const agentMessages = /* @__PURE__ */ new Map();
1521
- function upsertAgentMessage(agentId, targetTurn, eventTimestamp, update = {}) {
2343
+ function upsertAgentMessage(agentId, targetTurn, eventTimestamp2, update = {}) {
1522
2344
  const info = agentRegistry.get(agentId);
1523
2345
  let entry = agentMessages.get(agentId);
1524
2346
  if (!entry) {
@@ -1532,7 +2354,7 @@ function parseCodexSession(jsonlText, options) {
1532
2354
  toolCalls: [],
1533
2355
  thinking: [],
1534
2356
  text: [],
1535
- timestamp: info?.timestamp ?? eventTimestamp,
2357
+ timestamp: info?.timestamp ?? eventTimestamp2,
1536
2358
  tokenUsage: null,
1537
2359
  model: info?.model ?? null,
1538
2360
  isBackground: false,
@@ -1544,7 +2366,7 @@ function parseCodexSession(jsonlText, options) {
1544
2366
  if (lastBlock?.kind === "sub_agent") {
1545
2367
  lastBlock.messages.push(agentMessage2);
1546
2368
  } else {
1547
- targetTurn.contentBlocks.push({ kind: "sub_agent", messages: [agentMessage2], timestamp: eventTimestamp });
2369
+ targetTurn.contentBlocks.push({ kind: "sub_agent", messages: [agentMessage2], timestamp: eventTimestamp2 });
1548
2370
  }
1549
2371
  entry = { message: agentMessage2, turn: targetTurn };
1550
2372
  agentMessages.set(agentId, entry);
@@ -1566,9 +2388,9 @@ function parseCodexSession(jsonlText, options) {
1566
2388
  if (!(update.status === "running" && agentMessage.status && terminalStatuses.has(agentMessage.status))) {
1567
2389
  agentMessage.status = update.status;
1568
2390
  }
1569
- if (terminalStatuses.has(update.status) && agentMessage.timestamp && eventTimestamp) {
2391
+ if (terminalStatuses.has(update.status) && agentMessage.timestamp && eventTimestamp2) {
1570
2392
  const startedAt = new Date(agentMessage.timestamp).getTime();
1571
- const endedAt = new Date(eventTimestamp).getTime();
2393
+ const endedAt = new Date(eventTimestamp2).getTime();
1572
2394
  if (Number.isFinite(startedAt) && Number.isFinite(endedAt) && endedAt >= startedAt) {
1573
2395
  agentMessage.durationMs = endedAt - startedAt;
1574
2396
  }
@@ -1582,8 +2404,23 @@ function parseCodexSession(jsonlText, options) {
1582
2404
  let lastTurnTimestamp = "";
1583
2405
  let pendingCompaction = null;
1584
2406
  let sawTurnStart = false;
2407
+ const turnStartIndices = [];
2408
+ let recordIndex = 0;
2409
+ let pendingContextIndex = null;
2410
+ let pendingTurnStart = null;
2411
+ function commitTurn(turn) {
2412
+ const before = turns.length;
2413
+ const result = finalizeTurn(turns, turn, lastTurnTimestamp);
2414
+ if (turns.length > before && pendingTurnStart !== null) {
2415
+ turnStartIndices.push(pendingTurnStart);
2416
+ }
2417
+ pendingTurnStart = null;
2418
+ return result;
2419
+ }
1585
2420
  function createActiveTurn(turnId, timestamp, model) {
1586
2421
  const turn = createTurn2(turnId, timestamp, model);
2422
+ pendingTurnStart = pendingContextIndex ?? recordIndex;
2423
+ pendingContextIndex = null;
1587
2424
  if (!sawTurnStart) turn.isFragment = true;
1588
2425
  if (pendingCompaction) {
1589
2426
  turn.compactionSummary = pendingCompaction;
@@ -1610,18 +2447,22 @@ function parseCodexSession(jsonlText, options) {
1610
2447
  webSearchCalls.set(id, toolCall);
1611
2448
  appendToolCall(targetTurn, toolCall, timestamp);
1612
2449
  }
1613
- for (const record of records) {
1614
- const payload = isObject2(record.payload) ? record.payload : void 0;
2450
+ for (let index = 0; index < records.length; index++) {
2451
+ const record = records[index];
2452
+ if (!isCodexRecord(record)) continue;
2453
+ recordIndex = index;
2454
+ const payload = isObject3(record.payload) ? record.payload : void 0;
1615
2455
  const timestamp = record.timestamp ?? "";
1616
2456
  if (record.type === "compacted") {
1617
- current = finalizeTurn(turns, current, lastTurnTimestamp);
2457
+ current = commitTurn(current);
1618
2458
  pendingCompaction = "Conversation compacted";
1619
2459
  lastTurnTimestamp = timestamp;
1620
2460
  continue;
1621
2461
  }
1622
2462
  if (record.type === "world_state") continue;
1623
2463
  if (record.type === "turn_context") {
1624
- current = finalizeTurn(turns, current, lastTurnTimestamp);
2464
+ current = commitTurn(current);
2465
+ pendingContextIndex = index;
1625
2466
  currentTurnId = typeof payload?.turn_id === "string" ? payload.turn_id : null;
1626
2467
  currentModel = typeof payload?.model === "string" ? payload.model : currentModel;
1627
2468
  lastTurnTimestamp = timestamp;
@@ -1630,7 +2471,7 @@ function parseCodexSession(jsonlText, options) {
1630
2471
  }
1631
2472
  if (record.type === "event_msg" && payload?.type === "user_message" && typeof payload.message === "string") {
1632
2473
  if (current && (current.assistantText.length > 0 || current.toolCalls.length > 0 || current.thinking.length > 0)) {
1633
- current = finalizeTurn(turns, current, lastTurnTimestamp);
2474
+ current = commitTurn(current);
1634
2475
  }
1635
2476
  sawTurnStart = true;
1636
2477
  current ??= createActiveTurn(currentTurnId, timestamp, currentModel);
@@ -1658,30 +2499,31 @@ function parseCodexSession(jsonlText, options) {
1658
2499
  if (!current.timestamp) current.timestamp = timestamp;
1659
2500
  if (timestamp) lastTurnTimestamp = timestamp;
1660
2501
  if (record.type === "event_msg" && payload?.type === "token_count") {
1661
- const info = isObject2(payload.info) ? payload.info : null;
2502
+ const info = isObject3(payload.info) ? payload.info : null;
1662
2503
  const lastUsage = info ? parseTokenUsage(info.last_token_usage) : null;
1663
2504
  if (lastUsage) {
1664
- current.tokenUsage = mergeTokenUsage2(current.tokenUsage, lastUsage);
2505
+ current.tokenUsage = mergeTokenUsage(current.tokenUsage, lastUsage);
1665
2506
  }
1666
2507
  continue;
1667
2508
  }
1668
2509
  if (record.type === "event_msg" && payload?.type === "web_search_end" && typeof payload.call_id === "string") {
1669
2510
  const input = {};
1670
2511
  if (typeof payload.query === "string" && payload.query) input.query = payload.query;
1671
- if (isObject2(payload.action)) input.action = payload.action;
2512
+ if (isObject3(payload.action)) input.action = payload.action;
1672
2513
  upsertWebSearchCall(current, payload.call_id, input, "completed", timestamp);
1673
2514
  continue;
1674
2515
  }
1675
- if (record.type === "event_msg" && payload?.type === "mcp_tool_call_end" && typeof payload.call_id === "string" && isObject2(payload.invocation)) {
2516
+ if (record.type === "event_msg" && payload?.type === "mcp_tool_call_end" && typeof payload.call_id === "string" && isObject3(payload.invocation)) {
1676
2517
  const invocation = payload.invocation;
1677
2518
  const server = typeof invocation.server === "string" ? invocation.server : "mcp";
1678
2519
  const tool = typeof invocation.tool === "string" ? invocation.tool : "tool";
1679
- const input = isObject2(invocation.arguments) ? invocation.arguments : {};
2520
+ const input = isObject3(invocation.arguments) ? invocation.arguments : {};
1680
2521
  const result = parseMcpEventResult(payload.result);
1681
2522
  const existing = pendingToolCalls.get(payload.call_id);
1682
2523
  if (existing) {
1683
2524
  existing.result = result.text;
1684
2525
  existing.isError = result.isError;
2526
+ if (result.images) existing.resultImages = result.images;
1685
2527
  pendingToolCalls.delete(payload.call_id);
1686
2528
  } else if (!isCodeModeChild(payload.call_id)) {
1687
2529
  appendToolCall(current, {
@@ -1690,6 +2532,7 @@ function parseCodexSession(jsonlText, options) {
1690
2532
  input,
1691
2533
  result: result.text,
1692
2534
  isError: result.isError,
2535
+ ...result.images ? { resultImages: result.images } : {},
1693
2536
  timestamp
1694
2537
  }, timestamp);
1695
2538
  }
@@ -1697,9 +2540,9 @@ function parseCodexSession(jsonlText, options) {
1697
2540
  }
1698
2541
  if (record.type === "event_msg" && payload?.type === "sub_agent_activity" && typeof payload.agent_thread_id === "string" && typeof payload.agent_path === "string") {
1699
2542
  const agentId = payload.agent_thread_id;
1700
- const eventId = typeof payload.event_id === "string" ? payload.event_id : "";
2543
+ const eventId2 = typeof payload.event_id === "string" ? payload.event_id : "";
1701
2544
  const kind = typeof payload.kind === "string" ? payload.kind : "started";
1702
- const spawnInfo = eventId ? spawnAgentCalls.get(eventId) : void 0;
2545
+ const spawnInfo = eventId2 ? spawnAgentCalls.get(eventId2) : void 0;
1703
2546
  const existingId = agentIdByPath.get(payload.agent_path);
1704
2547
  const existing = agentRegistry.get(agentId) ?? (existingId ? agentRegistry.get(existingId) : void 0);
1705
2548
  if (kind !== "started" && !existing && !spawnInfo) continue;
@@ -1720,11 +2563,11 @@ function parseCodexSession(jsonlText, options) {
1720
2563
  model: existing?.model ?? spawnInfo?.model ?? null,
1721
2564
  agentType: existing?.agentType ?? spawnInfo?.agentType ?? null,
1722
2565
  timestamp: existing?.timestamp ?? (timestamp || spawnInfo?.timestamp || ""),
1723
- parentToolCallId: existing?.parentToolCallId ?? eventId
2566
+ parentToolCallId: existing?.parentToolCallId ?? eventId2
1724
2567
  };
1725
2568
  agentRegistry.set(agentId, info);
1726
2569
  agentIdByPath.set(payload.agent_path, agentId);
1727
- if (eventId && (kind === "started" || spawnInfo)) agentIdBySpawnCall.set(eventId, agentId);
2570
+ if (eventId2 && (kind === "started" || spawnInfo)) agentIdBySpawnCall.set(eventId2, agentId);
1728
2571
  const status = kind === "interrupted" ? "interrupted" : "running";
1729
2572
  upsertAgentMessage(agentId, current, timestamp, { status });
1730
2573
  continue;
@@ -1752,7 +2595,7 @@ function parseCodexSession(jsonlText, options) {
1752
2595
  }
1753
2596
  if (payload.type === "web_search_call" && typeof payload.id === "string") {
1754
2597
  const input = {};
1755
- if (isObject2(payload.action)) input.action = payload.action;
2598
+ if (isObject3(payload.action)) input.action = payload.action;
1756
2599
  const status = typeof payload.status === "string" ? payload.status : "completed";
1757
2600
  upsertWebSearchCall(current, payload.id, input, status, timestamp);
1758
2601
  continue;
@@ -1763,7 +2606,7 @@ function parseCodexSession(jsonlText, options) {
1763
2606
  const toolCall = {
1764
2607
  id: callId,
1765
2608
  name: "ToolSearch",
1766
- input: isObject2(payload.arguments) ? payload.arguments : {},
2609
+ input: isObject3(payload.arguments) ? payload.arguments : {},
1767
2610
  result: null,
1768
2611
  isError: false,
1769
2612
  timestamp
@@ -1777,7 +2620,7 @@ function parseCodexSession(jsonlText, options) {
1777
2620
  const toolCall = pendingToolCalls.get(payload.call_id);
1778
2621
  if (toolCall) {
1779
2622
  const tools = Array.isArray(payload.tools) ? payload.tools : [];
1780
- const names = tools.filter(isObject2).map((tool) => findString(tool.name) ?? "").filter(Boolean);
2623
+ const names = tools.filter(isObject3).map((tool) => findString(tool.name) ?? "").filter(Boolean);
1781
2624
  const count = names.length > 0 ? names.length : tools.length;
1782
2625
  const found = `Found ${count} tool${count === 1 ? "" : "s"}`;
1783
2626
  toolCall.result = names.length > 0 ? `${found}: ${names.join(", ")}` : found;
@@ -1830,6 +2673,9 @@ function parseCodexSession(jsonlText, options) {
1830
2673
  } else if (functionName === "update_plan") {
1831
2674
  name = "TodoWrite";
1832
2675
  input = normalizePlanToTodos(parsedInput);
2676
+ } else if (isCodexQuestionTool(functionName)) {
2677
+ name = "AskUserQuestion";
2678
+ input = normalizeQuestions(parsedInput);
1833
2679
  }
1834
2680
  if (functionName === "spawn_agent") {
1835
2681
  spawnAgentCalls.set(payload.call_id, {
@@ -1847,7 +2693,8 @@ function parseCodexSession(jsonlText, options) {
1847
2693
  input,
1848
2694
  result: null,
1849
2695
  isError: false,
1850
- timestamp
2696
+ timestamp,
2697
+ ...functionName === "request_user_input_async" ? { asyncQuestion: true } : {}
1851
2698
  };
1852
2699
  pendingToolCalls.set(toolCall.id, toolCall);
1853
2700
  appendToolCall(current, toolCall, timestamp);
@@ -1856,9 +2703,11 @@ function parseCodexSession(jsonlText, options) {
1856
2703
  if (payload.type === "function_call_output" && typeof payload.call_id === "string") {
1857
2704
  const toolCall = pendingToolCalls.get(payload.call_id);
1858
2705
  if (!toolCall) continue;
1859
- const output = typeof payload.output === "string" ? payload.output : null;
2706
+ const parsedOutput = parseCustomToolOutput(payload.output);
2707
+ const output = parsedOutput.text;
1860
2708
  toolCall.result = output;
1861
- toolCall.isError = inferToolError(output);
2709
+ toolCall.isError = parsedOutput.isError;
2710
+ if (parsedOutput.images) toolCall.resultImages = parsedOutput.images;
1862
2711
  pendingToolCalls.delete(payload.call_id);
1863
2712
  if (toolCall.name === "spawn_agent" && output) {
1864
2713
  try {
@@ -1890,9 +2739,9 @@ function parseCodexSession(jsonlText, options) {
1890
2739
  if (toolCall.name === "wait_agent" && output && current) {
1891
2740
  try {
1892
2741
  const result = JSON.parse(output);
1893
- const statusMap = isObject2(result.status) ? result.status : {};
2742
+ const statusMap = isObject3(result.status) ? result.status : {};
1894
2743
  for (const [agentId, status] of Object.entries(statusMap)) {
1895
- const statusValue = isObject2(status) ? status : {};
2744
+ const statusValue = isObject3(status) ? status : {};
1896
2745
  const completedText = typeof statusValue.completed === "string" ? statusValue.completed : "";
1897
2746
  const failedText = typeof statusValue.failed === "string" ? statusValue.failed : typeof statusValue.error === "string" ? statusValue.error : "";
1898
2747
  const interruptedText = typeof statusValue.interrupted === "string" ? statusValue.interrupted : "";
@@ -1920,7 +2769,7 @@ function parseCodexSession(jsonlText, options) {
1920
2769
  continue;
1921
2770
  }
1922
2771
  if (payload.type === "custom_tool_call" && typeof payload.call_id === "string") {
1923
- const name = typeof payload.name === "string" ? payload.name : "tool";
2772
+ const name = normalizeFunctionName(typeof payload.name === "string" ? payload.name : "tool");
1924
2773
  const rawInput = typeof payload.input === "string" ? payload.input : "";
1925
2774
  const callId = payload.call_id;
1926
2775
  const perFileCalls = rawInput ? parseCodexToolPatches(name, rawInput, callId, timestamp, metadata.cwd) : [];
@@ -1950,7 +2799,7 @@ function parseCodexSession(jsonlText, options) {
1950
2799
  }
1951
2800
  if (payload.type === "custom_tool_call_output" && typeof payload.call_id === "string") {
1952
2801
  const callId = payload.call_id;
1953
- const { text, isError } = parseCustomToolOutput(payload.output);
2802
+ const { text, isError, images } = parseCustomToolOutput(payload.output);
1954
2803
  const patchCalls = patchCallIds.get(callId);
1955
2804
  if (patchCalls) {
1956
2805
  const failedIds = patchCalls.direct && isError ? new Set(patchCalls.calls.map((call) => call.id)) : findFailedNestedPatchCallIds(text, isError, patchCalls.calls);
@@ -1968,13 +2817,15 @@ function parseCodexSession(jsonlText, options) {
1968
2817
  if (toolCall) {
1969
2818
  toolCall.result = text;
1970
2819
  toolCall.isError = isError;
2820
+ if (images) toolCall.resultImages = images;
1971
2821
  pendingToolCalls.delete(callId);
1972
2822
  }
1973
2823
  continue;
1974
2824
  }
1975
2825
  }
1976
- finalizeTurn(turns, current, lastTurnTimestamp);
2826
+ commitTurn(current);
1977
2827
  return {
2828
+ turnStartIndices,
1978
2829
  sessionId: metadata.sessionId,
1979
2830
  version: metadata.version,
1980
2831
  gitBranch: metadata.gitBranch,
@@ -1989,115 +2840,1120 @@ function parseCodexSession(jsonlText, options) {
1989
2840
  agentKind: "codex"
1990
2841
  };
1991
2842
  }
2843
+ function parseCodexSession(jsonlText, options) {
2844
+ const records = jsonlText.split("\n").map((line) => line.trim()).filter(Boolean).map(safeParseLine).filter(isCodexRecord);
2845
+ const { turnStartIndices: _starts, ...session } = walkCodexRecords(
2846
+ records,
2847
+ options
2848
+ );
2849
+ return session;
2850
+ }
2851
+ function appendCodexSession(existing, newJsonlText) {
2852
+ const prefix = existing.rawMessages.map((record) => JSON.stringify(record)).join("\n");
2853
+ return parseCodexSession(prefix ? `${prefix}
2854
+ ${newJsonlText}` : newJsonlText);
2855
+ }
2856
+ var TERMINAL_INTER_AGENT_TYPES = /* @__PURE__ */ new Set(["FINAL_ANSWER", "ERROR", "FAILED", "INTERRUPTED"]);
2857
+ function codexPayloadText(payload) {
2858
+ const content = payload.content;
2859
+ if (!Array.isArray(content)) return "";
2860
+ return content.filter((block) => isObject3(block) && block.type === "input_text" && typeof block.text === "string").map((block) => block.text).join("\n");
2861
+ }
2862
+ function parseJsonObject(value) {
2863
+ if (typeof value !== "string") return null;
2864
+ try {
2865
+ const parsed = JSON.parse(value);
2866
+ return isObject3(parsed) ? parsed : null;
2867
+ } catch {
2868
+ return null;
2869
+ }
2870
+ }
2871
+ function collectCodexPendingAgents(rawMessages) {
2872
+ const alive = /* @__PURE__ */ new Map();
2873
+ const done = /* @__PURE__ */ new Set();
2874
+ const agentIdByPath = /* @__PURE__ */ new Map();
2875
+ const spawnCalls = /* @__PURE__ */ new Map();
2876
+ const waitCalls = /* @__PURE__ */ new Set();
2877
+ const interruptCalls = /* @__PURE__ */ new Map();
2878
+ function markAlive(agentId, description) {
2879
+ if (!done.has(agentId)) alive.set(agentId, alive.get(agentId) || description);
2880
+ }
2881
+ for (const msg of rawMessages) {
2882
+ const payload = isObject3(msg.payload) ? msg.payload : null;
2883
+ if (!payload) continue;
2884
+ if (msg.type === "event_msg" && payload.type === "sub_agent_activity" && typeof payload.agent_thread_id === "string" && typeof payload.agent_path === "string") {
2885
+ const agentId = payload.agent_thread_id;
2886
+ const kind = typeof payload.kind === "string" ? payload.kind : "started";
2887
+ const eventId2 = typeof payload.event_id === "string" ? payload.event_id : "";
2888
+ const existingId = agentIdByPath.get(payload.agent_path);
2889
+ const known = alive.has(agentId) || done.has(agentId) || existingId !== void 0 || spawnCalls.has(eventId2);
2890
+ if (kind !== "started" && !known) continue;
2891
+ if (existingId && existingId !== agentId) {
2892
+ if (alive.has(existingId)) {
2893
+ markAlive(agentId, alive.get(existingId) ?? "");
2894
+ alive.delete(existingId);
2895
+ }
2896
+ if (done.has(existingId)) done.add(agentId);
2897
+ }
2898
+ agentIdByPath.set(payload.agent_path, agentId);
2899
+ if (kind === "interrupted") {
2900
+ done.add(agentId);
2901
+ alive.delete(agentId);
2902
+ } else {
2903
+ markAlive(agentId, spawnCalls.get(eventId2) ?? "");
2904
+ }
2905
+ continue;
2906
+ }
2907
+ if (msg.type !== "response_item") continue;
2908
+ if (payload.type === "function_call" && typeof payload.call_id === "string") {
2909
+ const name = normalizeFunctionName(typeof payload.name === "string" ? payload.name : "");
2910
+ if (name === "spawn_agent") {
2911
+ const input = parseJsonObject(payload.arguments);
2912
+ spawnCalls.set(payload.call_id, typeof input?.task_name === "string" ? input.task_name : "");
2913
+ } else if (name === "wait_agent") {
2914
+ waitCalls.add(payload.call_id);
2915
+ } else if (name === "interrupt_agent") {
2916
+ const input = parseJsonObject(payload.arguments);
2917
+ const targets = [input?.agent_id, ...Array.isArray(input?.agent_ids) ? input.agent_ids : []].filter((id) => typeof id === "string");
2918
+ interruptCalls.set(payload.call_id, targets);
2919
+ }
2920
+ continue;
2921
+ }
2922
+ if (payload.type === "function_call_output" && typeof payload.call_id === "string") {
2923
+ const callId = payload.call_id;
2924
+ if (spawnCalls.has(callId)) {
2925
+ const output = parseJsonObject(payload.output);
2926
+ const agentId = typeof output?.agent_id === "string" ? output.agent_id : "";
2927
+ if (agentId) {
2928
+ markAlive(agentId, spawnCalls.get(callId) ?? "");
2929
+ const agentPath = typeof output?.task_name === "string" ? output.task_name : "";
2930
+ if (agentPath && !agentIdByPath.has(agentPath)) agentIdByPath.set(agentPath, agentId);
2931
+ }
2932
+ } else if (waitCalls.has(callId)) {
2933
+ const output = parseJsonObject(payload.output);
2934
+ const statusMap = isObject3(output?.status) ? output.status : {};
2935
+ for (const [agentId, status] of Object.entries(statusMap)) {
2936
+ const statusValue = isObject3(status) ? status : {};
2937
+ const lifecycle = typeof statusValue.status === "string" ? statusValue.status : "";
2938
+ const isTerminal = typeof statusValue.completed === "string" || typeof statusValue.failed === "string" || typeof statusValue.error === "string" || typeof statusValue.interrupted === "string" || lifecycle === "completed" || lifecycle === "failed" || lifecycle === "interrupted";
2939
+ if (isTerminal) {
2940
+ done.add(agentId);
2941
+ alive.delete(agentId);
2942
+ }
2943
+ }
2944
+ } else if (interruptCalls.has(callId)) {
2945
+ for (const agentId of interruptCalls.get(callId) ?? []) {
2946
+ done.add(agentId);
2947
+ alive.delete(agentId);
2948
+ }
2949
+ }
2950
+ continue;
2951
+ }
2952
+ if (payload.type === "agent_message" && typeof payload.author === "string") {
2953
+ const text = codexPayloadText(payload);
2954
+ const messageType = text.match(/^Message Type:\s*([^\r\n]+)/)?.[1]?.trim().toUpperCase() ?? "";
2955
+ if (!TERMINAL_INTER_AGENT_TYPES.has(messageType)) continue;
2956
+ const agentId = agentIdByPath.get(payload.author) ?? payload.author;
2957
+ done.add(agentId);
2958
+ alive.delete(agentId);
2959
+ }
2960
+ }
2961
+ return [...alive.entries()].filter(([agentId]) => !done.has(agentId)).map(([, description]) => description);
2962
+ }
2963
+ function deriveCodexSessionStatus(rawMessages) {
2964
+ for (let i = rawMessages.length - 1; i >= 0; i--) {
2965
+ const msg = rawMessages[i];
2966
+ if (msg.type === "event_msg") {
2967
+ const payload = msg.payload;
2968
+ switch (payload?.type) {
2969
+ case "task_complete": {
2970
+ const pending = collectCodexPendingAgents(rawMessages);
2971
+ if (pending.length > 0) {
2972
+ return {
2973
+ status: "awaiting_agents",
2974
+ pendingQueue: 0,
2975
+ pendingAgents: pending.length,
2976
+ pendingAgentDescriptions: pending.filter((description) => description.length > 0)
2977
+ };
2978
+ }
2979
+ return { status: "completed" };
2980
+ }
2981
+ case "task_started":
2982
+ return { status: "processing" };
2983
+ case "agent_message":
2984
+ return { status: "thinking" };
2985
+ case "token_count":
2986
+ continue;
2987
+ }
2988
+ }
2989
+ if (msg.type === "response_item") {
2990
+ const payload = msg.payload;
2991
+ if (!payload) continue;
2992
+ if (payload.type === "function_call") {
2993
+ return { status: "tool_use", toolName: payload.name ? normalizeFunctionName(payload.name) : payload.name };
2994
+ }
2995
+ if (payload.type === "message") {
2996
+ if (payload.role === "assistant") return { status: "thinking" };
2997
+ if (payload.role === "user") return { status: "processing" };
2998
+ }
2999
+ }
3000
+ }
3001
+ return { status: "idle" };
3002
+ }
3003
+ function brandCodexBranch(firstRecord, sessionId, turnIndex) {
3004
+ const payload = isObject3(firstRecord.payload) ? { ...firstRecord.payload } : {};
3005
+ const originalId = typeof payload.id === "string" ? payload.id : "";
3006
+ payload.id = sessionId;
3007
+ payload.branchedFrom = { sessionId: originalId, turnIndex };
3008
+ return { record: { ...firstRecord, payload }, originalId };
3009
+ }
3010
+ function codexTurnBoundaries(records) {
3011
+ return walkCodexRecords(records, { skipStats: true }).turnStartIndices;
3012
+ }
1992
3013
 
1993
- // src/lib/parser.ts
1994
- function extractTextFromContent2(content) {
1995
- if (typeof content === "string") return content;
1996
- return content.filter((b) => b.type === "text").map((b) => b.text).join("\n");
3014
+ // src/lib/copilot.ts
3015
+ var COPILOT_EVENT_PREFIX = /^(?:assistant|auto_mode|auto_mode_switch|background_tasks|canvas|command|custom|elicitation|exit_plan_mode|extensions|external_tool|factory|hook|managed_settings|mcp|model|pending_messages|permission|sampling|session|skill|subagent|system|tool|ui|user_input)\./;
3016
+ function isObject4(value) {
3017
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1997
3018
  }
1998
- function parseLines(jsonlText) {
1999
- const messages = [];
3019
+ function isRootEvent(event) {
3020
+ return typeof event.agentId !== "string" || event.agentId.length === 0;
3021
+ }
3022
+ function isCopilotEvent(value) {
3023
+ if (!isObject4(value) || typeof value.type !== "string" || !isObject4(value.data)) return false;
3024
+ return value.type === "abort" || value.type === "binary_asset" || value.type === "user.message" || COPILOT_EVENT_PREFIX.test(value.type);
3025
+ }
3026
+ function safeParseLine2(line) {
3027
+ try {
3028
+ const value = JSON.parse(line);
3029
+ return isCopilotEvent(value) ? value : null;
3030
+ } catch {
3031
+ return null;
3032
+ }
3033
+ }
3034
+ function parseEvents(jsonlText) {
3035
+ return jsonlText.split("\n").map((line) => line.trim()).filter(Boolean).map(safeParseLine2).filter((event) => event !== null);
3036
+ }
3037
+ function readString(...values) {
3038
+ return values.find((value) => typeof value === "string") ?? "";
3039
+ }
3040
+ function readNumber(value) {
3041
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
3042
+ }
3043
+ function eventTimestamp(event) {
3044
+ return typeof event.timestamp === "string" ? event.timestamp : "";
3045
+ }
3046
+ function eventId(event, prefix) {
3047
+ if (typeof event.id === "string" && event.id) return event.id;
3048
+ return globalThis.crypto?.randomUUID?.() ?? `${prefix}-${Math.random().toString(36).slice(2, 10)}`;
3049
+ }
3050
+ function turnEventId(event) {
3051
+ const durableEventId = eventId(event, "copilot-turn");
3052
+ const sourceTurnId = readString(event.data.turnId);
3053
+ return sourceTurnId ? `${sourceTurnId}@${durableEventId}` : durableEventId;
3054
+ }
3055
+ function applyContext(context, current) {
3056
+ if (!isObject4(context)) return;
3057
+ const cwd = readString(context.cwd);
3058
+ const branch = readString(context.branch);
3059
+ if (cwd) current.cwd = cwd;
3060
+ if (branch) current.gitBranch = branch;
3061
+ }
3062
+ function extractMetadata(events) {
3063
+ const metadata = {
3064
+ sessionId: "",
3065
+ version: "",
3066
+ gitBranch: "",
3067
+ cwd: "",
3068
+ model: "",
3069
+ slug: "",
3070
+ name: "",
3071
+ firstUserMessage: "",
3072
+ lastUserMessage: "",
3073
+ timestamp: "",
3074
+ lastTimestamp: "",
3075
+ turnCount: 0,
3076
+ isSubagent: false,
3077
+ parentSessionId: null
3078
+ };
3079
+ for (const event of events) {
3080
+ if (!isRootEvent(event)) continue;
3081
+ const { data } = event;
3082
+ const timestamp = eventTimestamp(event);
3083
+ if (timestamp) metadata.lastTimestamp = timestamp;
3084
+ if (event.type === "session.start") {
3085
+ metadata.sessionId ||= readString(data.sessionId);
3086
+ metadata.version ||= readString(data.copilotVersion) || (typeof data.version === "number" ? String(data.version) : readString(data.version));
3087
+ applyContext(data.context, metadata);
3088
+ metadata.model = readString(data.selectedModel) || metadata.model;
3089
+ continue;
3090
+ }
3091
+ if (event.type === "session.resume") {
3092
+ applyContext(data.context, metadata);
3093
+ metadata.model = readString(data.selectedModel) || metadata.model;
3094
+ continue;
3095
+ }
3096
+ if (event.type === "session.context_changed") {
3097
+ applyContext(data, metadata);
3098
+ continue;
3099
+ }
3100
+ if (event.type === "session.auto_mode_resolved") {
3101
+ metadata.model = readString(data.chosenModel) || metadata.model;
3102
+ continue;
3103
+ }
3104
+ if (event.type === "session.model_change") {
3105
+ metadata.model = readString(data.newModel) || metadata.model;
3106
+ continue;
3107
+ }
3108
+ if (event.type === "session.title_changed") {
3109
+ metadata.name = readString(data.title) || metadata.name;
3110
+ continue;
3111
+ }
3112
+ if (event.type === "session.shutdown") {
3113
+ metadata.model = readString(data.currentModel) || metadata.model;
3114
+ continue;
3115
+ }
3116
+ if (event.type === "assistant.turn_start" || event.type === "assistant.turn_end" || event.type === "assistant.message" || event.type === "assistant.usage" || event.type === "tool.execution_start" || event.type === "tool.execution_complete") {
3117
+ metadata.model = readString(data.model) || metadata.model;
3118
+ }
3119
+ if (event.type !== "user.message") continue;
3120
+ metadata.turnCount++;
3121
+ metadata.timestamp ||= timestamp;
3122
+ const content = readString(data.content).trim();
3123
+ if (!content) continue;
3124
+ metadata.firstUserMessage ||= content;
3125
+ metadata.lastUserMessage = content;
3126
+ }
3127
+ metadata.lastTimestamp ||= metadata.timestamp;
3128
+ return metadata;
3129
+ }
3130
+ function isCopilotSessionText(jsonlText) {
2000
3131
  for (const line of jsonlText.split("\n")) {
2001
3132
  const trimmed = line.trim();
2002
3133
  if (!trimmed) continue;
2003
3134
  try {
2004
- messages.push(JSON.parse(trimmed));
3135
+ const value = JSON.parse(trimmed);
3136
+ if (isCopilotEvent(value)) return true;
2005
3137
  } catch {
2006
3138
  }
2007
3139
  }
2008
- return messages;
3140
+ return false;
2009
3141
  }
2010
- function isCodexRawMessages(rawMessages) {
2011
- const firstType = rawMessages[0]?.type;
2012
- return firstType === "session_meta" || firstType === "turn_context" || firstType === "event_msg" || firstType === "response_item" || firstType === "compacted" || firstType === "world_state" || firstType === "inter_agent_communication_metadata";
3142
+ function isCopilotRawRecords(records) {
3143
+ return isCopilotEventType(records[0]?.type);
2013
3144
  }
2014
- function serializeRawMessages(rawMessages) {
2015
- return rawMessages.map((msg) => JSON.stringify(msg)).join("\n");
3145
+ function extractCopilotMetadataFromLines(lines) {
3146
+ const events = lines.map((line) => safeParseLine2(line.trim())).filter(
3147
+ (event) => event !== null
3148
+ );
3149
+ return extractMetadata(events);
2016
3150
  }
2017
- function extractSessionMetadata(messages) {
2018
- const meta = { sessionId: "", version: "", gitBranch: "", cwd: "", slug: "", name: "", model: "", branchedFrom: void 0 };
2019
- for (const msg of messages) {
2020
- if (msg.sessionId && !meta.sessionId) meta.sessionId = msg.sessionId;
2021
- if (msg.version && !meta.version) meta.version = msg.version;
2022
- if (msg.gitBranch && !meta.gitBranch) meta.gitBranch = msg.gitBranch;
2023
- if (msg.cwd && !meta.cwd) meta.cwd = msg.cwd;
2024
- if (msg.slug && !meta.slug) meta.slug = msg.slug;
2025
- if (msg.name && !meta.name) meta.name = msg.name;
2026
- if (msg.branchedFrom && !meta.branchedFrom) {
2027
- meta.branchedFrom = msg.branchedFrom;
2028
- }
2029
- if (isAssistantMessage(msg) && msg.message.model && !meta.model) {
2030
- meta.model = msg.message.model;
2031
- }
2032
- if (meta.sessionId && meta.version && meta.gitBranch && meta.cwd && meta.model) break;
3151
+ function safeStringify2(value) {
3152
+ if (typeof value === "string") return value;
3153
+ if (value === void 0) return "";
3154
+ try {
3155
+ return JSON.stringify(value);
3156
+ } catch {
3157
+ return String(value);
2033
3158
  }
2034
- return meta;
2035
3159
  }
2036
- function parseSession(jsonlText, options) {
2037
- if (isCodexSessionText(jsonlText)) {
2038
- return parseCodexSession(jsonlText, options);
3160
+ function textFromValue(value) {
3161
+ if (typeof value === "string") return value;
3162
+ if (!Array.isArray(value)) return "";
3163
+ return value.flatMap((part) => {
3164
+ const text = typeof part === "string" ? part : isObject4(part) ? readString(part.text, part.content) : "";
3165
+ return text ? [text] : [];
3166
+ }).join("\n");
3167
+ }
3168
+ function collectBinaryAssets(events) {
3169
+ const assets = /* @__PURE__ */ new Map();
3170
+ for (const event of events) {
3171
+ if (event.type !== "session.binary_asset" && event.type !== "binary_asset") continue;
3172
+ const assetId = readString(event.data.assetId);
3173
+ const data = readString(event.data.data);
3174
+ const mimeType = readString(event.data.mimeType);
3175
+ if (assetId && data && mimeType) assets.set(assetId, { data, mimeType });
2039
3176
  }
2040
- const rawMessages = parseLines(jsonlText);
2041
- const metadata = extractSessionMetadata(rawMessages);
2042
- const turns = buildTurns(rawMessages);
2043
- const stats = options?.skipStats ? createEmptySessionStats(turns.length) : computeStats(turns);
3177
+ return assets;
3178
+ }
3179
+ function resolveImage(value, assets) {
3180
+ if (!isObject4(value)) return null;
3181
+ const asset = assets.get(readString(value.assetId));
3182
+ const mimeType = readString(value.mimeType, asset?.mimeType).toLowerCase();
3183
+ const data = readString(value.data, asset?.data);
3184
+ if (!mimeType.startsWith("image/") || !data) return null;
2044
3185
  return {
2045
- ...metadata,
2046
- turns,
2047
- stats,
2048
- rawMessages: options?.skipStats ? [] : rawMessages,
2049
- agentKind: "claude"
3186
+ type: "image",
3187
+ source: { type: "base64", media_type: mimeType, data }
2050
3188
  };
2051
3189
  }
2052
- function parseSessionAppend(existing, newJsonlText) {
2053
- if (isCodexRawMessages(existing.rawMessages) || isCodexSessionText(newJsonlText)) {
2054
- const prefix = serializeRawMessages(existing.rawMessages);
2055
- return parseCodexSession(prefix ? `${prefix}
2056
- ${newJsonlText}` : newJsonlText);
2057
- }
2058
- const newMessages = parseLines(newJsonlText);
2059
- if (newMessages.length === 0) return existing;
2060
- const existingRawMessages = existing.rawMessages;
2061
- const allRawMessages = [...existingRawMessages, ...newMessages];
2062
- const turnStarts = findTurnStartIndices(existingRawMessages);
2063
- const historyTurnCount = Math.max(0, existing.turns.length - turnStarts.length);
2064
- let rebuildFromTurn = Math.max(0, turnStarts.length - 1);
2065
- const progressParentIds = /* @__PURE__ */ new Set();
2066
- for (const msg of newMessages) {
2067
- if (msg.type === "progress") {
2068
- const parentId = msg.parentToolUseID;
2069
- if (parentId) progressParentIds.add(parentId);
2070
- }
3190
+ function extractImages(values, assets) {
3191
+ if (!Array.isArray(values)) return [];
3192
+ const images = [];
3193
+ const seen = /* @__PURE__ */ new Set();
3194
+ for (const value of values) {
3195
+ const image = resolveImage(value, assets);
3196
+ if (!image) continue;
3197
+ const key = `${image.source.media_type}:${image.source.data}`;
3198
+ if (seen.has(key)) continue;
3199
+ seen.add(key);
3200
+ images.push(image);
2071
3201
  }
2072
- if (progressParentIds.size > 0) {
2073
- for (let t = rebuildFromTurn - 1; t >= 0; t--) {
2074
- const turn = existing.turns[historyTurnCount + t];
2075
- if (turn?.toolCalls.some((tc) => progressParentIds.has(tc.id))) {
2076
- rebuildFromTurn = t;
2077
- break;
2078
- }
2079
- }
3202
+ return images;
3203
+ }
3204
+ function buildUserContent(data, assets) {
3205
+ const text = readString(data.content);
3206
+ const images = extractImages(data.attachments, assets);
3207
+ if (images.length === 0) return text;
3208
+ return [...images, ...text ? [{ type: "text", text }] : []];
3209
+ }
3210
+ function extractToolResultImages(data, assets) {
3211
+ if (!isObject4(data.result)) return [];
3212
+ return extractImages([
3213
+ ...Array.isArray(data.result.contents) ? data.result.contents : [],
3214
+ ...Array.isArray(data.result.binaryResultsForLlm) ? data.result.binaryResultsForLlm : []
3215
+ ], assets);
3216
+ }
3217
+ function parseUsage(data, inputIncludesCache = false) {
3218
+ const cacheCreationInputTokens = readNumber(data.cacheWriteTokens);
3219
+ const cacheReadInputTokens = readNumber(data.cacheReadTokens);
3220
+ const inputTokens = readNumber(data.inputTokens);
3221
+ const usage = {
3222
+ input_tokens: inputIncludesCache ? Math.max(0, inputTokens - cacheCreationInputTokens - cacheReadInputTokens) : inputTokens,
3223
+ output_tokens: readNumber(data.outputTokens),
3224
+ cache_creation_input_tokens: cacheCreationInputTokens,
3225
+ cache_read_input_tokens: cacheReadInputTokens
3226
+ };
3227
+ const thinkingTokens = readNumber(data.reasoningTokens);
3228
+ if (thinkingTokens) usage.output_tokens_details = { thinking_tokens: thinkingTokens };
3229
+ if (usage.input_tokens === 0 && usage.output_tokens === 0 && usage.cache_creation_input_tokens === 0 && usage.cache_read_input_tokens === 0 && thinkingTokens === 0) return null;
3230
+ return usage;
3231
+ }
3232
+ function parseModelMetrics(data) {
3233
+ if (!isObject4(data.modelMetrics)) return null;
3234
+ let total = null;
3235
+ for (const value of Object.values(data.modelMetrics)) {
3236
+ if (!isObject4(value) || !isObject4(value.usage)) continue;
3237
+ const usage = parseUsage(value.usage, true);
3238
+ if (usage) total = mergeTokenUsage(total, usage);
2080
3239
  }
2081
- const rebuildFrom = turnStarts[rebuildFromTurn] ?? existingRawMessages.length;
2082
- const keptTurns = existing.turns.slice(0, historyTurnCount + rebuildFromTurn);
2083
- const tailTurns = buildTurns(allRawMessages.slice(rebuildFrom));
2084
- const allTurns = pairAgentMessageReplies([...keptTurns, ...tailTurns]);
2085
- const stats = computeStats(allTurns);
3240
+ return total;
3241
+ }
3242
+ function normalizeChoice(value) {
3243
+ if (typeof value === "string") return { label: value };
3244
+ if (!isObject4(value)) return null;
3245
+ const label = readString(value.label, value.title, value.value, value.name);
3246
+ if (!label) return null;
3247
+ const description = readString(value.description);
3248
+ return description ? { label, description } : { label };
3249
+ }
3250
+ function normalizeChoices(value) {
3251
+ if (!Array.isArray(value)) return [];
3252
+ return value.map(normalizeChoice).filter((choice) => choice !== null);
3253
+ }
3254
+ function normalizeQuestion(value) {
3255
+ if (!isObject4(value)) return null;
3256
+ const question = readString(value.question, value.prompt, value.text, value.message);
3257
+ if (!question) return null;
3258
+ const header = readString(value.header, value.title);
3259
+ const multiSelect = typeof value.multiSelect === "boolean" ? value.multiSelect : typeof value.multi_select === "boolean" ? value.multi_select : void 0;
2086
3260
  return {
2087
- sessionId: existing.sessionId,
2088
- version: existing.version,
2089
- gitBranch: existing.gitBranch,
2090
- cwd: existing.cwd,
2091
- slug: existing.slug,
2092
- name: existing.name,
2093
- model: existing.model || (allTurns.length > 0 ? allTurns[allTurns.length - 1].model || "" : ""),
2094
- turns: allTurns,
3261
+ question,
3262
+ ...header ? { header } : {},
3263
+ options: normalizeChoices(value.options ?? value.choices),
3264
+ ...multiSelect === void 0 ? {} : { multiSelect }
3265
+ };
3266
+ }
3267
+ function normalizeAskUserInput(input) {
3268
+ const source = Array.isArray(input.questions) ? input.questions : [input];
3269
+ const questions = source.map(normalizeQuestion).filter((question) => question !== null);
3270
+ return questions.length > 0 ? { ...input, questions } : input;
3271
+ }
3272
+ function normalizeToolName(name) {
3273
+ switch (name.toLowerCase().replace(/-/g, "_")) {
3274
+ case "ask_user":
3275
+ return "AskUserQuestion";
3276
+ case "bash":
3277
+ case "powershell":
3278
+ case "shell":
3279
+ return "Bash";
3280
+ case "create":
3281
+ return "Write";
3282
+ case "edit":
3283
+ return "Edit";
3284
+ default:
3285
+ return name;
3286
+ }
3287
+ }
3288
+ function normalizeToolInput(name, value) {
3289
+ const input = isObject4(value) ? value : value === void 0 ? {} : { value };
3290
+ if (name === "AskUserQuestion") return normalizeAskUserInput(input);
3291
+ if (name === "Edit") {
3292
+ return {
3293
+ ...input,
3294
+ file_path: readString(input.file_path, input.path),
3295
+ old_string: readString(input.old_string, input.old_str),
3296
+ new_string: readString(input.new_string, input.new_str)
3297
+ };
3298
+ }
3299
+ if (name === "Write") {
3300
+ return {
3301
+ ...input,
3302
+ file_path: readString(input.file_path, input.path),
3303
+ content: readString(input.content, input.file_text)
3304
+ };
3305
+ }
3306
+ if (name === "Bash" && typeof input.command !== "string" && typeof input.cmd === "string") {
3307
+ return { ...input, command: input.cmd };
3308
+ }
3309
+ return input;
3310
+ }
3311
+ function extractToolResult(data) {
3312
+ const error = data.error;
3313
+ if (typeof error === "string") return error;
3314
+ if (isObject4(error)) {
3315
+ const message = readString(error.message, error.code);
3316
+ if (message) return message;
3317
+ }
3318
+ const result = data.result;
3319
+ if (typeof result === "string") return result;
3320
+ if (!isObject4(result)) return result === void 0 ? "" : safeStringify2(result);
3321
+ if (typeof result.detailedContent === "string") return result.detailedContent;
3322
+ if (typeof result.content === "string") return result.content;
3323
+ if (Array.isArray(result.contents)) {
3324
+ const content = result.contents.flatMap((part) => {
3325
+ const text = typeof part === "string" ? part : isObject4(part) ? readString(part.text, part.content, part.output) : "";
3326
+ return text ? [text] : [];
3327
+ }).join("\n");
3328
+ if (content) return content;
3329
+ }
3330
+ if (result.structuredContent !== void 0) return safeStringify2(result.structuredContent);
3331
+ return safeStringify2(result);
3332
+ }
3333
+ function appendThinking2(turn, text, timestamp, signature) {
3334
+ if (!text || turn.thinking.some((block2) => block2.thinking === text)) return;
3335
+ const block = { type: "thinking", thinking: text, signature };
3336
+ turn.thinking.push(block);
3337
+ const last = turn.contentBlocks[turn.contentBlocks.length - 1];
3338
+ if (last?.kind === "thinking") {
3339
+ last.blocks.push(block);
3340
+ return;
3341
+ }
3342
+ turn.contentBlocks.push({ kind: "thinking", blocks: [block], timestamp });
3343
+ }
3344
+ function appendToolCall2(turn, call, timestamp) {
3345
+ turn.toolCalls.push(call);
3346
+ const last = turn.contentBlocks[turn.contentBlocks.length - 1];
3347
+ if (last?.kind === "tool_calls") {
3348
+ last.toolCalls.push(call);
3349
+ return;
3350
+ }
3351
+ turn.contentBlocks.push({ kind: "tool_calls", toolCalls: [call], timestamp });
3352
+ }
3353
+ function newToolCall(id, name, input, timestamp) {
3354
+ return { id, name, input, result: null, isError: false, timestamp };
3355
+ }
3356
+ function toolCallsFromRequests(value, timestamp, isPending) {
3357
+ if (!Array.isArray(value)) return [];
3358
+ const calls = [];
3359
+ for (const request of value) {
3360
+ if (!isObject4(request)) continue;
3361
+ const id = readString(request.toolCallId);
3362
+ const rawName = readString(request.name);
3363
+ if (!id || !rawName || isPending(id)) continue;
3364
+ const name = normalizeToolName(rawName);
3365
+ calls.push(newToolCall(id, name, normalizeToolInput(name, request.arguments), timestamp));
3366
+ }
3367
+ return calls;
3368
+ }
3369
+ function updateToolCallStart(call, name, input, timestamp) {
3370
+ call.name = name;
3371
+ call.input = normalizeToolInput(name, input);
3372
+ call.timestamp = timestamp || call.timestamp;
3373
+ }
3374
+ function updateToolCallResult(call, data, binaryAssets) {
3375
+ call.result = extractToolResult(data);
3376
+ const resultImages = extractToolResultImages(data, binaryAssets);
3377
+ if (resultImages.length > 0) call.resultImages = resultImages;
3378
+ call.isError = data.success === false || data.error !== void 0 && data.error !== null;
3379
+ }
3380
+ function createTurn3(id, timestamp, userMessage, model, effort, isFragment) {
3381
+ return {
3382
+ id,
3383
+ userMessage,
3384
+ contentBlocks: [],
3385
+ thinking: [],
3386
+ assistantText: [],
3387
+ toolCalls: [],
3388
+ subAgentActivity: [],
3389
+ timestamp,
3390
+ durationMs: null,
3391
+ tokenUsage: null,
3392
+ model,
3393
+ ...effort ? { effort } : {},
3394
+ ...isFragment ? { isFragment: true } : {}
3395
+ };
3396
+ }
3397
+ function hasTurnContent(turn) {
3398
+ return turn.userMessage !== null || turn.assistantText.length > 0 || turn.thinking.length > 0 || turn.toolCalls.length > 0 || turn.subAgentActivity.length > 0;
3399
+ }
3400
+ function parseCopilotSession(jsonlText, options) {
3401
+ const events = parseEvents(jsonlText);
3402
+ const metadata = extractMetadata(events);
3403
+ const binaryAssets = collectBinaryAssets(events);
3404
+ const turns = [];
3405
+ const pendingToolCalls = /* @__PURE__ */ new Map();
3406
+ const agentMessages = /* @__PURE__ */ new Map();
3407
+ const pendingAgentToolCalls = /* @__PURE__ */ new Map();
3408
+ const endTimestamps = /* @__PURE__ */ new WeakMap();
3409
+ const apiDurationMs = /* @__PURE__ */ new WeakMap();
3410
+ let current = null;
3411
+ let currentModel = null;
3412
+ let currentEffort;
3413
+ let sawAssistantUsage = false;
3414
+ let sawAssistantDuration = false;
3415
+ let shutdownUsage = null;
3416
+ function noteTimestamp(turn, timestamp) {
3417
+ if (timestamp) endTimestamps.set(turn, timestamp);
3418
+ }
3419
+ function ensureTurn(event) {
3420
+ if (!current) {
3421
+ current = createTurn3(
3422
+ turnEventId(event),
3423
+ eventTimestamp(event),
3424
+ null,
3425
+ currentModel,
3426
+ currentEffort,
3427
+ true
3428
+ );
3429
+ }
3430
+ return current;
3431
+ }
3432
+ function finalizeCurrent() {
3433
+ if (!current) return;
3434
+ if (hasTurnContent(current)) turns.push(current);
3435
+ current = null;
3436
+ }
3437
+ function ensureAgent(event) {
3438
+ const agentId = typeof event.agentId === "string" ? event.agentId : "";
3439
+ if (!agentId) return null;
3440
+ const existing = agentMessages.get(agentId);
3441
+ if (existing) return existing.message;
3442
+ const data = event.data;
3443
+ const parentToolUseId = readString(data.toolCallId);
3444
+ const targetTurn = parentToolUseId ? pendingToolCalls.get(parentToolUseId)?.turn ?? current : current;
3445
+ if (!targetTurn) return null;
3446
+ const executionMode = readString(data.executionMode);
3447
+ const message = {
3448
+ agentId,
3449
+ ...parentToolUseId ? { parentToolUseId } : {},
3450
+ agentName: readString(data.agentDisplayName, data.agentName) || null,
3451
+ subagentType: readString(data.agentType, data.agentName) || null,
3452
+ type: "assistant",
3453
+ content: null,
3454
+ toolCalls: [],
3455
+ thinking: [],
3456
+ text: [],
3457
+ timestamp: eventTimestamp(event),
3458
+ tokenUsage: null,
3459
+ model: readString(data.model) || null,
3460
+ isBackground: executionMode === "background",
3461
+ ...readString(data.description) ? { prompt: readString(data.description) } : {},
3462
+ status: "running"
3463
+ };
3464
+ targetTurn.subAgentActivity.push(message);
3465
+ targetTurn.contentBlocks.push({
3466
+ kind: message.isBackground ? "background_agent" : "sub_agent",
3467
+ messages: [message],
3468
+ timestamp: message.timestamp
3469
+ });
3470
+ agentMessages.set(agentId, { message, turn: targetTurn });
3471
+ return message;
3472
+ }
3473
+ function applyAgentEvent(event) {
3474
+ if (isRootEvent(event)) return false;
3475
+ const message = ensureAgent(event);
3476
+ if (!message) return true;
3477
+ const { data } = event;
3478
+ const timestamp = eventTimestamp(event);
3479
+ if (event.type === "subagent.started" || event.type === "subagent.configured") {
3480
+ message.parentToolUseId ||= readString(data.toolCallId) || void 0;
3481
+ message.agentName = readString(data.agentDisplayName, data.agentName) || message.agentName;
3482
+ message.subagentType = readString(data.agentType, data.agentName) || message.subagentType;
3483
+ message.model = readString(data.model) || message.model;
3484
+ message.prompt ||= readString(data.description) || void 0;
3485
+ message.isBackground ||= readString(data.executionMode) === "background";
3486
+ message.status = "running";
3487
+ return true;
3488
+ }
3489
+ if (event.type === "subagent.completed" || event.type === "subagent.failed") {
3490
+ message.agentName = readString(data.agentDisplayName, data.agentName) || message.agentName;
3491
+ message.subagentType = readString(data.agentType, data.agentName) || message.subagentType;
3492
+ message.model = readString(data.model) || message.model;
3493
+ message.durationMs = readNumber(data.durationMs) || message.durationMs;
3494
+ message.toolUseCount = readNumber(data.totalToolCalls) || message.toolUseCount;
3495
+ message.status = event.type === "subagent.completed" ? "completed" : "failed";
3496
+ const failure = readString(data.error, data.message);
3497
+ if (failure && !message.text.includes(failure)) message.text.push(failure);
3498
+ return true;
3499
+ }
3500
+ if (event.type === "user.message") {
3501
+ message.prompt ||= readString(data.content) || void 0;
3502
+ return true;
3503
+ }
3504
+ if (event.type === "assistant.reasoning") {
3505
+ const reasoning = readString(data.content);
3506
+ if (reasoning && !message.thinking.includes(reasoning)) message.thinking.push(reasoning);
3507
+ return true;
3508
+ }
3509
+ if (event.type === "assistant.message") {
3510
+ message.model = readString(data.model) || message.model;
3511
+ const reasoning = readString(data.reasoningText);
3512
+ if (reasoning && !message.thinking.includes(reasoning)) message.thinking.push(reasoning);
3513
+ const text = textFromValue(data.content);
3514
+ if (text && !message.text.includes(text)) message.text.push(text);
3515
+ for (const call of toolCallsFromRequests(
3516
+ data.toolRequests,
3517
+ timestamp,
3518
+ (id) => pendingAgentToolCalls.has(id)
3519
+ )) {
3520
+ message.toolCalls.push(call);
3521
+ pendingAgentToolCalls.set(call.id, { call, message });
3522
+ }
3523
+ return true;
3524
+ }
3525
+ if (event.type === "assistant.usage") {
3526
+ message.model = readString(data.model) || message.model;
3527
+ const usage = parseUsage(data);
3528
+ if (usage) message.tokenUsage = mergeTokenUsage(message.tokenUsage, usage);
3529
+ return true;
3530
+ }
3531
+ if (event.type === "tool.execution_start") {
3532
+ const callId = readString(data.toolCallId) || eventId(event, "copilot-agent-tool");
3533
+ const rawName = readString(data.toolName) || "Tool";
3534
+ const name = normalizeToolName(rawName);
3535
+ const existing = pendingAgentToolCalls.get(callId);
3536
+ if (existing) {
3537
+ updateToolCallStart(existing.call, name, data.arguments, timestamp);
3538
+ } else {
3539
+ const call = newToolCall(
3540
+ callId,
3541
+ name,
3542
+ normalizeToolInput(name, data.arguments),
3543
+ timestamp
3544
+ );
3545
+ message.toolCalls.push(call);
3546
+ pendingAgentToolCalls.set(callId, { call, message });
3547
+ }
3548
+ return true;
3549
+ }
3550
+ if (event.type === "tool.execution_complete") {
3551
+ const callId = readString(data.toolCallId) || eventId(event, "copilot-agent-tool");
3552
+ let pending = pendingAgentToolCalls.get(callId);
3553
+ if (!pending) {
3554
+ const rawName = readString(data.toolName) || "Tool";
3555
+ const name = normalizeToolName(rawName);
3556
+ const call = newToolCall(callId, name, {}, timestamp);
3557
+ message.toolCalls.push(call);
3558
+ pending = { call, message };
3559
+ }
3560
+ updateToolCallResult(pending.call, data, binaryAssets);
3561
+ pendingAgentToolCalls.delete(callId);
3562
+ return true;
3563
+ }
3564
+ return true;
3565
+ }
3566
+ for (const event of events) {
3567
+ if (applyAgentEvent(event)) continue;
3568
+ const { data } = event;
3569
+ const timestamp = eventTimestamp(event);
3570
+ if (event.type === "session.start" || event.type === "session.resume") {
3571
+ currentModel = readString(data.selectedModel) || currentModel;
3572
+ currentEffort = readString(data.reasoningEffort) || currentEffort;
3573
+ continue;
3574
+ }
3575
+ if (event.type === "session.auto_mode_resolved") {
3576
+ currentModel = readString(data.chosenModel) || currentModel;
3577
+ if (current) current.model = currentModel;
3578
+ continue;
3579
+ }
3580
+ if (event.type === "session.model_change") {
3581
+ currentModel = readString(data.newModel) || currentModel;
3582
+ const effort = data.reasoningEffort;
3583
+ if (typeof effort === "string") currentEffort = effort;
3584
+ if (current) {
3585
+ current.model = currentModel;
3586
+ if (currentEffort) current.effort = currentEffort;
3587
+ }
3588
+ continue;
3589
+ }
3590
+ if (event.type === "session.shutdown") {
3591
+ currentModel = readString(data.currentModel) || currentModel;
3592
+ if (current && !current.model) current.model = currentModel;
3593
+ shutdownUsage = parseModelMetrics(data) ?? shutdownUsage;
3594
+ const duration = readNumber(data.totalApiDurationMs);
3595
+ if (current && !sawAssistantDuration && duration) apiDurationMs.set(current, duration);
3596
+ continue;
3597
+ }
3598
+ if (event.type === "user.message") {
3599
+ finalizeCurrent();
3600
+ current = createTurn3(
3601
+ turnEventId(event),
3602
+ timestamp,
3603
+ buildUserContent(data, binaryAssets),
3604
+ currentModel,
3605
+ currentEffort,
3606
+ false
3607
+ );
3608
+ noteTimestamp(current, timestamp);
3609
+ continue;
3610
+ }
3611
+ if (event.type === "assistant.turn_start") {
3612
+ currentModel = readString(data.model) || currentModel;
3613
+ const turn = ensureTurn(event);
3614
+ turn.model = currentModel;
3615
+ if (currentEffort) turn.effort = currentEffort;
3616
+ noteTimestamp(turn, timestamp);
3617
+ continue;
3618
+ }
3619
+ if (event.type === "assistant.reasoning") {
3620
+ const turn = ensureTurn(event);
3621
+ appendThinking2(
3622
+ turn,
3623
+ readString(data.content),
3624
+ timestamp,
3625
+ readString(data.reasoningId) || eventId(event, "copilot-thinking")
3626
+ );
3627
+ noteTimestamp(turn, timestamp);
3628
+ continue;
3629
+ }
3630
+ if (event.type === "assistant.message") {
3631
+ currentModel = readString(data.model) || currentModel;
3632
+ const turn = ensureTurn(event);
3633
+ turn.model = currentModel;
3634
+ if (currentEffort) turn.effort = currentEffort;
3635
+ appendThinking2(
3636
+ turn,
3637
+ readString(data.reasoningText),
3638
+ timestamp,
3639
+ eventId(event, "copilot-thinking")
3640
+ );
3641
+ appendAssistantText(turn, textFromValue(data.content), timestamp);
3642
+ for (const call of toolCallsFromRequests(
3643
+ data.toolRequests,
3644
+ timestamp,
3645
+ (id) => pendingToolCalls.has(id)
3646
+ )) {
3647
+ appendToolCall2(turn, call, timestamp);
3648
+ pendingToolCalls.set(call.id, { call, turn });
3649
+ }
3650
+ noteTimestamp(turn, timestamp);
3651
+ continue;
3652
+ }
3653
+ if (event.type === "assistant.usage") {
3654
+ currentModel = readString(data.model) || currentModel;
3655
+ currentEffort = readString(data.reasoningEffort) || currentEffort;
3656
+ const usage = parseUsage(data);
3657
+ const duration = readNumber(data.duration);
3658
+ if (usage) sawAssistantUsage = true;
3659
+ if (duration) sawAssistantDuration = true;
3660
+ const turn = ensureTurn(event);
3661
+ turn.model = currentModel;
3662
+ if (currentEffort) turn.effort = currentEffort;
3663
+ if (usage) turn.tokenUsage = mergeTokenUsage(turn.tokenUsage, usage);
3664
+ if (duration) apiDurationMs.set(turn, (apiDurationMs.get(turn) ?? 0) + duration);
3665
+ noteTimestamp(turn, timestamp);
3666
+ continue;
3667
+ }
3668
+ if (event.type === "tool.execution_start") {
3669
+ currentModel = readString(data.model) || currentModel;
3670
+ const turn = ensureTurn(event);
3671
+ turn.model = currentModel;
3672
+ const callId = readString(data.toolCallId) || eventId(event, "copilot-tool");
3673
+ const rawName = readString(data.toolName) || "Tool";
3674
+ const name = normalizeToolName(rawName);
3675
+ const existing = pendingToolCalls.get(callId);
3676
+ if (existing) {
3677
+ updateToolCallStart(existing.call, name, data.arguments, timestamp);
3678
+ } else {
3679
+ const call = newToolCall(
3680
+ callId,
3681
+ name,
3682
+ normalizeToolInput(name, data.arguments),
3683
+ timestamp
3684
+ );
3685
+ appendToolCall2(turn, call, timestamp);
3686
+ pendingToolCalls.set(callId, { call, turn });
3687
+ }
3688
+ noteTimestamp(turn, timestamp);
3689
+ continue;
3690
+ }
3691
+ if (event.type === "tool.execution_complete") {
3692
+ currentModel = readString(data.model) || currentModel;
3693
+ const callId = readString(data.toolCallId) || eventId(event, "copilot-tool");
3694
+ let pending = pendingToolCalls.get(callId);
3695
+ if (!pending) {
3696
+ const turn = ensureTurn(event);
3697
+ const rawName = readString(data.toolName) || "Tool";
3698
+ const name = normalizeToolName(rawName);
3699
+ const call = newToolCall(callId, name, {}, timestamp);
3700
+ appendToolCall2(turn, call, timestamp);
3701
+ pending = { call, turn };
3702
+ }
3703
+ updateToolCallResult(pending.call, data, binaryAssets);
3704
+ pendingToolCalls.delete(callId);
3705
+ noteTimestamp(pending.turn, timestamp);
3706
+ continue;
3707
+ }
3708
+ if (event.type === "session.error") {
3709
+ const turn = ensureTurn(event);
3710
+ const errorType = readString(data.errorType) || "unknown";
3711
+ const input = { errorType };
3712
+ if (typeof data.statusCode === "number") input.statusCode = data.statusCode;
3713
+ if (typeof data.errorCode === "string") input.errorCode = data.errorCode;
3714
+ appendToolCall2(turn, {
3715
+ id: eventId(event, "copilot-error"),
3716
+ name: "Error",
3717
+ input,
3718
+ result: readString(data.message) || "Copilot session error",
3719
+ isError: true,
3720
+ timestamp
3721
+ }, timestamp);
3722
+ noteTimestamp(turn, timestamp);
3723
+ continue;
3724
+ }
3725
+ if (event.type === "assistant.turn_end" || event.type === "assistant.idle" || event.type === "session.idle") {
3726
+ if (current) noteTimestamp(current, timestamp);
3727
+ }
3728
+ }
3729
+ finalizeCurrent();
3730
+ if (!sawAssistantUsage && shutdownUsage && turns.length === 1) {
3731
+ turns[0].tokenUsage = mergeTokenUsage(turns[0].tokenUsage, shutdownUsage);
3732
+ }
3733
+ if (metadata.model) {
3734
+ for (const turn of turns) turn.model ||= metadata.model;
3735
+ }
3736
+ for (const turn of turns) {
3737
+ const endTimestamp = endTimestamps.get(turn);
3738
+ const start = Date.parse(turn.timestamp);
3739
+ const end = endTimestamp ? Date.parse(endTimestamp) : Number.NaN;
3740
+ if (Number.isFinite(start) && Number.isFinite(end) && end >= start) {
3741
+ turn.durationMs = end - start;
3742
+ } else {
3743
+ const duration = apiDurationMs.get(turn) ?? 0;
3744
+ if (duration) turn.durationMs = duration;
3745
+ }
3746
+ }
3747
+ const stats = options?.skipStats ? createEmptySessionStats(turns.length) : computeStats(turns);
3748
+ if (!options?.skipStats && !sawAssistantUsage && shutdownUsage && turns.length > 1) {
3749
+ stats.totalInputTokens = shutdownUsage.input_tokens;
3750
+ stats.totalOutputTokens = shutdownUsage.output_tokens;
3751
+ stats.totalCacheCreationTokens = shutdownUsage.cache_creation_input_tokens ?? 0;
3752
+ stats.totalCacheReadTokens = shutdownUsage.cache_read_input_tokens ?? 0;
3753
+ }
3754
+ return {
3755
+ sessionId: metadata.sessionId,
3756
+ version: metadata.version,
3757
+ gitBranch: metadata.gitBranch,
3758
+ cwd: metadata.cwd,
3759
+ slug: metadata.slug,
3760
+ name: metadata.name,
3761
+ model: metadata.model,
3762
+ turns,
2095
3763
  stats,
2096
- rawMessages: allRawMessages,
2097
- branchedFrom: existing.branchedFrom,
2098
- agentKind: existing.agentKind
3764
+ rawMessages: options?.skipStats ? [] : events,
3765
+ agentKind: "copilot"
2099
3766
  };
2100
3767
  }
3768
+ function appendCopilotSession(existing, newJsonlText) {
3769
+ const prefix = existing.rawMessages.map((record) => JSON.stringify(record)).join("\n");
3770
+ return parseCopilotSession(prefix ? `${prefix}
3771
+ ${newJsonlText}` : newJsonlText);
3772
+ }
3773
+ function isCopilotEventType(type) {
3774
+ return typeof type === "string" && (type === "abort" || type === "binary_asset" || type === "user.message" || type.startsWith("assistant.") || type.startsWith("permission.") || type.startsWith("session.") || type.startsWith("subagent.") || type.startsWith("tool.") || type.startsWith("user_input."));
3775
+ }
3776
+ function deriveCopilotSessionStatus(rawMessages) {
3777
+ const pendingAgents = /* @__PURE__ */ new Map();
3778
+ for (const event of rawMessages) {
3779
+ const agentId = typeof event.agentId === "string" ? event.agentId : "";
3780
+ if (!agentId && (event.type === "abort" || event.type === "session.shutdown" || event.type === "session.error")) {
3781
+ pendingAgents.clear();
3782
+ continue;
3783
+ }
3784
+ if (!agentId || !event.type.startsWith("subagent.")) continue;
3785
+ const data = isObject4(event.data) ? event.data : {};
3786
+ if (event.type === "subagent.completed" || event.type === "subagent.failed") {
3787
+ pendingAgents.delete(agentId);
3788
+ continue;
3789
+ }
3790
+ if (event.type === "subagent.started" || event.type === "subagent.configured") {
3791
+ const description = [data.agentDisplayName, data.description, data.agentName].find((value) => typeof value === "string") ?? "";
3792
+ pendingAgents.set(agentId, description || pendingAgents.get(agentId) || "");
3793
+ }
3794
+ }
3795
+ if (pendingAgents.size > 0) {
3796
+ const descriptions = [...pendingAgents.values()];
3797
+ return {
3798
+ status: "awaiting_agents",
3799
+ pendingQueue: 0,
3800
+ pendingAgents: descriptions.length,
3801
+ pendingAgentDescriptions: descriptions.filter((description) => description.length > 0)
3802
+ };
3803
+ }
3804
+ let sawUserActivity = false;
3805
+ const completedPermissionRequests = /* @__PURE__ */ new Set();
3806
+ for (let i = rawMessages.length - 1; i >= 0; i--) {
3807
+ const event = rawMessages[i];
3808
+ if (typeof event.agentId === "string" && event.agentId) continue;
3809
+ const data = isObject4(event.data) ? event.data : {};
3810
+ switch (event.type) {
3811
+ case "abort":
3812
+ case "session.shutdown":
3813
+ return { status: "completed" };
3814
+ case "assistant.turn_end": {
3815
+ for (let j = i - 1; j >= 0; j--) {
3816
+ const previous = rawMessages[j];
3817
+ if (typeof previous.agentId === "string" && previous.agentId) continue;
3818
+ if (previous.type === "user.message") break;
3819
+ if (previous.type === "abort") return { status: "completed" };
3820
+ if (previous.type !== "assistant.message") continue;
3821
+ const previousData = isObject4(previous.data) ? previous.data : {};
3822
+ return Array.isArray(previousData.toolRequests) && previousData.toolRequests.length > 0 ? { status: "processing" } : { status: "completed" };
3823
+ }
3824
+ return { status: "processing" };
3825
+ }
3826
+ case "session.error":
3827
+ return {
3828
+ status: "completed",
3829
+ terminalReason: typeof data.message === "string" ? data.message : "Copilot session error"
3830
+ };
3831
+ case "assistant.idle":
3832
+ case "session.idle":
3833
+ return { status: sawUserActivity ? "completed" : "idle" };
3834
+ case "permission.completed": {
3835
+ const requestId = typeof data.requestId === "string" ? data.requestId : "";
3836
+ if (requestId) completedPermissionRequests.add(requestId);
3837
+ continue;
3838
+ }
3839
+ case "permission.requested": {
3840
+ const requestId = typeof data.requestId === "string" ? data.requestId : "";
3841
+ if (requestId && completedPermissionRequests.has(requestId)) continue;
3842
+ return { status: "deferred" };
3843
+ }
3844
+ case "user_input.requested":
3845
+ return { status: "tool_use", toolName: "AskUserQuestion" };
3846
+ case "tool.execution_start":
3847
+ if (completedPermissionRequests.size > 0) return { status: "thinking" };
3848
+ return {
3849
+ status: "tool_use",
3850
+ toolName: typeof data.toolName === "string" ? normalizeFunctionName(data.toolName) : typeof data.name === "string" ? normalizeFunctionName(data.name) : void 0
3851
+ };
3852
+ case "tool.execution_complete":
3853
+ return { status: "thinking" };
3854
+ case "assistant.turn_start":
3855
+ return { status: "processing" };
3856
+ case "assistant.message":
3857
+ case "assistant.message_delta":
3858
+ case "assistant.reasoning":
3859
+ case "assistant.reasoning_delta":
3860
+ return { status: "thinking" };
3861
+ case "user.message":
3862
+ sawUserActivity = true;
3863
+ return { status: "processing" };
3864
+ case "session.start":
3865
+ case "session.resume":
3866
+ return { status: sawUserActivity ? "processing" : "idle" };
3867
+ }
3868
+ }
3869
+ return { status: "idle" };
3870
+ }
3871
+ function brandCopilotBranch() {
3872
+ throw new Error("Copilot sessions branch through the CLI's fork API, not by copying the transcript");
3873
+ }
3874
+ function copilotTurnBoundaries(records) {
3875
+ const boundaries = [];
3876
+ for (let index = 0; index < records.length; index++) {
3877
+ const record = records[index];
3878
+ if (record.type !== "user.message" || typeof record.agentId === "string") continue;
3879
+ const data = record.data;
3880
+ if (!data || typeof data !== "object" || Array.isArray(data)) continue;
3881
+ if (typeof record.id !== "string" || !record.id) continue;
3882
+ boundaries.push(index);
3883
+ }
3884
+ return boundaries;
3885
+ }
3886
+
3887
+ // src/lib/agents.ts
3888
+ var TERMINAL_AGENT_KIND = AGENT_KINDS[AGENT_KINDS.length - 1];
3889
+ var codexFormat = {
3890
+ kind: "codex",
3891
+ descriptor: descriptorFor("codex"),
3892
+ detectsText: isCodexSessionText,
3893
+ detectsRecords: isCodexRawRecords,
3894
+ parse: parseCodexSession,
3895
+ append: appendCodexSession,
3896
+ metadataFromLines: extractCodexMetadataFromLines,
3897
+ status: deriveCodexSessionStatus,
3898
+ brandBranch: brandCodexBranch,
3899
+ turnBoundaries: codexTurnBoundaries
3900
+ };
3901
+ var copilotFormat = {
3902
+ kind: "copilot",
3903
+ descriptor: descriptorFor("copilot"),
3904
+ detectsText: isCopilotSessionText,
3905
+ detectsRecords: isCopilotRawRecords,
3906
+ parse: parseCopilotSession,
3907
+ append: appendCopilotSession,
3908
+ metadataFromLines: extractCopilotMetadataFromLines,
3909
+ status: deriveCopilotSessionStatus,
3910
+ brandBranch: brandCopilotBranch,
3911
+ turnBoundaries: copilotTurnBoundaries
3912
+ };
3913
+ var claudeFormat = {
3914
+ kind: "claude",
3915
+ descriptor: descriptorFor("claude"),
3916
+ // The terminal arm answers `true` for both, so order alone decides.
3917
+ detectsText: isClaudeSessionText,
3918
+ detectsRecords: isClaudeSessionText,
3919
+ parse: parseClaudeSession,
3920
+ append: appendClaudeSession,
3921
+ metadataFromLines: extractClaudeMetadataFromLines,
3922
+ status: deriveClaudeSessionStatus,
3923
+ brandBranch: brandClaudeBranch,
3924
+ turnBoundaries: claudeTurnBoundaries
3925
+ };
3926
+ var FORMATS = Object.freeze({
3927
+ claude: claudeFormat,
3928
+ codex: codexFormat,
3929
+ copilot: copilotFormat
3930
+ });
3931
+ function firstMatch(matches) {
3932
+ for (const kind of AGENT_KINDS) {
3933
+ if (matches(FORMATS[kind])) return FORMATS[kind];
3934
+ }
3935
+ return FORMATS[TERMINAL_AGENT_KIND];
3936
+ }
3937
+ function formatForText(jsonlText) {
3938
+ return firstMatch((format) => format.detectsText(jsonlText));
3939
+ }
3940
+ function formatForRecords(records) {
3941
+ return firstMatch((format) => format.detectsRecords(records));
3942
+ }
3943
+
3944
+ // src/lib/parser.ts
3945
+ function extractTextFromContent2(content) {
3946
+ if (typeof content === "string") return content;
3947
+ return content.filter((b) => b.type === "text").map((b) => b.text).join("\n");
3948
+ }
3949
+ function parseSession(jsonlText, options) {
3950
+ return formatForText(jsonlText).parse(jsonlText, options);
3951
+ }
3952
+ function parseSessionAppend(existing, newJsonlText) {
3953
+ const byRecords = formatForRecords(existing.rawMessages);
3954
+ const format = byRecords.kind === TERMINAL_AGENT_KIND ? formatForText(newJsonlText) : byRecords;
3955
+ return format.append(existing, newJsonlText);
3956
+ }
2101
3957
  function getUserMessageText(content) {
2102
3958
  if (content === null) return "";
2103
3959
  if (typeof content === "string") return content;
@@ -2108,20 +3964,272 @@ function getUserMessageImages(content) {
2108
3964
  return content.filter((b) => b.type === "image");
2109
3965
  }
2110
3966
 
3967
+ // src/lib/stores.ts
3968
+ var import_node_fs = require("node:fs");
3969
+ var import_node_path2 = require("node:path");
3970
+
3971
+ // src/lib/dirs.ts
3972
+ var import_node_path = require("node:path");
3973
+ var import_node_os = require("node:os");
3974
+ function agentHomeDir(kind, env = process.env, homeDirectory = (0, import_node_os.homedir)()) {
3975
+ const { homeEnvVar, homeDirName } = descriptorFor(kind).cli;
3976
+ const override = homeEnvVar ? env[homeEnvVar] : void 0;
3977
+ return override || (0, import_node_path.join)(homeDirectory, homeDirName);
3978
+ }
3979
+ var dirs = {
3980
+ PROJECTS_DIR: (0, import_node_path.join)(agentHomeDir("claude"), "projects"),
3981
+ TEAMS_DIR: (0, import_node_path.join)(agentHomeDir("claude"), "teams"),
3982
+ TASKS_DIR: (0, import_node_path.join)(agentHomeDir("claude"), "tasks"),
3983
+ CODEX_SESSIONS_DIR: (0, import_node_path.join)(agentHomeDir("codex"), "sessions"),
3984
+ COPILOT_SESSIONS_DIR: (0, import_node_path.join)(agentHomeDir("copilot"), "session-state")
3985
+ };
3986
+ var DEFAULT_DB_PATH = (0, import_node_path.join)(agentHomeDir("claude"), "cogpit-memory", "search-index.db");
3987
+
3988
+ // src/lib/stores.ts
3989
+ var NON_PROJECT_DIRS = /* @__PURE__ */ new Set(["memory"]);
3990
+ var MAX_WALK_DEPTH = 4;
3991
+ var SUBAGENTS_DIR = "subagents";
3992
+ function mtimeOf(filePath) {
3993
+ try {
3994
+ return (0, import_node_fs.statSync)(filePath).mtimeMs;
3995
+ } catch {
3996
+ return null;
3997
+ }
3998
+ }
3999
+ function readDirSafe(dir) {
4000
+ try {
4001
+ return (0, import_node_fs.readdirSync)(dir, { withFileTypes: true });
4002
+ } catch {
4003
+ return [];
4004
+ }
4005
+ }
4006
+ function isWithin(root, target) {
4007
+ const rel = (0, import_node_path2.relative)(root, target);
4008
+ return rel !== "" && !rel.startsWith("..") && !(0, import_node_path2.isAbsolute)(rel);
4009
+ }
4010
+ function canonicalWithinRoot(root, filePath) {
4011
+ try {
4012
+ const realRoot = (0, import_node_fs.realpathSync)(root);
4013
+ const real = (0, import_node_fs.realpathSync)(filePath);
4014
+ return isWithin(realRoot, real) ? real : null;
4015
+ } catch {
4016
+ return null;
4017
+ }
4018
+ }
4019
+ function segmentsUnder(root, filePath) {
4020
+ return (0, import_node_path2.relative)(root, filePath).split(import_node_path2.sep).filter(Boolean);
4021
+ }
4022
+ function claudeIdentify(filePath, root) {
4023
+ const segments = segmentsUnder(root, filePath);
4024
+ const subagentAt = segments.lastIndexOf(SUBAGENTS_DIR);
4025
+ if (subagentAt > 0) {
4026
+ const parentSessionId = segments[subagentAt - 1];
4027
+ return { sessionId: parentSessionId, isSubagent: true, parentSessionId };
4028
+ }
4029
+ const fileName = segments[segments.length - 1] ?? "";
4030
+ return {
4031
+ sessionId: descriptorFor("claude").sessionFile.sessionId(fileName) ?? fileName.replace(/\.jsonl$/, ""),
4032
+ isSubagent: false,
4033
+ parentSessionId: null
4034
+ };
4035
+ }
4036
+ function subagentDirOf(parentPath) {
4037
+ return (0, import_node_path2.join)(parentPath.replace(/\.jsonl$/, ""), SUBAGENTS_DIR);
4038
+ }
4039
+ function walkClaudeSubagents(parentPath, parentSessionId, cutoffMs, depth, out) {
4040
+ if (depth >= MAX_WALK_DEPTH) return;
4041
+ const dir = subagentDirOf(parentPath);
4042
+ for (const entry of readDirSafe(dir)) {
4043
+ if (!entry.name.startsWith("agent-") || !entry.name.endsWith(".jsonl")) continue;
4044
+ const filePath = (0, import_node_path2.join)(dir, entry.name);
4045
+ const mtimeMs = mtimeOf(filePath);
4046
+ if (mtimeMs !== null && mtimeMs >= cutoffMs) {
4047
+ out.push({
4048
+ path: filePath,
4049
+ mtimeMs,
4050
+ sessionId: parentSessionId,
4051
+ isSubagent: true,
4052
+ parentSessionId
4053
+ });
4054
+ }
4055
+ walkClaudeSubagents(filePath, parentSessionId, cutoffMs, depth + 1, out);
4056
+ }
4057
+ }
4058
+ function walkClaude(root, cutoffMs) {
4059
+ const found = [];
4060
+ for (const project of readDirSafe(root)) {
4061
+ if (!project.isDirectory() || NON_PROJECT_DIRS.has(project.name)) continue;
4062
+ const projectDir = (0, import_node_path2.join)(root, project.name);
4063
+ for (const entry of readDirSafe(projectDir)) {
4064
+ if (!entry.isFile() || !entry.name.endsWith(".jsonl")) continue;
4065
+ const filePath = (0, import_node_path2.join)(projectDir, entry.name);
4066
+ const mtimeMs = mtimeOf(filePath);
4067
+ const { sessionId } = claudeIdentify(filePath, root);
4068
+ if (mtimeMs !== null && mtimeMs >= cutoffMs) {
4069
+ found.push({ path: filePath, mtimeMs, sessionId, isSubagent: false, parentSessionId: null });
4070
+ }
4071
+ walkClaudeSubagents(filePath, sessionId, cutoffMs, 0, found);
4072
+ }
4073
+ }
4074
+ return found;
4075
+ }
4076
+ function walkCodex(root, cutoffMs, dir = root, depth = 0) {
4077
+ if (depth > MAX_WALK_DEPTH) return [];
4078
+ const found = [];
4079
+ for (const entry of readDirSafe(dir)) {
4080
+ const filePath = (0, import_node_path2.join)(dir, entry.name);
4081
+ if (entry.isDirectory()) {
4082
+ found.push(...walkCodex(root, cutoffMs, filePath, depth + 1));
4083
+ continue;
4084
+ }
4085
+ if (!entry.name.endsWith(".jsonl")) continue;
4086
+ const mtimeMs = mtimeOf(filePath);
4087
+ if (mtimeMs === null || mtimeMs < cutoffMs) continue;
4088
+ found.push({
4089
+ path: filePath,
4090
+ mtimeMs,
4091
+ sessionId: descriptorFor("codex").sessionFile.sessionId(entry.name) ?? "",
4092
+ isSubagent: false,
4093
+ parentSessionId: null
4094
+ });
4095
+ }
4096
+ return found;
4097
+ }
4098
+ function copilotTranscript(root, sessionId) {
4099
+ return (0, import_node_path2.join)(root, descriptorFor("copilot").sessionFile.name(sessionId));
4100
+ }
4101
+ function walkCopilot(root, cutoffMs) {
4102
+ const found = [];
4103
+ for (const entry of readDirSafe(root)) {
4104
+ if (!entry.isDirectory()) continue;
4105
+ const filePath = copilotTranscript(root, entry.name);
4106
+ const mtimeMs = mtimeOf(filePath);
4107
+ if (mtimeMs === null || mtimeMs < cutoffMs) continue;
4108
+ found.push({
4109
+ path: filePath,
4110
+ mtimeMs,
4111
+ sessionId: entry.name,
4112
+ isSubagent: false,
4113
+ parentSessionId: null
4114
+ });
4115
+ }
4116
+ return found;
4117
+ }
4118
+ var WALKERS = {
4119
+ claude: {
4120
+ root: () => dirs.PROJECTS_DIR,
4121
+ list: walkClaude,
4122
+ identify: claudeIdentify,
4123
+ find(root, sessionId) {
4124
+ const fileName = descriptorFor("claude").sessionFile.name(sessionId);
4125
+ for (const project of readDirSafe(root)) {
4126
+ if (!project.isDirectory() || NON_PROJECT_DIRS.has(project.name)) continue;
4127
+ const candidate = (0, import_node_path2.join)(root, project.name, fileName);
4128
+ if (mtimeOf(candidate) !== null) return candidate;
4129
+ }
4130
+ return null;
4131
+ }
4132
+ },
4133
+ codex: {
4134
+ root: () => dirs.CODEX_SESSIONS_DIR,
4135
+ list: (root, cutoffMs) => walkCodex(root, cutoffMs),
4136
+ identify: (filePath) => ({
4137
+ sessionId: descriptorFor("codex").sessionFile.sessionId(filePath) ?? "",
4138
+ isSubagent: false,
4139
+ parentSessionId: null
4140
+ }),
4141
+ // A rollout's date nesting cannot be rebuilt from the id, so the tree is
4142
+ // walked; every rollout carries the id in its file name.
4143
+ find(root, sessionId) {
4144
+ const walk = (dir, depth) => {
4145
+ if (depth > MAX_WALK_DEPTH) return null;
4146
+ for (const entry of readDirSafe(dir)) {
4147
+ const filePath = (0, import_node_path2.join)(dir, entry.name);
4148
+ if (entry.isDirectory()) {
4149
+ const match = walk(filePath, depth + 1);
4150
+ if (match) return match;
4151
+ continue;
4152
+ }
4153
+ if (entry.name.endsWith(`${sessionId}.jsonl`)) return filePath;
4154
+ }
4155
+ return null;
4156
+ };
4157
+ return walk(root, 0);
4158
+ }
4159
+ },
4160
+ copilot: {
4161
+ root: () => dirs.COPILOT_SESSIONS_DIR,
4162
+ list: walkCopilot,
4163
+ identify: (filePath, root) => ({
4164
+ sessionId: segmentsUnder(root, filePath)[0] ?? "",
4165
+ isSubagent: false,
4166
+ parentSessionId: null
4167
+ }),
4168
+ find(root, sessionId) {
4169
+ const candidate = copilotTranscript(root, sessionId);
4170
+ return mtimeOf(candidate) === null ? null : candidate;
4171
+ }
4172
+ }
4173
+ };
4174
+ function makeStore(kind) {
4175
+ const walker = WALKERS[kind];
4176
+ return {
4177
+ kind,
4178
+ descriptor: descriptorFor(kind),
4179
+ root: walker.root,
4180
+ ownsPath: (filePath) => isWithin((0, import_node_path2.resolve)(walker.root()), (0, import_node_path2.resolve)(filePath)),
4181
+ identify: (filePath, root = walker.root()) => walker.identify(filePath, root),
4182
+ list: (cutoffMs, root = walker.root()) => walker.list(root, cutoffMs),
4183
+ find: (sessionId) => walker.find(walker.root(), sessionId)
4184
+ };
4185
+ }
4186
+ var STORES = {
4187
+ claude: makeStore("claude"),
4188
+ codex: makeStore("codex"),
4189
+ copilot: makeStore("copilot")
4190
+ };
4191
+ function storeFor(kind) {
4192
+ return STORES[kind];
4193
+ }
4194
+ function defaultStore() {
4195
+ return storeFor(descriptorForDirName(null).kind);
4196
+ }
4197
+ function allStores() {
4198
+ return AGENT_KINDS.map((kind) => STORES[kind]);
4199
+ }
4200
+ function storeForPath(filePath) {
4201
+ return allStores().find((store) => store.ownsPath(filePath)) ?? null;
4202
+ }
4203
+ function listAllSessionFiles(cutoffMs) {
4204
+ return allStores().flatMap((store) => store.list(cutoffMs)).sort((a, b) => b.mtimeMs - a.mtimeMs);
4205
+ }
4206
+ function isSinglePathSegment(value) {
4207
+ return value.length > 0 && !value.includes("/") && !value.includes("\\") && !value.includes("\0") && value !== "." && value !== "..";
4208
+ }
4209
+ async function findSessionFile(sessionId) {
4210
+ if (!isSinglePathSegment(sessionId)) return null;
4211
+ for (const store of allStores()) {
4212
+ const filePath = store.find(sessionId);
4213
+ const canonical = filePath && canonicalWithinRoot(store.root(), filePath);
4214
+ if (canonical) return canonical;
4215
+ }
4216
+ return null;
4217
+ }
4218
+
2111
4219
  // src/lib/search-index.ts
2112
4220
  var MAX_CONTENT_LEN = 4096;
2113
4221
  function truncContent(text) {
2114
4222
  return text.length > MAX_CONTENT_LEN ? text.slice(0, MAX_CONTENT_LEN) : text;
2115
4223
  }
4224
+ function identifyTranscript(filePath) {
4225
+ const store = storeForPath(filePath) ?? defaultStore();
4226
+ return store.identify(filePath, store.root());
4227
+ }
2116
4228
  var SearchIndex = class {
2117
4229
  db;
2118
4230
  dbPath;
2119
- projectsDir = null;
2120
- _watcherRunning = false;
2121
4231
  _lastFullBuild = null;
2122
4232
  _lastUpdate = null;
2123
- watcher = null;
2124
- debounceTimers = /* @__PURE__ */ new Map();
2125
4233
  constructor(dbPath) {
2126
4234
  this.dbPath = dbPath;
2127
4235
  this.db = new Database(dbPath);
@@ -2157,7 +4265,7 @@ var SearchIndex = class {
2157
4265
  const { count: totalRows } = this.db.prepare("SELECT COUNT(*) as count FROM search_content").get();
2158
4266
  let dbSizeBytes = 0;
2159
4267
  try {
2160
- dbSizeBytes = (0, import_node_fs.statSync)(this.dbPath).size;
4268
+ dbSizeBytes = (0, import_node_fs2.statSync)(this.dbPath).size;
2161
4269
  } catch {
2162
4270
  }
2163
4271
  return {
@@ -2168,7 +4276,6 @@ var SearchIndex = class {
2168
4276
  indexedSessions,
2169
4277
  indexedSubagents,
2170
4278
  totalRows,
2171
- watcherRunning: this._watcherRunning,
2172
4279
  lastFullBuild: this._lastFullBuild,
2173
4280
  lastUpdate: this._lastUpdate
2174
4281
  };
@@ -2229,13 +4336,11 @@ var SearchIndex = class {
2229
4336
  * All inserts run in a single transaction for performance.
2230
4337
  */
2231
4338
  indexFile(filePath, sessionId, mtimeMs, opts) {
2232
- if (!sessionId) {
2233
- sessionId = (0, import_node_path.basename)(filePath, ".jsonl");
2234
- }
4339
+ const resolvedSessionId = sessionId ?? identifyTranscript(filePath).sessionId;
2235
4340
  if (mtimeMs == null) {
2236
- mtimeMs = (0, import_node_fs.statSync)(filePath).mtimeMs;
4341
+ mtimeMs = (0, import_node_fs2.statSync)(filePath).mtimeMs;
2237
4342
  }
2238
- const content = (0, import_node_fs.readFileSync)(filePath, "utf-8");
4343
+ const content = (0, import_node_fs2.readFileSync)(filePath, "utf-8");
2239
4344
  const session = parseSession(content, { skipStats: true });
2240
4345
  const isSubagent = opts?.isSubagent ? 1 : 0;
2241
4346
  const parentSessionId = opts?.parentSessionId ?? null;
@@ -2254,12 +4359,34 @@ var SearchIndex = class {
2254
4359
  const txn = this.db.transaction(() => {
2255
4360
  deleteContent.run(filePath);
2256
4361
  deleteFile.run(filePath);
2257
- this.insertSessionContent(insert, sessionId, filePath, session);
2258
- insertFile.run(filePath, mtimeMs, sessionId, isSubagent, parentSessionId);
4362
+ this.insertSessionContent(insert, resolvedSessionId, filePath, session);
4363
+ insertFile.run(filePath, mtimeMs, resolvedSessionId, isSubagent, parentSessionId);
2259
4364
  });
2260
4365
  txn();
2261
4366
  this._lastUpdate = (/* @__PURE__ */ new Date()).toISOString();
2262
4367
  }
4368
+ pruneMissingFiles() {
4369
+ const files = this.db.prepare("SELECT file_path FROM indexed_files").all();
4370
+ const missing = files.filter(({ file_path }) => {
4371
+ try {
4372
+ (0, import_node_fs2.statSync)(file_path);
4373
+ return false;
4374
+ } catch (error) {
4375
+ const code = error.code;
4376
+ return code === "ENOENT" || code === "ENOTDIR";
4377
+ }
4378
+ });
4379
+ if (missing.length === 0) return 0;
4380
+ this.db.transaction(() => {
4381
+ this.db.exec("CREATE TEMP TABLE missing_transcripts (file_path TEXT PRIMARY KEY)");
4382
+ const insert = this.db.prepare("INSERT INTO missing_transcripts VALUES (?)");
4383
+ for (const { file_path } of missing) insert.run(file_path);
4384
+ this.db.exec("DELETE FROM search_content WHERE source_file IN (SELECT file_path FROM missing_transcripts)");
4385
+ this.db.exec("DELETE FROM indexed_files WHERE file_path IN (SELECT file_path FROM missing_transcripts)");
4386
+ this.db.exec("DROP TABLE missing_transcripts");
4387
+ })();
4388
+ return missing.length;
4389
+ }
2263
4390
  /**
2264
4391
  * Query the FTS5 index and return structured search results.
2265
4392
  *
@@ -2272,6 +4399,7 @@ var SearchIndex = class {
2272
4399
  * - `limit` defaults to 200 and is clamped to a max of 200.
2273
4400
  */
2274
4401
  search(query, opts) {
4402
+ this.pruneMissingFiles();
2275
4403
  const limit = Math.min(Math.max(1, opts?.limit ?? 200), 200);
2276
4404
  const sessionId = opts?.sessionId;
2277
4405
  const maxAgeMs = opts?.maxAgeMs;
@@ -2294,6 +4422,10 @@ var SearchIndex = class {
2294
4422
  conditions.push("sc.session_id = ?");
2295
4423
  params.push(sessionId);
2296
4424
  }
4425
+ if (opts?.excludeSessionId) {
4426
+ conditions.push("sc.session_id != ?");
4427
+ params.push(opts.excludeSessionId);
4428
+ }
2297
4429
  sql += " WHERE " + conditions.join(" AND ");
2298
4430
  sql += " ORDER BY sc.rowid DESC";
2299
4431
  sql += " LIMIT ?";
@@ -2335,27 +4467,26 @@ var SearchIndex = class {
2335
4467
  conditions.push("sc.session_id = ?");
2336
4468
  params.push(opts.sessionId);
2337
4469
  }
4470
+ if (opts?.excludeSessionId) {
4471
+ conditions.push("sc.session_id != ?");
4472
+ params.push(opts.excludeSessionId);
4473
+ }
2338
4474
  sql += " WHERE " + conditions.join(" AND ");
2339
4475
  const row = this.db.prepare(sql).get(...params);
2340
4476
  return { totalHits: row.total, sessionsSearched: row.sessions };
2341
4477
  }
2342
4478
  /**
2343
- * Clear all indexed data and re-index every JSONL file under `projectsDir`.
2344
- * Structure: projectsDir/{projectName}/{sessionId}.jsonl
2345
- * Subagents: projectsDir/{projectName}/{sessionId}/subagents/agent-{id}.jsonl
4479
+ * Clear all indexed data and re-index every transcript, from every agent.
2346
4480
  *
2347
4481
  * Optimized: discovers all files first, then processes them in a single
2348
4482
  * SQLite transaction with pre-prepared statements. This avoids the overhead
2349
4483
  * of 3000+ individual transactions (each forcing a disk sync).
2350
- *
2351
- * Stores `projectsDir` as a class field so `rebuild()` can reuse it.
2352
4484
  */
2353
- buildFull(projectsDir) {
2354
- this.projectsDir = projectsDir;
4485
+ buildFull() {
2355
4486
  this.db.close();
2356
4487
  for (const suffix of ["", "-wal", "-shm"]) {
2357
4488
  try {
2358
- (0, import_node_fs.unlinkSync)(this.dbPath + suffix);
4489
+ (0, import_node_fs2.unlinkSync)(this.dbPath + suffix);
2359
4490
  } catch {
2360
4491
  }
2361
4492
  }
@@ -2367,8 +4498,8 @@ var SearchIndex = class {
2367
4498
  this.db.exec("PRAGMA mmap_size = 268435456");
2368
4499
  this.initSchema();
2369
4500
  const files = [];
2370
- this.discoverFiles(projectsDir, (filePath, sessionId, mtimeMs, isSubagent, parentSessionId) => {
2371
- files.push({ path: filePath, sessionId, mtimeMs, isSubagent, parentSessionId });
4501
+ this.discoverFiles((file) => {
4502
+ files.push(file);
2372
4503
  });
2373
4504
  const insert = this.db.prepare(
2374
4505
  "INSERT INTO search_content (session_id, source_file, location, content) VALUES (?, ?, ?, ?)"
@@ -2379,7 +4510,7 @@ var SearchIndex = class {
2379
4510
  const txn = this.db.transaction(() => {
2380
4511
  for (const file of files) {
2381
4512
  try {
2382
- const content = (0, import_node_fs.readFileSync)(file.path, "utf-8");
4513
+ const content = (0, import_node_fs2.readFileSync)(file.path, "utf-8");
2383
4514
  const session = parseSession(content, { skipStats: true });
2384
4515
  this.insertSessionContent(insert, file.sessionId, file.path, session);
2385
4516
  insertFile.run(file.path, file.mtimeMs, file.sessionId, file.isSubagent ? 1 : 0, file.parentSessionId);
@@ -2392,39 +4523,6 @@ var SearchIndex = class {
2392
4523
  this._lastFullBuild = (/* @__PURE__ */ new Date()).toISOString();
2393
4524
  this._lastUpdate = (/* @__PURE__ */ new Date()).toISOString();
2394
4525
  }
2395
- /**
2396
- * Incrementally re-index only files whose mtime has changed since last index.
2397
- * New files (not in indexed_files) are always indexed.
2398
- *
2399
- * WARNING: This walks ALL files under projectsDir and stats each one.
2400
- * On large session stores (3000+ files) this can take minutes.
2401
- * Prefer `updateRecent()` for CLI search paths.
2402
- */
2403
- updateStale(projectsDir) {
2404
- this.projectsDir = projectsDir;
2405
- const getIndexed = this.db.prepare(
2406
- "SELECT mtime_ms FROM indexed_files WHERE file_path = ?"
2407
- );
2408
- const filesToIndex = [];
2409
- this.discoverFiles(projectsDir, (filePath, sessionId, mtimeMs, isSubagent, parentSessionId) => {
2410
- const existing = getIndexed.get(filePath);
2411
- if (!existing || existing.mtime_ms < mtimeMs) {
2412
- filesToIndex.push({ path: filePath, sessionId, mtimeMs, isSubagent, parentSessionId });
2413
- }
2414
- });
2415
- for (const file of filesToIndex) {
2416
- try {
2417
- this.indexFile(file.path, file.sessionId, file.mtimeMs, {
2418
- isSubagent: file.isSubagent,
2419
- parentSessionId: file.parentSessionId
2420
- });
2421
- } catch {
2422
- }
2423
- }
2424
- if (filesToIndex.length > 0) {
2425
- this._lastUpdate = (/* @__PURE__ */ new Date()).toISOString();
2426
- }
2427
- }
2428
4526
  /**
2429
4527
  * Lightweight incremental update for CLI search paths.
2430
4528
  *
@@ -2433,8 +4531,7 @@ var SearchIndex = class {
2433
4531
  * Caps re-indexing to `maxFiles` to prevent blocking on large backlogs
2434
4532
  * (run `index rebuild` for a full catch-up).
2435
4533
  */
2436
- updateRecent(projectsDir, maxFiles = 50) {
2437
- this.projectsDir = projectsDir;
4534
+ updateRecent(maxFiles = 50) {
2438
4535
  const row = this.db.prepare(
2439
4536
  "SELECT MAX(mtime_ms) as max_mtime FROM indexed_files"
2440
4537
  ).get();
@@ -2443,12 +4540,12 @@ var SearchIndex = class {
2443
4540
  "SELECT mtime_ms FROM indexed_files WHERE file_path = ?"
2444
4541
  );
2445
4542
  const filesToIndex = [];
2446
- this.discoverFiles(projectsDir, (filePath, sessionId, mtimeMs, isSubagent, parentSessionId) => {
2447
- if (mtimeMs <= highWater) {
2448
- const existing = getIndexed.get(filePath);
2449
- if (existing && existing.mtime_ms >= mtimeMs) return;
4543
+ this.discoverFiles((file) => {
4544
+ if (file.mtimeMs <= highWater) {
4545
+ const existing = getIndexed.get(file.path);
4546
+ if (existing && existing.mtime_ms >= file.mtimeMs) return;
2450
4547
  }
2451
- filesToIndex.push({ path: filePath, sessionId, mtimeMs, isSubagent, parentSessionId });
4548
+ filesToIndex.push(file);
2452
4549
  });
2453
4550
  filesToIndex.sort((a, b) => b.mtimeMs - a.mtimeMs);
2454
4551
  const batch = filesToIndex.slice(0, maxFiles);
@@ -2465,179 +4562,19 @@ var SearchIndex = class {
2465
4562
  this._lastUpdate = (/* @__PURE__ */ new Date()).toISOString();
2466
4563
  }
2467
4564
  }
2468
- /**
2469
- * Re-run buildFull using the previously stored projectsDir.
2470
- * No-op if projectsDir was never set.
2471
- */
2472
- rebuild() {
2473
- if (!this.projectsDir) return;
2474
- this.buildFull(this.projectsDir);
2475
- }
2476
- /**
2477
- * Walk the projects directory tree and invoke `callback` for every JSONL file.
2478
- *
2479
- * Directory structure:
2480
- * projectsDir/
2481
- * {projectName}/
2482
- * {sessionId}.jsonl <- session file
2483
- * {sessionId}/subagents/
2484
- * agent-{agentId}.jsonl <- subagent file (recursive)
2485
- *
2486
- * Skips the "memory" directory.
2487
- */
2488
- discoverFiles(projectsDir, callback) {
2489
- let entries;
2490
- try {
2491
- entries = (0, import_node_fs.readdirSync)(projectsDir, { withFileTypes: true });
2492
- } catch {
2493
- return;
2494
- }
2495
- for (const entry of entries) {
2496
- if (entry.name === "memory" || !entry.isDirectory()) continue;
2497
- const projectDir = (0, import_node_path.join)(projectsDir, entry.name);
2498
- let files;
2499
- try {
2500
- files = (0, import_node_fs.readdirSync)(projectDir, { withFileTypes: true });
2501
- } catch {
2502
- continue;
2503
- }
2504
- for (const file of files) {
2505
- if (!file.name.endsWith(".jsonl") || !file.isFile()) continue;
2506
- const filePath = (0, import_node_path.join)(projectDir, file.name);
2507
- const sessionId = (0, import_node_path.basename)(file.name, ".jsonl");
2508
- try {
2509
- const s = (0, import_node_fs.statSync)(filePath);
2510
- callback(filePath, sessionId, s.mtimeMs, false, null);
2511
- } catch {
2512
- continue;
2513
- }
2514
- this.discoverSubagents(filePath, sessionId, callback, 0, 4);
2515
- }
2516
- }
2517
- }
2518
- /**
2519
- * Recursively discover subagent JSONL files under the subagents directory
2520
- * that corresponds to `parentPath`.
2521
- *
2522
- * For a parent at `/path/to/session-1.jsonl`, looks for subagents at
2523
- * `/path/to/session-1/subagents/agent-*.jsonl`.
2524
- *
2525
- * Recurses up to `maxDepth` levels (default 4).
2526
- */
2527
- discoverSubagents(parentPath, parentSessionId, callback, depth, maxDepth) {
2528
- if (depth >= maxDepth) return;
2529
- const subDir = parentPath.replace(/\.jsonl$/, "") + "/subagents";
2530
- let files;
2531
- try {
2532
- files = (0, import_node_fs.readdirSync)(subDir);
2533
- } catch {
2534
- return;
2535
- }
2536
- for (const file of files) {
2537
- if (!file.startsWith("agent-") || !file.endsWith(".jsonl")) continue;
2538
- const filePath = (0, import_node_path.join)(subDir, file);
2539
- try {
2540
- const s = (0, import_node_fs.statSync)(filePath);
2541
- callback(filePath, parentSessionId, s.mtimeMs, true, parentSessionId);
2542
- } catch {
2543
- continue;
2544
- }
2545
- this.discoverSubagents(filePath, parentSessionId, callback, depth + 1, maxDepth);
2546
- }
2547
- }
2548
- /**
2549
- * Start watching `projectsDir` for JSONL file changes.
2550
- * Runs `updateStale()` immediately for an initial sync, then sets up
2551
- * `fs.watch` with `{ recursive: true }` (macOS-compatible) to detect
2552
- * subsequent file changes and trigger debounced re-indexing.
2553
- */
2554
- startWatching(projectsDir) {
2555
- this.projectsDir = projectsDir;
2556
- this.updateStale(projectsDir);
2557
- try {
2558
- this.watcher = (0, import_node_fs.watch)(projectsDir, { recursive: true }, (_event, filename) => {
2559
- if (!filename || !filename.endsWith(".jsonl")) return;
2560
- this.debouncedReindex((0, import_node_path.join)(projectsDir, filename));
2561
- });
2562
- this._watcherRunning = true;
2563
- } catch (err) {
2564
- console.warn("[search-index] fs.watch failed (recursive may not be supported):", err);
2565
- this._watcherRunning = false;
2566
- }
2567
- }
2568
- /**
2569
- * Stop the file watcher and clear any pending debounce timers.
2570
- * Safe to call even when not watching (no-op).
2571
- */
2572
- stopWatching() {
2573
- if (this.watcher) {
2574
- this.watcher.close();
2575
- this.watcher = null;
2576
- }
2577
- for (const timer of this.debounceTimers.values()) {
2578
- clearTimeout(timer);
2579
- }
2580
- this.debounceTimers.clear();
2581
- this._watcherRunning = false;
2582
- }
2583
- /**
2584
- * Private helper: debounce re-indexing of a single file.
2585
- * Waits 2 seconds after the last change event for a given file path
2586
- * before actually calling `indexFile()`. This coalesces rapid writes
2587
- * (e.g. streaming JSONL appends) into a single index operation.
2588
- */
2589
- debouncedReindex(filePath) {
2590
- const existing = this.debounceTimers.get(filePath);
2591
- if (existing) clearTimeout(existing);
2592
- this.debounceTimers.set(
2593
- filePath,
2594
- setTimeout(() => {
2595
- this.debounceTimers.delete(filePath);
2596
- try {
2597
- const s = (0, import_node_fs.statSync)(filePath);
2598
- const parts = filePath.split("/");
2599
- const fileName = parts[parts.length - 1];
2600
- const isSubagent = parts.includes("subagents");
2601
- let sessionId;
2602
- let parentSessionId = null;
2603
- if (isSubagent) {
2604
- const subagentsIdx = parts.lastIndexOf("subagents");
2605
- const parentDir = parts[subagentsIdx - 1];
2606
- sessionId = parentDir;
2607
- parentSessionId = parentDir;
2608
- } else {
2609
- sessionId = (0, import_node_path.basename)(fileName, ".jsonl");
2610
- }
2611
- this.indexFile(filePath, sessionId, s.mtimeMs, {
2612
- isSubagent,
2613
- parentSessionId
2614
- });
2615
- } catch {
2616
- }
2617
- }, 2e3)
2618
- );
4565
+ /** Every transcript from every agent, tagged with the session it belongs to. */
4566
+ discoverFiles(callback) {
4567
+ for (const store of allStores()) for (const file of store.list(0)) callback(file);
2619
4568
  }
2620
4569
  close() {
2621
- this.stopWatching();
2622
4570
  this.db.close();
2623
4571
  }
2624
4572
  };
2625
4573
 
2626
- // src/lib/dirs.ts
2627
- var import_node_path2 = require("node:path");
2628
- var import_node_os = require("node:os");
2629
- var dirs = {
2630
- PROJECTS_DIR: (0, import_node_path2.join)((0, import_node_os.homedir)(), ".claude", "projects"),
2631
- TEAMS_DIR: (0, import_node_path2.join)((0, import_node_os.homedir)(), ".claude", "teams"),
2632
- TASKS_DIR: (0, import_node_path2.join)((0, import_node_os.homedir)(), ".claude", "tasks"),
2633
- CODEX_SESSIONS_DIR: (0, import_node_path2.join)((0, import_node_os.homedir)(), ".codex", "sessions")
2634
- };
2635
- var DEFAULT_DB_PATH = (0, import_node_path2.join)((0, import_node_os.homedir)(), ".claude", "cogpit-memory", "search-index.db");
2636
-
2637
4574
  // src/commands/search.ts
2638
- var import_node_fs2 = require("node:fs");
2639
- var import_promises2 = require("node:fs/promises");
2640
- var import_node_path4 = require("node:path");
4575
+ var import_node_fs3 = require("node:fs");
4576
+ var import_promises = require("node:fs/promises");
4577
+ var import_node_path3 = require("node:path");
2641
4578
 
2642
4579
  // src/lib/response.ts
2643
4580
  function parseMaxAge(raw) {
@@ -2657,98 +4594,6 @@ function parseMaxAge(raw) {
2657
4594
  }
2658
4595
  }
2659
4596
 
2660
- // src/lib/helpers.ts
2661
- var import_promises = require("node:fs/promises");
2662
- var import_node_path3 = require("node:path");
2663
- var import_node_os2 = require("node:os");
2664
- async function findJsonlPath(sessionId) {
2665
- const targetFile = `${sessionId}.jsonl`;
2666
- try {
2667
- const entries = await (0, import_promises.readdir)(dirs.PROJECTS_DIR, { withFileTypes: true });
2668
- for (const entry of entries) {
2669
- if (!entry.isDirectory() || entry.name === "memory") continue;
2670
- const projectDir = (0, import_node_path3.join)(dirs.PROJECTS_DIR, entry.name);
2671
- try {
2672
- const files = await (0, import_promises.readdir)(projectDir);
2673
- if (files.includes(targetFile)) {
2674
- return (0, import_node_path3.join)(projectDir, targetFile);
2675
- }
2676
- } catch {
2677
- continue;
2678
- }
2679
- }
2680
- } catch {
2681
- }
2682
- const codexRoot = (0, import_node_path3.join)((0, import_node_os2.homedir)(), ".codex", "sessions");
2683
- const walk = async (dir, depth) => {
2684
- if (depth > 4) return null;
2685
- let entries;
2686
- try {
2687
- entries = await (0, import_promises.readdir)(dir, { withFileTypes: true });
2688
- } catch {
2689
- return null;
2690
- }
2691
- for (const entry of entries) {
2692
- const filePath = (0, import_node_path3.join)(dir, entry.name);
2693
- if (entry.isDirectory()) {
2694
- const match = await walk(filePath, depth + 1);
2695
- if (match) return match;
2696
- continue;
2697
- }
2698
- if (!entry.name.endsWith(".jsonl")) continue;
2699
- if (entry.name.endsWith(`${sessionId}.jsonl`)) return filePath;
2700
- }
2701
- return null;
2702
- };
2703
- const codexMatch = await walk(codexRoot, 0);
2704
- if (codexMatch) return codexMatch;
2705
- return null;
2706
- }
2707
- async function matchSubagentToMember(leadSessionId, subagentFileName, members) {
2708
- const entries = await (0, import_promises.readdir)(dirs.PROJECTS_DIR, { withFileTypes: true });
2709
- for (const entry of entries) {
2710
- if (!entry.isDirectory() || entry.name === "memory") continue;
2711
- const filePath = (0, import_node_path3.join)(
2712
- dirs.PROJECTS_DIR,
2713
- entry.name,
2714
- leadSessionId,
2715
- "subagents",
2716
- subagentFileName
2717
- );
2718
- try {
2719
- const fh = await (0, import_promises.open)(filePath, "r");
2720
- try {
2721
- const buf = Buffer.alloc(16384);
2722
- const { bytesRead } = await fh.read(buf, 0, 16384, 0);
2723
- const firstLine = buf.subarray(0, bytesRead).toString("utf-8").split("\n")[0] || "";
2724
- for (const member of members) {
2725
- if (member.agentType === "team-lead") continue;
2726
- const prompt = member.prompt || "";
2727
- const snippet = prompt.slice(0, 120);
2728
- const terms = [
2729
- member.name,
2730
- member.name.replace(/-/g, " "),
2731
- ...snippet ? [snippet, snippet.replace(/"/g, '\\"')] : []
2732
- ];
2733
- if (terms.some((t) => firstLine.includes(t))) {
2734
- return member.name;
2735
- }
2736
- }
2737
- } finally {
2738
- await fh.close();
2739
- }
2740
- } catch {
2741
- continue;
2742
- }
2743
- }
2744
- return null;
2745
- }
2746
- function encodeClaudeDirName(cwd) {
2747
- const normalized = cwd.replace(/[\\/]+$/, "") || cwd;
2748
- return normalized.replace(/[^a-zA-Z0-9]/g, "-");
2749
- }
2750
- var HOME_PREFIX = encodeClaudeDirName((0, import_node_os2.homedir)()).replace(/^-/, "").toLowerCase();
2751
-
2752
4597
  // src/commands/search.ts
2753
4598
  async function searchSessions(query, opts, searchIndex) {
2754
4599
  if (!query || query.length < 2) {
@@ -2757,21 +4602,20 @@ async function searchSessions(query, opts, searchIndex) {
2757
4602
  const limit = Math.min(Math.max(1, opts.limit ?? 20), 200);
2758
4603
  const caseSensitive = opts.caseSensitive ?? false;
2759
4604
  const maxAgeMs = parseMaxAge(opts.maxAge ?? "5d");
2760
- const depth = Math.min(Math.max(1, opts.depth ?? 4), 4);
2761
4605
  let index = searchIndex ?? null;
2762
4606
  let ownedIndex = false;
2763
4607
  if (!index && searchIndex === void 0) {
2764
4608
  try {
2765
- const dbExists = (0, import_node_fs2.existsSync)(DEFAULT_DB_PATH);
4609
+ const dbExists = (0, import_node_fs3.existsSync)(DEFAULT_DB_PATH);
2766
4610
  if (!dbExists) {
2767
- (0, import_node_fs2.mkdirSync)((0, import_node_path4.dirname)(DEFAULT_DB_PATH), { recursive: true });
4611
+ (0, import_node_fs3.mkdirSync)((0, import_node_path3.dirname)(DEFAULT_DB_PATH), { recursive: true });
2768
4612
  }
2769
4613
  index = new SearchIndex(DEFAULT_DB_PATH);
2770
4614
  ownedIndex = true;
2771
4615
  if (!dbExists) {
2772
- index.buildFull(dirs.PROJECTS_DIR);
4616
+ index.buildFull();
2773
4617
  } else {
2774
- index.updateRecent(dirs.PROJECTS_DIR);
4618
+ index.updateRecent();
2775
4619
  }
2776
4620
  } catch {
2777
4621
  }
@@ -2781,9 +4625,14 @@ async function searchSessions(query, opts, searchIndex) {
2781
4625
  const hits = index.search(query, {
2782
4626
  limit,
2783
4627
  sessionId: opts.sessionId,
4628
+ excludeSessionId: opts.excludeSessionId,
2784
4629
  maxAgeMs,
2785
4630
  caseSensitive
2786
4631
  });
4632
+ if (opts.sessionId && hits.length === 0) {
4633
+ if (ownedIndex) index.close();
4634
+ return rawScanSearch(query, opts.sessionId, maxAgeMs, limit, caseSensitive, opts.excludeSessionId);
4635
+ }
2787
4636
  const grouped = /* @__PURE__ */ new Map();
2788
4637
  for (const hit of hits) {
2789
4638
  let entry = grouped.get(hit.sessionId);
@@ -2807,6 +4656,7 @@ async function searchSessions(query, opts, searchIndex) {
2807
4656
  if (hits.length >= limit) {
2808
4657
  const counts = index.countMatches(query, {
2809
4658
  sessionId: opts.sessionId,
4659
+ excludeSessionId: opts.excludeSessionId,
2810
4660
  maxAgeMs
2811
4661
  });
2812
4662
  totalHits = counts.totalHits;
@@ -2824,7 +4674,7 @@ async function searchSessions(query, opts, searchIndex) {
2824
4674
  if (ownedIndex) index.close();
2825
4675
  }
2826
4676
  }
2827
- return rawScanSearch(query, opts.sessionId ?? null, maxAgeMs, limit, caseSensitive, depth);
4677
+ return rawScanSearch(query, opts.sessionId ?? null, maxAgeMs, limit, caseSensitive, opts.excludeSessionId);
2828
4678
  }
2829
4679
  var CWD_READ_BYTES = 4096;
2830
4680
  var cwdCache = /* @__PURE__ */ new Map();
@@ -2832,30 +4682,16 @@ async function cwdFromFilePath(filePath) {
2832
4682
  const cached = cwdCache.get(filePath);
2833
4683
  if (cached !== void 0) return cached;
2834
4684
  try {
2835
- const fh = await (0, import_promises2.open)(filePath, "r");
4685
+ const fh = await (0, import_promises.open)(filePath, "r");
2836
4686
  try {
2837
4687
  const buf = Buffer.alloc(CWD_READ_BYTES);
2838
4688
  const { bytesRead } = await fh.read(buf, 0, CWD_READ_BYTES, 0);
2839
4689
  const head = buf.subarray(0, bytesRead).toString("utf-8");
2840
- const lines = head.split("\n", 10);
2841
- for (const line of lines) {
2842
- if (!line) continue;
2843
- try {
2844
- const obj = JSON.parse(line);
2845
- if (obj.cwd) {
2846
- cwdCache.set(filePath, obj.cwd);
2847
- return obj.cwd;
2848
- }
2849
- if (obj.type === "session_meta" && obj.payload?.cwd) {
2850
- cwdCache.set(filePath, obj.payload.cwd);
2851
- return obj.payload.cwd;
2852
- }
2853
- if (obj.type === "turn_context" && obj.payload?.cwd) {
2854
- cwdCache.set(filePath, obj.payload.cwd);
2855
- return obj.payload.cwd;
2856
- }
2857
- } catch {
2858
- }
4690
+ const lines = head.split("\n", 10).filter(Boolean);
4691
+ const cwd = formatForText(lines.join("\n")).metadataFromLines(lines).cwd;
4692
+ if (cwd) {
4693
+ cwdCache.set(filePath, cwd);
4694
+ return cwd;
2859
4695
  }
2860
4696
  } finally {
2861
4697
  await fh.close();
@@ -2866,6 +4702,7 @@ async function cwdFromFilePath(filePath) {
2866
4702
  return "";
2867
4703
  }
2868
4704
  var SNIPPET_WINDOW = 150;
4705
+ var MAX_SUBAGENT_DEPTH = 4;
2869
4706
  function generateSnippet(text, matchIdx, queryLen) {
2870
4707
  if (matchIdx === -1) return text.slice(0, SNIPPET_WINDOW);
2871
4708
  const halfWindow = Math.floor((SNIPPET_WINDOW - queryLen) / 2);
@@ -2901,49 +4738,17 @@ function searchField(text, location, query, caseSensitive, extras) {
2901
4738
  };
2902
4739
  }
2903
4740
  async function discoverSingleSession(sessionId) {
2904
- const jsonlPath = await findJsonlPath(sessionId);
4741
+ const jsonlPath = await findSessionFile(sessionId);
2905
4742
  if (!jsonlPath) return [];
2906
- const s = await (0, import_promises2.stat)(jsonlPath);
2907
- return [{ path: jsonlPath, mtimeMs: s.mtimeMs }];
4743
+ const s = await (0, import_promises.stat)(jsonlPath);
4744
+ return [{ path: jsonlPath, mtimeMs: s.mtimeMs, sessionId }];
2908
4745
  }
2909
- async function discoverAllSessions(maxAgeMs) {
2910
- const cutoff = Date.now() - maxAgeMs;
2911
- let entries;
2912
- try {
2913
- entries = await (0, import_promises2.readdir)(dirs.PROJECTS_DIR, { withFileTypes: true });
2914
- } catch {
2915
- return [];
2916
- }
2917
- const projectDirs = entries.filter((e) => e.isDirectory() && e.name !== "memory").map((e) => (0, import_node_path4.join)(dirs.PROJECTS_DIR, e.name));
2918
- const nested = await Promise.all(
2919
- projectDirs.map(async (projectDir) => {
2920
- try {
2921
- const files = await (0, import_promises2.readdir)(projectDir);
2922
- const jsonlFiles = files.filter((f) => f.endsWith(".jsonl"));
2923
- const statResults = await Promise.all(
2924
- jsonlFiles.map(async (f) => {
2925
- const filePath = (0, import_node_path4.join)(projectDir, f);
2926
- try {
2927
- const s = await (0, import_promises2.stat)(filePath);
2928
- return s.mtimeMs >= cutoff ? { path: filePath, mtimeMs: s.mtimeMs } : null;
2929
- } catch {
2930
- return null;
2931
- }
2932
- })
2933
- );
2934
- return statResults.filter((r) => r !== null);
2935
- } catch {
2936
- return [];
2937
- }
2938
- })
2939
- );
2940
- const results = nested.flat();
2941
- results.sort((a, b) => b.mtimeMs - a.mtimeMs);
2942
- return results;
4746
+ function discoverAllSessions(maxAgeMs) {
4747
+ return listAllSessionFiles(Date.now() - maxAgeMs).filter((file) => !file.isSubagent).map((file) => ({ path: file.path, mtimeMs: file.mtimeMs, sessionId: file.sessionId }));
2943
4748
  }
2944
4749
  async function rawTextMatch(filePath, query, caseSensitive) {
2945
4750
  try {
2946
- const content = await (0, import_promises2.readFile)(filePath, "utf-8");
4751
+ const content = await (0, import_promises.readFile)(filePath, "utf-8");
2947
4752
  const haystack = caseSensitive ? content : content.toLowerCase();
2948
4753
  const needle = caseSensitive ? query : query.toLowerCase();
2949
4754
  if (haystack.includes(needle)) return content;
@@ -3018,13 +4823,13 @@ async function walkSubagentFiles(parentJsonlPath, query, caseSensitive, currentD
3018
4823
  const subDir = parentJsonlPath.replace(/\.jsonl$/, "") + "/subagents";
3019
4824
  let files;
3020
4825
  try {
3021
- files = await (0, import_promises2.readdir)(subDir);
4826
+ files = await (0, import_promises.readdir)(subDir);
3022
4827
  } catch {
3023
4828
  return [];
3024
4829
  }
3025
4830
  const agentFiles = files.filter((f) => f.startsWith("agent-") && f.endsWith(".jsonl")).map((f) => ({
3026
4831
  agentId: f.slice("agent-".length, -".jsonl".length),
3027
- filePath: (0, import_node_path4.join)(subDir, f)
4832
+ filePath: (0, import_node_path3.join)(subDir, f)
3028
4833
  }));
3029
4834
  const matched = await Promise.all(
3030
4835
  agentFiles.map(async ({ agentId, filePath }) => ({
@@ -3043,26 +4848,27 @@ async function walkSubagentFiles(parentJsonlPath, query, caseSensitive, currentD
3043
4848
  );
3044
4849
  return hitArrays.flat();
3045
4850
  }
3046
- async function rawScanSearch(query, sessionId, maxAgeMs, limit, caseSensitive, depth) {
4851
+ async function rawScanSearch(query, sessionId, maxAgeMs, limit, caseSensitive, excludeSessionId) {
3047
4852
  try {
3048
- const files = sessionId ? await discoverSingleSession(sessionId) : await discoverAllSessions(maxAgeMs);
4853
+ const files = sessionId ? await discoverSingleSession(sessionId) : discoverAllSessions(maxAgeMs);
3049
4854
  let totalHits = 0;
3050
4855
  let returnedHits = 0;
3051
4856
  const results = [];
3052
4857
  let sessionsSearched = 0;
3053
4858
  for (const file of files) {
4859
+ if (file.sessionId === excludeSessionId) continue;
3054
4860
  if (returnedHits >= limit) break;
3055
4861
  const rawContent = await rawTextMatch(file.path, query, caseSensitive);
3056
4862
  sessionsSearched++;
3057
4863
  if (!rawContent) continue;
3058
4864
  const session = parseSession(rawContent);
3059
4865
  const sessionHits = walkSession(session, query, caseSensitive);
3060
- const subagentHits = await walkSubagentFiles(file.path, query, caseSensitive, 0, depth);
4866
+ const subagentHits = await walkSubagentFiles(file.path, query, caseSensitive, 0, MAX_SUBAGENT_DEPTH);
3061
4867
  const allHits = [...sessionHits, ...subagentHits];
3062
4868
  if (allHits.length === 0) continue;
3063
4869
  totalHits += allHits.length;
3064
4870
  const sessionResult = {
3065
- sessionId: session.sessionId || (0, import_node_path4.basename)(file.path, ".jsonl"),
4871
+ sessionId: session.sessionId || (0, import_node_path3.basename)(file.path, ".jsonl"),
3066
4872
  cwd: session.cwd || "",
3067
4873
  hits: []
3068
4874
  };
@@ -3097,6 +4903,51 @@ async function rawScanSearch(query, sessionId, maxAgeMs, limit, caseSensitive, d
3097
4903
  // src/commands/context.ts
3098
4904
  var import_promises3 = require("node:fs/promises");
3099
4905
  var import_node_path5 = require("node:path");
4906
+
4907
+ // src/lib/helpers.ts
4908
+ var import_promises2 = require("node:fs/promises");
4909
+ var import_node_path4 = require("node:path");
4910
+ async function matchSubagentToMember(leadSessionId, subagentFileName, members) {
4911
+ const entries = await (0, import_promises2.readdir)(dirs.PROJECTS_DIR, { withFileTypes: true });
4912
+ for (const entry of entries) {
4913
+ if (!entry.isDirectory() || entry.name === "memory") continue;
4914
+ const filePath = (0, import_node_path4.join)(
4915
+ dirs.PROJECTS_DIR,
4916
+ entry.name,
4917
+ leadSessionId,
4918
+ "subagents",
4919
+ subagentFileName
4920
+ );
4921
+ try {
4922
+ const fh = await (0, import_promises2.open)(filePath, "r");
4923
+ try {
4924
+ const buf = Buffer.alloc(16384);
4925
+ const { bytesRead } = await fh.read(buf, 0, 16384, 0);
4926
+ const firstLine = buf.subarray(0, bytesRead).toString("utf-8").split("\n")[0] || "";
4927
+ for (const member of members) {
4928
+ if (member.agentType === "team-lead") continue;
4929
+ const prompt = member.prompt || "";
4930
+ const snippet = prompt.slice(0, 120);
4931
+ const terms = [
4932
+ member.name,
4933
+ member.name.replace(/-/g, " "),
4934
+ ...snippet ? [snippet, snippet.replace(/"/g, '\\"')] : []
4935
+ ];
4936
+ if (terms.some((t) => firstLine.includes(t))) {
4937
+ return member.name;
4938
+ }
4939
+ }
4940
+ } finally {
4941
+ await fh.close();
4942
+ }
4943
+ } catch {
4944
+ continue;
4945
+ }
4946
+ }
4947
+ return null;
4948
+ }
4949
+
4950
+ // src/commands/context.ts
3100
4951
  var RESULT_TRUNCATE_LIMIT = 1e4;
3101
4952
  var L1_RESPONSE_LIMIT = 15e4;
3102
4953
  var COMPACTION_SUMMARY_PREVIEW = 400;
@@ -3268,562 +5119,236 @@ function mapContentBlock(block) {
3268
5119
  }
3269
5120
  }
3270
5121
  }
3271
- async function findSubagentFile(parentJsonlPath, agentId) {
3272
- const subagentsDir = parentJsonlPath.replace(/\.jsonl$/, "") + "/subagents";
3273
- try {
3274
- const files = await (0, import_promises3.readdir)(subagentsDir);
3275
- for (const f of files) {
3276
- if (!f.startsWith("agent-") || !f.endsWith(".jsonl")) continue;
3277
- const fileAgentId = f.replace("agent-", "").replace(".jsonl", "");
3278
- if (fileAgentId === agentId) {
3279
- return { filePath: (0, import_node_path5.join)(subagentsDir, f), fileName: f };
3280
- }
3281
- }
3282
- } catch {
3283
- }
3284
- return null;
3285
- }
3286
- async function findTeamContext(sessionId, subagentFileName) {
3287
- try {
3288
- const teamEntries = await (0, import_promises3.readdir)(dirs.TEAMS_DIR, { withFileTypes: true });
3289
- for (const entry of teamEntries) {
3290
- if (!entry.isDirectory()) continue;
3291
- const configPath = (0, import_node_path5.join)(dirs.TEAMS_DIR, entry.name, "config.json");
3292
- try {
3293
- const raw = await (0, import_promises3.readFile)(configPath, "utf-8");
3294
- const config = JSON.parse(raw);
3295
- if (config.leadSessionId !== sessionId) continue;
3296
- const members = config.members ?? [];
3297
- const memberName = await matchSubagentToMember(sessionId, subagentFileName, members);
3298
- if (!memberName) continue;
3299
- let currentTask = null;
3300
- try {
3301
- const taskDir = (0, import_node_path5.join)(dirs.TASKS_DIR, entry.name);
3302
- const taskFiles = await (0, import_promises3.readdir)(taskDir);
3303
- for (const tf of taskFiles) {
3304
- if (!tf.endsWith(".json")) continue;
3305
- const taskRaw = await (0, import_promises3.readFile)((0, import_node_path5.join)(taskDir, tf), "utf-8");
3306
- const task = JSON.parse(taskRaw);
3307
- if (task.owner === memberName && task.status === "in_progress") {
3308
- currentTask = { id: task.id, subject: task.subject, status: task.status };
3309
- break;
3310
- }
3311
- }
3312
- } catch {
3313
- }
3314
- return { teamName: entry.name, role: memberName, currentTask };
3315
- } catch {
3316
- }
3317
- }
3318
- } catch {
3319
- }
3320
- return null;
3321
- }
3322
- function findParentToolCallId(session, agentId) {
3323
- for (const turn of session.turns) {
3324
- for (const msg of turn.subAgentActivity) {
3325
- if (msg.agentId === agentId) {
3326
- for (const tc of turn.toolCalls) {
3327
- if (tc.name === "Task" || tc.name === "Agent") {
3328
- return tc.id;
3329
- }
3330
- }
3331
- }
3332
- }
3333
- }
3334
- return null;
3335
- }
3336
- function findAgentMetadata(session, agentId) {
3337
- for (const turn of session.turns) {
3338
- for (const msg of turn.subAgentActivity) {
3339
- if (msg.agentId === agentId) {
3340
- return {
3341
- name: msg.agentName,
3342
- type: msg.subagentType,
3343
- isBackground: msg.isBackground
3344
- };
3345
- }
3346
- }
3347
- }
3348
- return { name: null, type: null, isBackground: false };
3349
- }
3350
- async function getSessionOverview(sessionId) {
3351
- const jsonlPath = await findJsonlPath(sessionId);
3352
- if (!jsonlPath) return { error: "Session not found" };
3353
- const content = await (0, import_promises3.readFile)(jsonlPath, "utf-8");
3354
- const session = parseSession(content);
3355
- return mapSessionToOverview(session);
3356
- }
3357
- async function getTurnDetail(sessionId, turnIndex) {
3358
- const jsonlPath = await findJsonlPath(sessionId);
3359
- if (!jsonlPath) return { error: "Session not found" };
3360
- const content = await (0, import_promises3.readFile)(jsonlPath, "utf-8");
3361
- const session = parseSession(content);
3362
- if (turnIndex < 0 || turnIndex >= session.turns.length) return { error: "Turn not found" };
3363
- return mapTurnToDetail(session, turnIndex);
3364
- }
3365
- async function getAgentOverview(sessionId, agentId) {
3366
- const jsonlPath = await findJsonlPath(sessionId);
3367
- if (!jsonlPath) return { error: "Session not found" };
3368
- const content = await (0, import_promises3.readFile)(jsonlPath, "utf-8");
3369
- const session = parseSession(content);
3370
- const subagentFile = await findSubagentFile(jsonlPath, agentId);
3371
- if (!subagentFile) return { error: "Agent not found" };
3372
- const subagentContent = await (0, import_promises3.readFile)(subagentFile.filePath, "utf-8");
3373
- const subagentSession = parseSession(subagentContent);
3374
- const metadata = findAgentMetadata(session, agentId);
3375
- const parentToolCallId = findParentToolCallId(session, agentId);
3376
- const teamContext = await findTeamContext(sessionId, subagentFile.fileName);
3377
- return {
3378
- sessionId,
3379
- agentId,
3380
- name: metadata.name,
3381
- type: metadata.type,
3382
- parentToolCallId,
3383
- isBackground: metadata.isBackground,
3384
- teamContext,
3385
- overview: mapSessionToOverview(subagentSession)
3386
- };
3387
- }
3388
- async function getAgentTurnDetail(sessionId, agentId, turnIndex) {
3389
- const jsonlPath = await findJsonlPath(sessionId);
3390
- if (!jsonlPath) return { error: "Session not found" };
3391
- const subagentFile = await findSubagentFile(jsonlPath, agentId);
3392
- if (!subagentFile) return { error: "Agent not found" };
3393
- const subagentContent = await (0, import_promises3.readFile)(subagentFile.filePath, "utf-8");
3394
- const subagentSession = parseSession(subagentContent);
3395
- if (turnIndex < 0 || turnIndex >= subagentSession.turns.length) return { error: "Turn not found" };
3396
- return mapTurnToDetail(subagentSession, turnIndex);
3397
- }
3398
-
3399
- // src/commands/sessions.ts
3400
- var import_promises5 = require("node:fs/promises");
3401
- var import_node_path6 = require("node:path");
3402
-
3403
- // src/lib/metadata.ts
3404
- var import_promises4 = require("node:fs/promises");
3405
-
3406
- // src/lib/sessionStatus.ts
3407
- var TASK_NOTIFICATION_MARKER = "<task-notification>";
3408
- var TASK_ID_RE = /<task-id>([^<]+)<\/task-id>/g;
3409
- var TOOL_USE_ID_RE = /<tool-use-id>([^<]+)<\/tool-use-id>/g;
3410
- function isObject3(value) {
3411
- return typeof value === "object" && value !== null;
3412
- }
3413
- function userMessageTexts(msg) {
3414
- const message = isObject3(msg.message) ? msg.message : null;
3415
- const content = message?.content;
3416
- if (typeof content === "string") return [content];
3417
- if (!Array.isArray(content)) return [];
3418
- return content.filter((block) => isObject3(block) && typeof block.text === "string").map((block) => block.text);
3419
- }
3420
- function collectClaudeBackgroundWork(rawMessages) {
3421
- const launches = [];
3422
- const resolved = /* @__PURE__ */ new Set();
3423
- const queuedIds = /* @__PURE__ */ new Set();
3424
- const deliveredIds = /* @__PURE__ */ new Set();
3425
- function noteNotifications(text, sink) {
3426
- if (!text.includes(TASK_NOTIFICATION_MARKER)) return;
3427
- for (const match of text.matchAll(TASK_ID_RE)) {
3428
- resolved.add(match[1]);
3429
- sink?.add(match[1]);
3430
- }
3431
- for (const match of text.matchAll(TOOL_USE_ID_RE)) {
3432
- resolved.add(match[1]);
3433
- sink?.add(match[1]);
3434
- }
3435
- }
3436
- for (const msg of rawMessages) {
3437
- if (msg.type === "user") {
3438
- const toolUseResult = isObject3(msg.toolUseResult) ? msg.toolUseResult : null;
3439
- if (toolUseResult?.status === "async_launched") {
3440
- const message = isObject3(msg.message) ? msg.message : null;
3441
- const blocks = Array.isArray(message?.content) ? message.content : [];
3442
- const toolUseIds = blocks.filter((block) => isObject3(block) && block.type === "tool_result").map((block) => block.tool_use_id);
3443
- const ids = [toolUseResult.agentId, toolUseResult.taskId, ...toolUseIds].filter((id) => typeof id === "string" && id.length > 0);
3444
- const description = [toolUseResult.description, toolUseResult.workflowName, toolUseResult.summary].find((value) => typeof value === "string" && value.length > 0) ?? "";
3445
- if (ids.length > 0) launches.push({ ids, description });
3446
- }
3447
- for (const text of userMessageTexts(msg)) noteNotifications(text, deliveredIds);
3448
- continue;
3449
- }
3450
- if (msg.type === "assistant") {
3451
- const message = isObject3(msg.message) ? msg.message : null;
3452
- const blocks = Array.isArray(message?.content) ? message.content : [];
3453
- for (const block of blocks) {
3454
- if (!isObject3(block) || block.type !== "tool_use" || block.name !== "TaskStop") continue;
3455
- const input = isObject3(block.input) ? block.input : null;
3456
- const taskId = input?.task_id ?? input?.shell_id;
3457
- if (typeof taskId === "string") resolved.add(taskId);
3458
- }
3459
- continue;
3460
- }
3461
- if (msg.type === "attachment") {
3462
- const attachment = isObject3(msg.attachment) ? msg.attachment : null;
3463
- if (typeof attachment?.prompt === "string") noteNotifications(attachment.prompt, deliveredIds);
3464
- continue;
3465
- }
3466
- if (msg.type === "queue-operation" && typeof msg.content === "string") {
3467
- const op = msg.operation;
3468
- noteNotifications(msg.content, op === "enqueue" ? queuedIds : deliveredIds);
3469
- }
3470
- }
3471
- return {
3472
- pendingDescriptions: launches.filter((launch) => !launch.ids.some((id) => resolved.has(id))).map((launch) => launch.description),
3473
- undeliveredNotifications: [...queuedIds].filter((id) => !deliveredIds.has(id)).length
3474
- };
3475
- }
3476
- function awaitingAgentsResult(pendingDescriptions, pendingQueue) {
3477
- return {
3478
- status: "awaiting_agents",
3479
- pendingQueue,
3480
- pendingAgents: pendingDescriptions.length,
3481
- pendingAgentDescriptions: pendingDescriptions.filter((description) => description.length > 0)
3482
- };
3483
- }
3484
- function deriveSessionStatus(rawMessages) {
3485
- const firstType = rawMessages[0]?.type;
3486
- if (firstType === "session_meta" || firstType === "turn_context" || firstType === "event_msg" || firstType === "response_item") {
3487
- return deriveCodexSessionStatus(rawMessages);
3488
- }
3489
- let pendingEnqueues = 0;
3490
- let backgroundWork = null;
3491
- let deliveringNotification = false;
3492
- function result(status, toolName) {
3493
- const info = { status, pendingQueue: Math.max(0, pendingEnqueues) };
3494
- if (toolName) info.toolName = toolName;
3495
- return info;
3496
- }
3497
- function getBackgroundWork() {
3498
- return backgroundWork ??= collectClaudeBackgroundWork(rawMessages);
3499
- }
3500
- for (let i = rawMessages.length - 1; i >= 0; i--) {
3501
- const msg = rawMessages[i];
3502
- if (msg.type === "queue-operation") continue;
3503
- if (msg.type === "progress") {
3504
- const data = msg.data;
3505
- if (data?.type === "hook_progress") {
3506
- const decision = data.decision ?? data.hookSpecificOutput?.permissionDecision;
3507
- if (decision === "defer") return result("deferred");
3508
- }
3509
- break;
3510
- }
3511
- break;
3512
- }
3513
- for (let i = rawMessages.length - 1; i >= 0; i--) {
3514
- const msg = rawMessages[i];
3515
- if (msg.type === "queue-operation") {
3516
- if (typeof msg.content === "string" && msg.content.includes(TASK_NOTIFICATION_MARKER)) continue;
3517
- const op = msg.operation;
3518
- if (op === "enqueue") pendingEnqueues++;
3519
- else if (op === "dequeue" || op === "remove") pendingEnqueues--;
3520
- continue;
3521
- }
3522
- if (msg.type === "attachment") {
3523
- const attachment = isObject3(msg.attachment) ? msg.attachment : null;
3524
- if (attachment?.commandMode === "task-notification") deliveringNotification = true;
3525
- continue;
3526
- }
3527
- if (msg.type === "assistant") {
3528
- const message = msg.message;
3529
- const stopReason = message?.stop_reason;
3530
- if (stopReason === "end_turn") {
3531
- const work = getBackgroundWork();
3532
- if (deliveringNotification || work.undeliveredNotifications > 0) return result("processing");
3533
- if (work.pendingDescriptions.length > 0) {
3534
- return awaitingAgentsResult(work.pendingDescriptions, Math.max(0, pendingEnqueues));
3535
- }
3536
- let hasActivity = false;
3537
- for (let j = i - 1; j >= 0; j--) {
3538
- const m = rawMessages[j];
3539
- if (m.type === "user" && !m.isMeta) {
3540
- hasActivity = true;
3541
- break;
3542
- }
3543
- }
3544
- return result(hasActivity ? "completed" : "idle");
3545
- }
3546
- if (stopReason === "tool_use") {
3547
- const content = message?.content;
3548
- const toolUseBlock = content?.findLast?.((b) => b.type === "tool_use");
3549
- return result("tool_use", toolUseBlock?.name);
3550
- }
3551
- return result("thinking");
3552
- }
3553
- if (msg.type === "user") {
3554
- const isMeta = msg.isMeta;
3555
- if (isMeta) continue;
3556
- return result("processing");
3557
- }
3558
- if (msg.type === "system" && msg.subtype === "terminal_reason") {
3559
- const reason = msg.reason;
3560
- if (reason) {
3561
- return { status: "completed", terminalReason: reason, pendingQueue: Math.max(0, pendingEnqueues) };
3562
- }
3563
- continue;
3564
- }
3565
- if (msg.type === "summary") continue;
3566
- if (msg.type === "system" && msg.subtype === "compact_boundary") continue;
3567
- }
3568
- return { status: "idle" };
3569
- }
3570
- var TERMINAL_INTER_AGENT_TYPES = /* @__PURE__ */ new Set(["FINAL_ANSWER", "ERROR", "FAILED", "INTERRUPTED"]);
3571
- function codexPayloadText(payload) {
3572
- const content = payload.content;
3573
- if (!Array.isArray(content)) return "";
3574
- return content.filter((block) => isObject3(block) && block.type === "input_text" && typeof block.text === "string").map((block) => block.text).join("\n");
3575
- }
3576
- function parseJsonObject(value) {
3577
- if (typeof value !== "string") return null;
3578
- try {
3579
- const parsed = JSON.parse(value);
3580
- return isObject3(parsed) ? parsed : null;
3581
- } catch {
3582
- return null;
3583
- }
3584
- }
3585
- function collectCodexPendingAgents(rawMessages) {
3586
- const alive = /* @__PURE__ */ new Map();
3587
- const done = /* @__PURE__ */ new Set();
3588
- const agentIdByPath = /* @__PURE__ */ new Map();
3589
- const spawnCalls = /* @__PURE__ */ new Map();
3590
- const waitCalls = /* @__PURE__ */ new Set();
3591
- const interruptCalls = /* @__PURE__ */ new Map();
3592
- function markAlive(agentId, description) {
3593
- if (!done.has(agentId)) alive.set(agentId, alive.get(agentId) || description);
3594
- }
3595
- for (const msg of rawMessages) {
3596
- const payload = isObject3(msg.payload) ? msg.payload : null;
3597
- if (!payload) continue;
3598
- if (msg.type === "event_msg" && payload.type === "sub_agent_activity" && typeof payload.agent_thread_id === "string" && typeof payload.agent_path === "string") {
3599
- const agentId = payload.agent_thread_id;
3600
- const kind = typeof payload.kind === "string" ? payload.kind : "started";
3601
- const eventId = typeof payload.event_id === "string" ? payload.event_id : "";
3602
- const existingId = agentIdByPath.get(payload.agent_path);
3603
- const known = alive.has(agentId) || done.has(agentId) || existingId !== void 0 || spawnCalls.has(eventId);
3604
- if (kind !== "started" && !known) continue;
3605
- if (existingId && existingId !== agentId) {
3606
- if (alive.has(existingId)) {
3607
- markAlive(agentId, alive.get(existingId) ?? "");
3608
- alive.delete(existingId);
3609
- }
3610
- if (done.has(existingId)) done.add(agentId);
3611
- }
3612
- agentIdByPath.set(payload.agent_path, agentId);
3613
- if (kind === "interrupted") {
3614
- done.add(agentId);
3615
- alive.delete(agentId);
3616
- } else {
3617
- markAlive(agentId, spawnCalls.get(eventId) ?? "");
3618
- }
3619
- continue;
3620
- }
3621
- if (msg.type !== "response_item") continue;
3622
- if (payload.type === "function_call" && typeof payload.call_id === "string") {
3623
- const name = normalizeFunctionName(typeof payload.name === "string" ? payload.name : "");
3624
- if (name === "spawn_agent") {
3625
- const input = parseJsonObject(payload.arguments);
3626
- spawnCalls.set(payload.call_id, typeof input?.task_name === "string" ? input.task_name : "");
3627
- } else if (name === "wait_agent") {
3628
- waitCalls.add(payload.call_id);
3629
- } else if (name === "interrupt_agent") {
3630
- const input = parseJsonObject(payload.arguments);
3631
- const targets = [input?.agent_id, ...Array.isArray(input?.agent_ids) ? input.agent_ids : []].filter((id) => typeof id === "string");
3632
- interruptCalls.set(payload.call_id, targets);
3633
- }
3634
- continue;
3635
- }
3636
- if (payload.type === "function_call_output" && typeof payload.call_id === "string") {
3637
- const callId = payload.call_id;
3638
- if (spawnCalls.has(callId)) {
3639
- const output = parseJsonObject(payload.output);
3640
- const agentId = typeof output?.agent_id === "string" ? output.agent_id : "";
3641
- if (agentId) {
3642
- markAlive(agentId, spawnCalls.get(callId) ?? "");
3643
- const agentPath = typeof output?.task_name === "string" ? output.task_name : "";
3644
- if (agentPath && !agentIdByPath.has(agentPath)) agentIdByPath.set(agentPath, agentId);
3645
- }
3646
- } else if (waitCalls.has(callId)) {
3647
- const output = parseJsonObject(payload.output);
3648
- const statusMap = isObject3(output?.status) ? output.status : {};
3649
- for (const [agentId, status] of Object.entries(statusMap)) {
3650
- const statusValue = isObject3(status) ? status : {};
3651
- const lifecycle = typeof statusValue.status === "string" ? statusValue.status : "";
3652
- const isTerminal = typeof statusValue.completed === "string" || typeof statusValue.failed === "string" || typeof statusValue.error === "string" || typeof statusValue.interrupted === "string" || lifecycle === "completed" || lifecycle === "failed" || lifecycle === "interrupted";
3653
- if (isTerminal) {
3654
- done.add(agentId);
3655
- alive.delete(agentId);
5122
+ async function findSubagentFile(parentJsonlPath, agentId) {
5123
+ const subagentsDir = parentJsonlPath.replace(/\.jsonl$/, "") + "/subagents";
5124
+ try {
5125
+ const files = await (0, import_promises3.readdir)(subagentsDir);
5126
+ for (const f of files) {
5127
+ if (!f.startsWith("agent-") || !f.endsWith(".jsonl")) continue;
5128
+ const fileAgentId = f.replace("agent-", "").replace(".jsonl", "");
5129
+ if (fileAgentId === agentId) {
5130
+ return { filePath: (0, import_node_path5.join)(subagentsDir, f), fileName: f };
5131
+ }
5132
+ }
5133
+ } catch {
5134
+ }
5135
+ return null;
5136
+ }
5137
+ async function findTeamContext(sessionId, subagentFileName) {
5138
+ try {
5139
+ const teamEntries = await (0, import_promises3.readdir)(dirs.TEAMS_DIR, { withFileTypes: true });
5140
+ for (const entry of teamEntries) {
5141
+ if (!entry.isDirectory()) continue;
5142
+ const configPath = (0, import_node_path5.join)(dirs.TEAMS_DIR, entry.name, "config.json");
5143
+ try {
5144
+ const raw = await (0, import_promises3.readFile)(configPath, "utf-8");
5145
+ const config = JSON.parse(raw);
5146
+ if (config.leadSessionId !== sessionId) continue;
5147
+ const members = config.members ?? [];
5148
+ const memberName = await matchSubagentToMember(sessionId, subagentFileName, members);
5149
+ if (!memberName) continue;
5150
+ let currentTask = null;
5151
+ try {
5152
+ const taskDir = (0, import_node_path5.join)(dirs.TASKS_DIR, entry.name);
5153
+ const taskFiles = await (0, import_promises3.readdir)(taskDir);
5154
+ for (const tf of taskFiles) {
5155
+ if (!tf.endsWith(".json")) continue;
5156
+ const taskRaw = await (0, import_promises3.readFile)((0, import_node_path5.join)(taskDir, tf), "utf-8");
5157
+ const task = JSON.parse(taskRaw);
5158
+ if (task.owner === memberName && task.status === "in_progress") {
5159
+ currentTask = { id: task.id, subject: task.subject, status: task.status };
5160
+ break;
5161
+ }
3656
5162
  }
5163
+ } catch {
3657
5164
  }
3658
- } else if (interruptCalls.has(callId)) {
3659
- for (const agentId of interruptCalls.get(callId) ?? []) {
3660
- done.add(agentId);
3661
- alive.delete(agentId);
3662
- }
5165
+ return { teamName: entry.name, role: memberName, currentTask };
5166
+ } catch {
3663
5167
  }
3664
- continue;
3665
- }
3666
- if (payload.type === "agent_message" && typeof payload.author === "string") {
3667
- const text = codexPayloadText(payload);
3668
- const messageType = text.match(/^Message Type:\s*([^\r\n]+)/)?.[1]?.trim().toUpperCase() ?? "";
3669
- if (!TERMINAL_INTER_AGENT_TYPES.has(messageType)) continue;
3670
- const agentId = agentIdByPath.get(payload.author) ?? payload.author;
3671
- done.add(agentId);
3672
- alive.delete(agentId);
3673
5168
  }
5169
+ } catch {
3674
5170
  }
3675
- return [...alive.entries()].filter(([agentId]) => !done.has(agentId)).map(([, description]) => description);
5171
+ return null;
3676
5172
  }
3677
- function deriveCodexSessionStatus(rawMessages) {
3678
- for (let i = rawMessages.length - 1; i >= 0; i--) {
3679
- const msg = rawMessages[i];
3680
- if (msg.type === "event_msg") {
3681
- const payload = msg.payload;
3682
- switch (payload?.type) {
3683
- case "task_complete": {
3684
- const pending = collectCodexPendingAgents(rawMessages);
3685
- if (pending.length > 0) return awaitingAgentsResult(pending, 0);
3686
- return { status: "completed" };
5173
+ function findParentToolCallId(session, agentId) {
5174
+ for (const turn of session.turns) {
5175
+ for (const msg of turn.subAgentActivity) {
5176
+ if (msg.agentId === agentId) {
5177
+ for (const tc of turn.toolCalls) {
5178
+ if (tc.name === "Task" || tc.name === "Agent") {
5179
+ return tc.id;
5180
+ }
3687
5181
  }
3688
- case "task_started":
3689
- return { status: "processing" };
3690
- case "agent_message":
3691
- return { status: "thinking" };
3692
- case "token_count":
3693
- continue;
3694
5182
  }
3695
5183
  }
3696
- if (msg.type === "response_item") {
3697
- const payload = msg.payload;
3698
- if (!payload) continue;
3699
- if (payload.type === "function_call") {
3700
- return { status: "tool_use", toolName: payload.name ? normalizeFunctionName(payload.name) : payload.name };
3701
- }
3702
- if (payload.type === "message") {
3703
- if (payload.role === "assistant") return { status: "thinking" };
3704
- if (payload.role === "user") return { status: "processing" };
5184
+ }
5185
+ return null;
5186
+ }
5187
+ function findAgentMetadata(session, agentId) {
5188
+ for (const turn of session.turns) {
5189
+ for (const msg of turn.subAgentActivity) {
5190
+ if (msg.agentId === agentId) {
5191
+ return {
5192
+ name: msg.agentName,
5193
+ type: msg.subagentType,
5194
+ isBackground: msg.isBackground
5195
+ };
3705
5196
  }
3706
5197
  }
3707
5198
  }
3708
- return { status: "idle" };
5199
+ return { name: null, type: null, isBackground: false };
5200
+ }
5201
+ async function getSessionOverview(sessionId) {
5202
+ const jsonlPath = await findSessionFile(sessionId);
5203
+ if (!jsonlPath) return { error: "Session not found" };
5204
+ const content = await (0, import_promises3.readFile)(jsonlPath, "utf-8");
5205
+ const session = parseSession(content);
5206
+ return mapSessionToOverview(session);
5207
+ }
5208
+ async function getTurnDetail(sessionId, turnIndex) {
5209
+ const jsonlPath = await findSessionFile(sessionId);
5210
+ if (!jsonlPath) return { error: "Session not found" };
5211
+ const content = await (0, import_promises3.readFile)(jsonlPath, "utf-8");
5212
+ const session = parseSession(content);
5213
+ if (turnIndex < 0 || turnIndex >= session.turns.length) return { error: "Turn not found" };
5214
+ return mapTurnToDetail(session, turnIndex);
5215
+ }
5216
+ async function getAgentOverview(sessionId, agentId) {
5217
+ const jsonlPath = await findSessionFile(sessionId);
5218
+ if (!jsonlPath) return { error: "Session not found" };
5219
+ const content = await (0, import_promises3.readFile)(jsonlPath, "utf-8");
5220
+ const session = parseSession(content);
5221
+ const subagentFile = await findSubagentFile(jsonlPath, agentId);
5222
+ if (!subagentFile) return { error: "Agent not found" };
5223
+ const subagentContent = await (0, import_promises3.readFile)(subagentFile.filePath, "utf-8");
5224
+ const subagentSession = parseSession(subagentContent);
5225
+ const metadata = findAgentMetadata(session, agentId);
5226
+ const parentToolCallId = findParentToolCallId(session, agentId);
5227
+ const teamContext = await findTeamContext(sessionId, subagentFile.fileName);
5228
+ return {
5229
+ sessionId,
5230
+ agentId,
5231
+ name: metadata.name,
5232
+ type: metadata.type,
5233
+ parentToolCallId,
5234
+ isBackground: metadata.isBackground,
5235
+ teamContext,
5236
+ overview: mapSessionToOverview(subagentSession)
5237
+ };
5238
+ }
5239
+ async function getAgentTurnDetail(sessionId, agentId, turnIndex) {
5240
+ const jsonlPath = await findSessionFile(sessionId);
5241
+ if (!jsonlPath) return { error: "Session not found" };
5242
+ const subagentFile = await findSubagentFile(jsonlPath, agentId);
5243
+ if (!subagentFile) return { error: "Agent not found" };
5244
+ const subagentContent = await (0, import_promises3.readFile)(subagentFile.filePath, "utf-8");
5245
+ const subagentSession = parseSession(subagentContent);
5246
+ if (turnIndex < 0 || turnIndex >= subagentSession.turns.length) return { error: "Turn not found" };
5247
+ return mapTurnToDetail(subagentSession, turnIndex);
3709
5248
  }
3710
5249
 
5250
+ // src/commands/sessions.ts
5251
+ var import_node_path6 = require("node:path");
5252
+
3711
5253
  // src/lib/metadata.ts
3712
- async function getSessionMeta(filePath) {
3713
- let lines;
3714
- let isPartialRead = false;
3715
- const fileStat = await (0, import_promises4.stat)(filePath);
3716
- if (fileStat.size > 65536) {
3717
- const fh = await (0, import_promises4.open)(filePath, "r");
3718
- try {
3719
- const buf = Buffer.alloc(32768);
3720
- const { bytesRead } = await fh.read(buf, 0, 32768, 0);
3721
- const text = buf.subarray(0, bytesRead).toString("utf-8");
3722
- const lastNewline = text.lastIndexOf("\n");
3723
- lines = (lastNewline > 0 ? text.slice(0, lastNewline) : text).split("\n").filter(Boolean);
3724
- isPartialRead = true;
3725
- } finally {
3726
- await fh.close();
3727
- }
3728
- } else {
3729
- const content = await (0, import_promises4.readFile)(filePath, "utf-8");
3730
- lines = content.split("\n").filter(Boolean);
3731
- }
3732
- let firstParsed = null;
3733
- if (lines.length > 0) {
5254
+ var import_promises4 = require("node:fs/promises");
5255
+
5256
+ // src/lib/sessionStatus.ts
5257
+ function deriveSessionStatus(rawMessages) {
5258
+ return formatForRecords(rawMessages).status(rawMessages);
5259
+ }
5260
+
5261
+ // src/lib/metadata.ts
5262
+ var FULL_READ_LIMIT = 65536;
5263
+ var HEAD_READ_BYTES = 32768;
5264
+ function readHeader(format, lines) {
5265
+ return format.metadataFromLines(lines);
5266
+ }
5267
+ function cleanUserText(text) {
5268
+ const cleaned = text.replace(/<[^>]+>[\s\S]*?<\/[^>]+>/g, "").trim();
5269
+ return cleaned.length > 5 ? cleaned.slice(0, 120) : "";
5270
+ }
5271
+ function scanClaudeMessages(lines) {
5272
+ const meta = {
5273
+ firstUserMessage: "",
5274
+ lastUserMessage: "",
5275
+ timestamp: "",
5276
+ turnCount: 0
5277
+ };
5278
+ for (const line of lines) {
5279
+ let record;
3734
5280
  try {
3735
- firstParsed = JSON.parse(lines[0]);
5281
+ record = JSON.parse(line);
3736
5282
  } catch {
3737
- firstParsed = null;
5283
+ continue;
3738
5284
  }
3739
- }
3740
- const isCodex = firstParsed?.type === "session_meta" || firstParsed?.type === "turn_context";
3741
- if (isCodex) {
3742
- if (isPartialRead) {
3743
- const content = await (0, import_promises4.readFile)(filePath, "utf-8");
3744
- lines = content.split("\n").filter(Boolean);
5285
+ if (record.type !== "user" || record.isMeta) continue;
5286
+ if (!meta.timestamp) meta.timestamp = record.timestamp || "";
5287
+ const content = record.message?.content;
5288
+ let extracted = "";
5289
+ if (typeof content === "string") {
5290
+ extracted = cleanUserText(content);
5291
+ } else if (Array.isArray(content)) {
5292
+ for (const block of content) {
5293
+ if (block.type !== "text" || typeof block.text !== "string") continue;
5294
+ extracted = cleanUserText(block.text);
5295
+ if (extracted) break;
5296
+ }
3745
5297
  }
3746
- const meta = extractCodexMetadataFromLines(lines);
5298
+ if (extracted) {
5299
+ if (!meta.firstUserMessage) meta.firstUserMessage = extracted;
5300
+ meta.lastUserMessage = extracted;
5301
+ }
5302
+ meta.turnCount += 1;
5303
+ }
5304
+ return meta;
5305
+ }
5306
+ async function readLines(filePath, size) {
5307
+ if (size <= FULL_READ_LIMIT) {
5308
+ const content = await (0, import_promises4.readFile)(filePath, "utf-8");
5309
+ return { lines: content.split("\n").filter(Boolean), isPartialRead: false };
5310
+ }
5311
+ const handle = await (0, import_promises4.open)(filePath, "r");
5312
+ try {
5313
+ const buffer = Buffer.alloc(HEAD_READ_BYTES);
5314
+ const { bytesRead } = await handle.read(buffer, 0, HEAD_READ_BYTES, 0);
5315
+ const text = buffer.subarray(0, bytesRead).toString("utf-8");
5316
+ const lastNewline = text.lastIndexOf("\n");
3747
5317
  return {
3748
- sessionId: meta.sessionId,
3749
- version: meta.version,
3750
- gitBranch: meta.gitBranch,
3751
- model: meta.model,
3752
- slug: meta.slug,
3753
- cwd: meta.cwd,
3754
- firstUserMessage: meta.firstUserMessage,
3755
- lastUserMessage: meta.lastUserMessage,
3756
- timestamp: meta.timestamp,
3757
- turnCount: meta.turnCount,
3758
- lineCount: lines.length,
3759
- branchedFrom: meta.branchedFrom
5318
+ lines: (lastNewline > 0 ? text.slice(0, lastNewline) : text).split("\n").filter(Boolean),
5319
+ isPartialRead: true
3760
5320
  };
5321
+ } finally {
5322
+ await handle.close();
3761
5323
  }
3762
- let sessionId = "";
3763
- let version = "";
3764
- let gitBranch = "";
3765
- let model = "";
3766
- let slug = "";
3767
- let cwd = "";
3768
- let firstUserMessage = "";
3769
- let lastUserMessage = "";
3770
- let timestamp = "";
3771
- let turnCount = 0;
3772
- let branchedFrom;
3773
- for (const line of lines) {
3774
- try {
3775
- const obj = JSON.parse(line);
3776
- if (obj.sessionId && !sessionId) sessionId = obj.sessionId;
3777
- if (obj.version && !version) version = obj.version;
3778
- if (obj.gitBranch && !gitBranch) gitBranch = obj.gitBranch;
3779
- if (obj.slug && !slug) slug = obj.slug;
3780
- if (obj.cwd && !cwd) cwd = obj.cwd;
3781
- if (obj.branchedFrom && !branchedFrom) branchedFrom = obj.branchedFrom;
3782
- if (obj.type === "assistant" && obj.message?.model && !model) {
3783
- model = obj.message.model;
3784
- }
3785
- if (obj.type === "user" && !obj.isMeta && !timestamp) {
3786
- timestamp = obj.timestamp || "";
3787
- }
3788
- if (obj.type === "user" && !obj.isMeta) {
3789
- const c = obj.message?.content;
3790
- let extracted = "";
3791
- if (typeof c === "string") {
3792
- const cleaned = c.replace(/<[^>]+>[\s\S]*?<\/[^>]+>/g, "").trim();
3793
- if (cleaned && cleaned.length > 5) extracted = cleaned.slice(0, 120);
3794
- } else if (Array.isArray(c)) {
3795
- for (const block of c) {
3796
- if (block.type === "text") {
3797
- const cleaned = block.text.replace(/<[^>]+>[\s\S]*?<\/[^>]+>/g, "").trim();
3798
- if (cleaned && cleaned.length > 5) {
3799
- extracted = cleaned.slice(0, 120);
3800
- break;
3801
- }
3802
- }
3803
- }
3804
- }
3805
- if (extracted) {
3806
- if (!firstUserMessage) firstUserMessage = extracted;
3807
- lastUserMessage = extracted;
3808
- }
3809
- turnCount++;
3810
- }
3811
- } catch {
3812
- }
5324
+ }
5325
+ async function getSessionMeta(filePath) {
5326
+ const fileStat = await (0, import_promises4.stat)(filePath);
5327
+ let { lines, isPartialRead } = await readLines(filePath, fileStat.size);
5328
+ const format = formatForText(lines.join("\n"));
5329
+ if (isPartialRead && !format.descriptor.metadataFromHead) {
5330
+ lines = (await (0, import_promises4.readFile)(filePath, "utf-8")).split("\n").filter(Boolean);
5331
+ isPartialRead = false;
3813
5332
  }
5333
+ const header = readHeader(format, lines);
5334
+ const conversation = header.turnCount === void 0 ? scanClaudeMessages(lines) : {
5335
+ firstUserMessage: header.firstUserMessage ?? "",
5336
+ lastUserMessage: header.lastUserMessage ?? "",
5337
+ timestamp: header.timestamp ?? "",
5338
+ turnCount: header.turnCount
5339
+ };
3814
5340
  return {
3815
- sessionId,
3816
- version,
3817
- gitBranch,
3818
- model,
3819
- slug,
3820
- cwd,
3821
- firstUserMessage,
3822
- lastUserMessage,
3823
- timestamp,
3824
- turnCount,
3825
- lineCount: isPartialRead ? Math.round(fileStat.size / (32768 / lines.length)) : lines.length,
3826
- branchedFrom
5341
+ sessionId: header.sessionId,
5342
+ version: header.version,
5343
+ gitBranch: header.gitBranch,
5344
+ model: header.model,
5345
+ slug: header.slug,
5346
+ cwd: header.cwd,
5347
+ ...conversation,
5348
+ // A head read saw a known number of lines in a known number of bytes;
5349
+ // scaling that by the file size is the only estimate available.
5350
+ lineCount: isPartialRead ? Math.round(fileStat.size / (HEAD_READ_BYTES / lines.length)) : lines.length,
5351
+ branchedFrom: header.branchedFrom
3827
5352
  };
3828
5353
  }
3829
5354
  async function getSessionStatus(filePath) {
@@ -3837,6 +5362,7 @@ async function getSessionStatus(filePath) {
3837
5362
  const meaningful = [];
3838
5363
  let cursor = fileStat.size;
3839
5364
  let leftover = "";
5365
+ let copilotTurnEnded = false;
3840
5366
  for (let chunk = 0; chunk < MAX_CHUNKS && cursor > 0; chunk++) {
3841
5367
  const readSize = Math.min(CHUNK, cursor);
3842
5368
  cursor -= readSize;
@@ -3855,6 +5381,10 @@ async function getSessionStatus(filePath) {
3855
5381
  } catch {
3856
5382
  continue;
3857
5383
  }
5384
+ if (obj.type.startsWith("subagent.") && typeof obj.agentId === "string" && obj.agentId.length > 0) {
5385
+ meaningful.unshift(obj);
5386
+ continue;
5387
+ }
3858
5388
  if (obj.type === "event_msg") {
3859
5389
  const payload = obj.payload;
3860
5390
  switch (payload?.type) {
@@ -3879,6 +5409,20 @@ async function getSessionStatus(filePath) {
3879
5409
  if (role === "user") return { status: "processing" };
3880
5410
  }
3881
5411
  }
5412
+ if (obj.type === "abort" || obj.type === "user.message" || obj.type.startsWith("assistant.") || obj.type.startsWith("permission.") || obj.type.startsWith("session.") || obj.type.startsWith("tool.") || obj.type.startsWith("user_input.")) {
5413
+ if (typeof obj.agentId === "string" && obj.agentId) continue;
5414
+ meaningful.unshift(obj);
5415
+ if (obj.type === "assistant.turn_end") {
5416
+ copilotTurnEnded = true;
5417
+ continue;
5418
+ }
5419
+ if (copilotTurnEnded && obj.type !== "assistant.message" && obj.type !== "user.message") {
5420
+ continue;
5421
+ }
5422
+ const status = deriveSessionStatus(meaningful);
5423
+ if (status.status !== "idle") return status;
5424
+ continue;
5425
+ }
3882
5426
  if (obj.type === "assistant" || obj.type === "user" || obj.type === "queue-operation") {
3883
5427
  meaningful.unshift(obj);
3884
5428
  const isEndTurn = obj.type === "assistant" && obj.message?.stop_reason === "end_turn";
@@ -3911,75 +5455,15 @@ function toSessionSummary(file, meta, status) {
3911
5455
  turnCount: meta.turnCount,
3912
5456
  status,
3913
5457
  mtime: file.mtimeMs,
3914
- source: file.path.startsWith(dirs.CODEX_SESSIONS_DIR + "/") ? "codex" : "claude"
3915
- };
3916
- }
3917
- async function listCodexSessionFiles(cutoff) {
3918
- const walk = async (dir, depth) => {
3919
- if (depth > 4) return [];
3920
- let entries;
3921
- try {
3922
- entries = await (0, import_promises5.readdir)(dir, { withFileTypes: true });
3923
- } catch {
3924
- return [];
3925
- }
3926
- const results = [];
3927
- for (const entry of entries) {
3928
- const filePath = (0, import_node_path6.join)(dir, entry.name);
3929
- if (entry.isDirectory()) {
3930
- results.push(...await walk(filePath, depth + 1));
3931
- continue;
3932
- }
3933
- if (!entry.name.endsWith(".jsonl")) continue;
3934
- try {
3935
- const s = await (0, import_promises5.stat)(filePath);
3936
- if (s.mtimeMs >= cutoff) results.push({ path: filePath, mtimeMs: s.mtimeMs });
3937
- } catch {
3938
- continue;
3939
- }
3940
- }
3941
- return results;
5458
+ // Which root the transcript came out of, which is known before it is read.
5459
+ source: storeForPath(file.path)?.kind ?? descriptorFor("claude").kind
3942
5460
  };
3943
- return walk(dirs.CODEX_SESSIONS_DIR, 0);
3944
5461
  }
3945
5462
  async function listSessions(opts = {}) {
3946
5463
  const limit = Math.min(Math.max(1, opts.limit ?? 20), 100);
3947
5464
  const maxAgeMs = parseMaxAge(opts.maxAge ?? "7d");
3948
5465
  const cutoff = Date.now() - maxAgeMs;
3949
- let entries;
3950
- try {
3951
- entries = await (0, import_promises5.readdir)(dirs.PROJECTS_DIR, { withFileTypes: true });
3952
- } catch {
3953
- return [];
3954
- }
3955
- const projectDirs = entries.filter((e) => e.isDirectory() && e.name !== "memory").map((e) => (0, import_node_path6.join)(dirs.PROJECTS_DIR, e.name));
3956
- const [nested, codexFiles] = await Promise.all([
3957
- Promise.all(
3958
- projectDirs.map(async (projectDir) => {
3959
- try {
3960
- const files = await (0, import_promises5.readdir)(projectDir);
3961
- const jsonlFiles = files.filter((f) => f.endsWith(".jsonl"));
3962
- const statResults = await Promise.all(
3963
- jsonlFiles.map(async (f) => {
3964
- const filePath = (0, import_node_path6.join)(projectDir, f);
3965
- try {
3966
- const s = await (0, import_promises5.stat)(filePath);
3967
- return s.mtimeMs >= cutoff ? { path: filePath, mtimeMs: s.mtimeMs } : null;
3968
- } catch {
3969
- return null;
3970
- }
3971
- })
3972
- );
3973
- return statResults.filter((r) => r !== null);
3974
- } catch {
3975
- return [];
3976
- }
3977
- })
3978
- ),
3979
- listCodexSessionFiles(cutoff)
3980
- ]);
3981
- const allFiles = [...nested.flat(), ...codexFiles];
3982
- allFiles.sort((a, b) => b.mtimeMs - a.mtimeMs);
5466
+ const allFiles = listAllSessionFiles(cutoff).filter((file) => !file.isSubagent);
3983
5467
  const results = [];
3984
5468
  for (const file of allFiles) {
3985
5469
  if (results.length >= limit) break;
@@ -3996,41 +5480,19 @@ async function listSessions(opts = {}) {
3996
5480
  return results;
3997
5481
  }
3998
5482
  async function currentSession(cwd) {
3999
- const projectDir = (0, import_node_path6.join)(dirs.PROJECTS_DIR, encodeClaudeDirName(cwd));
4000
- let files;
4001
- try {
4002
- files = await (0, import_promises5.readdir)(projectDir);
4003
- } catch {
4004
- files = [];
4005
- }
4006
- const jsonlFiles = files.filter((f) => f.endsWith(".jsonl"));
4007
- const [statResults, codexCandidates] = await Promise.all([
4008
- Promise.all(
4009
- jsonlFiles.map(async (f) => {
4010
- const filePath = (0, import_node_path6.join)(projectDir, f);
4011
- try {
4012
- const s = await (0, import_promises5.stat)(filePath);
4013
- return { path: filePath, mtimeMs: s.mtimeMs };
4014
- } catch {
4015
- return null;
4016
- }
4017
- })
4018
- ),
4019
- listCodexSessionFiles(0)
4020
- ]);
4021
- const codexMatches = [];
4022
- for (const file of codexCandidates) {
5483
+ const claude2 = storeFor("claude");
5484
+ const projectDir = (0, import_node_path6.join)(claude2.root(), claude2.descriptor.dirName.encode(cwd));
5485
+ const direct = claude2.list(0, claude2.root()).filter((file) => !file.isSubagent && file.path.startsWith(projectDir + import_node_path6.sep));
5486
+ const searched = [];
5487
+ for (const file of listAllSessionFiles(0)) {
5488
+ if (file.isSubagent || storeForPath(file.path)?.kind === "claude") continue;
4023
5489
  try {
4024
- const meta2 = await getSessionMeta(file.path);
4025
- if (meta2.cwd === cwd) codexMatches.push(file);
5490
+ if ((await getSessionMeta(file.path)).cwd === cwd) searched.push(file);
4026
5491
  } catch {
4027
5492
  continue;
4028
5493
  }
4029
5494
  }
4030
- const valid = [
4031
- ...statResults.filter((r) => r !== null),
4032
- ...codexMatches
4033
- ];
5495
+ const valid = [...direct, ...searched];
4034
5496
  if (valid.length === 0) return null;
4035
5497
  valid.sort((a, b) => b.mtimeMs - a.mtimeMs);
4036
5498
  const latest = valid[0];
@@ -4043,11 +5505,11 @@ async function currentSession(cwd) {
4043
5505
  }
4044
5506
 
4045
5507
  // src/commands/index-cmd.ts
4046
- var import_node_fs3 = require("node:fs");
5508
+ var import_node_fs4 = require("node:fs");
4047
5509
  var import_node_path7 = require("node:path");
4048
5510
  async function indexStats(dbPath) {
4049
5511
  const path = dbPath ?? DEFAULT_DB_PATH;
4050
- if (!(0, import_node_fs3.existsSync)(path)) {
5512
+ if (!(0, import_node_fs4.existsSync)(path)) {
4051
5513
  return { error: `Database not found at ${path}. Run 'cogpit-memory index rebuild' to create it.` };
4052
5514
  }
4053
5515
  const index = new SearchIndex(path);
@@ -4057,9 +5519,9 @@ async function indexStats(dbPath) {
4057
5519
  }
4058
5520
  async function indexRebuild(dbPath) {
4059
5521
  const path = dbPath ?? DEFAULT_DB_PATH;
4060
- (0, import_node_fs3.mkdirSync)((0, import_node_path7.dirname)(path), { recursive: true });
5522
+ (0, import_node_fs4.mkdirSync)((0, import_node_path7.dirname)(path), { recursive: true });
4061
5523
  const index = new SearchIndex(path);
4062
- index.buildFull(dirs.PROJECTS_DIR);
5524
+ index.buildFull();
4063
5525
  const stats = index.getStats();
4064
5526
  index.close();
4065
5527
  return { status: "rebuilt", stats };