codesesh 0.13.0 → 0.15.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.
@@ -72,15 +72,18 @@ function getRegisteredAgents() {
72
72
  return registrations;
73
73
  }
74
74
  function getAgentInfoMap(sessionsByAgent) {
75
- return registrations.map((r) => ({
76
- name: r.name,
77
- displayName: r.displayName,
78
- icon: r.icon,
79
- count: sessionsByAgent[r.name] ?? 0
80
- }));
75
+ return registrations.map((registration) => {
76
+ const agent = registration.create();
77
+ return {
78
+ name: agent.name,
79
+ displayName: agent.displayName,
80
+ icon: registration.icon,
81
+ count: sessionsByAgent[agent.name] ?? 0
82
+ };
83
+ });
81
84
  }
82
85
  function getAgentByName(name) {
83
- return registrations.find((r) => r.name === name);
86
+ return registrations.find((registration) => registration.create().name === name);
84
87
  }
85
88
  function parsedSession(session) {
86
89
  return { status: "parsed", data: session };
@@ -106,6 +109,26 @@ var BaseAgent = class {
106
109
  };
107
110
  var FileSystemSessionSource = class extends BaseAgent {
108
111
  sessionMetaMap = /* @__PURE__ */ new Map();
112
+ scan(options) {
113
+ const sources = this.listSessionSources(options);
114
+ const sessions = [];
115
+ options?.onProgress?.({ total: sources.length, processed: 0, sessions: 0 });
116
+ for (const [index, source] of sources.entries()) {
117
+ try {
118
+ const session = this.scanSessionSource(source.sourcePath, options);
119
+ if (session) sessions.push(session);
120
+ } catch {
121
+ continue;
122
+ } finally {
123
+ options?.onProgress?.({
124
+ total: sources.length,
125
+ processed: index + 1,
126
+ sessions: sessions.length
127
+ });
128
+ }
129
+ }
130
+ return sessions;
131
+ }
109
132
  getSessionMetaMap() {
110
133
  return this.sessionMetaMap;
111
134
  }
@@ -380,15 +403,6 @@ function resolveSessionTitle(explicit, message, directory) {
380
403
  }
381
404
  return UNTITLED_SESSION;
382
405
  }
383
- function parsed(data) {
384
- return { status: "parsed", data };
385
- }
386
- function skipped(reason) {
387
- return reason ? { status: "skipped", reason } : { status: "skipped" };
388
- }
389
- function filtered(reason) {
390
- return reason ? { status: "filtered", reason } : { status: "filtered" };
391
- }
392
406
  function isInternalEventType2(value) {
393
407
  return isInternalEventType(value);
394
408
  }
@@ -457,80 +471,6 @@ function firstUserMessageTitle(messages) {
457
471
  }
458
472
  return null;
459
473
  }
460
- var PerfTracer = class {
461
- rootMarkers = [];
462
- activeStack = [];
463
- enabled = false;
464
- enable() {
465
- this.enabled = true;
466
- }
467
- start(name) {
468
- const marker = {
469
- name,
470
- startTime: performance.now(),
471
- children: []
472
- };
473
- if (!this.enabled) return marker;
474
- const parent = this.activeStack[this.activeStack.length - 1];
475
- if (parent) {
476
- marker.parent = parent;
477
- parent.children.push(marker);
478
- } else {
479
- this.rootMarkers.push(marker);
480
- }
481
- this.activeStack.push(marker);
482
- return marker;
483
- }
484
- end(marker) {
485
- if (!this.enabled) return;
486
- const target = marker ?? this.activeStack[this.activeStack.length - 1];
487
- if (!target) return;
488
- target.endTime = performance.now();
489
- target.duration = target.endTime - target.startTime;
490
- while (this.activeStack.length > 0) {
491
- const popped = this.activeStack.pop();
492
- if (popped === target) break;
493
- }
494
- }
495
- measure(name, fn) {
496
- const marker = this.start(name);
497
- try {
498
- return fn();
499
- } finally {
500
- this.end(marker);
501
- }
502
- }
503
- async measureAsync(name, fn) {
504
- const marker = this.start(name);
505
- try {
506
- return await fn();
507
- } finally {
508
- this.end(marker);
509
- }
510
- }
511
- getReport() {
512
- if (!this.enabled) return "Performance tracing disabled";
513
- const lines = [];
514
- lines.push("\n=== Performance Report ===\n");
515
- for (const marker of this.rootMarkers) {
516
- this.formatMarker(marker, 0, lines);
517
- }
518
- return lines.join("\n");
519
- }
520
- formatMarker(marker, depth, lines) {
521
- const indent = " ".repeat(depth);
522
- const duration = marker.duration?.toFixed(2) ?? "?";
523
- lines.push(`${indent}${marker.name}: ${duration}ms`);
524
- for (const child of marker.children) {
525
- this.formatMarker(child, depth + 1, lines);
526
- }
527
- }
528
- reset() {
529
- this.rootMarkers = [];
530
- this.activeStack = [];
531
- }
532
- };
533
- var perf = new PerfTracer();
534
474
  var aliases_default = {
535
475
  "anthropic--claude-4.6-opus": "claude-opus-4-6",
536
476
  "anthropic--claude-4.6-sonnet": "claude-sonnet-4-6",
@@ -854,6 +794,179 @@ function withEstimatedSessionCost(stats, model) {
854
794
  function estimateTokenCost(model, tokens) {
855
795
  return estimateCostForTokens(model, tokens)?.cost ?? null;
856
796
  }
797
+ var TranscriptBuilder = class {
798
+ constructor(options = {}) {
799
+ this.options = options;
800
+ }
801
+ options;
802
+ messages = [];
803
+ pendingToolCalls = /* @__PURE__ */ new Map();
804
+ currentAssistant = null;
805
+ latestTextAssistant = null;
806
+ beginTurn() {
807
+ this.currentAssistant = null;
808
+ this.latestTextAssistant = null;
809
+ }
810
+ appendMessage(input) {
811
+ const message = this.createMessage(input);
812
+ this.messages.push(message);
813
+ this.registerToolCalls(message.parts);
814
+ if (message.role === "assistant") {
815
+ this.currentAssistant = message;
816
+ if (message.parts.some((part) => part.type === "text")) {
817
+ this.latestTextAssistant = message;
818
+ }
819
+ } else if (message.role === "user") {
820
+ this.beginTurn();
821
+ }
822
+ return message;
823
+ }
824
+ appendAssistantPart(part, input, options = {}) {
825
+ const current = this.currentAssistant;
826
+ const canReuse = current !== null && (options.grouping === "current" || (part.type === "text" ? !current.parts.some((item) => item.type === "tool") : part.type === "reasoning" ? !current.parts.some((item) => item.type === "text" || item.type === "tool") : false));
827
+ const message = canReuse ? current : this.appendMessage({ ...input, role: "assistant", parts: [part] });
828
+ if (canReuse) {
829
+ if (options.deduplicateTail) this.appendPartIfNew(message, part);
830
+ else message.parts.push(part);
831
+ this.applyMissingMetadata(message, input);
832
+ }
833
+ if (part.type === "text") {
834
+ this.latestTextAssistant = message;
835
+ } else if (options.resetLatestText) {
836
+ this.latestTextAssistant = null;
837
+ }
838
+ return message;
839
+ }
840
+ appendToolCall(part, input, options = {}) {
841
+ const target = options.target === "current" ? this.currentAssistant : this.latestTextAssistant ?? this.currentAssistant;
842
+ const message = target ? target : this.appendMessage({
843
+ ...input,
844
+ role: "assistant",
845
+ mode: options.modeOnCreate ?? (options.markModeAsTool ? "tool" : input.mode),
846
+ parts: [part]
847
+ });
848
+ if (target) {
849
+ message.parts.push(part);
850
+ this.applyMissingMetadata(message, input);
851
+ if (options.markModeAsTool) message.mode = "tool";
852
+ this.registerToolCall(part);
853
+ }
854
+ this.currentAssistant = message;
855
+ return message;
856
+ }
857
+ appendToCurrentAssistant(part) {
858
+ if (!this.currentAssistant) return false;
859
+ this.currentAssistant.parts.push(part);
860
+ return true;
861
+ }
862
+ updateToolCall(callId, update) {
863
+ const part = this.pendingToolCalls.get(callId);
864
+ if (!part) return false;
865
+ update(part);
866
+ return true;
867
+ }
868
+ resolveToolCall(callId, resolution) {
869
+ return this.updateToolCall(callId, (part) => {
870
+ const state = part.state ?? (part.state = {});
871
+ if (resolution.output !== void 0) state.output = resolution.output;
872
+ if (resolution.status !== void 0) state.status = resolution.status;
873
+ if (resolution.metadata !== void 0) state.metadata = resolution.metadata;
874
+ if (resolution.consume) this.pendingToolCalls.delete(callId);
875
+ });
876
+ }
877
+ attachUsageToLatestAssistant(tokens, options = {}) {
878
+ for (let index = this.messages.length - 1; index >= 0; index -= 1) {
879
+ const message = this.messages[index];
880
+ if (message.role !== "assistant" || message.tokens) continue;
881
+ message.tokens = tokens;
882
+ if (options.model !== void 0) message.model ??= options.model;
883
+ if (options.cost !== void 0) message.cost = options.cost;
884
+ if (options.costSource !== void 0) message.cost_source = options.costSource;
885
+ return true;
886
+ }
887
+ return false;
888
+ }
889
+ finish(baseStats) {
890
+ const messages = cleanParsedMessages(this.messages);
891
+ const derived = this.deriveStats(messages);
892
+ if (!baseStats) return { messages, stats: derived };
893
+ return {
894
+ messages,
895
+ stats: {
896
+ ...derived,
897
+ ...baseStats,
898
+ message_count: messages.length,
899
+ cost_source: baseStats.cost_source
900
+ }
901
+ };
902
+ }
903
+ createMessage(input) {
904
+ const sparse = this.options.messageDefaults === "sparse";
905
+ return {
906
+ id: input.id,
907
+ role: input.role,
908
+ agent: sparse ? input.agent : input.agent ?? null,
909
+ time_created: input.timestampMs,
910
+ mode: sparse ? input.mode : input.mode ?? null,
911
+ model: sparse ? input.model : input.model ?? null,
912
+ provider: sparse ? input.provider : input.provider ?? null,
913
+ tokens: input.tokens,
914
+ cost: sparse ? input.cost : input.cost ?? 0,
915
+ cost_source: input.costSource,
916
+ parts: input.parts ?? [],
917
+ subagent_id: input.subagentId,
918
+ nickname: input.nickname
919
+ };
920
+ }
921
+ registerToolCalls(parts) {
922
+ for (const part of parts) this.registerToolCall(part);
923
+ }
924
+ registerToolCall(part) {
925
+ if (part.type === "tool" && part.callID) {
926
+ this.pendingToolCalls.set(part.callID, part);
927
+ }
928
+ }
929
+ appendPartIfNew(message, part) {
930
+ const tail = message.parts.at(-1);
931
+ if (tail?.type === part.type && tail.text === part.text) return;
932
+ message.parts.push(part);
933
+ }
934
+ applyMissingMetadata(message, input) {
935
+ if (!message.id && input.id) message.id = input.id;
936
+ if (message.agent == null && input.agent !== void 0) message.agent = input.agent;
937
+ if (message.mode == null && input.mode !== void 0) message.mode = input.mode;
938
+ if (message.model == null && input.model !== void 0) message.model = input.model;
939
+ if (message.provider == null && input.provider !== void 0) message.provider = input.provider;
940
+ if (!message.tokens && input.tokens) message.tokens = input.tokens;
941
+ if ((message.cost ?? 0) === 0 && input.cost !== void 0) message.cost = input.cost;
942
+ if (!message.cost_source && input.costSource) message.cost_source = input.costSource;
943
+ }
944
+ deriveStats(messages) {
945
+ let totalInputTokens = 0;
946
+ let totalOutputTokens = 0;
947
+ let totalCacheReadTokens = 0;
948
+ let totalCacheCreateTokens = 0;
949
+ let totalCost = 0;
950
+ let hasEstimatedCost = false;
951
+ for (const message of messages) {
952
+ totalInputTokens += message.tokens?.input ?? 0;
953
+ totalOutputTokens += message.tokens?.output ?? 0;
954
+ totalCacheReadTokens += message.tokens?.cache_read ?? 0;
955
+ totalCacheCreateTokens += message.tokens?.cache_create ?? 0;
956
+ totalCost += message.cost ?? 0;
957
+ if (message.cost_source === "estimated") hasEstimatedCost = true;
958
+ }
959
+ return {
960
+ message_count: messages.length,
961
+ total_input_tokens: totalInputTokens,
962
+ total_output_tokens: totalOutputTokens,
963
+ total_cache_read_tokens: totalCacheReadTokens || void 0,
964
+ total_cache_create_tokens: totalCacheCreateTokens || void 0,
965
+ total_cost: totalCost,
966
+ cost_source: totalCost > 0 ? hasEstimatedCost ? "estimated" : "recorded" : void 0
967
+ };
968
+ }
969
+ };
857
970
  var HEAD_INDEX_VERSION = "claudecode-head-v2";
858
971
  function parseTimestampMs(data) {
859
972
  const raw = String(data["timestamp"] ?? "").trim();
@@ -908,48 +1021,6 @@ var ClaudeCodeAgent = class extends FileSystemSessionSource {
908
1021
  }
909
1022
  return false;
910
1023
  }
911
- scan(options) {
912
- if (!this.basePath) return [];
913
- const scanMarker = perf.start("claudecode:scan");
914
- const heads = [];
915
- const listMarker = perf.start("listProjectDirs");
916
- const projectDirs = this.listProjectDirs();
917
- perf.end(listMarker);
918
- const filesByProject = projectDirs.map((projectDir) => {
919
- const fileMarker = perf.start(`listJsonlFiles:${basename2(projectDir)}`);
920
- const files = this.listJsonlFiles(projectDir).filter((file) => {
921
- try {
922
- return matchesScanWindow(statSync2(file).mtimeMs, options);
923
- } catch {
924
- return false;
925
- }
926
- });
927
- perf.end(fileMarker);
928
- return { projectDir, files };
929
- });
930
- const totalFiles = filesByProject.reduce((total, item) => total + item.files.length, 0);
931
- options?.onProgress?.({ total: totalFiles, processed: 0, sessions: 0 });
932
- let processed = 0;
933
- for (const { projectDir, files } of filesByProject) {
934
- for (const file of files) {
935
- try {
936
- const parseMarker = perf.start(`parseSessionHead:${basename2(file)}`);
937
- const head = getParsedSession(this.parseSessionHeadResult(file, projectDir));
938
- perf.end(parseMarker);
939
- if (head) {
940
- heads.push(head);
941
- this.sessionMetaMap.set(head.id, this.buildSessionMeta(head, file, projectDir));
942
- }
943
- } catch {
944
- } finally {
945
- processed += 1;
946
- options?.onProgress?.({ total: totalFiles, processed, sessions: heads.length });
947
- }
948
- }
949
- }
950
- perf.end(scanMarker);
951
- return heads;
952
- }
953
1024
  listSessionSources(options) {
954
1025
  if (!this.basePath) return [];
955
1026
  const refs = [];
@@ -987,42 +1058,16 @@ var ClaudeCodeAgent = class extends FileSystemSessionSource {
987
1058
  throw new Error(`Session file missing: ${meta.sourcePath}`);
988
1059
  }
989
1060
  const content = readFileSync2(meta.sourcePath, "utf-8");
990
- const messages = [];
991
- const pendingToolCalls = /* @__PURE__ */ new Map();
992
- const ignoredToolCallIds = /* @__PURE__ */ new Set();
1061
+ const builder = new TranscriptBuilder();
993
1062
  const assistantUuidToToolCalls = /* @__PURE__ */ new Map();
994
1063
  const countedUsageKeys = /* @__PURE__ */ new Set();
995
- const assistantState = {
996
- currentIndex: null,
997
- latestTextIndex: null
998
- };
999
- let totalCost = 0;
1000
- let totalInputTokens = 0;
1001
- let totalOutputTokens = 0;
1002
- let totalCacheRead = 0;
1003
- let totalCacheCreate = 0;
1004
1064
  for (const record of parseJsonlLines(content)) {
1005
1065
  try {
1006
- this.convertRecord(
1007
- record,
1008
- messages,
1009
- pendingToolCalls,
1010
- ignoredToolCallIds,
1011
- assistantUuidToToolCalls,
1012
- countedUsageKeys,
1013
- assistantState
1014
- );
1066
+ this.convertRecord(record, builder, assistantUuidToToolCalls, countedUsageKeys);
1015
1067
  } catch {
1016
1068
  }
1017
1069
  }
1018
- const cleanedMessages = cleanParsedMessages(messages);
1019
- for (const msg of cleanedMessages) {
1020
- totalCost += msg.cost ?? 0;
1021
- totalInputTokens += msg.tokens?.input ?? 0;
1022
- totalOutputTokens += msg.tokens?.output ?? 0;
1023
- totalCacheRead += msg.tokens?.cache_read ?? 0;
1024
- totalCacheCreate += msg.tokens?.cache_create ?? 0;
1025
- }
1070
+ const transcript = builder.finish();
1026
1071
  return {
1027
1072
  id: meta.id,
1028
1073
  title: meta.title,
@@ -1031,16 +1076,8 @@ var ClaudeCodeAgent = class extends FileSystemSessionSource {
1031
1076
  version: void 0,
1032
1077
  time_created: meta.createdAt,
1033
1078
  time_updated: meta.updatedAt,
1034
- stats: {
1035
- message_count: cleanedMessages.length,
1036
- total_input_tokens: totalInputTokens,
1037
- total_output_tokens: totalOutputTokens,
1038
- total_cost: totalCost,
1039
- cost_source: totalCost > 0 ? "estimated" : void 0,
1040
- total_cache_read_tokens: totalCacheRead,
1041
- total_cache_create_tokens: totalCacheCreate
1042
- },
1043
- messages: cleanedMessages
1079
+ stats: transcript.stats,
1080
+ messages: transcript.messages
1044
1081
  };
1045
1082
  }
1046
1083
  // --- Private helpers ---
@@ -1251,41 +1288,24 @@ var ClaudeCodeAgent = class extends FileSystemSessionSource {
1251
1288
  return null;
1252
1289
  }
1253
1290
  // --- Record conversion ---
1254
- convertRecord(data, messages, pendingToolCalls, ignoredToolCallIds, assistantUuidToToolCalls, countedUsageKeys, assistantState) {
1291
+ convertRecord(data, builder, assistantUuidToToolCalls, countedUsageKeys) {
1255
1292
  if (data["isMeta"] === true) return;
1256
1293
  const msgType = String(data["type"] ?? "");
1257
1294
  if (isInternalEventType(msgType)) return;
1258
1295
  if (msgType === "assistant") {
1259
- this.convertAssistantRecord(
1260
- data,
1261
- messages,
1262
- pendingToolCalls,
1263
- ignoredToolCallIds,
1264
- assistantUuidToToolCalls,
1265
- countedUsageKeys,
1266
- assistantState
1267
- );
1296
+ this.convertAssistantRecord(data, builder, assistantUuidToToolCalls, countedUsageKeys);
1268
1297
  } else if (msgType === "user") {
1269
- this.convertUserRecord(
1270
- data,
1271
- messages,
1272
- pendingToolCalls,
1273
- ignoredToolCallIds,
1274
- assistantUuidToToolCalls,
1275
- assistantState
1276
- );
1298
+ this.convertUserRecord(data, builder, assistantUuidToToolCalls);
1277
1299
  } else if (msgType === "tool_result") {
1278
- this.convertToolResultRecord(data, messages, assistantState);
1300
+ this.convertToolResultRecord(data, builder);
1279
1301
  }
1280
1302
  }
1281
- convertAssistantRecord(data, messages, pendingToolCalls, ignoredToolCallIds, assistantUuidToToolCalls, countedUsageKeys, assistantState) {
1303
+ convertAssistantRecord(data, builder, assistantUuidToToolCalls, countedUsageKeys) {
1282
1304
  const msg = data["message"] ?? {};
1283
1305
  const timestampMs = parseTimestampMs(data);
1284
1306
  const rawContent = msg["content"] ?? [];
1285
1307
  const uuid = String(data["uuid"] ?? "");
1286
1308
  const toolCallIds = [];
1287
- let currentAssistantIndex = assistantState.currentIndex;
1288
- let latestAssistantTextIndex = assistantState.latestTextIndex;
1289
1309
  if (Array.isArray(rawContent)) {
1290
1310
  for (const item of rawContent) {
1291
1311
  if (!item || typeof item !== "object") continue;
@@ -1294,46 +1314,41 @@ var ClaudeCodeAgent = class extends FileSystemSessionSource {
1294
1314
  if (partType === "thinking") {
1295
1315
  const text = cleanInternalText(String(part["thinking"] ?? ""));
1296
1316
  if (text) {
1297
- currentAssistantIndex = this.appendAssistantReasoning(
1298
- messages,
1299
- { messageId: uuid, data, msg, timestampMs, text, countedUsageKeys },
1300
- currentAssistantIndex
1317
+ const message2 = builder.appendAssistantPart(
1318
+ this.buildReasoningPart(text, timestampMs),
1319
+ { id: uuid, timestampMs, agent: "claude" },
1320
+ { deduplicateTail: true }
1301
1321
  );
1322
+ this.applyAssistantMetadata(message2, data, msg, countedUsageKeys);
1302
1323
  }
1303
1324
  continue;
1304
1325
  }
1305
1326
  if (partType === "text") {
1306
1327
  const text = cleanInternalText(String(part["text"] ?? ""));
1307
1328
  if (text) {
1308
- currentAssistantIndex = this.appendAssistantText(
1309
- messages,
1310
- { messageId: uuid, data, msg, timestampMs, text, countedUsageKeys },
1311
- currentAssistantIndex
1329
+ const message2 = builder.appendAssistantPart(
1330
+ this.buildTextPart(text, timestampMs),
1331
+ {
1332
+ id: uuid,
1333
+ timestampMs,
1334
+ agent: "claude"
1335
+ },
1336
+ { deduplicateTail: true }
1312
1337
  );
1313
- latestAssistantTextIndex = currentAssistantIndex;
1338
+ this.applyAssistantMetadata(message2, data, msg, countedUsageKeys);
1314
1339
  }
1315
1340
  continue;
1316
1341
  }
1317
1342
  if (partType !== "tool_use") continue;
1318
- const toolName = String(part["name"] ?? "").trim();
1319
1343
  const toolCallId = String(part["id"] ?? "").trim();
1320
- if (toolName && toolCallId && this.shouldIgnoreTool(toolName)) {
1321
- ignoredToolCallIds.add(toolCallId);
1322
- continue;
1323
- }
1324
1344
  const toolPart = this.buildToolPart(part, timestampMs);
1325
- const [msgIndex, partIndex] = this.attachToolCallToLatestAssistant(messages, {
1326
- messageId: uuid,
1327
- data,
1328
- msg,
1329
- timestampMs,
1345
+ const message = builder.appendToolCall(
1330
1346
  toolPart,
1331
- latestTextIndex: latestAssistantTextIndex,
1332
- countedUsageKeys
1333
- });
1334
- currentAssistantIndex = msgIndex;
1347
+ { id: uuid, timestampMs, agent: "claude" },
1348
+ { modeOnCreate: "tool" }
1349
+ );
1350
+ this.applyAssistantMetadata(message, data, msg, countedUsageKeys);
1335
1351
  if (toolCallId) {
1336
- pendingToolCalls.set(toolCallId, [msgIndex, partIndex]);
1337
1352
  toolCallIds.push(toolCallId);
1338
1353
  }
1339
1354
  }
@@ -1341,10 +1356,8 @@ var ClaudeCodeAgent = class extends FileSystemSessionSource {
1341
1356
  if (toolCallIds.length > 0) {
1342
1357
  assistantUuidToToolCalls.set(uuid, toolCallIds);
1343
1358
  }
1344
- assistantState.currentIndex = currentAssistantIndex;
1345
- assistantState.latestTextIndex = latestAssistantTextIndex;
1346
1359
  }
1347
- convertUserRecord(data, messages, pendingToolCalls, ignoredToolCallIds, assistantUuidToToolCalls, assistantState) {
1360
+ convertUserRecord(data, builder, assistantUuidToToolCalls) {
1348
1361
  const msg = data["message"] ?? {};
1349
1362
  const timestampMs = parseTimestampMs(data);
1350
1363
  const content = msg["content"] ?? "";
@@ -1352,18 +1365,14 @@ var ClaudeCodeAgent = class extends FileSystemSessionSource {
1352
1365
  if (typeof content === "string") {
1353
1366
  const parts = this.normalizeUserTextParts(content, timestampMs);
1354
1367
  if (parts.length === 0) {
1355
- assistantState.currentIndex = null;
1356
- assistantState.latestTextIndex = null;
1368
+ builder.beginTurn();
1357
1369
  return;
1358
1370
  }
1359
- messages.push(this.buildMessage({ messageId: uuid, role: "user", timestampMs, parts }));
1360
- assistantState.currentIndex = null;
1361
- assistantState.latestTextIndex = null;
1371
+ builder.appendMessage({ id: uuid, role: "user", timestampMs, parts });
1362
1372
  return;
1363
1373
  }
1364
1374
  if (!Array.isArray(content)) {
1365
- assistantState.currentIndex = null;
1366
- assistantState.latestTextIndex = null;
1375
+ builder.beginTurn();
1367
1376
  return;
1368
1377
  }
1369
1378
  const visibleParts = this.normalizeUserTextParts(content, timestampMs);
@@ -1373,15 +1382,8 @@ var ClaudeCodeAgent = class extends FileSystemSessionSource {
1373
1382
  const ci = item;
1374
1383
  if (ci["type"] !== "tool_result") continue;
1375
1384
  const toolCallId = this.resolveToolCallId(data, ci, assistantUuidToToolCalls);
1376
- if (toolCallId && ignoredToolCallIds.has(toolCallId)) continue;
1377
1385
  const outputParts = this.normalizeClaudeToolOutput(ci["content"], timestampMs);
1378
- if (this.backfillToolOutput(
1379
- messages,
1380
- pendingToolCalls,
1381
- toolCallId,
1382
- outputParts,
1383
- toolStateUpdates
1384
- )) {
1386
+ if (this.backfillToolOutput(builder, toolCallId, outputParts, toolStateUpdates)) {
1385
1387
  continue;
1386
1388
  }
1387
1389
  const fallback = this.buildFallbackToolMessage({
@@ -1390,17 +1392,14 @@ var ClaudeCodeAgent = class extends FileSystemSessionSource {
1390
1392
  toolCallId,
1391
1393
  outputParts
1392
1394
  });
1393
- if (fallback) messages.push(fallback);
1395
+ if (fallback) builder.appendMessage(fallback);
1394
1396
  }
1395
1397
  if (visibleParts.length > 0) {
1396
- messages.push(
1397
- this.buildMessage({ messageId: uuid, role: "user", timestampMs, parts: visibleParts })
1398
- );
1398
+ builder.appendMessage({ id: uuid, role: "user", timestampMs, parts: visibleParts });
1399
1399
  }
1400
- assistantState.currentIndex = null;
1401
- assistantState.latestTextIndex = null;
1400
+ builder.beginTurn();
1402
1401
  }
1403
- convertToolResultRecord(data, messages, assistantState) {
1402
+ convertToolResultRecord(data, builder) {
1404
1403
  const timestampMs = parseTimestampMs(data);
1405
1404
  const msg = data["message"] ?? {};
1406
1405
  const outputParts = this.normalizeClaudeToolOutput(msg["content"], timestampMs);
@@ -1411,25 +1410,8 @@ var ClaudeCodeAgent = class extends FileSystemSessionSource {
1411
1410
  toolCallId: null,
1412
1411
  outputParts
1413
1412
  });
1414
- if (fallback) messages.push(fallback);
1415
- assistantState.currentIndex = null;
1416
- assistantState.latestTextIndex = null;
1417
- }
1418
- // --- Message building ---
1419
- buildMessage(opts) {
1420
- return {
1421
- id: opts.messageId,
1422
- role: opts.role,
1423
- agent: opts.agent ?? null,
1424
- time_created: opts.timestampMs,
1425
- mode: opts.mode ?? null,
1426
- model: opts.model ?? null,
1427
- provider: opts.provider ?? null,
1428
- tokens: opts.tokens ? opts.tokens : void 0,
1429
- cost: opts.cost ?? 0,
1430
- cost_source: opts.cost_source,
1431
- parts: opts.parts
1432
- };
1413
+ if (fallback) builder.appendMessage(fallback);
1414
+ builder.beginTurn();
1433
1415
  }
1434
1416
  buildTextPart(text, timestampMs) {
1435
1417
  return { type: "text", text, time_created: timestampMs };
@@ -1472,71 +1454,6 @@ var ClaudeCodeAgent = class extends FileSystemSessionSource {
1472
1454
  }
1473
1455
  }
1474
1456
  }
1475
- // --- Assistant message grouping ---
1476
- appendAssistantReasoning(messages, opts, currentIndex) {
1477
- const part = this.buildReasoningPart(opts.text, opts.timestampMs);
1478
- if (currentIndex !== null) {
1479
- const message2 = messages[currentIndex];
1480
- const hasText = message2.parts.some((p) => p.type === "text");
1481
- const hasTool = message2.parts.some((p) => p.type === "tool");
1482
- if (!hasText && !hasTool) {
1483
- this.appendPartIfNew(message2, part);
1484
- this.applyAssistantMetadata(message2, opts.data, opts.msg, opts.countedUsageKeys);
1485
- return currentIndex;
1486
- }
1487
- }
1488
- const message = this.buildMessage({
1489
- messageId: opts.messageId,
1490
- role: "assistant",
1491
- timestampMs: opts.timestampMs,
1492
- parts: [part],
1493
- agent: "claude"
1494
- });
1495
- this.applyAssistantMetadata(message, opts.data, opts.msg, opts.countedUsageKeys);
1496
- messages.push(message);
1497
- return messages.length - 1;
1498
- }
1499
- appendAssistantText(messages, opts, currentIndex) {
1500
- const part = this.buildTextPart(opts.text, opts.timestampMs);
1501
- if (currentIndex !== null) {
1502
- const message2 = messages[currentIndex];
1503
- const hasTool = message2.parts.some((p) => p.type === "tool");
1504
- if (!hasTool) {
1505
- this.appendPartIfNew(message2, part);
1506
- this.applyAssistantMetadata(message2, opts.data, opts.msg, opts.countedUsageKeys);
1507
- return currentIndex;
1508
- }
1509
- }
1510
- const message = this.buildMessage({
1511
- messageId: opts.messageId,
1512
- role: "assistant",
1513
- timestampMs: opts.timestampMs,
1514
- parts: [part],
1515
- agent: "claude"
1516
- });
1517
- this.applyAssistantMetadata(message, opts.data, opts.msg, opts.countedUsageKeys);
1518
- messages.push(message);
1519
- return messages.length - 1;
1520
- }
1521
- attachToolCallToLatestAssistant(messages, opts) {
1522
- if (opts.latestTextIndex !== null) {
1523
- const message2 = messages[opts.latestTextIndex];
1524
- message2.parts.push(opts.toolPart);
1525
- this.applyAssistantMetadata(message2, opts.data, opts.msg, opts.countedUsageKeys);
1526
- return [opts.latestTextIndex, message2.parts.length - 1];
1527
- }
1528
- const message = this.buildMessage({
1529
- messageId: opts.messageId,
1530
- role: "assistant",
1531
- timestampMs: opts.timestampMs,
1532
- parts: [opts.toolPart],
1533
- agent: "claude",
1534
- mode: "tool"
1535
- });
1536
- this.applyAssistantMetadata(message, opts.data, opts.msg, opts.countedUsageKeys);
1537
- messages.push(message);
1538
- return [messages.length - 1, 0];
1539
- }
1540
1457
  // --- User content normalization ---
1541
1458
  normalizeUserTextParts(content, timestampMs) {
1542
1459
  if (typeof content === "string") {
@@ -1568,9 +1485,17 @@ var ClaudeCodeAgent = class extends FileSystemSessionSource {
1568
1485
  const parts = [];
1569
1486
  for (const item of content) {
1570
1487
  if (typeof item === "object" && item !== null) {
1571
- const text2 = String(
1572
- item["text"] ?? item["content"] ?? ""
1573
- );
1488
+ const itemRecord = item;
1489
+ const source = itemRecord["source"];
1490
+ if (itemRecord["type"] === "image" && source) {
1491
+ const data = typeof source["data"] === "string" ? source["data"] : "";
1492
+ const mimeType = typeof source["media_type"] === "string" ? source["media_type"] : "";
1493
+ if (data && mimeType.startsWith("image/")) {
1494
+ parts.push({ type: "image", data, mime_type: mimeType, time_created: timestampMs });
1495
+ }
1496
+ continue;
1497
+ }
1498
+ const text2 = String(itemRecord["text"] ?? itemRecord["content"] ?? "");
1574
1499
  const cleaned = cleanInternalText(text2);
1575
1500
  if (cleaned) parts.push(this.buildTextPart(cleaned, timestampMs));
1576
1501
  } else if (typeof item === "string") {
@@ -1584,29 +1509,19 @@ var ClaudeCodeAgent = class extends FileSystemSessionSource {
1584
1509
  return text ? [this.buildTextPart(text, timestampMs)] : [];
1585
1510
  }
1586
1511
  // --- Tool backfill ---
1587
- backfillToolOutput(messages, pendingToolCalls, callId, outputParts, stateUpdates) {
1512
+ backfillToolOutput(builder, callId, outputParts, stateUpdates) {
1588
1513
  if (!callId) return false;
1589
- const location = pendingToolCalls.get(callId);
1590
- if (location === void 0) return false;
1591
- const [msgIndex, partIndex] = location;
1592
- const state = messages[msgIndex].parts[partIndex].state ?? (messages[msgIndex].parts[partIndex].state = {});
1593
- if (outputParts.length > 0) {
1594
- const existing = state.output;
1595
- if (Array.isArray(existing)) {
1596
- existing.push(...outputParts);
1597
- } else if (existing === null || existing === void 0) {
1598
- state.output = [...outputParts];
1599
- } else {
1600
- state.output = [existing, ...outputParts];
1514
+ return builder.updateToolCall(callId, (part) => {
1515
+ const state = part.state ?? (part.state = {});
1516
+ if (outputParts.length > 0) {
1517
+ const existing = state.output;
1518
+ if (Array.isArray(existing)) existing.push(...outputParts);
1519
+ else if (existing == null) state.output = [...outputParts];
1520
+ else state.output = [existing, ...outputParts];
1601
1521
  }
1602
- }
1603
- if (stateUpdates) {
1604
- Object.assign(state, stateUpdates);
1605
- }
1606
- if (outputParts.length > 0 && !state.status) {
1607
- state.status = "completed";
1608
- }
1609
- return outputParts.length > 0 || !!stateUpdates;
1522
+ if (stateUpdates) Object.assign(state, stateUpdates);
1523
+ if (outputParts.length > 0 && !state.status) state.status = "completed";
1524
+ });
1610
1525
  }
1611
1526
  resolveToolCallId(data, item, assistantUuidToToolCalls) {
1612
1527
  const directId = String(item["tool_use_id"] ?? "").trim();
@@ -1634,24 +1549,12 @@ var ClaudeCodeAgent = class extends FileSystemSessionSource {
1634
1549
  // --- Fallback ---
1635
1550
  buildFallbackToolMessage(opts) {
1636
1551
  if (opts.outputParts.length === 0) return null;
1637
- return this.buildMessage({
1638
- messageId: opts.messageId,
1552
+ return {
1553
+ id: opts.messageId,
1639
1554
  role: "tool",
1640
1555
  timestampMs: opts.timestampMs,
1641
1556
  parts: opts.outputParts
1642
- });
1643
- }
1644
- // --- Utilities ---
1645
- shouldIgnoreTool(toolName) {
1646
- return toolName === "TodoWrite";
1647
- }
1648
- appendPartIfNew(message, part) {
1649
- const parts = message.parts;
1650
- if (parts.length > 0 && parts[parts.length - 1].type === part.type) {
1651
- const tail = parts[parts.length - 1];
1652
- if (tail.text === part.text) return;
1653
- }
1654
- parts.push(part);
1557
+ };
1655
1558
  }
1656
1559
  };
1657
1560
  var DatabaseConstructor = null;
@@ -2318,50 +2221,6 @@ var KimiAgent = class extends FileSystemSessionSource {
2318
2221
  return skippedSession("malformed metadata");
2319
2222
  }
2320
2223
  }
2321
- scan(options) {
2322
- if (!this.basePath) return [];
2323
- const scanMarker = perf.start("kimi:scan");
2324
- const listMarker = perf.start("listSessionDirs");
2325
- const sessionDirs = this.listSessionDirs();
2326
- perf.end(listMarker);
2327
- const metas = [];
2328
- for (const dir of sessionDirs) {
2329
- try {
2330
- const parseMarker = perf.start(`parseSessionDir:${basename4(dir)}`);
2331
- const meta = getParsedSession(this.parseSessionDirResult(dir));
2332
- perf.end(parseMarker);
2333
- if (meta && matchesScanWindow(meta.createdAt, options)) {
2334
- metas.push(meta);
2335
- }
2336
- } catch {
2337
- }
2338
- }
2339
- options?.onProgress?.({ total: metas.length, processed: 0, sessions: 0 });
2340
- const heads = [];
2341
- let processed = 0;
2342
- for (const meta of metas) {
2343
- try {
2344
- meta.sourceFingerprint = this.sourceFingerprint(meta);
2345
- this.sessionMetaMap.set(meta.id, meta);
2346
- const stats = this.extractStats(meta.sourcePath);
2347
- heads.push({
2348
- id: meta.id,
2349
- slug: `kimi/${meta.id}`,
2350
- title: meta.title,
2351
- directory: meta.cwd,
2352
- time_created: meta.createdAt,
2353
- time_updated: meta.createdAt,
2354
- stats
2355
- });
2356
- } catch {
2357
- } finally {
2358
- processed += 1;
2359
- options?.onProgress?.({ total: metas.length, processed, sessions: heads.length });
2360
- }
2361
- }
2362
- perf.end(scanMarker);
2363
- return heads;
2364
- }
2365
2224
  listSessionSources(options) {
2366
2225
  if (!this.basePath) return [];
2367
2226
  const refs = [];
@@ -2403,8 +2262,7 @@ var KimiAgent = class extends FileSystemSessionSource {
2403
2262
  getSessionDataFromContext(meta) {
2404
2263
  if (!meta.contextFile) throw new Error("context.jsonl is missing");
2405
2264
  const content = readFileSync3(meta.contextFile, "utf-8");
2406
- const messages = [];
2407
- const pendingToolCalls = /* @__PURE__ */ new Map();
2265
+ const builder = new TranscriptBuilder();
2408
2266
  const ignoredToolCallIds = /* @__PURE__ */ new Set();
2409
2267
  let seq = 0;
2410
2268
  const fallbackTs = meta.createdAt;
@@ -2416,65 +2274,55 @@ var KimiAgent = class extends FileSystemSessionSource {
2416
2274
  if (role === "user") {
2417
2275
  const text = cleanInternalText(kimiContentText(record.content));
2418
2276
  if (text) {
2419
- messages.push(
2420
- this.buildMessage({
2421
- messageId: `context-${seq}`,
2422
- role: "user",
2423
- timestampMs: fallbackTs,
2424
- parts: [{ type: "text", text, time_created: fallbackTs }]
2425
- })
2426
- );
2277
+ builder.appendMessage({
2278
+ id: `context-${seq}`,
2279
+ role: "user",
2280
+ timestampMs: fallbackTs,
2281
+ parts: [{ type: "text", text, time_created: fallbackTs }]
2282
+ });
2427
2283
  }
2428
2284
  continue;
2429
2285
  }
2430
2286
  if (role === "assistant") {
2431
- const { message, toolIndexes } = this.buildContextAssistantMessage(
2287
+ const message = this.buildContextAssistantMessage(
2432
2288
  record,
2433
2289
  seq,
2434
2290
  ignoredToolCallIds,
2435
2291
  fallbackTs
2436
2292
  );
2437
2293
  if (!message) continue;
2438
- const msgIndex = messages.length;
2439
- messages.push(message);
2440
- for (const [callId, partIndex] of toolIndexes) {
2441
- pendingToolCalls.set(callId, [msgIndex, partIndex]);
2442
- }
2294
+ builder.appendMessage(message);
2443
2295
  continue;
2444
2296
  }
2445
2297
  if (role === "tool") {
2446
2298
  const callId = String(record.tool_call_id ?? "").trim();
2447
2299
  if (callId && ignoredToolCallIds.has(callId)) continue;
2448
2300
  const outputParts = normalizeToolOutputParts(record.content, fallbackTs);
2449
- if (callId && this.backfillToolOutput(messages, pendingToolCalls, callId, outputParts)) {
2301
+ if (callId && this.backfillToolOutput(builder, callId, outputParts)) {
2450
2302
  continue;
2451
2303
  }
2452
2304
  if (outputParts.length > 0) {
2453
- messages.push(
2454
- this.buildMessage({
2455
- messageId: `context-${seq}`,
2456
- role: "tool",
2457
- timestampMs: fallbackTs,
2458
- parts: outputParts
2459
- })
2460
- );
2305
+ builder.appendMessage({
2306
+ id: `context-${seq}`,
2307
+ role: "tool",
2308
+ timestampMs: fallbackTs,
2309
+ parts: outputParts
2310
+ });
2461
2311
  }
2462
2312
  }
2463
2313
  } catch {
2464
2314
  }
2465
2315
  }
2466
2316
  const stats = this.extractStats(meta.sourcePath);
2467
- return this.buildSessionData(meta, messages, stats);
2317
+ return this.buildSessionData(meta, builder, stats);
2468
2318
  }
2469
2319
  getSessionDataFromWire(meta) {
2470
2320
  const wirePath = meta.wireFile ?? join6(meta.sourcePath, "wire.jsonl");
2471
2321
  if (!existsSync6(wirePath)) throw new Error("wire.jsonl is missing");
2472
2322
  const content = readFileSync3(wirePath, "utf-8");
2473
- const messages = [];
2474
- const pendingToolCalls = /* @__PURE__ */ new Map();
2323
+ const builder = new TranscriptBuilder();
2475
2324
  const ignoredToolCallIds = /* @__PURE__ */ new Set();
2476
2325
  const openToolArgumentBuffer = /* @__PURE__ */ new Map();
2477
- let currentAssistantIndex = null;
2478
2326
  let openToolCallId = null;
2479
2327
  let seq = 0;
2480
2328
  for (const record of parseJsonlLines(content)) {
@@ -2491,19 +2339,13 @@ var KimiAgent = class extends FileSystemSessionSource {
2491
2339
  const inputTokens = Number(usage["input_tokens"] ?? 0);
2492
2340
  const outputTokens = Number(usage["output_tokens"] ?? 0);
2493
2341
  if (inputTokens || outputTokens) {
2494
- for (let i = messages.length - 1; i >= 0; i--) {
2495
- const msg = messages[i];
2496
- if (msg.role === "assistant" && !msg.tokens) {
2497
- msg.tokens = { input: inputTokens, output: outputTokens };
2498
- msg.model ??= this.defaultModel;
2499
- const cost = estimateTokenCost(msg.model, msg.tokens);
2500
- if (cost !== null) {
2501
- msg.cost = cost;
2502
- msg.cost_source = "estimated";
2503
- }
2504
- break;
2505
- }
2506
- }
2342
+ const tokens = { input: inputTokens, output: outputTokens };
2343
+ const cost = estimateTokenCost(this.defaultModel, tokens);
2344
+ builder.attachUsageToLatestAssistant(tokens, {
2345
+ model: this.defaultModel,
2346
+ cost: cost ?? void 0,
2347
+ costSource: cost === null ? void 0 : "estimated"
2348
+ });
2507
2349
  }
2508
2350
  }
2509
2351
  if (msgType === "TurnBegin") {
@@ -2511,37 +2353,37 @@ var KimiAgent = class extends FileSystemSessionSource {
2511
2353
  if (Array.isArray(userInput) && userInput.length > 0) {
2512
2354
  const text = cleanInternalText(kimiContentText(userInput));
2513
2355
  if (text) {
2514
- messages.push(
2515
- this.buildMessage({
2516
- messageId: `wire-${seq}`,
2517
- role: "user",
2518
- timestampMs,
2519
- parts: [{ type: "text", text, time_created: timestampMs }]
2520
- })
2521
- );
2356
+ builder.appendMessage({
2357
+ id: `wire-${seq}`,
2358
+ role: "user",
2359
+ timestampMs,
2360
+ parts: [{ type: "text", text, time_created: timestampMs }]
2361
+ });
2522
2362
  }
2523
2363
  }
2524
- currentAssistantIndex = null;
2364
+ builder.beginTurn();
2525
2365
  openToolCallId = null;
2526
2366
  continue;
2527
2367
  }
2528
2368
  if (msgType === "ContentPart") {
2529
- currentAssistantIndex = this.getOrCreateWireAssistant(
2530
- messages,
2531
- currentAssistantIndex,
2532
- `wire-${seq}`
2533
- );
2534
- const assistant = messages[currentAssistantIndex];
2535
2369
  const partType = String(payload.type ?? "");
2536
2370
  if (partType === "think") {
2537
2371
  const text = cleanInternalText(String(payload.think ?? ""));
2538
2372
  if (text) {
2539
- assistant.parts.push({ type: "reasoning", text, time_created: timestampMs });
2373
+ builder.appendAssistantPart(
2374
+ { type: "reasoning", text, time_created: timestampMs },
2375
+ { id: `wire-${seq}`, timestampMs: 0, agent: "kimi" },
2376
+ { grouping: "current" }
2377
+ );
2540
2378
  }
2541
2379
  } else if (partType === "text") {
2542
2380
  const text = cleanInternalText(String(payload.text ?? ""));
2543
2381
  if (text) {
2544
- assistant.parts.push({ type: "text", text, time_created: timestampMs });
2382
+ builder.appendAssistantPart(
2383
+ { type: "text", text, time_created: timestampMs },
2384
+ { id: `wire-${seq}`, timestampMs: 0, agent: "kimi" },
2385
+ { grouping: "current" }
2386
+ );
2545
2387
  }
2546
2388
  }
2547
2389
  continue;
@@ -2556,12 +2398,6 @@ var KimiAgent = class extends FileSystemSessionSource {
2556
2398
  continue;
2557
2399
  }
2558
2400
  if (!function_ || !callId || !toolName) continue;
2559
- currentAssistantIndex = this.getOrCreateWireAssistant(
2560
- messages,
2561
- currentAssistantIndex,
2562
- `wire-${seq}`
2563
- );
2564
- const assistant = messages[currentAssistantIndex];
2565
2401
  const rawArgs = function_.arguments;
2566
2402
  const normalizedArgs = normalizeToolArguments(rawArgs);
2567
2403
  const buffer = typeof rawArgs === "string" && typeof normalizedArgs !== "string" ? rawArgs : null;
@@ -2573,10 +2409,11 @@ var KimiAgent = class extends FileSystemSessionSource {
2573
2409
  state: { arguments: normalizedArgs, output: null },
2574
2410
  time_created: timestampMs
2575
2411
  };
2576
- const partIndex = assistant.parts.length;
2577
- assistant.parts.push(toolPart);
2578
- assistant.mode = "tool";
2579
- pendingToolCalls.set(callId, [currentAssistantIndex, partIndex]);
2412
+ builder.appendToolCall(
2413
+ toolPart,
2414
+ { id: `wire-${seq}`, timestampMs: 0, agent: "kimi" },
2415
+ { markModeAsTool: true, target: "current" }
2416
+ );
2580
2417
  openToolCallId = callId;
2581
2418
  if (buffer !== null) {
2582
2419
  openToolArgumentBuffer.set(callId, buffer);
@@ -2590,8 +2427,7 @@ var KimiAgent = class extends FileSystemSessionSource {
2590
2427
  argumentsPart,
2591
2428
  openToolCallId,
2592
2429
  openToolArgumentBuffer,
2593
- messages,
2594
- pendingToolCalls
2430
+ builder
2595
2431
  );
2596
2432
  continue;
2597
2433
  }
@@ -2599,27 +2435,24 @@ var KimiAgent = class extends FileSystemSessionSource {
2599
2435
  const callId = String(payload.tool_call_id ?? "").trim();
2600
2436
  if (callId && ignoredToolCallIds.has(callId)) continue;
2601
2437
  const outputParts = normalizeWireToolOutputParts(payload.return_value, timestampMs);
2602
- if (callId && this.backfillToolOutput(messages, pendingToolCalls, callId, outputParts)) {
2438
+ if (callId && this.backfillToolOutput(builder, callId, outputParts)) {
2603
2439
  continue;
2604
2440
  }
2605
2441
  if (outputParts.length > 0) {
2606
- messages.push(
2607
- this.buildMessage({
2608
- messageId: `wire-${seq}`,
2609
- role: "tool",
2610
- timestampMs,
2611
- parts: outputParts
2612
- })
2613
- );
2442
+ builder.appendMessage({
2443
+ id: `wire-${seq}`,
2444
+ role: "tool",
2445
+ timestampMs,
2446
+ parts: outputParts
2447
+ });
2614
2448
  }
2615
2449
  continue;
2616
2450
  }
2617
2451
  } catch {
2618
2452
  }
2619
2453
  }
2620
- const filteredMessages = messages.filter((m) => m.parts.length > 0);
2621
2454
  const stats = this.extractStats(meta.sourcePath);
2622
- return this.buildSessionData(meta, filteredMessages, stats);
2455
+ return this.buildSessionData(meta, builder, stats);
2623
2456
  }
2624
2457
  // --- Helpers ---
2625
2458
  sourceFingerprint(meta) {
@@ -2637,23 +2470,8 @@ var KimiAgent = class extends FileSystemSessionSource {
2637
2470
  fileMtime(meta.wireFile)
2638
2471
  ]);
2639
2472
  }
2640
- buildMessage(opts) {
2641
- return {
2642
- id: opts.messageId,
2643
- role: opts.role,
2644
- agent: opts.agent ?? null,
2645
- time_created: opts.timestampMs,
2646
- mode: opts.mode ?? null,
2647
- model: opts.model ?? null,
2648
- provider: opts.provider ?? null,
2649
- tokens: opts.tokens ? opts.tokens : void 0,
2650
- cost: opts.cost ?? 0,
2651
- parts: opts.parts
2652
- };
2653
- }
2654
2473
  buildContextAssistantMessage(record, seq, ignoredToolCallIds, fallbackTs) {
2655
2474
  const parts = [];
2656
- const toolIndexes = /* @__PURE__ */ new Map();
2657
2475
  const content = record.content;
2658
2476
  if (Array.isArray(content)) {
2659
2477
  for (const item of content) {
@@ -2691,71 +2509,41 @@ var KimiAgent = class extends FileSystemSessionSource {
2691
2509
  state: { arguments: normalizeToolArguments(function_.arguments), output: null },
2692
2510
  time_created: fallbackTs
2693
2511
  };
2694
- toolIndexes.set(callId, parts.length);
2695
2512
  parts.push(part);
2696
2513
  }
2697
2514
  }
2698
2515
  if (parts.length === 0) {
2699
- return {
2700
- message: this.buildMessage({
2701
- messageId: `context-${seq}`,
2702
- role: "assistant",
2703
- timestampMs: fallbackTs,
2704
- parts: []
2705
- }),
2706
- toolIndexes
2707
- };
2516
+ return null;
2708
2517
  }
2709
2518
  const allTools = parts.every((p) => p.type === "tool");
2710
- const message = this.buildMessage({
2711
- messageId: `context-${seq}`,
2519
+ return {
2520
+ id: `context-${seq}`,
2712
2521
  role: "assistant",
2713
2522
  timestampMs: fallbackTs,
2714
2523
  parts,
2715
2524
  agent: "kimi",
2716
2525
  mode: allTools ? "tool" : void 0
2717
- });
2718
- return { message, toolIndexes };
2719
- }
2720
- getOrCreateWireAssistant(messages, currentIndex, messageId) {
2721
- if (currentIndex !== null) return currentIndex;
2722
- messages.push(
2723
- this.buildMessage({
2724
- messageId,
2725
- role: "assistant",
2726
- timestampMs: 0,
2727
- parts: [],
2728
- agent: "kimi"
2729
- })
2730
- );
2731
- return messages.length - 1;
2526
+ };
2732
2527
  }
2733
- appendWireToolCallPart(argumentsPart, openCallId, buffer, messages, pendingToolCalls) {
2734
- if (!openCallId || !pendingToolCalls.has(openCallId)) return;
2528
+ appendWireToolCallPart(argumentsPart, openCallId, buffer, builder) {
2529
+ if (!openCallId) return;
2735
2530
  const existing = buffer.get(openCallId) ?? "";
2736
2531
  const combined = existing + argumentsPart;
2737
2532
  try {
2738
- const parsed2 = JSON.parse(combined);
2739
- const location = pendingToolCalls.get(openCallId);
2740
- if (!location) return;
2741
- const msgPart = messages[location[0]]?.parts[location[1]];
2742
- if (msgPart?.state) {
2743
- msgPart.state.arguments = parsed2;
2533
+ const parsed = JSON.parse(combined);
2534
+ if (builder.updateToolCall(openCallId, (part) => {
2535
+ const state = part.state ?? (part.state = {});
2536
+ state.arguments = parsed;
2537
+ })) {
2538
+ buffer.delete(openCallId);
2744
2539
  }
2745
- buffer.delete(openCallId);
2746
2540
  } catch {
2747
2541
  buffer.set(openCallId, combined);
2748
2542
  }
2749
2543
  }
2750
- backfillToolOutput(messages, pendingToolCalls, callId, outputParts) {
2544
+ backfillToolOutput(builder, callId, outputParts) {
2751
2545
  if (!outputParts.length || !callId) return false;
2752
- const location = pendingToolCalls.get(callId);
2753
- if (!location) return false;
2754
- const part = messages[location[0]]?.parts[location[1]];
2755
- if (!part) return false;
2756
- if (!part.state) part.state = {};
2757
- part.state.output = [...outputParts];
2758
- return true;
2546
+ return builder.resolveToolCall(callId, { output: [...outputParts] });
2759
2547
  }
2760
2548
  extractStats(sessionDir) {
2761
2549
  let totalCost = 0;
@@ -2811,14 +2599,8 @@ var KimiAgent = class extends FileSystemSessionSource {
2811
2599
  }
2812
2600
  return stats;
2813
2601
  }
2814
- buildSessionData(meta, messages, stats) {
2815
- const cleanedMessages = cleanParsedMessages(messages);
2816
- stats.message_count = cleanedMessages.length;
2817
- const totalCost = cleanedMessages.reduce((sum, message) => sum + (message.cost ?? 0), 0);
2818
- if (totalCost > 0) {
2819
- stats.total_cost = Number(totalCost.toFixed(8));
2820
- stats.cost_source = "estimated";
2821
- }
2602
+ buildSessionData(meta, builder, stats) {
2603
+ const transcript = builder.finish(stats);
2822
2604
  return {
2823
2605
  id: meta.id,
2824
2606
  title: meta.title,
@@ -2826,16 +2608,273 @@ var KimiAgent = class extends FileSystemSessionSource {
2826
2608
  directory: meta.cwd,
2827
2609
  time_created: meta.createdAt,
2828
2610
  time_updated: meta.createdAt,
2829
- stats,
2830
- messages: cleanedMessages
2611
+ stats: transcript.stats,
2612
+ messages: transcript.messages
2831
2613
  };
2832
2614
  }
2833
2615
  };
2616
+ var PARSE_FAIL = /* @__PURE__ */ Symbol("parse-fail");
2617
+ var EXEC_OUTPUT_ENVELOPE_RE = /^Script completed\nWall time [^\n]*\nOutput:\n?/;
2618
+ function stripExecOutputEnvelope(text) {
2619
+ return text.replace(EXEC_OUTPUT_ENVELOPE_RE, "");
2620
+ }
2621
+ function splitExecToolName(name) {
2622
+ if (name.startsWith("mcp__")) {
2623
+ const separator = name.lastIndexOf("__");
2624
+ if (separator > 0 && separator + 2 < name.length) {
2625
+ return { name: name.slice(separator + 2), namespace: name.slice(0, separator + 2) };
2626
+ }
2627
+ }
2628
+ return { name };
2629
+ }
2630
+ function pickExecOutputTarget(calls) {
2631
+ for (let index = calls.length - 1; index >= 0; index -= 1) {
2632
+ const { name } = splitExecToolName(calls[index].name);
2633
+ if (name !== "apply_patch" && name !== "update_plan") return index;
2634
+ }
2635
+ return calls.length - 1;
2636
+ }
2637
+ function getExecPatchText(args) {
2638
+ if (typeof args === "string") return args;
2639
+ if (args && typeof args === "object") {
2640
+ const patch = args["patch"];
2641
+ if (typeof patch === "string") return patch;
2642
+ }
2643
+ return "";
2644
+ }
2645
+ function decodeExecCalls(input) {
2646
+ if (typeof input !== "string" || !input.includes("tools.")) return [];
2647
+ const scope = collectStringVars(input);
2648
+ const calls = [];
2649
+ const callRe = /tools\.([A-Za-z_$][\w$]*)\s*\(/g;
2650
+ let match;
2651
+ while ((match = callRe.exec(input)) !== null) {
2652
+ const reader = new JsValueReader(input, callRe.lastIndex, scope);
2653
+ const args = reader.parseValue();
2654
+ if (args !== PARSE_FAIL) {
2655
+ calls.push({ name: match[1], args });
2656
+ callRe.lastIndex = reader.pos;
2657
+ }
2658
+ }
2659
+ return calls;
2660
+ }
2661
+ function collectStringVars(input) {
2662
+ const scope = /* @__PURE__ */ new Map();
2663
+ const assignRe = /(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*/g;
2664
+ let match;
2665
+ while ((match = assignRe.exec(input)) !== null) {
2666
+ const reader = new JsValueReader(input, assignRe.lastIndex, scope);
2667
+ const value = reader.parseValue();
2668
+ if (value !== PARSE_FAIL) {
2669
+ if (typeof value === "string") scope.set(match[1], value);
2670
+ assignRe.lastIndex = reader.pos;
2671
+ }
2672
+ }
2673
+ return scope;
2674
+ }
2675
+ var IDENT_START_RE = /[A-Za-z_$]/;
2676
+ var IDENT_PART_RE = /[\w$]/;
2677
+ var JsValueReader = class {
2678
+ pos;
2679
+ src;
2680
+ scope;
2681
+ constructor(src, start, scope) {
2682
+ this.src = src;
2683
+ this.pos = start;
2684
+ this.scope = scope;
2685
+ }
2686
+ parseValue() {
2687
+ this.skipTrivia();
2688
+ const char = this.src[this.pos];
2689
+ if (char === void 0) return PARSE_FAIL;
2690
+ if (char === "{") return this.parseObject();
2691
+ if (char === "[") return this.parseArray();
2692
+ if (char === '"' || char === "'" || char === "`") return this.parseString(char);
2693
+ if (char === "-" || char === "+" || char >= "0" && char <= "9") return this.parseNumber();
2694
+ if (IDENT_START_RE.test(char)) return this.parseIdentifierValue();
2695
+ return PARSE_FAIL;
2696
+ }
2697
+ parseObject() {
2698
+ this.pos++;
2699
+ const result = {};
2700
+ this.skipTrivia();
2701
+ if (this.src[this.pos] === "}") {
2702
+ this.pos++;
2703
+ return result;
2704
+ }
2705
+ while (this.pos < this.src.length) {
2706
+ this.skipTrivia();
2707
+ const key = this.parseKey();
2708
+ if (key === PARSE_FAIL) return PARSE_FAIL;
2709
+ this.skipTrivia();
2710
+ if (this.src[this.pos] === ":") {
2711
+ this.pos++;
2712
+ const value = this.parseValue();
2713
+ if (value === PARSE_FAIL) return PARSE_FAIL;
2714
+ result[key] = value;
2715
+ } else {
2716
+ result[key] = this.resolveIdentifier(key);
2717
+ }
2718
+ this.skipTrivia();
2719
+ const next = this.src[this.pos];
2720
+ if (next === ",") {
2721
+ this.pos++;
2722
+ this.skipTrivia();
2723
+ if (this.src[this.pos] === "}") {
2724
+ this.pos++;
2725
+ return result;
2726
+ }
2727
+ continue;
2728
+ }
2729
+ if (next === "}") {
2730
+ this.pos++;
2731
+ return result;
2732
+ }
2733
+ return PARSE_FAIL;
2734
+ }
2735
+ return PARSE_FAIL;
2736
+ }
2737
+ parseArray() {
2738
+ this.pos++;
2739
+ const result = [];
2740
+ this.skipTrivia();
2741
+ if (this.src[this.pos] === "]") {
2742
+ this.pos++;
2743
+ return result;
2744
+ }
2745
+ while (this.pos < this.src.length) {
2746
+ const value = this.parseValue();
2747
+ if (value === PARSE_FAIL) return PARSE_FAIL;
2748
+ result.push(value);
2749
+ this.skipTrivia();
2750
+ const next = this.src[this.pos];
2751
+ if (next === ",") {
2752
+ this.pos++;
2753
+ this.skipTrivia();
2754
+ if (this.src[this.pos] === "]") {
2755
+ this.pos++;
2756
+ return result;
2757
+ }
2758
+ continue;
2759
+ }
2760
+ if (next === "]") {
2761
+ this.pos++;
2762
+ return result;
2763
+ }
2764
+ return PARSE_FAIL;
2765
+ }
2766
+ return PARSE_FAIL;
2767
+ }
2768
+ parseKey() {
2769
+ const char = this.src[this.pos];
2770
+ if (char === '"' || char === "'" || char === "`") {
2771
+ const value = this.parseString(char);
2772
+ return typeof value === "string" ? value : PARSE_FAIL;
2773
+ }
2774
+ if (char !== void 0 && IDENT_START_RE.test(char)) return this.readIdentifier();
2775
+ return PARSE_FAIL;
2776
+ }
2777
+ parseString(quote) {
2778
+ this.pos++;
2779
+ let out = "";
2780
+ while (this.pos < this.src.length) {
2781
+ const char = this.src[this.pos++];
2782
+ if (char === "\\") {
2783
+ out += this.readEscape();
2784
+ continue;
2785
+ }
2786
+ if (char === quote) break;
2787
+ out += char;
2788
+ }
2789
+ return out;
2790
+ }
2791
+ readEscape() {
2792
+ const char = this.src[this.pos++];
2793
+ switch (char) {
2794
+ case "n":
2795
+ return "\n";
2796
+ case "t":
2797
+ return " ";
2798
+ case "r":
2799
+ return "\r";
2800
+ case "b":
2801
+ return "\b";
2802
+ case "f":
2803
+ return "\f";
2804
+ case "v":
2805
+ return "\v";
2806
+ case "0":
2807
+ return "\0";
2808
+ case "u": {
2809
+ const hex = this.src.slice(this.pos, this.pos + 4);
2810
+ if (/^[0-9a-fA-F]{4}$/.test(hex)) {
2811
+ this.pos += 4;
2812
+ return String.fromCharCode(parseInt(hex, 16));
2813
+ }
2814
+ return "u";
2815
+ }
2816
+ case "x": {
2817
+ const hex = this.src.slice(this.pos, this.pos + 2);
2818
+ if (/^[0-9a-fA-F]{2}$/.test(hex)) {
2819
+ this.pos += 2;
2820
+ return String.fromCharCode(parseInt(hex, 16));
2821
+ }
2822
+ return "x";
2823
+ }
2824
+ default:
2825
+ return char ?? "";
2826
+ }
2827
+ }
2828
+ parseNumber() {
2829
+ const numberRe = /[-+]?(?:0[xX][0-9a-fA-F]+|(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?)/y;
2830
+ numberRe.lastIndex = this.pos;
2831
+ const match = numberRe.exec(this.src);
2832
+ if (!match) return PARSE_FAIL;
2833
+ this.pos += match[0].length;
2834
+ return Number(match[0]);
2835
+ }
2836
+ parseIdentifierValue() {
2837
+ const name = this.readIdentifier();
2838
+ if (name === "true") return true;
2839
+ if (name === "false") return false;
2840
+ if (name === "null") return null;
2841
+ if (name === "undefined") return void 0;
2842
+ return this.resolveIdentifier(name);
2843
+ }
2844
+ resolveIdentifier(name) {
2845
+ return this.scope.has(name) ? this.scope.get(name) : void 0;
2846
+ }
2847
+ readIdentifier() {
2848
+ const start = this.pos;
2849
+ while (this.pos < this.src.length && IDENT_PART_RE.test(this.src[this.pos])) this.pos++;
2850
+ return this.src.slice(start, this.pos);
2851
+ }
2852
+ skipTrivia() {
2853
+ while (this.pos < this.src.length) {
2854
+ const char = this.src[this.pos];
2855
+ if (char === " " || char === " " || char === "\n" || char === "\r") {
2856
+ this.pos++;
2857
+ continue;
2858
+ }
2859
+ if (char === "/" && this.src[this.pos + 1] === "/") {
2860
+ const newline = this.src.indexOf("\n", this.pos + 2);
2861
+ this.pos = newline === -1 ? this.src.length : newline + 1;
2862
+ continue;
2863
+ }
2864
+ if (char === "/" && this.src[this.pos + 1] === "*") {
2865
+ const close = this.src.indexOf("*/", this.pos + 2);
2866
+ this.pos = close === -1 ? this.src.length : close + 2;
2867
+ continue;
2868
+ }
2869
+ break;
2870
+ }
2871
+ }
2872
+ };
2834
2873
  var PROPOSED_PLAN_PATTERN = /<proposed_plan>\s*([\s\S]*?)\s*<\/proposed_plan>/;
2835
2874
  var PLAN_APPROVAL_PREFIX = "PLEASE IMPLEMENT THIS PLAN";
2836
2875
  var SUBAGENT_NOTIFICATION_PATTERN = /<subagent_notification>\s*([\s\S]*?)\s*<\/subagent_notification>/;
2837
2876
  var HEAD_INDEX_VERSION2 = "codex-head-v1";
2838
- var PARSER_VERSION = "codex-parser-v3";
2877
+ var PARSER_VERSION = "codex-parser-v4";
2839
2878
  var DEVELOPER_LIKE_USER_MARKERS = [
2840
2879
  "agents.md instructions for",
2841
2880
  "<instructions>",
@@ -2912,6 +2951,20 @@ function normalizeCustomToolArguments(toolName, input) {
2912
2951
  }
2913
2952
  return input;
2914
2953
  }
2954
+ function flattenOutputText(output) {
2955
+ if (typeof output === "string") return output;
2956
+ if (Array.isArray(output)) {
2957
+ return output.map((item) => {
2958
+ if (typeof item === "string") return item;
2959
+ if (item && typeof item === "object") {
2960
+ const text = item["text"];
2961
+ if (typeof text === "string") return text;
2962
+ }
2963
+ return "";
2964
+ }).join("");
2965
+ }
2966
+ return "";
2967
+ }
2915
2968
  var PATCH_BEGIN_RE = /\*\*\* Begin Patch/;
2916
2969
  var PATCH_END_RE = /\*\*\* End Patch/;
2917
2970
  var PATCH_HEADER_RE = /\*\*\*\s+(Add|Delete|Update|Move)\s+File:\s*(.+)/;
@@ -3015,36 +3068,6 @@ var CodexAgent = class extends FileSystemSessionSource {
3015
3068
  return false;
3016
3069
  }
3017
3070
  }
3018
- scan(options) {
3019
- if (!this.basePath) return [];
3020
- const scanMarker = perf.start("codex:scan");
3021
- const indexMarker = perf.start("loadSessionIndex");
3022
- this.loadSessionIndex();
3023
- perf.end(indexMarker);
3024
- const heads = [];
3025
- const listMarker = perf.start("listRolloutFiles");
3026
- const files = this.listRolloutFiles(options);
3027
- perf.end(listMarker);
3028
- options?.onProgress?.({ total: files.length, processed: 0, sessions: 0 });
3029
- let processed = 0;
3030
- for (const file of files) {
3031
- try {
3032
- const parseMarker = perf.start(`parseSessionHead:${basename5(file)}`);
3033
- const head = getParsedSession(this.parseSessionHeadResult(file, options));
3034
- perf.end(parseMarker);
3035
- if (head) {
3036
- heads.push(head);
3037
- this.sessionMetaMap.set(head.id, this.buildSessionMeta(head, file));
3038
- }
3039
- } catch {
3040
- } finally {
3041
- processed += 1;
3042
- options?.onProgress?.({ total: files.length, processed, sessions: heads.length });
3043
- }
3044
- }
3045
- perf.end(scanMarker);
3046
- return heads;
3047
- }
3048
3071
  listSessionSources(options) {
3049
3072
  if (!this.basePath) return [];
3050
3073
  this.loadSessionIndex();
@@ -3054,9 +3077,9 @@ var CodexAgent = class extends FileSystemSessionSource {
3054
3077
  fingerprint: this.sourceFingerprint(file)
3055
3078
  }));
3056
3079
  }
3057
- scanSessionSource(sourcePath) {
3080
+ scanSessionSource(sourcePath, options) {
3058
3081
  this.loadSessionIndex();
3059
- const head = getParsedSession(this.parseSessionHeadResult(sourcePath));
3082
+ const head = getParsedSession(this.parseSessionHeadResult(sourcePath, options));
3060
3083
  if (head) {
3061
3084
  this.sessionMetaMap.set(head.id, this.buildSessionMeta(head, sourcePath));
3062
3085
  }
@@ -3066,14 +3089,11 @@ var CodexAgent = class extends FileSystemSessionSource {
3066
3089
  const meta = this.sessionMetaMap.get(sessionId);
3067
3090
  if (!meta) throw new Error(`Session not found: ${sessionId}`);
3068
3091
  if (!existsSync7(meta.sourcePath)) throw new Error(`Session file missing: ${meta.sourcePath}`);
3069
- const messages = [];
3070
- const pendingToolCalls = /* @__PURE__ */ new Map();
3092
+ const transcript = new TranscriptBuilder();
3071
3093
  let totalInputTokens = 0;
3072
3094
  let totalOutputTokens = 0;
3073
3095
  let totalCacheReadTokens = 0;
3074
3096
  let totalCost = 0;
3075
- let currentAssistantIndex = null;
3076
- let latestAssistantTextIndex = null;
3077
3097
  let pendingPlan = null;
3078
3098
  let activeModel = meta.model;
3079
3099
  let prevCumulativeTotal = 0;
@@ -3088,24 +3108,7 @@ var CodexAgent = class extends FileSystemSessionSource {
3088
3108
  const payload = record["payload"] ?? {};
3089
3109
  activeModel = extractModelName(payload["model"]) ?? activeModel;
3090
3110
  }
3091
- const result = this.convertRecord(
3092
- record,
3093
- messages,
3094
- pendingToolCalls,
3095
- meta.id,
3096
- currentAssistantIndex,
3097
- latestAssistantTextIndex,
3098
- pendingPlan
3099
- );
3100
- currentAssistantIndex = result.currentAssistantIndex;
3101
- latestAssistantTextIndex = result.latestAssistantTextIndex;
3102
- pendingPlan = result.pendingPlan;
3103
- if (currentAssistantIndex !== null && activeModel) {
3104
- const message = messages[currentAssistantIndex];
3105
- if (message?.role === "assistant" && !message.model) {
3106
- message.model = activeModel;
3107
- }
3108
- }
3111
+ pendingPlan = this.convertRecord(record, transcript, pendingPlan, activeModel);
3109
3112
  if (recordType === "event_msg") {
3110
3113
  const payload = record["payload"] ?? {};
3111
3114
  if (String(payload["type"] ?? "") === "token_count") {
@@ -3141,24 +3144,19 @@ var CodexAgent = class extends FileSystemSessionSource {
3141
3144
  totalInputTokens += totalInput;
3142
3145
  totalOutputTokens += outputTokens + reasoningTokens;
3143
3146
  totalCacheReadTokens += totalCacheRead;
3144
- for (let i = messages.length - 1; i >= 0; i--) {
3145
- const msg = messages[i];
3146
- if (msg.role === "assistant" && !msg.tokens) {
3147
- msg.tokens = {
3148
- input: totalInput,
3149
- output: outputTokens,
3150
- reasoning: reasoningTokens || void 0,
3151
- cache_read: totalCacheRead || void 0
3152
- };
3153
- const cost = estimateTokenCost(msg.model ?? activeModel, msg.tokens);
3154
- if (cost !== null) {
3155
- msg.cost = cost;
3156
- msg.cost_source = "estimated";
3157
- totalCost += cost;
3158
- }
3159
- break;
3160
- }
3161
- }
3147
+ const tokens = {
3148
+ input: totalInput,
3149
+ output: outputTokens,
3150
+ reasoning: reasoningTokens || void 0,
3151
+ cache_read: totalCacheRead || void 0
3152
+ };
3153
+ const cost = estimateTokenCost(activeModel, tokens);
3154
+ transcript.attachUsageToLatestAssistant(tokens, {
3155
+ model: activeModel,
3156
+ cost: cost ?? void 0,
3157
+ costSource: cost === null ? void 0 : "estimated"
3158
+ });
3159
+ totalCost += cost ?? 0;
3162
3160
  }
3163
3161
  }
3164
3162
  }
@@ -3166,10 +3164,15 @@ var CodexAgent = class extends FileSystemSessionSource {
3166
3164
  } catch {
3167
3165
  }
3168
3166
  }
3169
- if (pendingPlan && currentAssistantIndex !== null) {
3170
- messages[currentAssistantIndex].parts.push(pendingPlan);
3171
- }
3172
- const cleanedMessages = cleanParsedMessages(messages);
3167
+ if (pendingPlan) transcript.appendToCurrentAssistant(pendingPlan);
3168
+ const result = transcript.finish({
3169
+ message_count: 0,
3170
+ total_input_tokens: totalInputTokens,
3171
+ total_output_tokens: totalOutputTokens,
3172
+ total_cache_read_tokens: totalCacheReadTokens || void 0,
3173
+ total_cost: totalCost,
3174
+ cost_source: totalCost > 0 ? "estimated" : void 0
3175
+ });
3173
3176
  return {
3174
3177
  id: meta.id,
3175
3178
  title: meta.title,
@@ -3177,15 +3180,8 @@ var CodexAgent = class extends FileSystemSessionSource {
3177
3180
  directory: meta.directory,
3178
3181
  time_created: meta.createdAt,
3179
3182
  time_updated: meta.updatedAt,
3180
- stats: {
3181
- message_count: cleanedMessages.length,
3182
- total_input_tokens: totalInputTokens,
3183
- total_output_tokens: totalOutputTokens,
3184
- total_cache_read_tokens: totalCacheReadTokens || void 0,
3185
- total_cost: totalCost,
3186
- cost_source: totalCost > 0 ? "estimated" : void 0
3187
- },
3188
- messages: cleanedMessages
3183
+ stats: result.stats,
3184
+ messages: result.messages
3189
3185
  };
3190
3186
  }
3191
3187
  // ---- File listing ----
@@ -3504,22 +3500,16 @@ var CodexAgent = class extends FileSystemSessionSource {
3504
3500
  return null;
3505
3501
  }
3506
3502
  // ---- Record conversion ----
3507
- convertRecord(data, messages, pendingToolCalls, sessionId, currentAssistantIndex, latestAssistantTextIndex, pendingPlan) {
3503
+ convertRecord(data, transcript, pendingPlan, activeModel) {
3508
3504
  const recordType = String(data["type"] ?? "");
3509
- if (isInternalEventType2(recordType)) {
3510
- return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan };
3511
- }
3505
+ if (isInternalEventType2(recordType)) return pendingPlan;
3512
3506
  if (recordType === "session_meta" || recordType === "event_msg") {
3513
- return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan };
3514
- }
3515
- if (recordType !== "response_item") {
3516
- return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan };
3507
+ return pendingPlan;
3517
3508
  }
3509
+ if (recordType !== "response_item") return pendingPlan;
3518
3510
  const payload = data["payload"] ?? {};
3519
3511
  const payloadType = String(payload["type"] ?? "");
3520
- if (isInternalEventType2(payloadType)) {
3521
- return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan };
3522
- }
3512
+ if (isInternalEventType2(payloadType)) return pendingPlan;
3523
3513
  const timestampMs = parseTimestampMs2(data) || parseTimestampMs2(payload);
3524
3514
  switch (payloadType) {
3525
3515
  case "message": {
@@ -3527,60 +3517,39 @@ var CodexAgent = class extends FileSystemSessionSource {
3527
3517
  if (role === "assistant") {
3528
3518
  return this.convertAssistantMessage(
3529
3519
  payload,
3530
- messages,
3520
+ transcript,
3531
3521
  timestampMs,
3532
- currentAssistantIndex,
3533
- latestAssistantTextIndex,
3534
- pendingPlan
3522
+ pendingPlan,
3523
+ activeModel
3535
3524
  );
3536
3525
  }
3537
3526
  if (role === "user") {
3538
- return this.convertUserMessage(
3539
- payload,
3540
- messages,
3541
- timestampMs,
3542
- currentAssistantIndex,
3543
- latestAssistantTextIndex,
3544
- pendingPlan
3545
- );
3527
+ return this.convertUserMessage(payload, transcript, timestampMs, pendingPlan);
3546
3528
  }
3547
3529
  break;
3548
3530
  }
3549
3531
  case "reasoning":
3550
- return this.convertReasoning(payload, messages, timestampMs, currentAssistantIndex);
3532
+ this.convertReasoning(payload, transcript, timestampMs, activeModel);
3533
+ return null;
3551
3534
  case "function_call":
3552
- return this.convertFunctionCall(
3553
- payload,
3554
- messages,
3555
- pendingToolCalls,
3556
- timestampMs,
3557
- currentAssistantIndex,
3558
- latestAssistantTextIndex
3559
- );
3535
+ this.convertFunctionCall(payload, transcript, timestampMs, activeModel);
3536
+ return null;
3560
3537
  case "function_call_output":
3561
- this.convertFunctionCallOutput(payload, messages, pendingToolCalls, timestampMs);
3562
- return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan };
3538
+ this.convertToolCallOutput(payload, transcript, timestampMs);
3539
+ return pendingPlan;
3563
3540
  case "custom_tool_call":
3564
- return this.convertCustomToolCall(
3565
- payload,
3566
- messages,
3567
- pendingToolCalls,
3568
- timestampMs,
3569
- currentAssistantIndex,
3570
- latestAssistantTextIndex
3571
- );
3541
+ this.convertCustomToolCall(payload, transcript, timestampMs, activeModel);
3542
+ return null;
3572
3543
  case "custom_tool_call_output":
3573
- this.convertCustomToolCallOutput(payload, messages, pendingToolCalls, timestampMs);
3574
- return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan };
3544
+ this.convertToolCallOutput(payload, transcript, timestampMs);
3545
+ return pendingPlan;
3575
3546
  }
3576
- return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan };
3547
+ return pendingPlan;
3577
3548
  }
3578
3549
  // ---- Assistant message ----
3579
- convertAssistantMessage(payload, messages, timestampMs, currentAssistantIndex, latestAssistantTextIndex, pendingPlan) {
3550
+ convertAssistantMessage(payload, transcript, timestampMs, pendingPlan, activeModel) {
3580
3551
  const content = payload["content"];
3581
- if (!Array.isArray(content)) {
3582
- return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan };
3583
- }
3552
+ if (!Array.isArray(content)) return pendingPlan;
3584
3553
  const textParts = [];
3585
3554
  for (const item of content) {
3586
3555
  if (typeof item !== "object" || item === null) continue;
@@ -3590,9 +3559,7 @@ var CodexAgent = class extends FileSystemSessionSource {
3590
3559
  if (text.trim()) textParts.push(text);
3591
3560
  }
3592
3561
  }
3593
- if (textParts.length === 0) {
3594
- return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan };
3595
- }
3562
+ if (textParts.length === 0) return pendingPlan;
3596
3563
  const fullText = textParts.join("\n");
3597
3564
  const planMatch = fullText.match(PROPOSED_PLAN_PATTERN);
3598
3565
  if (planMatch) {
@@ -3606,61 +3573,34 @@ var CodexAgent = class extends FileSystemSessionSource {
3606
3573
  pendingPlan = planPart;
3607
3574
  }
3608
3575
  const displayText = cleanInternalText(fullText.replace(PROPOSED_PLAN_PATTERN, ""));
3609
- if (!displayText) {
3610
- return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan };
3611
- }
3576
+ if (!displayText) return pendingPlan;
3612
3577
  const textPart = { type: "text", text: displayText, time_created: timestampMs };
3613
- if (currentAssistantIndex !== null) {
3614
- const message = messages[currentAssistantIndex];
3615
- const hasTool = message.parts.some((p) => p.type === "tool");
3616
- if (!hasTool) {
3617
- message.parts.push(textPart);
3618
- latestAssistantTextIndex = currentAssistantIndex;
3619
- return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan };
3620
- }
3621
- }
3622
- messages.push(
3623
- this.buildMessage({
3624
- messageId: "",
3625
- role: "assistant",
3626
- timestampMs,
3627
- parts: [textPart],
3628
- agent: "codex"
3629
- })
3630
- );
3631
- currentAssistantIndex = messages.length - 1;
3632
- latestAssistantTextIndex = currentAssistantIndex;
3633
- return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan };
3578
+ transcript.appendAssistantPart(textPart, {
3579
+ id: "",
3580
+ timestampMs,
3581
+ agent: "codex",
3582
+ model: activeModel
3583
+ });
3584
+ return pendingPlan;
3634
3585
  }
3635
3586
  // ---- User message ----
3636
- convertUserMessage(payload, messages, timestampMs, currentAssistantIndex, latestAssistantTextIndex, pendingPlan) {
3587
+ convertUserMessage(payload, transcript, timestampMs, pendingPlan) {
3637
3588
  const content = payload["content"];
3638
3589
  const text = Array.isArray(content) ? content.map(
3639
3590
  (c) => typeof c === "object" && c !== null ? String(c["text"] ?? "") : String(c ?? "")
3640
3591
  ).join(" ") : String(content ?? "");
3641
3592
  const visibleText = cleanInternalText(text);
3642
- if (!visibleText) {
3643
- return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan };
3644
- }
3645
- if (isDeveloperLikeUserMessage(visibleText)) {
3646
- return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan };
3647
- }
3593
+ if (!visibleText) return pendingPlan;
3594
+ if (isDeveloperLikeUserMessage(visibleText)) return pendingPlan;
3648
3595
  if (visibleText.trimStart().startsWith(PLAN_APPROVAL_PREFIX)) {
3649
- if (pendingPlan && currentAssistantIndex !== null) {
3650
- messages[currentAssistantIndex].parts.push(pendingPlan);
3651
- }
3652
- pendingPlan = null;
3653
- messages.push(
3654
- this.buildMessage({
3655
- messageId: "",
3656
- role: "user",
3657
- timestampMs,
3658
- parts: [{ type: "text", text: visibleText, time_created: timestampMs }]
3659
- })
3660
- );
3661
- currentAssistantIndex = null;
3662
- latestAssistantTextIndex = null;
3663
- return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan };
3596
+ if (pendingPlan) transcript.appendToCurrentAssistant(pendingPlan);
3597
+ transcript.appendMessage({
3598
+ id: "",
3599
+ role: "user",
3600
+ timestampMs,
3601
+ parts: [{ type: "text", text: visibleText, time_created: timestampMs }]
3602
+ });
3603
+ return null;
3664
3604
  }
3665
3605
  const subagentMatch = visibleText.match(SUBAGENT_NOTIFICATION_PATTERN);
3666
3606
  if (subagentMatch) {
@@ -3674,41 +3614,32 @@ var CodexAgent = class extends FileSystemSessionSource {
3674
3614
  text: completedText || `Subagent ${nickname} completed`,
3675
3615
  time_created: timestampMs
3676
3616
  };
3677
- messages.push(
3678
- this.buildMessage({
3679
- messageId: "",
3680
- role: "assistant",
3681
- timestampMs,
3682
- parts: [textPart],
3683
- agent: "codex",
3684
- subagent_id: agentId || void 0,
3685
- nickname: nickname || void 0
3686
- })
3687
- );
3688
- currentAssistantIndex = null;
3689
- latestAssistantTextIndex = null;
3690
- return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan };
3617
+ transcript.appendMessage({
3618
+ id: "",
3619
+ role: "assistant",
3620
+ timestampMs,
3621
+ parts: [textPart],
3622
+ agent: "codex",
3623
+ subagentId: agentId || void 0,
3624
+ nickname: nickname || void 0
3625
+ });
3626
+ transcript.beginTurn();
3627
+ return pendingPlan;
3691
3628
  } catch {
3692
3629
  }
3693
3630
  }
3694
- messages.push(
3695
- this.buildMessage({
3696
- messageId: "",
3697
- role: "user",
3698
- timestampMs,
3699
- parts: [{ type: "text", text: visibleText, time_created: timestampMs }]
3700
- })
3701
- );
3702
- currentAssistantIndex = null;
3703
- latestAssistantTextIndex = null;
3704
- return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan };
3631
+ transcript.appendMessage({
3632
+ id: "",
3633
+ role: "user",
3634
+ timestampMs,
3635
+ parts: [{ type: "text", text: visibleText, time_created: timestampMs }]
3636
+ });
3637
+ return pendingPlan;
3705
3638
  }
3706
3639
  // ---- Reasoning ----
3707
- convertReasoning(payload, messages, timestampMs, currentAssistantIndex) {
3640
+ convertReasoning(payload, transcript, timestampMs, activeModel) {
3708
3641
  const summary = payload["summary"];
3709
- if (!Array.isArray(summary)) {
3710
- return { currentAssistantIndex, latestAssistantTextIndex: null, pendingPlan: null };
3711
- }
3642
+ if (!Array.isArray(summary)) return;
3712
3643
  const texts = [];
3713
3644
  for (const item of summary) {
3714
3645
  if (typeof item === "object" && item !== null) {
@@ -3719,42 +3650,25 @@ var CodexAgent = class extends FileSystemSessionSource {
3719
3650
  }
3720
3651
  }
3721
3652
  }
3722
- if (texts.length === 0) {
3723
- return { currentAssistantIndex, latestAssistantTextIndex: null, pendingPlan: null };
3724
- }
3653
+ if (texts.length === 0) return;
3725
3654
  const reasoningText = texts.join("\n");
3726
3655
  const part = { type: "reasoning", text: reasoningText, time_created: timestampMs };
3727
- if (currentAssistantIndex !== null) {
3728
- const message = messages[currentAssistantIndex];
3729
- const hasText = message.parts.some((p) => p.type === "text");
3730
- const hasTool = message.parts.some((p) => p.type === "tool");
3731
- if (!hasText && !hasTool) {
3732
- message.parts.push(part);
3733
- return { currentAssistantIndex, latestAssistantTextIndex: null, pendingPlan: null };
3734
- }
3735
- }
3736
- messages.push(
3737
- this.buildMessage({
3738
- messageId: "",
3739
- role: "assistant",
3656
+ transcript.appendAssistantPart(
3657
+ part,
3658
+ {
3659
+ id: "",
3740
3660
  timestampMs,
3741
- parts: [part],
3742
- agent: "codex"
3743
- })
3661
+ agent: "codex",
3662
+ model: activeModel
3663
+ },
3664
+ { resetLatestText: true }
3744
3665
  );
3745
- return {
3746
- currentAssistantIndex: messages.length - 1,
3747
- latestAssistantTextIndex: null,
3748
- pendingPlan: null
3749
- };
3750
3666
  }
3751
3667
  // ---- Function call ----
3752
- convertFunctionCall(payload, messages, pendingToolCalls, timestampMs, currentAssistantIndex, latestAssistantTextIndex) {
3668
+ convertFunctionCall(payload, transcript, timestampMs, activeModel) {
3753
3669
  const callId = String(payload["call_id"] ?? "").trim();
3754
3670
  const name = String(payload["name"] ?? "").trim();
3755
- if (!name) {
3756
- return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan: null };
3757
- }
3671
+ if (!name) return;
3758
3672
  const toolIdentity = resolveToolIdentity(name, payload["namespace"]);
3759
3673
  const arguments_ = normalizeToolArguments2(payload["arguments"]);
3760
3674
  const toolPart = {
@@ -3769,58 +3683,35 @@ var CodexAgent = class extends FileSystemSessionSource {
3769
3683
  },
3770
3684
  time_created: timestampMs
3771
3685
  };
3772
- const targetIndex = latestAssistantTextIndex ?? currentAssistantIndex;
3773
- if (targetIndex !== null) {
3774
- const message = messages[targetIndex];
3775
- const partIndex = message.parts.length;
3776
- message.parts.push(toolPart);
3777
- message.mode = "tool";
3778
- if (callId) {
3779
- pendingToolCalls.set(callId, [targetIndex, partIndex]);
3780
- }
3781
- return {
3782
- currentAssistantIndex: targetIndex,
3783
- latestAssistantTextIndex: targetIndex,
3784
- pendingPlan: null
3785
- };
3786
- }
3787
- messages.push(
3788
- this.buildMessage({
3789
- messageId: "",
3790
- role: "assistant",
3791
- timestampMs,
3792
- parts: [toolPart],
3793
- agent: "codex",
3794
- mode: "tool"
3795
- })
3686
+ transcript.appendToolCall(
3687
+ toolPart,
3688
+ { id: "", timestampMs, agent: "codex", model: activeModel },
3689
+ { markModeAsTool: true }
3796
3690
  );
3797
- const newIndex = messages.length - 1;
3798
- if (callId) {
3799
- pendingToolCalls.set(callId, [newIndex, 0]);
3800
- }
3801
- return { currentAssistantIndex: newIndex, latestAssistantTextIndex: null, pendingPlan: null };
3802
3691
  }
3803
3692
  // ---- Function call output ----
3804
- convertFunctionCallOutput(payload, messages, pendingToolCalls, timestampMs) {
3693
+ convertToolCallOutput(payload, transcript, timestampMs) {
3805
3694
  const callId = String(payload["call_id"] ?? "").trim();
3806
3695
  if (!callId) return;
3807
- const location = pendingToolCalls.get(callId);
3808
- if (!location) return;
3809
- const outputText = cleanInternalText(String(payload["output"] ?? ""));
3696
+ const outputText = cleanInternalText(
3697
+ stripExecOutputEnvelope(flattenOutputText(payload["output"]))
3698
+ );
3810
3699
  const outputParts = outputText ? [{ type: "text", text: outputText, time_created: timestampMs }] : [];
3811
- const [msgIndex, partIndex] = location;
3812
- const state = messages[msgIndex].parts[partIndex].state ?? (messages[msgIndex].parts[partIndex].state = {});
3813
3700
  if (outputParts.length > 0) {
3814
- state.output = [...outputParts];
3815
- state.status = "completed";
3701
+ transcript.resolveToolCall(callId, { output: outputParts, status: "completed" });
3816
3702
  }
3817
3703
  }
3818
3704
  // ---- Custom tool call ----
3819
- convertCustomToolCall(payload, messages, pendingToolCalls, timestampMs, currentAssistantIndex, latestAssistantTextIndex) {
3705
+ convertCustomToolCall(payload, transcript, timestampMs, activeModel) {
3820
3706
  const callId = String(payload["call_id"] ?? "").trim();
3821
3707
  const name = String(payload["name"] ?? "").trim();
3822
- if (!name) {
3823
- return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan: null };
3708
+ if (!name) return;
3709
+ if (name === "exec") {
3710
+ const decoded = decodeExecCalls(payload["input"]);
3711
+ if (decoded.length > 0) {
3712
+ this.appendDecodedExecCalls(decoded, callId, transcript, timestampMs, activeModel);
3713
+ return;
3714
+ }
3824
3715
  }
3825
3716
  const toolIdentity = resolveToolIdentity(name, payload["namespace"]);
3826
3717
  const rawInput = payload["input"];
@@ -3837,70 +3728,117 @@ var CodexAgent = class extends FileSystemSessionSource {
3837
3728
  },
3838
3729
  time_created: timestampMs
3839
3730
  };
3840
- const targetIndex = latestAssistantTextIndex ?? currentAssistantIndex;
3841
- if (targetIndex !== null) {
3842
- const message = messages[targetIndex];
3843
- const partIndex = message.parts.length;
3844
- message.parts.push(toolPart);
3845
- message.mode = "tool";
3846
- if (callId) {
3847
- pendingToolCalls.set(callId, [targetIndex, partIndex]);
3848
- }
3849
- return {
3850
- currentAssistantIndex: targetIndex,
3851
- latestAssistantTextIndex: targetIndex,
3852
- pendingPlan: null
3853
- };
3854
- }
3855
- messages.push(
3856
- this.buildMessage({
3857
- messageId: "",
3858
- role: "assistant",
3859
- timestampMs,
3860
- parts: [toolPart],
3861
- agent: "codex",
3862
- mode: "tool"
3863
- })
3731
+ transcript.appendToolCall(
3732
+ toolPart,
3733
+ { id: "", timestampMs, agent: "codex", model: activeModel },
3734
+ { markModeAsTool: true }
3864
3735
  );
3865
- const newIndex = messages.length - 1;
3866
- if (callId) {
3867
- pendingToolCalls.set(callId, [newIndex, 0]);
3736
+ }
3737
+ // ---- Decoded code-mode exec calls ----
3738
+ appendDecodedExecCalls(calls, callId, transcript, timestampMs, activeModel) {
3739
+ const outputIndex = pickExecOutputTarget(calls);
3740
+ calls.forEach((call, index) => {
3741
+ const partCallId = index === outputIndex ? callId : `${callId}#${index}`;
3742
+ this.appendDecodedExecCall(call, partCallId, transcript, timestampMs, activeModel);
3743
+ });
3744
+ }
3745
+ appendDecodedExecCall(call, callId, transcript, timestampMs, activeModel) {
3746
+ const { name, namespace } = splitExecToolName(call.name);
3747
+ const toolIdentity = resolveToolIdentity(name, namespace);
3748
+ const arguments_ = name === "apply_patch" ? parseApplyPatchInput(getExecPatchText(call.args)) : call.args;
3749
+ const toolPart = {
3750
+ type: "tool",
3751
+ tool: toolIdentity.tool,
3752
+ callID: callId,
3753
+ title: `Tool: ${toolIdentity.tool}`,
3754
+ state: {
3755
+ arguments: arguments_,
3756
+ output: null,
3757
+ metadata: toolIdentity.metadata
3758
+ },
3759
+ time_created: timestampMs
3760
+ };
3761
+ transcript.appendToolCall(
3762
+ toolPart,
3763
+ { id: "", timestampMs, agent: "codex", model: activeModel },
3764
+ { markModeAsTool: true }
3765
+ );
3766
+ }
3767
+ };
3768
+ var PerfTracer = class {
3769
+ rootMarkers = [];
3770
+ activeStack = [];
3771
+ enabled = false;
3772
+ enable() {
3773
+ this.enabled = true;
3774
+ }
3775
+ start(name) {
3776
+ const marker = {
3777
+ name,
3778
+ startTime: performance.now(),
3779
+ children: []
3780
+ };
3781
+ if (!this.enabled) return marker;
3782
+ const parent = this.activeStack[this.activeStack.length - 1];
3783
+ if (parent) {
3784
+ marker.parent = parent;
3785
+ parent.children.push(marker);
3786
+ } else {
3787
+ this.rootMarkers.push(marker);
3788
+ }
3789
+ this.activeStack.push(marker);
3790
+ return marker;
3791
+ }
3792
+ end(marker) {
3793
+ if (!this.enabled) return;
3794
+ const target = marker ?? this.activeStack[this.activeStack.length - 1];
3795
+ if (!target) return;
3796
+ target.endTime = performance.now();
3797
+ target.duration = target.endTime - target.startTime;
3798
+ while (this.activeStack.length > 0) {
3799
+ const popped = this.activeStack.pop();
3800
+ if (popped === target) break;
3801
+ }
3802
+ }
3803
+ measure(name, fn) {
3804
+ const marker = this.start(name);
3805
+ try {
3806
+ return fn();
3807
+ } finally {
3808
+ this.end(marker);
3809
+ }
3810
+ }
3811
+ async measureAsync(name, fn) {
3812
+ const marker = this.start(name);
3813
+ try {
3814
+ return await fn();
3815
+ } finally {
3816
+ this.end(marker);
3817
+ }
3818
+ }
3819
+ getReport() {
3820
+ if (!this.enabled) return "Performance tracing disabled";
3821
+ const lines = [];
3822
+ lines.push("\n=== Performance Report ===\n");
3823
+ for (const marker of this.rootMarkers) {
3824
+ this.formatMarker(marker, 0, lines);
3868
3825
  }
3869
- return { currentAssistantIndex: newIndex, latestAssistantTextIndex: null, pendingPlan: null };
3826
+ return lines.join("\n");
3870
3827
  }
3871
- // ---- Custom tool call output ----
3872
- convertCustomToolCallOutput(payload, messages, pendingToolCalls, timestampMs) {
3873
- const callId = String(payload["call_id"] ?? "").trim();
3874
- if (!callId) return;
3875
- const location = pendingToolCalls.get(callId);
3876
- if (!location) return;
3877
- const outputText = cleanInternalText(String(payload["output"] ?? ""));
3878
- const outputParts = outputText ? [{ type: "text", text: outputText, time_created: timestampMs }] : [];
3879
- const [msgIndex, partIndex] = location;
3880
- const state = messages[msgIndex].parts[partIndex].state ?? (messages[msgIndex].parts[partIndex].state = {});
3881
- if (outputParts.length > 0) {
3882
- state.output = [...outputParts];
3883
- state.status = "completed";
3828
+ formatMarker(marker, depth, lines) {
3829
+ const indent = " ".repeat(depth);
3830
+ const duration = marker.duration?.toFixed(2) ?? "?";
3831
+ lines.push(`${indent}${marker.name}: ${duration}ms`);
3832
+ for (const child of marker.children) {
3833
+ this.formatMarker(child, depth + 1, lines);
3884
3834
  }
3885
3835
  }
3886
- // ---- Message builder ----
3887
- buildMessage(opts) {
3888
- return {
3889
- id: opts.messageId,
3890
- role: opts.role,
3891
- agent: opts.agent ?? null,
3892
- time_created: opts.timestampMs,
3893
- mode: opts.mode ?? null,
3894
- model: opts.model ?? null,
3895
- provider: opts.provider ?? null,
3896
- tokens: opts.tokens ? opts.tokens : void 0,
3897
- cost: opts.cost ?? 0,
3898
- parts: opts.parts,
3899
- subagent_id: opts.subagent_id,
3900
- nickname: opts.nickname
3901
- };
3836
+ reset() {
3837
+ this.rootMarkers = [];
3838
+ this.activeStack = [];
3902
3839
  }
3903
3840
  };
3841
+ var perf = new PerfTracer();
3904
3842
  var CURSOR_TOOL_TITLE_MAP = {
3905
3843
  read_file_v2: "read",
3906
3844
  edit_file_v2: "edit",
@@ -4068,12 +4006,12 @@ var CursorAgent = class extends DatabaseSessionSource {
4068
4006
  try {
4069
4007
  const row = wsDb.prepare("SELECT value FROM ItemTable WHERE key = 'composer.composerData'").get();
4070
4008
  if (!row?.value) continue;
4071
- const parsed2 = JSON.parse(row.value);
4009
+ const parsed = JSON.parse(row.value);
4072
4010
  let composers;
4073
- if (parsed2 !== null && typeof parsed2 === "object" && "allComposers" in parsed2 && Array.isArray(parsed2["allComposers"])) {
4074
- composers = parsed2.allComposers;
4075
- } else if (Array.isArray(parsed2)) {
4076
- composers = parsed2;
4011
+ if (parsed !== null && typeof parsed === "object" && "allComposers" in parsed && Array.isArray(parsed["allComposers"])) {
4012
+ composers = parsed.allComposers;
4013
+ } else if (Array.isArray(parsed)) {
4014
+ composers = parsed;
4077
4015
  } else {
4078
4016
  continue;
4079
4017
  }
@@ -4452,10 +4390,10 @@ var CursorAgent = class extends DatabaseSessionSource {
4452
4390
  if (toolData.result !== void 0) {
4453
4391
  if (typeof toolData.result === "string") {
4454
4392
  try {
4455
- const parsed2 = JSON.parse(toolData.result);
4456
- state.output = parsed2;
4457
- if (parsed2.error || parsed2.message || parsed2.stderr) {
4458
- state.error = parsed2.error || parsed2.message || parsed2.stderr;
4393
+ const parsed = JSON.parse(toolData.result);
4394
+ state.output = parsed;
4395
+ if (parsed.error || parsed.message || parsed.stderr) {
4396
+ state.error = parsed.error || parsed.message || parsed.stderr;
4459
4397
  state.status = "error";
4460
4398
  }
4461
4399
  } catch {
@@ -4577,20 +4515,6 @@ function normalizeTextParts(content, timestampMs) {
4577
4515
  const text = cleanInternalText(contentToText(content));
4578
4516
  return text ? [{ type: "text", text, time_created: timestampMs }] : [];
4579
4517
  }
4580
- function buildMessage(params) {
4581
- return {
4582
- id: params.id,
4583
- role: params.role,
4584
- agent: params.agent,
4585
- time_created: params.timestampMs,
4586
- provider: params.provider,
4587
- model: params.model,
4588
- tokens: params.tokens,
4589
- cost: params.cost,
4590
- cost_source: params.costSource,
4591
- parts: params.parts
4592
- };
4593
- }
4594
4518
  function getEntryTimestamp(entry) {
4595
4519
  return parseTimestampMs3(entry["timestamp"]);
4596
4520
  }
@@ -4634,29 +4558,6 @@ var PiAgent = class extends FileSystemSessionSource {
4634
4558
  if (!this.basePath) return false;
4635
4559
  return this.listSessionFiles().length > 0;
4636
4560
  }
4637
- scan(options) {
4638
- if (!this.basePath) return [];
4639
- const scanMarker = perf.start("pi:scan");
4640
- const files = this.listSessionFiles(options);
4641
- options?.onProgress?.({ total: files.length, processed: 0, sessions: 0 });
4642
- const heads = [];
4643
- let processed = 0;
4644
- for (const file of files) {
4645
- try {
4646
- const head = getParsedSession(this.parseSessionHeadResult(file));
4647
- if (head) {
4648
- heads.push(head);
4649
- this.sessionMetaMap.set(head.id, this.buildSessionMeta(head, file));
4650
- }
4651
- } catch {
4652
- } finally {
4653
- processed += 1;
4654
- options?.onProgress?.({ total: files.length, processed, sessions: heads.length });
4655
- }
4656
- }
4657
- perf.end(scanMarker);
4658
- return heads;
4659
- }
4660
4561
  listSessionSources(options) {
4661
4562
  if (!this.basePath) return [];
4662
4563
  return this.listSessionFiles(options).map((file) => ({
@@ -4676,9 +4577,8 @@ var PiAgent = class extends FileSystemSessionSource {
4676
4577
  const meta = this.sessionMetaMap.get(sessionId);
4677
4578
  if (!meta) throw new Error(`Session not found: ${sessionId}`);
4678
4579
  if (!existsSync9(meta.sourcePath)) throw new Error(`Session file missing: ${meta.sourcePath}`);
4679
- const parsed2 = this.parsePiFile(meta.sourcePath);
4680
- const state = this.convertEntries(parsed2.pathEntries);
4681
- const cleanedMessages = cleanParsedMessages(state.messages);
4580
+ const parsed = this.parsePiFile(meta.sourcePath);
4581
+ const state = this.convertEntries(parsed.pathEntries);
4682
4582
  return {
4683
4583
  id: meta.id,
4684
4584
  title: meta.title,
@@ -4687,7 +4587,7 @@ var PiAgent = class extends FileSystemSessionSource {
4687
4587
  time_created: meta.createdAt,
4688
4588
  time_updated: meta.updatedAt,
4689
4589
  stats: {
4690
- message_count: cleanedMessages.length,
4590
+ message_count: state.messages.length,
4691
4591
  total_input_tokens: state.totalInputTokens,
4692
4592
  total_output_tokens: state.totalOutputTokens,
4693
4593
  total_cache_read_tokens: state.totalCacheReadTokens || void 0,
@@ -4695,7 +4595,7 @@ var PiAgent = class extends FileSystemSessionSource {
4695
4595
  total_cost: state.totalCost,
4696
4596
  cost_source: state.totalCost > 0 ? "recorded" : void 0
4697
4597
  },
4698
- messages: cleanedMessages
4598
+ messages: state.messages
4699
4599
  };
4700
4600
  }
4701
4601
  listSessionFiles(options) {
@@ -4739,18 +4639,18 @@ var PiAgent = class extends FileSystemSessionSource {
4739
4639
  return JSON.stringify([HEAD_INDEX_VERSION3, PARSER_VERSION2, stat.mtimeMs, stat.size]);
4740
4640
  }
4741
4641
  parseSessionHeadResult(filePath) {
4742
- const parsed2 = this.parsePiFile(filePath);
4743
- const state = this.convertEntries(parsed2.pathEntries);
4642
+ const parsed = this.parsePiFile(filePath);
4643
+ const state = this.convertEntries(parsed.pathEntries);
4744
4644
  const messageCount = state.messages.length;
4745
4645
  if (messageCount === 0) return filteredSession("no visible messages");
4746
4646
  const modelUsage = Object.keys(state.modelUsage).length > 0 ? state.modelUsage : void 0;
4747
4647
  return parsedSession({
4748
- id: parsed2.sessionId,
4749
- slug: `pi/${parsed2.sessionId}`,
4750
- title: parsed2.title,
4751
- directory: parsed2.directory,
4752
- time_created: parsed2.createdAt,
4753
- time_updated: parsed2.updatedAt,
4648
+ id: parsed.sessionId,
4649
+ slug: `pi/${parsed.sessionId}`,
4650
+ title: parsed.title,
4651
+ directory: parsed.directory,
4652
+ time_created: parsed.createdAt,
4653
+ time_updated: parsed.updatedAt,
4754
4654
  stats: {
4755
4655
  message_count: messageCount,
4756
4656
  total_input_tokens: state.totalInputTokens,
@@ -4812,74 +4712,52 @@ var PiAgent = class extends FileSystemSessionSource {
4812
4712
  return null;
4813
4713
  }
4814
4714
  convertEntries(entries) {
4815
- const messages = [];
4816
- const pendingToolCalls = /* @__PURE__ */ new Map();
4715
+ const builder = new TranscriptBuilder({ messageDefaults: "sparse" });
4817
4716
  const modelUsage = {};
4818
- let totalInputTokens = 0;
4819
- let totalOutputTokens = 0;
4820
- let totalCacheReadTokens = 0;
4821
- let totalCacheCreateTokens = 0;
4822
- let totalCost = 0;
4823
4717
  for (const entry of entries) {
4824
4718
  const timestampMs = getEntryTimestamp(entry);
4825
4719
  const type = String(entry["type"] ?? "");
4826
4720
  if (type === "message") {
4827
4721
  const message = entry["message"];
4828
4722
  if (!isObject(message)) continue;
4829
- const result = this.convertAgentMessage(
4830
- entry,
4831
- message,
4832
- timestampMs,
4833
- pendingToolCalls,
4834
- messages.length,
4835
- messages
4836
- );
4837
- if (!result) continue;
4838
- if (result.message) messages.push(result.message);
4839
- totalInputTokens += result.inputTokens;
4840
- totalOutputTokens += result.outputTokens;
4841
- totalCacheReadTokens += result.cacheReadTokens;
4842
- totalCacheCreateTokens += result.cacheCreateTokens;
4843
- totalCost += result.cost;
4844
- if (result.model && result.totalTokens > 0) {
4845
- modelUsage[result.model] = (modelUsage[result.model] ?? 0) + result.totalTokens;
4723
+ const result2 = this.convertAgentMessage(entry, message, timestampMs, builder);
4724
+ if (!result2) continue;
4725
+ if (result2.message) builder.appendMessage(result2.message);
4726
+ if (result2.model && result2.totalTokens > 0) {
4727
+ modelUsage[result2.model] = (modelUsage[result2.model] ?? 0) + result2.totalTokens;
4846
4728
  }
4847
4729
  continue;
4848
4730
  }
4849
4731
  const summary = this.convertSummaryEntry(entry, timestampMs);
4850
- if (summary) messages.push(summary);
4732
+ if (summary) builder.appendMessage(summary);
4851
4733
  }
4734
+ const result = builder.finish();
4852
4735
  return {
4853
- messages,
4854
- totalInputTokens,
4855
- totalOutputTokens,
4856
- totalCacheReadTokens,
4857
- totalCacheCreateTokens,
4858
- totalCost,
4736
+ messages: result.messages,
4737
+ totalInputTokens: result.stats.total_input_tokens,
4738
+ totalOutputTokens: result.stats.total_output_tokens,
4739
+ totalCacheReadTokens: result.stats.total_cache_read_tokens ?? 0,
4740
+ totalCacheCreateTokens: result.stats.total_cache_create_tokens ?? 0,
4741
+ totalCost: result.stats.total_cost,
4859
4742
  modelUsage
4860
4743
  };
4861
4744
  }
4862
- convertAgentMessage(entry, message, timestampMs, pendingToolCalls, nextMessageIndex, messages) {
4745
+ convertAgentMessage(entry, message, timestampMs, builder) {
4863
4746
  const id = String(entry["id"] ?? "");
4864
4747
  const role = String(message["role"] ?? "");
4865
4748
  if (role === "user") {
4866
4749
  const parts = normalizeTextParts(message["content"], timestampMs);
4867
4750
  if (parts.length === 0) return null;
4868
- return this.emptyUsageResult(buildMessage({ id, role: "user", timestampMs, parts }));
4751
+ return this.emptyUsageResult({ id, role: "user", timestampMs, parts });
4869
4752
  }
4870
4753
  if (role === "assistant") {
4871
- const parts = this.normalizeAssistantParts(
4872
- message["content"],
4873
- timestampMs,
4874
- pendingToolCalls,
4875
- nextMessageIndex
4876
- );
4754
+ const parts = this.normalizeAssistantParts(message["content"], timestampMs);
4877
4755
  if (parts.length === 0) return null;
4878
4756
  const usage = this.normalizeUsage(message["usage"]);
4879
4757
  const model = typeof message["model"] === "string" ? message["model"].trim() : null;
4880
4758
  const cost = usage.cost ?? estimateTokenCost(model, usage.tokens) ?? 0;
4881
4759
  return {
4882
- message: buildMessage({
4760
+ message: {
4883
4761
  id,
4884
4762
  role: "assistant",
4885
4763
  agent: "pi",
@@ -4890,18 +4768,13 @@ var PiAgent = class extends FileSystemSessionSource {
4890
4768
  tokens: usage.tokens,
4891
4769
  cost: cost || void 0,
4892
4770
  costSource: cost > 0 ? "recorded" : void 0
4893
- }),
4894
- inputTokens: usage.inputTokens,
4895
- outputTokens: usage.outputTokens,
4896
- cacheReadTokens: usage.cacheReadTokens,
4897
- cacheCreateTokens: usage.cacheCreateTokens,
4771
+ },
4898
4772
  totalTokens: usage.totalTokens,
4899
- cost,
4900
4773
  model
4901
4774
  };
4902
4775
  }
4903
4776
  if (role === "toolResult") {
4904
- this.attachToolResult(message, timestampMs, pendingToolCalls, messages);
4777
+ this.attachToolResult(message, timestampMs, builder);
4905
4778
  return this.emptyUsageResult();
4906
4779
  }
4907
4780
  if (role === "bashExecution") {
@@ -4910,24 +4783,22 @@ var PiAgent = class extends FileSystemSessionSource {
4910
4783
  if (role === "custom" && message["display"] === true) {
4911
4784
  const parts = normalizeTextParts(message["content"], timestampMs);
4912
4785
  if (parts.length === 0) return null;
4913
- return this.emptyUsageResult(buildMessage({ id, role: "user", timestampMs, parts }));
4786
+ return this.emptyUsageResult({ id, role: "user", timestampMs, parts });
4914
4787
  }
4915
4788
  if (role === "branchSummary" || role === "compactionSummary") {
4916
4789
  const summary = String(message["summary"] ?? "").trim();
4917
4790
  if (!summary) return null;
4918
- return this.emptyUsageResult(
4919
- buildMessage({
4920
- id,
4921
- role: "assistant",
4922
- agent: "pi",
4923
- timestampMs,
4924
- parts: [{ type: "text", text: summary, time_created: timestampMs }]
4925
- })
4926
- );
4791
+ return this.emptyUsageResult({
4792
+ id,
4793
+ role: "assistant",
4794
+ agent: "pi",
4795
+ timestampMs,
4796
+ parts: [{ type: "text", text: summary, time_created: timestampMs }]
4797
+ });
4927
4798
  }
4928
4799
  return null;
4929
4800
  }
4930
- normalizeAssistantParts(content, timestampMs, pendingToolCalls, messageIndex) {
4801
+ normalizeAssistantParts(content, timestampMs) {
4931
4802
  if (!Array.isArray(content)) return [];
4932
4803
  const parts = [];
4933
4804
  for (const item of content) {
@@ -4958,29 +4829,26 @@ var PiAgent = class extends FileSystemSessionSource {
4958
4829
  }
4959
4830
  };
4960
4831
  parts.push(toolPart);
4961
- if (callId) pendingToolCalls.set(callId, [messageIndex, parts.length - 1]);
4962
4832
  }
4963
4833
  }
4964
4834
  return parts;
4965
4835
  }
4966
- attachToolResult(message, timestampMs, pendingToolCalls, messages) {
4836
+ attachToolResult(message, timestampMs, builder) {
4967
4837
  const callId = String(message["toolCallId"] ?? "").trim();
4968
4838
  const output = normalizeTextParts(message["content"], timestampMs);
4969
- const location = callId ? pendingToolCalls.get(callId) : void 0;
4970
- if (!location) return;
4971
- const [messageIndex, partIndex] = location;
4972
- const target = messages[messageIndex]?.parts[partIndex];
4973
- if (!target?.state) return;
4974
- target.state.output = output;
4975
- target.state.status = message["isError"] === true ? "error" : "completed";
4976
- target.state.metadata = message["details"];
4977
- pendingToolCalls.delete(callId);
4839
+ if (!callId) return;
4840
+ builder.resolveToolCall(callId, {
4841
+ output,
4842
+ status: message["isError"] === true ? "error" : "completed",
4843
+ metadata: message["details"],
4844
+ consume: true
4845
+ });
4978
4846
  }
4979
4847
  convertBashExecution(id, message, timestampMs) {
4980
4848
  const command = String(message["command"] ?? "");
4981
4849
  const output = String(message["output"] ?? "");
4982
4850
  const isError = Number(message["exitCode"] ?? 0) !== 0 || message["cancelled"] === true;
4983
- return buildMessage({
4851
+ return {
4984
4852
  id,
4985
4853
  role: "tool",
4986
4854
  timestampMs,
@@ -5003,7 +4871,7 @@ var PiAgent = class extends FileSystemSessionSource {
5003
4871
  }
5004
4872
  }
5005
4873
  ]
5006
- });
4874
+ };
5007
4875
  }
5008
4876
  convertSummaryEntry(entry, timestampMs) {
5009
4877
  const type = entry["type"];
@@ -5014,13 +4882,13 @@ var PiAgent = class extends FileSystemSessionSource {
5014
4882
  const rawText = type === "custom_message" ? contentToText(entry["content"]) : String(entry["summary"] ?? "");
5015
4883
  const text = cleanInternalText(rawText);
5016
4884
  if (!text) return null;
5017
- return buildMessage({
4885
+ return {
5018
4886
  id: String(entry["id"] ?? ""),
5019
4887
  role: type === "custom_message" ? "user" : "assistant",
5020
4888
  agent: type === "custom_message" ? void 0 : "pi",
5021
4889
  timestampMs,
5022
4890
  parts: [{ type: "text", text, time_created: timestampMs }]
5023
- });
4891
+ };
5024
4892
  }
5025
4893
  normalizeUsage(raw) {
5026
4894
  const usage = isObject(raw) ? raw : {};
@@ -5050,12 +4918,7 @@ var PiAgent = class extends FileSystemSessionSource {
5050
4918
  emptyUsageResult(message) {
5051
4919
  return {
5052
4920
  message,
5053
- inputTokens: 0,
5054
- outputTokens: 0,
5055
- cacheReadTokens: 0,
5056
- cacheCreateTokens: 0,
5057
4921
  totalTokens: 0,
5058
- cost: 0,
5059
4922
  model: null
5060
4923
  };
5061
4924
  }
@@ -5076,44 +4939,30 @@ var ZCodeAgent = class extends OpenCodeSqliteAgent {
5076
4939
  }
5077
4940
  };
5078
4941
  registerAgent({
5079
- name: "claudecode",
5080
- displayName: "Claude Code",
5081
4942
  icon: "/icon/agent/claudecode.svg",
5082
4943
  create: () => new ClaudeCodeAgent()
5083
4944
  });
5084
4945
  registerAgent({
5085
- name: "opencode",
5086
- displayName: "OpenCode",
5087
4946
  icon: "/icon/agent/opencode.svg",
5088
4947
  create: () => new OpenCodeAgent()
5089
4948
  });
5090
4949
  registerAgent({
5091
- name: "zcode",
5092
- displayName: "ZCode",
5093
4950
  icon: "/icon/agent/zcode.svg",
5094
4951
  create: () => new ZCodeAgent()
5095
4952
  });
5096
4953
  registerAgent({
5097
- name: "kimi",
5098
- displayName: "Kimi-Cli",
5099
4954
  icon: "/icon/agent/kimi.svg",
5100
4955
  create: () => new KimiAgent()
5101
4956
  });
5102
4957
  registerAgent({
5103
- name: "codex",
5104
- displayName: "Codex",
5105
4958
  icon: "/icon/agent/codex.svg",
5106
4959
  create: () => new CodexAgent()
5107
4960
  });
5108
4961
  registerAgent({
5109
- name: "pi",
5110
- displayName: "Pi",
5111
4962
  icon: "/icon/agent/pi.svg",
5112
4963
  create: () => new PiAgent()
5113
4964
  });
5114
4965
  registerAgent({
5115
- name: "cursor",
5116
- displayName: "Cursor",
5117
4966
  icon: "/icon/agent/cursor.svg",
5118
4967
  create: () => new CursorAgent()
5119
4968
  });
@@ -6103,7 +5952,7 @@ function buildSessionContentFromMessages(title, messages) {
6103
5952
  }
6104
5953
  return chunks.join("\n");
6105
5954
  }
6106
- var CACHE_SCHEMA_VERSION = 13;
5955
+ var CACHE_SCHEMA_VERSION = 14;
6107
5956
  function withCacheDb(fn) {
6108
5957
  const cachePath = getCachePath2();
6109
5958
  const db = openDb(cachePath);
@@ -6157,6 +6006,12 @@ function createCacheTables(db) {
6157
6006
  index_version TEXT NOT NULL,
6158
6007
  last_sync_at INTEGER NOT NULL
6159
6008
  );
6009
+
6010
+ CREATE TABLE IF NOT EXISTS pending_reindex (
6011
+ agent_name TEXT NOT NULL,
6012
+ session_id TEXT NOT NULL,
6013
+ PRIMARY KEY (agent_name, session_id)
6014
+ );
6160
6015
  `);
6161
6016
  }
6162
6017
  function createSessionTables(db) {
@@ -6376,6 +6231,7 @@ function createSearchTables(db) {
6376
6231
  activity_time INTEGER NOT NULL,
6377
6232
  content_text TEXT NOT NULL,
6378
6233
  content_hash TEXT NOT NULL,
6234
+ indexed_message_count INTEGER NOT NULL,
6379
6235
  indexed_at INTEGER NOT NULL,
6380
6236
  UNIQUE(agent_name, session_id)
6381
6237
  );
@@ -6409,6 +6265,24 @@ function createSearchTriggers(db) {
6409
6265
  END;
6410
6266
  `);
6411
6267
  }
6268
+ function addIndexedMessageCount(db) {
6269
+ if (!tableExists(db, "session_documents")) return;
6270
+ if (!columnExists(db, "session_documents", "indexed_message_count")) {
6271
+ db.exec(
6272
+ "ALTER TABLE session_documents ADD COLUMN indexed_message_count INTEGER NOT NULL DEFAULT 0"
6273
+ );
6274
+ }
6275
+ if (!tableExists(db, "messages")) return;
6276
+ db.exec(`
6277
+ UPDATE session_documents
6278
+ SET indexed_message_count = (
6279
+ SELECT COUNT(*)
6280
+ FROM messages
6281
+ WHERE messages.agent_name = session_documents.agent_name
6282
+ AND messages.session_id = session_documents.session_id
6283
+ )
6284
+ `);
6285
+ }
6412
6286
  function dropSearchTriggers(db) {
6413
6287
  db.exec(`
6414
6288
  DROP TRIGGER IF EXISTS session_documents_ai;
@@ -6514,6 +6388,9 @@ function readLegacyCacheVersion(db) {
6514
6388
  return Number(versionRow?.value ?? 0);
6515
6389
  }
6516
6390
  function inferCacheSchemaVersion(db) {
6391
+ if (columnExists(db, "session_documents", "indexed_message_count")) {
6392
+ return 14;
6393
+ }
6517
6394
  if (tableExists(db, "message_tools")) {
6518
6395
  return 11;
6519
6396
  }
@@ -6832,6 +6709,20 @@ function invalidateSearchContentHashes(db) {
6832
6709
  db.exec("UPDATE session_documents SET content_hash = ''");
6833
6710
  }
6834
6711
  }
6712
+ var CODEX_EXEC_DECODE_MIGRATION_KEY = "codex_exec_decode_migrated_v3";
6713
+ function migrateCodexExecDecode(db) {
6714
+ if (!tableExists(db, "cache_meta")) return;
6715
+ const done = db.prepare("SELECT value FROM cache_meta WHERE key = ?").get(CODEX_EXEC_DECODE_MIGRATION_KEY);
6716
+ if (done) return;
6717
+ if (tableExists(db, "sessions") && tableExists(db, "pending_reindex")) {
6718
+ db.exec(
6719
+ "INSERT OR IGNORE INTO pending_reindex(agent_name, session_id) SELECT agent_name, session_id FROM sessions WHERE agent_name = 'codex'"
6720
+ );
6721
+ }
6722
+ db.prepare(
6723
+ "INSERT INTO cache_meta(key, value) VALUES (?, '1') ON CONFLICT(key) DO UPDATE SET value = '1'"
6724
+ ).run(CODEX_EXEC_DECODE_MIGRATION_KEY);
6725
+ }
6835
6726
  function rebuildSearchIndex(db) {
6836
6727
  if (!tableExists(db, "session_documents_fts")) {
6837
6728
  return;
@@ -6889,6 +6780,7 @@ function ensureSchema(db, dbPath) {
6889
6780
  if (currentVersion === 0 && !hasAnyCacheSchema(db)) {
6890
6781
  createLatestCacheSchema(db);
6891
6782
  setCacheSchemaVersion(db);
6783
+ migrateCodexExecDecode(db);
6892
6784
  return;
6893
6785
  }
6894
6786
  runSchemaMigrations(db, {
@@ -6948,13 +6840,15 @@ function ensureSchema(db, dbPath) {
6948
6840
  refreshProjectIdentities(db2);
6949
6841
  }
6950
6842
  },
6951
- { version: 13, migrate: createCacheTables }
6843
+ { version: 13, migrate: createCacheTables },
6844
+ { version: 14, migrate: addIndexedMessageCount }
6952
6845
  ]
6953
6846
  });
6954
6847
  createLatestCacheSchema(db);
6955
6848
  if (getUserVersion(db) <= CACHE_SCHEMA_VERSION) {
6956
6849
  setCacheSchemaVersion(db);
6957
6850
  }
6851
+ migrateCodexExecDecode(db);
6958
6852
  }
6959
6853
  function escapeFtsTerm(value) {
6960
6854
  return value.replaceAll('"', '""');
@@ -7080,6 +6974,10 @@ function toFtsQuery(input) {
7080
6974
  (token, index, values) => token !== "OR" || index > 0 && index < values.length - 1 && values[index - 1] !== "OR" && values[index + 1] !== "OR"
7081
6975
  ).join(" ");
7082
6976
  }
6977
+ function readPendingReindexIds(db, agentName) {
6978
+ const rows = db.prepare("SELECT session_id FROM pending_reindex WHERE agent_name = ?").all(agentName);
6979
+ return new Set(rows.map((row) => String(row.session_id)));
6980
+ }
7083
6981
  var SEARCH_INDEX_STATE_BATCH_SIZE = 900;
7084
6982
  function shouldBulkSyncSearchIndex(options, changedCount) {
7085
6983
  if (options.isBulk != null) {
@@ -7105,14 +7003,18 @@ function sessionContentHash(session) {
7105
7003
  session.stats.total_tokens ?? 0
7106
7004
  ]);
7107
7005
  }
7108
- function searchIndexStateFromRows(indexedRows, messageCountRows) {
7006
+ function searchIndexStateFromRows(indexedRows, messageCountRows, pendingReindexSessionIds = /* @__PURE__ */ new Set()) {
7109
7007
  return {
7110
7008
  contentHashBySessionId: new Map(
7111
7009
  indexedRows.map((row) => [String(row.session_id), String(row.content_hash ?? "")])
7112
7010
  ),
7011
+ indexedMessageCountBySessionId: new Map(
7012
+ indexedRows.map((row) => [String(row.session_id), Number(row.indexed_message_count ?? 0)])
7013
+ ),
7113
7014
  messageCountBySessionId: new Map(
7114
7015
  messageCountRows.map((row) => [String(row.session_id), Number(row.value ?? 0)])
7115
- )
7016
+ ),
7017
+ pendingReindexSessionIds
7116
7018
  };
7117
7019
  }
7118
7020
  function readSearchIndexState(db, agentName, sessionIds) {
@@ -7127,18 +7029,26 @@ function readSearchIndexState(db, agentName, sessionIds) {
7127
7029
  SELECT
7128
7030
  requested.session_id,
7129
7031
  documents.content_hash,
7032
+ documents.indexed_message_count,
7130
7033
  COUNT(messages.message_index) AS value
7131
7034
  FROM requested_session_ids AS requested
7132
7035
  LEFT JOIN session_documents AS documents
7133
7036
  ON documents.agent_name = ? AND documents.session_id = requested.session_id
7134
7037
  LEFT JOIN messages
7135
7038
  ON messages.agent_name = ? AND messages.session_id = requested.session_id
7136
- GROUP BY requested.session_id, documents.content_hash
7039
+ GROUP BY
7040
+ requested.session_id,
7041
+ documents.content_hash,
7042
+ documents.indexed_message_count
7137
7043
  `
7138
7044
  ).all(...batch, agentName, agentName);
7139
7045
  rows.push(...batchRows);
7140
7046
  }
7141
- return searchIndexStateFromRows(rows, rows);
7047
+ return searchIndexStateFromRows(rows, rows, readPendingReindexIds(db, agentName));
7048
+ }
7049
+ function searchIndexEntryNeedsUpdate(state, session) {
7050
+ const sessionId = session.id;
7051
+ return state.pendingReindexSessionIds.has(sessionId) || state.contentHashBySessionId.get(sessionId) !== sessionContentHash(session) || state.indexedMessageCountBySessionId.get(sessionId) !== (state.messageCountBySessionId.get(sessionId) ?? 0);
7142
7052
  }
7143
7053
  function loadSearchIndexEntry(agentName, change, loadSessionData) {
7144
7054
  try {
@@ -7163,6 +7073,12 @@ function loadSearchIndexEntry(agentName, change, loadSessionData) {
7163
7073
  return null;
7164
7074
  }
7165
7075
  }
7076
+ function* loadSearchIndexEntries(agentName, changes, loadSessionData) {
7077
+ for (const change of changes) {
7078
+ const entry = loadSearchIndexEntry(agentName, change, loadSessionData);
7079
+ if (entry) yield entry;
7080
+ }
7081
+ }
7166
7082
  function writeSearchIndexRows(db, agentName, removedSessionIds, entries) {
7167
7083
  const deleteRow = db.prepare(
7168
7084
  "DELETE FROM session_documents WHERE agent_name = ? AND session_id = ?"
@@ -7234,8 +7150,9 @@ function writeSearchIndexRows(db, agentName, removedSessionIds, entries) {
7234
7150
  activity_time,
7235
7151
  content_text,
7236
7152
  content_hash,
7153
+ indexed_message_count,
7237
7154
  indexed_at
7238
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
7155
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
7239
7156
  ON CONFLICT(agent_name, session_id) DO UPDATE SET
7240
7157
  slug = excluded.slug,
7241
7158
  title = excluded.title,
@@ -7248,19 +7165,26 @@ function writeSearchIndexRows(db, agentName, removedSessionIds, entries) {
7248
7165
  activity_time = excluded.activity_time,
7249
7166
  content_text = excluded.content_text,
7250
7167
  content_hash = excluded.content_hash,
7168
+ indexed_message_count = excluded.indexed_message_count,
7251
7169
  indexed_at = excluded.indexed_at
7252
7170
  `);
7171
+ const clearPendingReindex = db.prepare(
7172
+ "DELETE FROM pending_reindex WHERE agent_name = ? AND session_id = ?"
7173
+ );
7253
7174
  for (const sessionId of new Set(removedSessionIds)) {
7254
7175
  deleteRow.run(agentName, sessionId);
7255
7176
  deleteFileActivity.run(agentName, sessionId);
7256
7177
  deleteMessageTools.run(agentName, sessionId, 0);
7257
7178
  deleteMessages.run(agentName, sessionId, 0);
7179
+ clearPendingReindex.run(agentName, sessionId);
7258
7180
  }
7181
+ let indexed = 0;
7259
7182
  for (const entry of entries) {
7260
7183
  const activityTime = entry.session.time_updated ?? entry.session.time_created;
7261
7184
  upsertSessionRow(upsertIndexedSession, agentName, entry.session, null, entry.sortIndex, null);
7262
7185
  deleteFileActivity.run(agentName, entry.session.id);
7263
7186
  deleteMessageTools.run(agentName, entry.session.id, 0);
7187
+ clearPendingReindex.run(agentName, entry.session.id);
7264
7188
  writeFileActivityRows(insertFileActivity, entry.fileActivity);
7265
7189
  for (const message of entry.messages) {
7266
7190
  upsertMessage.run(
@@ -7303,39 +7227,51 @@ function writeSearchIndexRows(db, agentName, removedSessionIds, entries) {
7303
7227
  activityTime,
7304
7228
  entry.contentText,
7305
7229
  entry.contentHash,
7230
+ entry.messages.length,
7306
7231
  Date.now()
7307
7232
  );
7233
+ indexed += 1;
7308
7234
  }
7235
+ return indexed;
7309
7236
  }
7310
7237
  function syncSessionSearchIndex(agentName, sessions, loadSessionData, options = {}) {
7311
7238
  return withCacheDb((db) => {
7312
7239
  ensureFtsConsistency(db);
7313
7240
  const startedAt = performance.now();
7314
7241
  const existingRows = db.prepare(
7315
- "SELECT session_id, content_hash FROM session_documents WHERE agent_name = ? ORDER BY id"
7242
+ "SELECT session_id, content_hash, indexed_message_count FROM session_documents WHERE agent_name = ? ORDER BY id"
7316
7243
  ).all(agentName);
7317
7244
  const sessionSortIndexMap = new Map(sessions.map((session, index) => [session.id, index]));
7318
7245
  const messageCountRows = db.prepare(
7319
7246
  "SELECT session_id, COUNT(*) AS value FROM messages WHERE agent_name = ? GROUP BY session_id"
7320
7247
  ).all(agentName);
7321
- const searchIndexState = searchIndexStateFromRows(existingRows, messageCountRows);
7248
+ const searchIndexState = searchIndexStateFromRows(
7249
+ existingRows,
7250
+ messageCountRows,
7251
+ readPendingReindexIds(db, agentName)
7252
+ );
7322
7253
  const sessionMap = new Map(sessions.map((session) => [session.id, session]));
7323
7254
  const toDelete = existingRows.map((row) => String(row.session_id)).filter((sessionId) => !sessionMap.has(sessionId));
7324
7255
  const toUpsert = sessions.filter(
7325
- (session) => searchIndexState.contentHashBySessionId.get(session.id) !== sessionContentHash(session) || searchIndexState.messageCountBySessionId.get(session.id) !== session.stats.message_count
7256
+ (session) => searchIndexEntryNeedsUpdate(searchIndexState, session)
7326
7257
  );
7327
7258
  const changedCount = toDelete.length + toUpsert.length;
7328
7259
  const isBulk = shouldBulkSyncSearchIndex(options, changedCount);
7329
- const loaded = toUpsert.map(
7330
- (session) => loadSearchIndexEntry(
7260
+ const changes = toUpsert.map((session) => ({
7261
+ session,
7262
+ sortIndex: sessionSortIndexMap.get(session.id) ?? 0
7263
+ }));
7264
+ let indexed = 0;
7265
+ const writeRows = () => {
7266
+ indexed = writeSearchIndexRows(
7267
+ db,
7331
7268
  agentName,
7332
- { session, sortIndex: sessionSortIndexMap.get(session.id) ?? 0 },
7333
- loadSessionData
7334
- )
7335
- ).filter((entry) => entry !== null);
7336
- const writeRows = () => writeSearchIndexRows(db, agentName, toDelete, loaded);
7269
+ toDelete,
7270
+ loadSearchIndexEntries(agentName, changes, loadSessionData)
7271
+ );
7272
+ };
7337
7273
  let rebuildDurationMs;
7338
- const needsRebuild = isBulk && (toDelete.length > 0 || loaded.length > 0);
7274
+ const needsRebuild = isBulk && changedCount > 0;
7339
7275
  if (needsRebuild) {
7340
7276
  db.transaction(() => {
7341
7277
  dropSearchTriggers(db);
@@ -7357,8 +7293,8 @@ function syncSessionSearchIndex(agentName, sessions, loadSessionData, options =
7357
7293
  sessions: sessions.length,
7358
7294
  changed: toUpsert.length,
7359
7295
  deleted: toDelete.length,
7360
- indexed: loaded.length,
7361
- skipped: toUpsert.length - loaded.length,
7296
+ indexed,
7297
+ skipped: toUpsert.length - indexed,
7362
7298
  durationMs: performance.now() - startedAt,
7363
7299
  rebuildDurationMs
7364
7300
  };
@@ -7386,15 +7322,22 @@ function syncSessionSearchIndexChanges(agentName, changes, removedSessionIds, lo
7386
7322
  changes.map(({ session }) => session.id)
7387
7323
  );
7388
7324
  const toUpsert = changes.filter(
7389
- ({ session }) => (searchIndexState.contentHashBySessionId.get(session.id) ?? "") !== sessionContentHash(session) || (searchIndexState.messageCountBySessionId.get(session.id) ?? 0) !== session.stats.message_count
7325
+ ({ session }) => searchIndexEntryNeedsUpdate(searchIndexState, session)
7390
7326
  );
7391
7327
  const uniqueRemovedSessionIds = Array.from(new Set(removedSessionIds));
7392
7328
  const changedCount = uniqueRemovedSessionIds.length + toUpsert.length;
7393
7329
  const isBulk = shouldBulkSyncSearchIndex(options, changedCount);
7394
- const loaded = toUpsert.map((change) => loadSearchIndexEntry(agentName, change, loadSessionData)).filter((entry) => entry !== null);
7395
- const writeRows = () => writeSearchIndexRows(db, agentName, uniqueRemovedSessionIds, loaded);
7330
+ let indexed = 0;
7331
+ const writeRows = () => {
7332
+ indexed = writeSearchIndexRows(
7333
+ db,
7334
+ agentName,
7335
+ uniqueRemovedSessionIds,
7336
+ loadSearchIndexEntries(agentName, toUpsert, loadSessionData)
7337
+ );
7338
+ };
7396
7339
  let rebuildDurationMs;
7397
- const needsRebuild = isBulk && (uniqueRemovedSessionIds.length > 0 || loaded.length > 0);
7340
+ const needsRebuild = isBulk && changedCount > 0;
7398
7341
  if (needsRebuild) {
7399
7342
  db.transaction(() => {
7400
7343
  dropSearchTriggers(db);
@@ -7416,8 +7359,8 @@ function syncSessionSearchIndexChanges(agentName, changes, removedSessionIds, lo
7416
7359
  sessions: changes.length,
7417
7360
  changed: toUpsert.length,
7418
7361
  deleted: uniqueRemovedSessionIds.length,
7419
- indexed: loaded.length,
7420
- skipped: toUpsert.length - loaded.length,
7362
+ indexed,
7363
+ skipped: toUpsert.length - indexed,
7421
7364
  durationMs: performance.now() - startedAt,
7422
7365
  rebuildDurationMs
7423
7366
  };
@@ -7431,26 +7374,26 @@ function mergeSearchLists(left, right) {
7431
7374
  return values.length > 0 ? [...new Set(values)] : void 0;
7432
7375
  }
7433
7376
  function mergeSearchQueryOptions(query, options) {
7434
- const parsed2 = parseSearchQuery(query);
7377
+ const parsed = parseSearchQuery(query);
7435
7378
  return {
7436
- text: parsed2.text || (parsed2.hasQualifiers ? "" : query.trim()),
7379
+ text: parsed.text || (parsed.hasQualifiers ? "" : query.trim()),
7437
7380
  options: {
7438
7381
  ...options,
7439
- agent: options.agent ?? parsed2.filters.agent,
7440
- project: options.project ?? parsed2.filters.project,
7441
- projectKind: options.projectKind ?? parsed2.filters.projectKind,
7442
- projectKey: options.projectKey ?? parsed2.filters.projectKey,
7443
- cwd: options.cwd ?? parsed2.filters.cwd,
7444
- tags: mergeSearchLists(options.tags, parsed2.filters.tags),
7445
- tools: mergeSearchLists(options.tools, parsed2.filters.tools),
7446
- file: options.file ?? parsed2.filters.file,
7447
- fileKind: options.fileKind ?? parsed2.filters.fileKind,
7448
- costMin: options.costMin ?? parsed2.filters.costMin,
7449
- costMax: options.costMax ?? parsed2.filters.costMax,
7450
- costMinExclusive: options.costMinExclusive ?? parsed2.filters.costMinExclusive,
7451
- costMaxExclusive: options.costMaxExclusive ?? parsed2.filters.costMaxExclusive
7382
+ agent: options.agent ?? parsed.filters.agent,
7383
+ project: options.project ?? parsed.filters.project,
7384
+ projectKind: options.projectKind ?? parsed.filters.projectKind,
7385
+ projectKey: options.projectKey ?? parsed.filters.projectKey,
7386
+ cwd: options.cwd ?? parsed.filters.cwd,
7387
+ tags: mergeSearchLists(options.tags, parsed.filters.tags),
7388
+ tools: mergeSearchLists(options.tools, parsed.filters.tools),
7389
+ file: options.file ?? parsed.filters.file,
7390
+ fileKind: options.fileKind ?? parsed.filters.fileKind,
7391
+ costMin: options.costMin ?? parsed.filters.costMin,
7392
+ costMax: options.costMax ?? parsed.filters.costMax,
7393
+ costMinExclusive: options.costMinExclusive ?? parsed.filters.costMinExclusive,
7394
+ costMaxExclusive: options.costMaxExclusive ?? parsed.filters.costMaxExclusive
7452
7395
  },
7453
- parsed: parsed2
7396
+ parsed
7454
7397
  };
7455
7398
  }
7456
7399
  function sessionMatchesSearchCost(session, options) {
@@ -8067,7 +8010,8 @@ function loadCachedSessionData(agentName, sessionId) {
8067
8010
  if (!row) {
8068
8011
  return null;
8069
8012
  }
8070
- const messageRows = db.prepare(
8013
+ const pendingReindex = db.prepare("SELECT 1 FROM pending_reindex WHERE agent_name = ? AND session_id = ?").get(agentName, sessionId) != null;
8014
+ const messageRows = pendingReindex ? [] : db.prepare(
8071
8015
  `
8072
8016
  SELECT
8073
8017
  message_id,
@@ -8504,6 +8448,47 @@ async function ensureSessionTags(agent, sessions, workerUrl) {
8504
8448
  return ensureSessionTagsSync(agent, sessions);
8505
8449
  }
8506
8450
  }
8451
+ async function finalizeAgentScan(agent, sessions, context) {
8452
+ const { finalization, options, timing, agentStart, onProgress } = context;
8453
+ const isIncremental = finalization.kind === "incremental";
8454
+ if (!isIncremental) {
8455
+ onProgress?.({ agent: agent.name, phase: "complete", newCount: sessions.length });
8456
+ }
8457
+ const identityStart = performance.now();
8458
+ const sessionsWithIdentity = attachMissingProjectIdentities(sessions);
8459
+ timing.identity = performance.now() - identityStart;
8460
+ let tagged = { sessions: sessionsWithIdentity, changed: false };
8461
+ if (finalization.kind !== "cache-only") {
8462
+ const tagsStart = performance.now();
8463
+ tagged = options.includeSmartTags === false ? tagged : await ensureSessionTags(agent, sessionsWithIdentity, options.smartTagWorkerUrl);
8464
+ timing.tags = performance.now() - tagsStart;
8465
+ }
8466
+ if (options.writeCache !== false) {
8467
+ if (finalization.kind === "incremental") {
8468
+ saveCachedSessionDiff(
8469
+ agent,
8470
+ finalization.cached.sessions,
8471
+ tagged.sessions,
8472
+ finalization.changedIds
8473
+ );
8474
+ } else if (finalization.kind === "unchanged" && tagged.changed) {
8475
+ saveCachedSessionDiff(agent, finalization.cached.sessions, tagged.sessions);
8476
+ }
8477
+ }
8478
+ if (isIncremental) {
8479
+ onProgress?.({ agent: agent.name, phase: "complete", newCount: tagged.sessions.length });
8480
+ }
8481
+ const heads = filterSessions(tagged.sessions, options);
8482
+ timing.total = performance.now() - agentStart;
8483
+ return {
8484
+ agent,
8485
+ heads,
8486
+ fromCache: true,
8487
+ ...isIncremental ? { refreshed: true } : {},
8488
+ timing,
8489
+ cacheTimestamp: isIncremental ? finalization.cacheTimestamp : finalization.cached.timestamp
8490
+ };
8491
+ }
8507
8492
  async function scanAgentSmart(agent, options, onProgress) {
8508
8493
  const agentStart = performance.now();
8509
8494
  const timing = { total: 0 };
@@ -8524,19 +8509,13 @@ async function scanAgentSmart(agent, options, onProgress) {
8524
8509
  phase: "cache",
8525
8510
  cachedCount: cached.sessions.length
8526
8511
  });
8527
- onProgress?.({ agent: agent.name, phase: "complete", newCount: cached.sessions.length });
8528
- const t32 = performance.now();
8529
- const cachedWithIdentity2 = attachMissingProjectIdentities(cached.sessions);
8530
- timing.identity = performance.now() - t32;
8531
- const filtered3 = filterSessions(cachedWithIdentity2, options);
8532
- timing.total = performance.now() - agentStart;
8533
- return {
8534
- agent,
8535
- heads: filtered3,
8536
- fromCache: true,
8512
+ return finalizeAgentScan(agent, cached.sessions, {
8513
+ finalization: { kind: "cache-only", cached },
8514
+ options,
8537
8515
  timing,
8538
- cacheTimestamp: cached.timestamp
8539
- };
8516
+ agentStart,
8517
+ onProgress
8518
+ });
8540
8519
  }
8541
8520
  const isAvail = agent.isAvailable();
8542
8521
  if (!isAvail) {
@@ -8564,55 +8543,26 @@ async function scanAgentSmart(agent, options, onProgress) {
8564
8543
  agent.incrementalScan(cached.sessions, checkResult.changedIds || [])
8565
8544
  );
8566
8545
  timing.scan = performance.now() - t2;
8567
- const t32 = performance.now();
8568
- const sessionsWithIdentity = attachMissingProjectIdentities(updatedSessions);
8569
- timing.identity = performance.now() - t32;
8570
- const t42 = performance.now();
8571
- const tagged2 = options.includeSmartTags === false ? { sessions: sessionsWithIdentity, changed: false } : await ensureSessionTags(agent, sessionsWithIdentity, options.smartTagWorkerUrl);
8572
- timing.tags = performance.now() - t42;
8573
- if (options.writeCache !== false) {
8574
- saveCachedSessionDiff(
8575
- agent,
8576
- cached.sessions,
8577
- tagged2.sessions,
8578
- checkResult.changedIds ?? []
8579
- );
8580
- }
8581
- onProgress?.({
8582
- agent: agent.name,
8583
- phase: "complete",
8584
- newCount: tagged2.sessions.length
8585
- });
8586
- const filtered3 = filterSessions(tagged2.sessions, options);
8587
- timing.total = performance.now() - agentStart;
8588
- return {
8589
- agent,
8590
- heads: filtered3,
8591
- fromCache: true,
8592
- refreshed: true,
8546
+ return finalizeAgentScan(agent, updatedSessions, {
8547
+ finalization: {
8548
+ kind: "incremental",
8549
+ cached,
8550
+ changedIds: checkResult.changedIds ?? [],
8551
+ cacheTimestamp: checkResult.timestamp
8552
+ },
8553
+ options,
8593
8554
  timing,
8594
- cacheTimestamp: checkResult.timestamp
8595
- };
8596
- }
8597
- onProgress?.({ agent: agent.name, phase: "complete", newCount: cached.sessions.length });
8598
- const t3 = performance.now();
8599
- const cachedWithIdentity = attachMissingProjectIdentities(cached.sessions);
8600
- timing.identity = performance.now() - t3;
8601
- const t4 = performance.now();
8602
- const tagged = options.includeSmartTags === false ? { sessions: cachedWithIdentity, changed: false } : await ensureSessionTags(agent, cachedWithIdentity, options.smartTagWorkerUrl);
8603
- timing.tags = performance.now() - t4;
8604
- if (tagged.changed && options.writeCache !== false) {
8605
- saveCachedSessionDiff(agent, cached.sessions, tagged.sessions);
8555
+ agentStart,
8556
+ onProgress
8557
+ });
8606
8558
  }
8607
- const filtered2 = filterSessions(tagged.sessions, options);
8608
- timing.total = performance.now() - agentStart;
8609
- return {
8610
- agent,
8611
- heads: filtered2,
8612
- fromCache: true,
8559
+ return finalizeAgentScan(agent, cached.sessions, {
8560
+ finalization: { kind: "unchanged", cached },
8561
+ options,
8613
8562
  timing,
8614
- cacheTimestamp: cached.timestamp
8615
- };
8563
+ agentStart,
8564
+ onProgress
8565
+ });
8616
8566
  }
8617
8567
  }
8618
8568
  if (options.cacheOnly) {
@@ -8660,9 +8610,9 @@ async function scanAgentFull(agent, options, onProgress, timing = { total: 0 },
8660
8610
  markAgentFullSyncCompleted(agent.name);
8661
8611
  }
8662
8612
  onProgress?.({ agent: agent.name, phase: "complete", newCount: tagged.sessions.length });
8663
- const filtered2 = filterSessions(tagged.sessions, options);
8613
+ const filtered = filterSessions(tagged.sessions, options);
8664
8614
  timing.total = performance.now() - agentStart;
8665
- return { agent, heads: filtered2, fromCache: false, timing };
8615
+ return { agent, heads: filtered, fromCache: false, timing };
8666
8616
  } catch (err) {
8667
8617
  console.error(`Error scanning ${agent.name}:`, err);
8668
8618
  return { agent, heads: [], fromCache: false };
@@ -8710,67 +8660,35 @@ async function scanSessions(options = {}, onProgress) {
8710
8660
  async function scanSessionsAsync(options = {}, onProgress) {
8711
8661
  return scanSessions(options, onProgress);
8712
8662
  }
8713
- var BOOKMARK_DB_FILENAME = "state.db";
8714
- var BOOKMARK_SCHEMA_VERSION = 1;
8663
+ var STATE_DB_FILENAME = "state.db";
8664
+ var STATE_SCHEMA_VERSION = 2;
8715
8665
  var MEMORY_STATE_STORE = "memory";
8716
- var memoryBookmarks = /* @__PURE__ */ new Map();
8717
- var BookmarkStorageUnavailableError = class extends Error {
8666
+ var StateStorageUnavailableError = class extends Error {
8718
8667
  constructor() {
8719
8668
  super("SQLite state database is unavailable");
8720
- this.name = "BookmarkStorageUnavailableError";
8669
+ this.name = "StateStorageUnavailableError";
8721
8670
  }
8722
8671
  };
8723
8672
  function getStateDir() {
8724
- if (process.env.CODESESH_STATE_DIR) {
8725
- return process.env.CODESESH_STATE_DIR;
8726
- }
8727
- const p = platform2();
8728
- if (p === "darwin") {
8673
+ if (process.env.CODESESH_STATE_DIR) return process.env.CODESESH_STATE_DIR;
8674
+ const currentPlatform = platform2();
8675
+ if (currentPlatform === "darwin") {
8729
8676
  return join12(homedir5(), "Library", "Application Support", "codesesh");
8730
8677
  }
8731
- if (p === "win32") {
8678
+ if (currentPlatform === "win32") {
8732
8679
  const appData = process.env.APPDATA ?? process.env.LOCALAPPDATA;
8733
8680
  return join12(appData ?? join12(homedir5(), "AppData", "Roaming"), "codesesh");
8734
8681
  }
8735
8682
  return join12(process.env.XDG_DATA_HOME ?? join12(homedir5(), ".local", "share"), "codesesh");
8736
8683
  }
8737
8684
  function getStateDbPath() {
8738
- return join12(getStateDir(), BOOKMARK_DB_FILENAME);
8685
+ return join12(getStateDir(), STATE_DB_FILENAME);
8739
8686
  }
8740
8687
  function useMemoryStateStore() {
8741
8688
  return process.env.CODESESH_STATE_STORE === MEMORY_STATE_STORE;
8742
8689
  }
8743
- function getBookmarkKey(agentKey, sessionId) {
8744
- return JSON.stringify([agentKey, sessionId]);
8745
- }
8746
- function getActivityTime(bookmark) {
8747
- return bookmark.time_updated ?? bookmark.time_created;
8748
- }
8749
- function sortBookmarks(bookmarks) {
8750
- return bookmarks.sort((a, b) => {
8751
- const activityDelta = getActivityTime(b) - getActivityTime(a);
8752
- return activityDelta || b.bookmarked_at - a.bookmarked_at;
8753
- });
8754
- }
8755
- function listMemoryBookmarks() {
8756
- return sortBookmarks(Array.from(memoryBookmarks.values()));
8757
- }
8758
- function upsertMemoryBookmark(bookmark) {
8759
- const key = getBookmarkKey(bookmark.agentKey, bookmark.sessionId);
8760
- const saved = {
8761
- ...bookmark,
8762
- bookmarked_at: memoryBookmarks.get(key)?.bookmarked_at ?? Date.now()
8763
- };
8764
- memoryBookmarks.set(key, saved);
8765
- return saved;
8766
- }
8767
- function createStateSchema(db) {
8690
+ function createBookmarksTable(db) {
8768
8691
  db.exec(`
8769
- CREATE TABLE IF NOT EXISTS state_meta (
8770
- key TEXT PRIMARY KEY,
8771
- value TEXT NOT NULL
8772
- );
8773
-
8774
8692
  CREATE TABLE IF NOT EXISTS bookmarks (
8775
8693
  agent_name TEXT NOT NULL,
8776
8694
  session_id TEXT NOT NULL,
@@ -8785,6 +8703,27 @@ function createStateSchema(db) {
8785
8703
  );
8786
8704
  `);
8787
8705
  }
8706
+ function createSessionAliasesTable(db) {
8707
+ db.exec(`
8708
+ CREATE TABLE IF NOT EXISTS session_aliases (
8709
+ agent_name TEXT NOT NULL,
8710
+ session_id TEXT NOT NULL,
8711
+ alias TEXT NOT NULL,
8712
+ updated_at INTEGER NOT NULL,
8713
+ PRIMARY KEY (agent_name, session_id)
8714
+ );
8715
+ `);
8716
+ }
8717
+ function createStateSchema(db) {
8718
+ db.exec(`
8719
+ CREATE TABLE IF NOT EXISTS state_meta (
8720
+ key TEXT PRIMARY KEY,
8721
+ value TEXT NOT NULL
8722
+ );
8723
+ `);
8724
+ createBookmarksTable(db);
8725
+ createSessionAliasesTable(db);
8726
+ }
8788
8727
  function readLegacyStateVersion(db) {
8789
8728
  if (!tableExists(db, "state_meta") || !columnExists(db, "state_meta", "key") || !columnExists(db, "state_meta", "value")) {
8790
8729
  return 0;
@@ -8794,13 +8733,9 @@ function readLegacyStateVersion(db) {
8794
8733
  }
8795
8734
  function getCurrentStateSchemaVersion(db) {
8796
8735
  const userVersion = getUserVersion(db);
8797
- if (userVersion > 0) {
8798
- return userVersion;
8799
- }
8736
+ if (userVersion > 0) return userVersion;
8800
8737
  const legacyVersion = readLegacyStateVersion(db);
8801
- if (legacyVersion > 0) {
8802
- return legacyVersion;
8803
- }
8738
+ if (legacyVersion > 0) return legacyVersion;
8804
8739
  return tableExists(db, "bookmarks") ? 1 : 0;
8805
8740
  }
8806
8741
  function hasAnyStateSchema(db) {
@@ -8808,14 +8743,14 @@ function hasAnyStateSchema(db) {
8808
8743
  }
8809
8744
  function setStateSchemaVersion(db) {
8810
8745
  createStateSchema(db);
8811
- setUserVersion(db, BOOKMARK_SCHEMA_VERSION);
8746
+ setUserVersion(db, STATE_SCHEMA_VERSION);
8812
8747
  db.prepare(
8813
8748
  `
8814
8749
  INSERT INTO state_meta(key, value)
8815
8750
  VALUES ('version', ?)
8816
8751
  ON CONFLICT(key) DO UPDATE SET value = excluded.value
8817
8752
  `
8818
- ).run(String(BOOKMARK_SCHEMA_VERSION));
8753
+ ).run(String(STATE_SCHEMA_VERSION));
8819
8754
  }
8820
8755
  function ensureSchema2(db, dbPath) {
8821
8756
  const currentVersion = getCurrentStateSchemaVersion(db);
@@ -8826,22 +8761,22 @@ function ensureSchema2(db, dbPath) {
8826
8761
  runSchemaMigrations(db, {
8827
8762
  dbPath,
8828
8763
  currentVersion,
8829
- targetVersion: BOOKMARK_SCHEMA_VERSION,
8764
+ targetVersion: STATE_SCHEMA_VERSION,
8830
8765
  backupLabel: "state-migration",
8831
- backupTables: ["bookmarks"],
8832
- migrations: [{ version: 1, migrate: createStateSchema }]
8766
+ backupTables: ["bookmarks", "session_aliases"],
8767
+ migrations: [
8768
+ { version: 1, migrate: createBookmarksTable },
8769
+ { version: 2, migrate: createSessionAliasesTable }
8770
+ ]
8833
8771
  });
8834
- createStateSchema(db);
8835
- if (getUserVersion(db) <= BOOKMARK_SCHEMA_VERSION) {
8772
+ if (currentVersion <= STATE_SCHEMA_VERSION) {
8836
8773
  setStateSchemaVersion(db);
8837
8774
  }
8838
8775
  }
8839
8776
  function withStateDb(fn) {
8840
8777
  const statePath = getStateDbPath();
8841
8778
  const db = openDb(statePath);
8842
- if (!db) {
8843
- throw new BookmarkStorageUnavailableError();
8844
- }
8779
+ if (!db) throw new StateStorageUnavailableError();
8845
8780
  try {
8846
8781
  ensureSchema2(db, statePath);
8847
8782
  return fn(db);
@@ -8849,6 +8784,31 @@ function withStateDb(fn) {
8849
8784
  db.close();
8850
8785
  }
8851
8786
  }
8787
+ var memoryBookmarks = /* @__PURE__ */ new Map();
8788
+ function getBookmarkKey(agentKey, sessionId) {
8789
+ return JSON.stringify([agentKey, sessionId]);
8790
+ }
8791
+ function getActivityTime(bookmark) {
8792
+ return bookmark.time_updated ?? bookmark.time_created;
8793
+ }
8794
+ function sortBookmarks(bookmarks) {
8795
+ return bookmarks.sort((a, b) => {
8796
+ const activityDelta = getActivityTime(b) - getActivityTime(a);
8797
+ return activityDelta || b.bookmarked_at - a.bookmarked_at;
8798
+ });
8799
+ }
8800
+ function listMemoryBookmarks() {
8801
+ return sortBookmarks(Array.from(memoryBookmarks.values()));
8802
+ }
8803
+ function upsertMemoryBookmark(bookmark) {
8804
+ const key = getBookmarkKey(bookmark.agentKey, bookmark.sessionId);
8805
+ const saved = {
8806
+ ...bookmark,
8807
+ bookmarked_at: memoryBookmarks.get(key)?.bookmarked_at ?? Date.now()
8808
+ };
8809
+ memoryBookmarks.set(key, saved);
8810
+ return saved;
8811
+ }
8852
8812
  function toBookmarkRecord(row) {
8853
8813
  return {
8854
8814
  agentKey: String(row.agent_name ?? ""),
@@ -9021,6 +8981,78 @@ function deleteBookmark(agentKey, sessionId) {
9021
8981
  ).run(agentKey, sessionId);
9022
8982
  });
9023
8983
  }
8984
+ var SESSION_ALIAS_MAX_LENGTH = 160;
8985
+ var memoryAliases = /* @__PURE__ */ new Map();
8986
+ function getAliasKey(agentKey, sessionId) {
8987
+ return JSON.stringify([agentKey, sessionId]);
8988
+ }
8989
+ function toSessionAlias(row) {
8990
+ return {
8991
+ agentKey: String(row.agent_name ?? ""),
8992
+ sessionId: String(row.session_id ?? ""),
8993
+ alias: String(row.alias ?? ""),
8994
+ updated_at: Number(row.updated_at ?? 0)
8995
+ };
8996
+ }
8997
+ function normalizeSessionAlias(value) {
8998
+ const alias = value.trim();
8999
+ if (!alias || alias.length > SESSION_ALIAS_MAX_LENGTH) return null;
9000
+ return alias;
9001
+ }
9002
+ function listSessionAliases() {
9003
+ if (useMemoryStateStore()) return [...memoryAliases.values()];
9004
+ return withStateDb(
9005
+ (db) => db.prepare(
9006
+ `
9007
+ SELECT agent_name, session_id, alias, updated_at
9008
+ FROM session_aliases
9009
+ ORDER BY updated_at DESC
9010
+ `
9011
+ ).all().map(toSessionAlias)
9012
+ );
9013
+ }
9014
+ function upsertSessionAlias(agentKey, sessionId, alias) {
9015
+ const normalizedAlias = normalizeSessionAlias(alias);
9016
+ if (!normalizedAlias) {
9017
+ throw new TypeError("Invalid session alias");
9018
+ }
9019
+ const saved = {
9020
+ agentKey,
9021
+ sessionId,
9022
+ alias: normalizedAlias,
9023
+ updated_at: Date.now()
9024
+ };
9025
+ if (useMemoryStateStore()) {
9026
+ memoryAliases.set(getAliasKey(agentKey, sessionId), saved);
9027
+ return saved;
9028
+ }
9029
+ return withStateDb((db) => {
9030
+ db.prepare(
9031
+ `
9032
+ INSERT INTO session_aliases(agent_name, session_id, alias, updated_at)
9033
+ VALUES (?, ?, ?, ?)
9034
+ ON CONFLICT(agent_name, session_id) DO UPDATE SET
9035
+ alias = excluded.alias,
9036
+ updated_at = excluded.updated_at
9037
+ `
9038
+ ).run(saved.agentKey, saved.sessionId, saved.alias, saved.updated_at);
9039
+ return saved;
9040
+ });
9041
+ }
9042
+ function deleteSessionAlias(agentKey, sessionId) {
9043
+ if (useMemoryStateStore()) {
9044
+ memoryAliases.delete(getAliasKey(agentKey, sessionId));
9045
+ return;
9046
+ }
9047
+ withStateDb((db) => {
9048
+ db.prepare(
9049
+ `
9050
+ DELETE FROM session_aliases
9051
+ WHERE agent_name = ? AND session_id = ?
9052
+ `
9053
+ ).run(agentKey, sessionId);
9054
+ });
9055
+ }
9024
9056
  var DASHBOARD_RECENT_LIMIT = 10;
9025
9057
  function getTotalTokens(stats) {
9026
9058
  return stats.total_tokens ?? stats.total_input_tokens + stats.total_output_tokens;
@@ -9235,8 +9267,8 @@ function searchRecentSessions(snapshot, options) {
9235
9267
  matchType: "recent"
9236
9268
  }));
9237
9269
  }
9238
- function deriveFileQuery(query, parsed2, options) {
9239
- return options.file ?? (!parsed2.text ? parsed2.filters.file : void 0) ?? (!parsed2.hasQualifiers && query ? parsed2.text || query : "");
9270
+ function deriveFileQuery(query, parsed, options) {
9271
+ return options.file ?? (!parsed.text ? parsed.filters.file : void 0) ?? (!parsed.hasQualifiers && query ? parsed.text || query : "");
9240
9272
  }
9241
9273
  function mergeSearchResultSources(results, limit) {
9242
9274
  const seen = /* @__PURE__ */ new Set();
@@ -9255,8 +9287,8 @@ function canSkipSessionsSearch(fileQuery, textQuery, options) {
9255
9287
  fileQuery && !textQuery && !options.tools?.length && !options.tags?.length && options.from == null && options.to == null
9256
9288
  );
9257
9289
  }
9258
- function searchIndexedSessions(query, textQuery, parsed2, options) {
9259
- const fileQuery = deriveFileQuery(query, parsed2, options);
9290
+ function searchIndexedSessions(query, textQuery, parsed, options) {
9291
+ const fileQuery = deriveFileQuery(query, parsed, options);
9260
9292
  const fileResults = fileQuery ? searchFileActivitySessions(fileQuery, options) : [];
9261
9293
  const sessionResults = canSkipSessionsSearch(fileQuery, textQuery, options) ? [] : searchSessions(query, options);
9262
9294
  return mergeSearchResultSources([...fileResults, ...sessionResults], options.limit ?? 50);
@@ -9286,16 +9318,12 @@ export {
9286
9318
  normalizeTitleText,
9287
9319
  basenameTitle,
9288
9320
  resolveSessionTitle,
9289
- parsed,
9290
- skipped,
9291
- filtered,
9292
9321
  cleanInternalText,
9293
9322
  cleanMessagePart,
9294
9323
  cleanMessageParts,
9295
9324
  cleanParsedMessage,
9296
9325
  cleanParsedMessages,
9297
9326
  firstUserMessageTitle,
9298
- perf,
9299
9327
  getPricingRegistry,
9300
9328
  hasBillablePricing,
9301
9329
  refreshPricingCache,
@@ -9308,6 +9336,7 @@ export {
9308
9336
  openDbReadOnly,
9309
9337
  openDb,
9310
9338
  isSqliteAvailable,
9339
+ perf,
9311
9340
  fallbackDisplayName,
9312
9341
  realFs,
9313
9342
  isProjectIdentityKind,
@@ -9329,6 +9358,7 @@ export {
9329
9358
  parseSearchQuery,
9330
9359
  syncSessionSearchIndex,
9331
9360
  syncSessionSearchIndexChanges,
9361
+ mergeSearchQueryOptions,
9332
9362
  searchSessions,
9333
9363
  listFileActivity,
9334
9364
  listSessionFileActivity,
@@ -9353,11 +9383,16 @@ export {
9353
9383
  ensureSessionTagsSync,
9354
9384
  scanSessions,
9355
9385
  scanSessionsAsync,
9356
- BookmarkStorageUnavailableError,
9386
+ StateStorageUnavailableError,
9357
9387
  listBookmarks,
9358
9388
  upsertBookmark,
9359
9389
  importBookmarks,
9360
9390
  deleteBookmark,
9391
+ SESSION_ALIAS_MAX_LENGTH,
9392
+ normalizeSessionAlias,
9393
+ listSessionAliases,
9394
+ upsertSessionAlias,
9395
+ deleteSessionAlias,
9361
9396
  DASHBOARD_RECENT_LIMIT,
9362
9397
  getTotalTokens,
9363
9398
  getSessionAgentName,
@@ -9367,4 +9402,4 @@ export {
9367
9402
  buildDashboard,
9368
9403
  executeSessionSearch
9369
9404
  };
9370
- //# sourceMappingURL=chunk-BV65IEWZ.js.map
9405
+ //# sourceMappingURL=chunk-NBCLV4CX.js.map