codesesh 0.13.0 → 0.14.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,23 @@ 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();
1061
+ const builder = new TranscriptBuilder();
992
1062
  const ignoredToolCallIds = /* @__PURE__ */ new Set();
993
1063
  const assistantUuidToToolCalls = /* @__PURE__ */ new Map();
994
1064
  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
1065
  for (const record of parseJsonlLines(content)) {
1005
1066
  try {
1006
1067
  this.convertRecord(
1007
1068
  record,
1008
- messages,
1009
- pendingToolCalls,
1069
+ builder,
1010
1070
  ignoredToolCallIds,
1011
1071
  assistantUuidToToolCalls,
1012
- countedUsageKeys,
1013
- assistantState
1072
+ countedUsageKeys
1014
1073
  );
1015
1074
  } catch {
1016
1075
  }
1017
1076
  }
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
- }
1077
+ const transcript = builder.finish();
1026
1078
  return {
1027
1079
  id: meta.id,
1028
1080
  title: meta.title,
@@ -1031,16 +1083,8 @@ var ClaudeCodeAgent = class extends FileSystemSessionSource {
1031
1083
  version: void 0,
1032
1084
  time_created: meta.createdAt,
1033
1085
  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
1086
+ stats: transcript.stats,
1087
+ messages: transcript.messages
1044
1088
  };
1045
1089
  }
1046
1090
  // --- Private helpers ---
@@ -1251,41 +1295,30 @@ var ClaudeCodeAgent = class extends FileSystemSessionSource {
1251
1295
  return null;
1252
1296
  }
1253
1297
  // --- Record conversion ---
1254
- convertRecord(data, messages, pendingToolCalls, ignoredToolCallIds, assistantUuidToToolCalls, countedUsageKeys, assistantState) {
1298
+ convertRecord(data, builder, ignoredToolCallIds, assistantUuidToToolCalls, countedUsageKeys) {
1255
1299
  if (data["isMeta"] === true) return;
1256
1300
  const msgType = String(data["type"] ?? "");
1257
1301
  if (isInternalEventType(msgType)) return;
1258
1302
  if (msgType === "assistant") {
1259
1303
  this.convertAssistantRecord(
1260
1304
  data,
1261
- messages,
1262
- pendingToolCalls,
1305
+ builder,
1263
1306
  ignoredToolCallIds,
1264
1307
  assistantUuidToToolCalls,
1265
- countedUsageKeys,
1266
- assistantState
1308
+ countedUsageKeys
1267
1309
  );
1268
1310
  } else if (msgType === "user") {
1269
- this.convertUserRecord(
1270
- data,
1271
- messages,
1272
- pendingToolCalls,
1273
- ignoredToolCallIds,
1274
- assistantUuidToToolCalls,
1275
- assistantState
1276
- );
1311
+ this.convertUserRecord(data, builder, ignoredToolCallIds, assistantUuidToToolCalls);
1277
1312
  } else if (msgType === "tool_result") {
1278
- this.convertToolResultRecord(data, messages, assistantState);
1313
+ this.convertToolResultRecord(data, builder);
1279
1314
  }
1280
1315
  }
1281
- convertAssistantRecord(data, messages, pendingToolCalls, ignoredToolCallIds, assistantUuidToToolCalls, countedUsageKeys, assistantState) {
1316
+ convertAssistantRecord(data, builder, ignoredToolCallIds, assistantUuidToToolCalls, countedUsageKeys) {
1282
1317
  const msg = data["message"] ?? {};
1283
1318
  const timestampMs = parseTimestampMs(data);
1284
1319
  const rawContent = msg["content"] ?? [];
1285
1320
  const uuid = String(data["uuid"] ?? "");
1286
1321
  const toolCallIds = [];
1287
- let currentAssistantIndex = assistantState.currentIndex;
1288
- let latestAssistantTextIndex = assistantState.latestTextIndex;
1289
1322
  if (Array.isArray(rawContent)) {
1290
1323
  for (const item of rawContent) {
1291
1324
  if (!item || typeof item !== "object") continue;
@@ -1294,23 +1327,28 @@ var ClaudeCodeAgent = class extends FileSystemSessionSource {
1294
1327
  if (partType === "thinking") {
1295
1328
  const text = cleanInternalText(String(part["thinking"] ?? ""));
1296
1329
  if (text) {
1297
- currentAssistantIndex = this.appendAssistantReasoning(
1298
- messages,
1299
- { messageId: uuid, data, msg, timestampMs, text, countedUsageKeys },
1300
- currentAssistantIndex
1330
+ const message2 = builder.appendAssistantPart(
1331
+ this.buildReasoningPart(text, timestampMs),
1332
+ { id: uuid, timestampMs, agent: "claude" },
1333
+ { deduplicateTail: true }
1301
1334
  );
1335
+ this.applyAssistantMetadata(message2, data, msg, countedUsageKeys);
1302
1336
  }
1303
1337
  continue;
1304
1338
  }
1305
1339
  if (partType === "text") {
1306
1340
  const text = cleanInternalText(String(part["text"] ?? ""));
1307
1341
  if (text) {
1308
- currentAssistantIndex = this.appendAssistantText(
1309
- messages,
1310
- { messageId: uuid, data, msg, timestampMs, text, countedUsageKeys },
1311
- currentAssistantIndex
1342
+ const message2 = builder.appendAssistantPart(
1343
+ this.buildTextPart(text, timestampMs),
1344
+ {
1345
+ id: uuid,
1346
+ timestampMs,
1347
+ agent: "claude"
1348
+ },
1349
+ { deduplicateTail: true }
1312
1350
  );
1313
- latestAssistantTextIndex = currentAssistantIndex;
1351
+ this.applyAssistantMetadata(message2, data, msg, countedUsageKeys);
1314
1352
  }
1315
1353
  continue;
1316
1354
  }
@@ -1322,18 +1360,13 @@ var ClaudeCodeAgent = class extends FileSystemSessionSource {
1322
1360
  continue;
1323
1361
  }
1324
1362
  const toolPart = this.buildToolPart(part, timestampMs);
1325
- const [msgIndex, partIndex] = this.attachToolCallToLatestAssistant(messages, {
1326
- messageId: uuid,
1327
- data,
1328
- msg,
1329
- timestampMs,
1363
+ const message = builder.appendToolCall(
1330
1364
  toolPart,
1331
- latestTextIndex: latestAssistantTextIndex,
1332
- countedUsageKeys
1333
- });
1334
- currentAssistantIndex = msgIndex;
1365
+ { id: uuid, timestampMs, agent: "claude" },
1366
+ { modeOnCreate: "tool" }
1367
+ );
1368
+ this.applyAssistantMetadata(message, data, msg, countedUsageKeys);
1335
1369
  if (toolCallId) {
1336
- pendingToolCalls.set(toolCallId, [msgIndex, partIndex]);
1337
1370
  toolCallIds.push(toolCallId);
1338
1371
  }
1339
1372
  }
@@ -1341,10 +1374,8 @@ var ClaudeCodeAgent = class extends FileSystemSessionSource {
1341
1374
  if (toolCallIds.length > 0) {
1342
1375
  assistantUuidToToolCalls.set(uuid, toolCallIds);
1343
1376
  }
1344
- assistantState.currentIndex = currentAssistantIndex;
1345
- assistantState.latestTextIndex = latestAssistantTextIndex;
1346
1377
  }
1347
- convertUserRecord(data, messages, pendingToolCalls, ignoredToolCallIds, assistantUuidToToolCalls, assistantState) {
1378
+ convertUserRecord(data, builder, ignoredToolCallIds, assistantUuidToToolCalls) {
1348
1379
  const msg = data["message"] ?? {};
1349
1380
  const timestampMs = parseTimestampMs(data);
1350
1381
  const content = msg["content"] ?? "";
@@ -1352,18 +1383,14 @@ var ClaudeCodeAgent = class extends FileSystemSessionSource {
1352
1383
  if (typeof content === "string") {
1353
1384
  const parts = this.normalizeUserTextParts(content, timestampMs);
1354
1385
  if (parts.length === 0) {
1355
- assistantState.currentIndex = null;
1356
- assistantState.latestTextIndex = null;
1386
+ builder.beginTurn();
1357
1387
  return;
1358
1388
  }
1359
- messages.push(this.buildMessage({ messageId: uuid, role: "user", timestampMs, parts }));
1360
- assistantState.currentIndex = null;
1361
- assistantState.latestTextIndex = null;
1389
+ builder.appendMessage({ id: uuid, role: "user", timestampMs, parts });
1362
1390
  return;
1363
1391
  }
1364
1392
  if (!Array.isArray(content)) {
1365
- assistantState.currentIndex = null;
1366
- assistantState.latestTextIndex = null;
1393
+ builder.beginTurn();
1367
1394
  return;
1368
1395
  }
1369
1396
  const visibleParts = this.normalizeUserTextParts(content, timestampMs);
@@ -1375,13 +1402,7 @@ var ClaudeCodeAgent = class extends FileSystemSessionSource {
1375
1402
  const toolCallId = this.resolveToolCallId(data, ci, assistantUuidToToolCalls);
1376
1403
  if (toolCallId && ignoredToolCallIds.has(toolCallId)) continue;
1377
1404
  const outputParts = this.normalizeClaudeToolOutput(ci["content"], timestampMs);
1378
- if (this.backfillToolOutput(
1379
- messages,
1380
- pendingToolCalls,
1381
- toolCallId,
1382
- outputParts,
1383
- toolStateUpdates
1384
- )) {
1405
+ if (this.backfillToolOutput(builder, toolCallId, outputParts, toolStateUpdates)) {
1385
1406
  continue;
1386
1407
  }
1387
1408
  const fallback = this.buildFallbackToolMessage({
@@ -1390,17 +1411,14 @@ var ClaudeCodeAgent = class extends FileSystemSessionSource {
1390
1411
  toolCallId,
1391
1412
  outputParts
1392
1413
  });
1393
- if (fallback) messages.push(fallback);
1414
+ if (fallback) builder.appendMessage(fallback);
1394
1415
  }
1395
1416
  if (visibleParts.length > 0) {
1396
- messages.push(
1397
- this.buildMessage({ messageId: uuid, role: "user", timestampMs, parts: visibleParts })
1398
- );
1417
+ builder.appendMessage({ id: uuid, role: "user", timestampMs, parts: visibleParts });
1399
1418
  }
1400
- assistantState.currentIndex = null;
1401
- assistantState.latestTextIndex = null;
1419
+ builder.beginTurn();
1402
1420
  }
1403
- convertToolResultRecord(data, messages, assistantState) {
1421
+ convertToolResultRecord(data, builder) {
1404
1422
  const timestampMs = parseTimestampMs(data);
1405
1423
  const msg = data["message"] ?? {};
1406
1424
  const outputParts = this.normalizeClaudeToolOutput(msg["content"], timestampMs);
@@ -1411,25 +1429,8 @@ var ClaudeCodeAgent = class extends FileSystemSessionSource {
1411
1429
  toolCallId: null,
1412
1430
  outputParts
1413
1431
  });
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
- };
1432
+ if (fallback) builder.appendMessage(fallback);
1433
+ builder.beginTurn();
1433
1434
  }
1434
1435
  buildTextPart(text, timestampMs) {
1435
1436
  return { type: "text", text, time_created: timestampMs };
@@ -1472,71 +1473,6 @@ var ClaudeCodeAgent = class extends FileSystemSessionSource {
1472
1473
  }
1473
1474
  }
1474
1475
  }
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
1476
  // --- User content normalization ---
1541
1477
  normalizeUserTextParts(content, timestampMs) {
1542
1478
  if (typeof content === "string") {
@@ -1584,29 +1520,19 @@ var ClaudeCodeAgent = class extends FileSystemSessionSource {
1584
1520
  return text ? [this.buildTextPart(text, timestampMs)] : [];
1585
1521
  }
1586
1522
  // --- Tool backfill ---
1587
- backfillToolOutput(messages, pendingToolCalls, callId, outputParts, stateUpdates) {
1523
+ backfillToolOutput(builder, callId, outputParts, stateUpdates) {
1588
1524
  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];
1525
+ return builder.updateToolCall(callId, (part) => {
1526
+ const state = part.state ?? (part.state = {});
1527
+ if (outputParts.length > 0) {
1528
+ const existing = state.output;
1529
+ if (Array.isArray(existing)) existing.push(...outputParts);
1530
+ else if (existing == null) state.output = [...outputParts];
1531
+ else state.output = [existing, ...outputParts];
1601
1532
  }
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;
1533
+ if (stateUpdates) Object.assign(state, stateUpdates);
1534
+ if (outputParts.length > 0 && !state.status) state.status = "completed";
1535
+ });
1610
1536
  }
1611
1537
  resolveToolCallId(data, item, assistantUuidToToolCalls) {
1612
1538
  const directId = String(item["tool_use_id"] ?? "").trim();
@@ -1634,25 +1560,17 @@ var ClaudeCodeAgent = class extends FileSystemSessionSource {
1634
1560
  // --- Fallback ---
1635
1561
  buildFallbackToolMessage(opts) {
1636
1562
  if (opts.outputParts.length === 0) return null;
1637
- return this.buildMessage({
1638
- messageId: opts.messageId,
1563
+ return {
1564
+ id: opts.messageId,
1639
1565
  role: "tool",
1640
1566
  timestampMs: opts.timestampMs,
1641
1567
  parts: opts.outputParts
1642
- });
1568
+ };
1643
1569
  }
1644
1570
  // --- Utilities ---
1645
1571
  shouldIgnoreTool(toolName) {
1646
1572
  return toolName === "TodoWrite";
1647
1573
  }
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);
1655
- }
1656
1574
  };
1657
1575
  var DatabaseConstructor = null;
1658
1576
  try {
@@ -2318,50 +2236,6 @@ var KimiAgent = class extends FileSystemSessionSource {
2318
2236
  return skippedSession("malformed metadata");
2319
2237
  }
2320
2238
  }
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
2239
  listSessionSources(options) {
2366
2240
  if (!this.basePath) return [];
2367
2241
  const refs = [];
@@ -2403,8 +2277,7 @@ var KimiAgent = class extends FileSystemSessionSource {
2403
2277
  getSessionDataFromContext(meta) {
2404
2278
  if (!meta.contextFile) throw new Error("context.jsonl is missing");
2405
2279
  const content = readFileSync3(meta.contextFile, "utf-8");
2406
- const messages = [];
2407
- const pendingToolCalls = /* @__PURE__ */ new Map();
2280
+ const builder = new TranscriptBuilder();
2408
2281
  const ignoredToolCallIds = /* @__PURE__ */ new Set();
2409
2282
  let seq = 0;
2410
2283
  const fallbackTs = meta.createdAt;
@@ -2416,65 +2289,55 @@ var KimiAgent = class extends FileSystemSessionSource {
2416
2289
  if (role === "user") {
2417
2290
  const text = cleanInternalText(kimiContentText(record.content));
2418
2291
  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
- );
2292
+ builder.appendMessage({
2293
+ id: `context-${seq}`,
2294
+ role: "user",
2295
+ timestampMs: fallbackTs,
2296
+ parts: [{ type: "text", text, time_created: fallbackTs }]
2297
+ });
2427
2298
  }
2428
2299
  continue;
2429
2300
  }
2430
2301
  if (role === "assistant") {
2431
- const { message, toolIndexes } = this.buildContextAssistantMessage(
2302
+ const message = this.buildContextAssistantMessage(
2432
2303
  record,
2433
2304
  seq,
2434
2305
  ignoredToolCallIds,
2435
2306
  fallbackTs
2436
2307
  );
2437
2308
  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
- }
2309
+ builder.appendMessage(message);
2443
2310
  continue;
2444
2311
  }
2445
2312
  if (role === "tool") {
2446
2313
  const callId = String(record.tool_call_id ?? "").trim();
2447
2314
  if (callId && ignoredToolCallIds.has(callId)) continue;
2448
2315
  const outputParts = normalizeToolOutputParts(record.content, fallbackTs);
2449
- if (callId && this.backfillToolOutput(messages, pendingToolCalls, callId, outputParts)) {
2316
+ if (callId && this.backfillToolOutput(builder, callId, outputParts)) {
2450
2317
  continue;
2451
2318
  }
2452
2319
  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
- );
2320
+ builder.appendMessage({
2321
+ id: `context-${seq}`,
2322
+ role: "tool",
2323
+ timestampMs: fallbackTs,
2324
+ parts: outputParts
2325
+ });
2461
2326
  }
2462
2327
  }
2463
2328
  } catch {
2464
2329
  }
2465
2330
  }
2466
2331
  const stats = this.extractStats(meta.sourcePath);
2467
- return this.buildSessionData(meta, messages, stats);
2332
+ return this.buildSessionData(meta, builder, stats);
2468
2333
  }
2469
2334
  getSessionDataFromWire(meta) {
2470
2335
  const wirePath = meta.wireFile ?? join6(meta.sourcePath, "wire.jsonl");
2471
2336
  if (!existsSync6(wirePath)) throw new Error("wire.jsonl is missing");
2472
2337
  const content = readFileSync3(wirePath, "utf-8");
2473
- const messages = [];
2474
- const pendingToolCalls = /* @__PURE__ */ new Map();
2338
+ const builder = new TranscriptBuilder();
2475
2339
  const ignoredToolCallIds = /* @__PURE__ */ new Set();
2476
2340
  const openToolArgumentBuffer = /* @__PURE__ */ new Map();
2477
- let currentAssistantIndex = null;
2478
2341
  let openToolCallId = null;
2479
2342
  let seq = 0;
2480
2343
  for (const record of parseJsonlLines(content)) {
@@ -2491,19 +2354,13 @@ var KimiAgent = class extends FileSystemSessionSource {
2491
2354
  const inputTokens = Number(usage["input_tokens"] ?? 0);
2492
2355
  const outputTokens = Number(usage["output_tokens"] ?? 0);
2493
2356
  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
- }
2357
+ const tokens = { input: inputTokens, output: outputTokens };
2358
+ const cost = estimateTokenCost(this.defaultModel, tokens);
2359
+ builder.attachUsageToLatestAssistant(tokens, {
2360
+ model: this.defaultModel,
2361
+ cost: cost ?? void 0,
2362
+ costSource: cost === null ? void 0 : "estimated"
2363
+ });
2507
2364
  }
2508
2365
  }
2509
2366
  if (msgType === "TurnBegin") {
@@ -2511,37 +2368,37 @@ var KimiAgent = class extends FileSystemSessionSource {
2511
2368
  if (Array.isArray(userInput) && userInput.length > 0) {
2512
2369
  const text = cleanInternalText(kimiContentText(userInput));
2513
2370
  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
- );
2371
+ builder.appendMessage({
2372
+ id: `wire-${seq}`,
2373
+ role: "user",
2374
+ timestampMs,
2375
+ parts: [{ type: "text", text, time_created: timestampMs }]
2376
+ });
2522
2377
  }
2523
2378
  }
2524
- currentAssistantIndex = null;
2379
+ builder.beginTurn();
2525
2380
  openToolCallId = null;
2526
2381
  continue;
2527
2382
  }
2528
2383
  if (msgType === "ContentPart") {
2529
- currentAssistantIndex = this.getOrCreateWireAssistant(
2530
- messages,
2531
- currentAssistantIndex,
2532
- `wire-${seq}`
2533
- );
2534
- const assistant = messages[currentAssistantIndex];
2535
2384
  const partType = String(payload.type ?? "");
2536
2385
  if (partType === "think") {
2537
2386
  const text = cleanInternalText(String(payload.think ?? ""));
2538
2387
  if (text) {
2539
- assistant.parts.push({ type: "reasoning", text, time_created: timestampMs });
2388
+ builder.appendAssistantPart(
2389
+ { type: "reasoning", text, time_created: timestampMs },
2390
+ { id: `wire-${seq}`, timestampMs: 0, agent: "kimi" },
2391
+ { grouping: "current" }
2392
+ );
2540
2393
  }
2541
2394
  } else if (partType === "text") {
2542
2395
  const text = cleanInternalText(String(payload.text ?? ""));
2543
2396
  if (text) {
2544
- assistant.parts.push({ type: "text", text, time_created: timestampMs });
2397
+ builder.appendAssistantPart(
2398
+ { type: "text", text, time_created: timestampMs },
2399
+ { id: `wire-${seq}`, timestampMs: 0, agent: "kimi" },
2400
+ { grouping: "current" }
2401
+ );
2545
2402
  }
2546
2403
  }
2547
2404
  continue;
@@ -2556,12 +2413,6 @@ var KimiAgent = class extends FileSystemSessionSource {
2556
2413
  continue;
2557
2414
  }
2558
2415
  if (!function_ || !callId || !toolName) continue;
2559
- currentAssistantIndex = this.getOrCreateWireAssistant(
2560
- messages,
2561
- currentAssistantIndex,
2562
- `wire-${seq}`
2563
- );
2564
- const assistant = messages[currentAssistantIndex];
2565
2416
  const rawArgs = function_.arguments;
2566
2417
  const normalizedArgs = normalizeToolArguments(rawArgs);
2567
2418
  const buffer = typeof rawArgs === "string" && typeof normalizedArgs !== "string" ? rawArgs : null;
@@ -2573,10 +2424,11 @@ var KimiAgent = class extends FileSystemSessionSource {
2573
2424
  state: { arguments: normalizedArgs, output: null },
2574
2425
  time_created: timestampMs
2575
2426
  };
2576
- const partIndex = assistant.parts.length;
2577
- assistant.parts.push(toolPart);
2578
- assistant.mode = "tool";
2579
- pendingToolCalls.set(callId, [currentAssistantIndex, partIndex]);
2427
+ builder.appendToolCall(
2428
+ toolPart,
2429
+ { id: `wire-${seq}`, timestampMs: 0, agent: "kimi" },
2430
+ { markModeAsTool: true, target: "current" }
2431
+ );
2580
2432
  openToolCallId = callId;
2581
2433
  if (buffer !== null) {
2582
2434
  openToolArgumentBuffer.set(callId, buffer);
@@ -2590,8 +2442,7 @@ var KimiAgent = class extends FileSystemSessionSource {
2590
2442
  argumentsPart,
2591
2443
  openToolCallId,
2592
2444
  openToolArgumentBuffer,
2593
- messages,
2594
- pendingToolCalls
2445
+ builder
2595
2446
  );
2596
2447
  continue;
2597
2448
  }
@@ -2599,27 +2450,24 @@ var KimiAgent = class extends FileSystemSessionSource {
2599
2450
  const callId = String(payload.tool_call_id ?? "").trim();
2600
2451
  if (callId && ignoredToolCallIds.has(callId)) continue;
2601
2452
  const outputParts = normalizeWireToolOutputParts(payload.return_value, timestampMs);
2602
- if (callId && this.backfillToolOutput(messages, pendingToolCalls, callId, outputParts)) {
2453
+ if (callId && this.backfillToolOutput(builder, callId, outputParts)) {
2603
2454
  continue;
2604
2455
  }
2605
2456
  if (outputParts.length > 0) {
2606
- messages.push(
2607
- this.buildMessage({
2608
- messageId: `wire-${seq}`,
2609
- role: "tool",
2610
- timestampMs,
2611
- parts: outputParts
2612
- })
2613
- );
2457
+ builder.appendMessage({
2458
+ id: `wire-${seq}`,
2459
+ role: "tool",
2460
+ timestampMs,
2461
+ parts: outputParts
2462
+ });
2614
2463
  }
2615
2464
  continue;
2616
2465
  }
2617
2466
  } catch {
2618
2467
  }
2619
2468
  }
2620
- const filteredMessages = messages.filter((m) => m.parts.length > 0);
2621
2469
  const stats = this.extractStats(meta.sourcePath);
2622
- return this.buildSessionData(meta, filteredMessages, stats);
2470
+ return this.buildSessionData(meta, builder, stats);
2623
2471
  }
2624
2472
  // --- Helpers ---
2625
2473
  sourceFingerprint(meta) {
@@ -2637,23 +2485,8 @@ var KimiAgent = class extends FileSystemSessionSource {
2637
2485
  fileMtime(meta.wireFile)
2638
2486
  ]);
2639
2487
  }
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
2488
  buildContextAssistantMessage(record, seq, ignoredToolCallIds, fallbackTs) {
2655
2489
  const parts = [];
2656
- const toolIndexes = /* @__PURE__ */ new Map();
2657
2490
  const content = record.content;
2658
2491
  if (Array.isArray(content)) {
2659
2492
  for (const item of content) {
@@ -2691,71 +2524,41 @@ var KimiAgent = class extends FileSystemSessionSource {
2691
2524
  state: { arguments: normalizeToolArguments(function_.arguments), output: null },
2692
2525
  time_created: fallbackTs
2693
2526
  };
2694
- toolIndexes.set(callId, parts.length);
2695
2527
  parts.push(part);
2696
2528
  }
2697
2529
  }
2698
2530
  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
- };
2531
+ return null;
2708
2532
  }
2709
2533
  const allTools = parts.every((p) => p.type === "tool");
2710
- const message = this.buildMessage({
2711
- messageId: `context-${seq}`,
2534
+ return {
2535
+ id: `context-${seq}`,
2712
2536
  role: "assistant",
2713
2537
  timestampMs: fallbackTs,
2714
2538
  parts,
2715
2539
  agent: "kimi",
2716
2540
  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;
2541
+ };
2732
2542
  }
2733
- appendWireToolCallPart(argumentsPart, openCallId, buffer, messages, pendingToolCalls) {
2734
- if (!openCallId || !pendingToolCalls.has(openCallId)) return;
2543
+ appendWireToolCallPart(argumentsPart, openCallId, buffer, builder) {
2544
+ if (!openCallId) return;
2735
2545
  const existing = buffer.get(openCallId) ?? "";
2736
2546
  const combined = existing + argumentsPart;
2737
2547
  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;
2548
+ const parsed = JSON.parse(combined);
2549
+ if (builder.updateToolCall(openCallId, (part) => {
2550
+ const state = part.state ?? (part.state = {});
2551
+ state.arguments = parsed;
2552
+ })) {
2553
+ buffer.delete(openCallId);
2744
2554
  }
2745
- buffer.delete(openCallId);
2746
2555
  } catch {
2747
2556
  buffer.set(openCallId, combined);
2748
2557
  }
2749
2558
  }
2750
- backfillToolOutput(messages, pendingToolCalls, callId, outputParts) {
2559
+ backfillToolOutput(builder, callId, outputParts) {
2751
2560
  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;
2561
+ return builder.resolveToolCall(callId, { output: [...outputParts] });
2759
2562
  }
2760
2563
  extractStats(sessionDir) {
2761
2564
  let totalCost = 0;
@@ -2811,14 +2614,8 @@ var KimiAgent = class extends FileSystemSessionSource {
2811
2614
  }
2812
2615
  return stats;
2813
2616
  }
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
- }
2617
+ buildSessionData(meta, builder, stats) {
2618
+ const transcript = builder.finish(stats);
2822
2619
  return {
2823
2620
  id: meta.id,
2824
2621
  title: meta.title,
@@ -2826,8 +2623,8 @@ var KimiAgent = class extends FileSystemSessionSource {
2826
2623
  directory: meta.cwd,
2827
2624
  time_created: meta.createdAt,
2828
2625
  time_updated: meta.createdAt,
2829
- stats,
2830
- messages: cleanedMessages
2626
+ stats: transcript.stats,
2627
+ messages: transcript.messages
2831
2628
  };
2832
2629
  }
2833
2630
  };
@@ -3015,36 +2812,6 @@ var CodexAgent = class extends FileSystemSessionSource {
3015
2812
  return false;
3016
2813
  }
3017
2814
  }
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
2815
  listSessionSources(options) {
3049
2816
  if (!this.basePath) return [];
3050
2817
  this.loadSessionIndex();
@@ -3054,9 +2821,9 @@ var CodexAgent = class extends FileSystemSessionSource {
3054
2821
  fingerprint: this.sourceFingerprint(file)
3055
2822
  }));
3056
2823
  }
3057
- scanSessionSource(sourcePath) {
2824
+ scanSessionSource(sourcePath, options) {
3058
2825
  this.loadSessionIndex();
3059
- const head = getParsedSession(this.parseSessionHeadResult(sourcePath));
2826
+ const head = getParsedSession(this.parseSessionHeadResult(sourcePath, options));
3060
2827
  if (head) {
3061
2828
  this.sessionMetaMap.set(head.id, this.buildSessionMeta(head, sourcePath));
3062
2829
  }
@@ -3066,14 +2833,11 @@ var CodexAgent = class extends FileSystemSessionSource {
3066
2833
  const meta = this.sessionMetaMap.get(sessionId);
3067
2834
  if (!meta) throw new Error(`Session not found: ${sessionId}`);
3068
2835
  if (!existsSync7(meta.sourcePath)) throw new Error(`Session file missing: ${meta.sourcePath}`);
3069
- const messages = [];
3070
- const pendingToolCalls = /* @__PURE__ */ new Map();
2836
+ const transcript = new TranscriptBuilder();
3071
2837
  let totalInputTokens = 0;
3072
2838
  let totalOutputTokens = 0;
3073
2839
  let totalCacheReadTokens = 0;
3074
2840
  let totalCost = 0;
3075
- let currentAssistantIndex = null;
3076
- let latestAssistantTextIndex = null;
3077
2841
  let pendingPlan = null;
3078
2842
  let activeModel = meta.model;
3079
2843
  let prevCumulativeTotal = 0;
@@ -3088,24 +2852,7 @@ var CodexAgent = class extends FileSystemSessionSource {
3088
2852
  const payload = record["payload"] ?? {};
3089
2853
  activeModel = extractModelName(payload["model"]) ?? activeModel;
3090
2854
  }
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
- }
2855
+ pendingPlan = this.convertRecord(record, transcript, pendingPlan, activeModel);
3109
2856
  if (recordType === "event_msg") {
3110
2857
  const payload = record["payload"] ?? {};
3111
2858
  if (String(payload["type"] ?? "") === "token_count") {
@@ -3141,24 +2888,19 @@ var CodexAgent = class extends FileSystemSessionSource {
3141
2888
  totalInputTokens += totalInput;
3142
2889
  totalOutputTokens += outputTokens + reasoningTokens;
3143
2890
  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
- }
2891
+ const tokens = {
2892
+ input: totalInput,
2893
+ output: outputTokens,
2894
+ reasoning: reasoningTokens || void 0,
2895
+ cache_read: totalCacheRead || void 0
2896
+ };
2897
+ const cost = estimateTokenCost(activeModel, tokens);
2898
+ transcript.attachUsageToLatestAssistant(tokens, {
2899
+ model: activeModel,
2900
+ cost: cost ?? void 0,
2901
+ costSource: cost === null ? void 0 : "estimated"
2902
+ });
2903
+ totalCost += cost ?? 0;
3162
2904
  }
3163
2905
  }
3164
2906
  }
@@ -3166,10 +2908,15 @@ var CodexAgent = class extends FileSystemSessionSource {
3166
2908
  } catch {
3167
2909
  }
3168
2910
  }
3169
- if (pendingPlan && currentAssistantIndex !== null) {
3170
- messages[currentAssistantIndex].parts.push(pendingPlan);
3171
- }
3172
- const cleanedMessages = cleanParsedMessages(messages);
2911
+ if (pendingPlan) transcript.appendToCurrentAssistant(pendingPlan);
2912
+ const result = transcript.finish({
2913
+ message_count: 0,
2914
+ total_input_tokens: totalInputTokens,
2915
+ total_output_tokens: totalOutputTokens,
2916
+ total_cache_read_tokens: totalCacheReadTokens || void 0,
2917
+ total_cost: totalCost,
2918
+ cost_source: totalCost > 0 ? "estimated" : void 0
2919
+ });
3173
2920
  return {
3174
2921
  id: meta.id,
3175
2922
  title: meta.title,
@@ -3177,15 +2924,8 @@ var CodexAgent = class extends FileSystemSessionSource {
3177
2924
  directory: meta.directory,
3178
2925
  time_created: meta.createdAt,
3179
2926
  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
2927
+ stats: result.stats,
2928
+ messages: result.messages
3189
2929
  };
3190
2930
  }
3191
2931
  // ---- File listing ----
@@ -3504,22 +3244,16 @@ var CodexAgent = class extends FileSystemSessionSource {
3504
3244
  return null;
3505
3245
  }
3506
3246
  // ---- Record conversion ----
3507
- convertRecord(data, messages, pendingToolCalls, sessionId, currentAssistantIndex, latestAssistantTextIndex, pendingPlan) {
3247
+ convertRecord(data, transcript, pendingPlan, activeModel) {
3508
3248
  const recordType = String(data["type"] ?? "");
3509
- if (isInternalEventType2(recordType)) {
3510
- return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan };
3511
- }
3249
+ if (isInternalEventType2(recordType)) return pendingPlan;
3512
3250
  if (recordType === "session_meta" || recordType === "event_msg") {
3513
- return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan };
3514
- }
3515
- if (recordType !== "response_item") {
3516
- return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan };
3251
+ return pendingPlan;
3517
3252
  }
3253
+ if (recordType !== "response_item") return pendingPlan;
3518
3254
  const payload = data["payload"] ?? {};
3519
3255
  const payloadType = String(payload["type"] ?? "");
3520
- if (isInternalEventType2(payloadType)) {
3521
- return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan };
3522
- }
3256
+ if (isInternalEventType2(payloadType)) return pendingPlan;
3523
3257
  const timestampMs = parseTimestampMs2(data) || parseTimestampMs2(payload);
3524
3258
  switch (payloadType) {
3525
3259
  case "message": {
@@ -3527,60 +3261,39 @@ var CodexAgent = class extends FileSystemSessionSource {
3527
3261
  if (role === "assistant") {
3528
3262
  return this.convertAssistantMessage(
3529
3263
  payload,
3530
- messages,
3264
+ transcript,
3531
3265
  timestampMs,
3532
- currentAssistantIndex,
3533
- latestAssistantTextIndex,
3534
- pendingPlan
3266
+ pendingPlan,
3267
+ activeModel
3535
3268
  );
3536
3269
  }
3537
3270
  if (role === "user") {
3538
- return this.convertUserMessage(
3539
- payload,
3540
- messages,
3541
- timestampMs,
3542
- currentAssistantIndex,
3543
- latestAssistantTextIndex,
3544
- pendingPlan
3545
- );
3271
+ return this.convertUserMessage(payload, transcript, timestampMs, pendingPlan);
3546
3272
  }
3547
3273
  break;
3548
3274
  }
3549
3275
  case "reasoning":
3550
- return this.convertReasoning(payload, messages, timestampMs, currentAssistantIndex);
3276
+ this.convertReasoning(payload, transcript, timestampMs, activeModel);
3277
+ return null;
3551
3278
  case "function_call":
3552
- return this.convertFunctionCall(
3553
- payload,
3554
- messages,
3555
- pendingToolCalls,
3556
- timestampMs,
3557
- currentAssistantIndex,
3558
- latestAssistantTextIndex
3559
- );
3279
+ this.convertFunctionCall(payload, transcript, timestampMs, activeModel);
3280
+ return null;
3560
3281
  case "function_call_output":
3561
- this.convertFunctionCallOutput(payload, messages, pendingToolCalls, timestampMs);
3562
- return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan };
3282
+ this.convertToolCallOutput(payload, transcript, timestampMs);
3283
+ return pendingPlan;
3563
3284
  case "custom_tool_call":
3564
- return this.convertCustomToolCall(
3565
- payload,
3566
- messages,
3567
- pendingToolCalls,
3568
- timestampMs,
3569
- currentAssistantIndex,
3570
- latestAssistantTextIndex
3571
- );
3285
+ this.convertCustomToolCall(payload, transcript, timestampMs, activeModel);
3286
+ return null;
3572
3287
  case "custom_tool_call_output":
3573
- this.convertCustomToolCallOutput(payload, messages, pendingToolCalls, timestampMs);
3574
- return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan };
3288
+ this.convertToolCallOutput(payload, transcript, timestampMs);
3289
+ return pendingPlan;
3575
3290
  }
3576
- return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan };
3291
+ return pendingPlan;
3577
3292
  }
3578
3293
  // ---- Assistant message ----
3579
- convertAssistantMessage(payload, messages, timestampMs, currentAssistantIndex, latestAssistantTextIndex, pendingPlan) {
3294
+ convertAssistantMessage(payload, transcript, timestampMs, pendingPlan, activeModel) {
3580
3295
  const content = payload["content"];
3581
- if (!Array.isArray(content)) {
3582
- return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan };
3583
- }
3296
+ if (!Array.isArray(content)) return pendingPlan;
3584
3297
  const textParts = [];
3585
3298
  for (const item of content) {
3586
3299
  if (typeof item !== "object" || item === null) continue;
@@ -3590,9 +3303,7 @@ var CodexAgent = class extends FileSystemSessionSource {
3590
3303
  if (text.trim()) textParts.push(text);
3591
3304
  }
3592
3305
  }
3593
- if (textParts.length === 0) {
3594
- return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan };
3595
- }
3306
+ if (textParts.length === 0) return pendingPlan;
3596
3307
  const fullText = textParts.join("\n");
3597
3308
  const planMatch = fullText.match(PROPOSED_PLAN_PATTERN);
3598
3309
  if (planMatch) {
@@ -3606,61 +3317,34 @@ var CodexAgent = class extends FileSystemSessionSource {
3606
3317
  pendingPlan = planPart;
3607
3318
  }
3608
3319
  const displayText = cleanInternalText(fullText.replace(PROPOSED_PLAN_PATTERN, ""));
3609
- if (!displayText) {
3610
- return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan };
3611
- }
3320
+ if (!displayText) return pendingPlan;
3612
3321
  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 };
3322
+ transcript.appendAssistantPart(textPart, {
3323
+ id: "",
3324
+ timestampMs,
3325
+ agent: "codex",
3326
+ model: activeModel
3327
+ });
3328
+ return pendingPlan;
3634
3329
  }
3635
3330
  // ---- User message ----
3636
- convertUserMessage(payload, messages, timestampMs, currentAssistantIndex, latestAssistantTextIndex, pendingPlan) {
3331
+ convertUserMessage(payload, transcript, timestampMs, pendingPlan) {
3637
3332
  const content = payload["content"];
3638
3333
  const text = Array.isArray(content) ? content.map(
3639
3334
  (c) => typeof c === "object" && c !== null ? String(c["text"] ?? "") : String(c ?? "")
3640
3335
  ).join(" ") : String(content ?? "");
3641
3336
  const visibleText = cleanInternalText(text);
3642
- if (!visibleText) {
3643
- return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan };
3644
- }
3645
- if (isDeveloperLikeUserMessage(visibleText)) {
3646
- return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan };
3647
- }
3337
+ if (!visibleText) return pendingPlan;
3338
+ if (isDeveloperLikeUserMessage(visibleText)) return pendingPlan;
3648
3339
  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 };
3340
+ if (pendingPlan) transcript.appendToCurrentAssistant(pendingPlan);
3341
+ transcript.appendMessage({
3342
+ id: "",
3343
+ role: "user",
3344
+ timestampMs,
3345
+ parts: [{ type: "text", text: visibleText, time_created: timestampMs }]
3346
+ });
3347
+ return null;
3664
3348
  }
3665
3349
  const subagentMatch = visibleText.match(SUBAGENT_NOTIFICATION_PATTERN);
3666
3350
  if (subagentMatch) {
@@ -3674,41 +3358,32 @@ var CodexAgent = class extends FileSystemSessionSource {
3674
3358
  text: completedText || `Subagent ${nickname} completed`,
3675
3359
  time_created: timestampMs
3676
3360
  };
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 };
3361
+ transcript.appendMessage({
3362
+ id: "",
3363
+ role: "assistant",
3364
+ timestampMs,
3365
+ parts: [textPart],
3366
+ agent: "codex",
3367
+ subagentId: agentId || void 0,
3368
+ nickname: nickname || void 0
3369
+ });
3370
+ transcript.beginTurn();
3371
+ return pendingPlan;
3691
3372
  } catch {
3692
3373
  }
3693
3374
  }
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 };
3375
+ transcript.appendMessage({
3376
+ id: "",
3377
+ role: "user",
3378
+ timestampMs,
3379
+ parts: [{ type: "text", text: visibleText, time_created: timestampMs }]
3380
+ });
3381
+ return pendingPlan;
3705
3382
  }
3706
3383
  // ---- Reasoning ----
3707
- convertReasoning(payload, messages, timestampMs, currentAssistantIndex) {
3384
+ convertReasoning(payload, transcript, timestampMs, activeModel) {
3708
3385
  const summary = payload["summary"];
3709
- if (!Array.isArray(summary)) {
3710
- return { currentAssistantIndex, latestAssistantTextIndex: null, pendingPlan: null };
3711
- }
3386
+ if (!Array.isArray(summary)) return;
3712
3387
  const texts = [];
3713
3388
  for (const item of summary) {
3714
3389
  if (typeof item === "object" && item !== null) {
@@ -3719,42 +3394,25 @@ var CodexAgent = class extends FileSystemSessionSource {
3719
3394
  }
3720
3395
  }
3721
3396
  }
3722
- if (texts.length === 0) {
3723
- return { currentAssistantIndex, latestAssistantTextIndex: null, pendingPlan: null };
3724
- }
3397
+ if (texts.length === 0) return;
3725
3398
  const reasoningText = texts.join("\n");
3726
3399
  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",
3400
+ transcript.appendAssistantPart(
3401
+ part,
3402
+ {
3403
+ id: "",
3740
3404
  timestampMs,
3741
- parts: [part],
3742
- agent: "codex"
3743
- })
3405
+ agent: "codex",
3406
+ model: activeModel
3407
+ },
3408
+ { resetLatestText: true }
3744
3409
  );
3745
- return {
3746
- currentAssistantIndex: messages.length - 1,
3747
- latestAssistantTextIndex: null,
3748
- pendingPlan: null
3749
- };
3750
3410
  }
3751
3411
  // ---- Function call ----
3752
- convertFunctionCall(payload, messages, pendingToolCalls, timestampMs, currentAssistantIndex, latestAssistantTextIndex) {
3412
+ convertFunctionCall(payload, transcript, timestampMs, activeModel) {
3753
3413
  const callId = String(payload["call_id"] ?? "").trim();
3754
3414
  const name = String(payload["name"] ?? "").trim();
3755
- if (!name) {
3756
- return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan: null };
3757
- }
3415
+ if (!name) return;
3758
3416
  const toolIdentity = resolveToolIdentity(name, payload["namespace"]);
3759
3417
  const arguments_ = normalizeToolArguments2(payload["arguments"]);
3760
3418
  const toolPart = {
@@ -3769,59 +3427,27 @@ var CodexAgent = class extends FileSystemSessionSource {
3769
3427
  },
3770
3428
  time_created: timestampMs
3771
3429
  };
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
- })
3430
+ transcript.appendToolCall(
3431
+ toolPart,
3432
+ { id: "", timestampMs, agent: "codex", model: activeModel },
3433
+ { markModeAsTool: true }
3796
3434
  );
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
3435
  }
3803
3436
  // ---- Function call output ----
3804
- convertFunctionCallOutput(payload, messages, pendingToolCalls, timestampMs) {
3437
+ convertToolCallOutput(payload, transcript, timestampMs) {
3805
3438
  const callId = String(payload["call_id"] ?? "").trim();
3806
3439
  if (!callId) return;
3807
- const location = pendingToolCalls.get(callId);
3808
- if (!location) return;
3809
3440
  const outputText = cleanInternalText(String(payload["output"] ?? ""));
3810
3441
  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
3442
  if (outputParts.length > 0) {
3814
- state.output = [...outputParts];
3815
- state.status = "completed";
3443
+ transcript.resolveToolCall(callId, { output: outputParts, status: "completed" });
3816
3444
  }
3817
3445
  }
3818
3446
  // ---- Custom tool call ----
3819
- convertCustomToolCall(payload, messages, pendingToolCalls, timestampMs, currentAssistantIndex, latestAssistantTextIndex) {
3447
+ convertCustomToolCall(payload, transcript, timestampMs, activeModel) {
3820
3448
  const callId = String(payload["call_id"] ?? "").trim();
3821
3449
  const name = String(payload["name"] ?? "").trim();
3822
- if (!name) {
3823
- return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan: null };
3824
- }
3450
+ if (!name) return;
3825
3451
  const toolIdentity = resolveToolIdentity(name, payload["namespace"]);
3826
3452
  const rawInput = payload["input"];
3827
3453
  const normalizedInput = normalizeCustomToolArguments(name, rawInput);
@@ -3837,70 +3463,87 @@ var CodexAgent = class extends FileSystemSessionSource {
3837
3463
  },
3838
3464
  time_created: timestampMs
3839
3465
  };
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
- };
3466
+ transcript.appendToolCall(
3467
+ toolPart,
3468
+ { id: "", timestampMs, agent: "codex", model: activeModel },
3469
+ { markModeAsTool: true }
3470
+ );
3471
+ }
3472
+ };
3473
+ var PerfTracer = class {
3474
+ rootMarkers = [];
3475
+ activeStack = [];
3476
+ enabled = false;
3477
+ enable() {
3478
+ this.enabled = true;
3479
+ }
3480
+ start(name) {
3481
+ const marker = {
3482
+ name,
3483
+ startTime: performance.now(),
3484
+ children: []
3485
+ };
3486
+ if (!this.enabled) return marker;
3487
+ const parent = this.activeStack[this.activeStack.length - 1];
3488
+ if (parent) {
3489
+ marker.parent = parent;
3490
+ parent.children.push(marker);
3491
+ } else {
3492
+ this.rootMarkers.push(marker);
3493
+ }
3494
+ this.activeStack.push(marker);
3495
+ return marker;
3496
+ }
3497
+ end(marker) {
3498
+ if (!this.enabled) return;
3499
+ const target = marker ?? this.activeStack[this.activeStack.length - 1];
3500
+ if (!target) return;
3501
+ target.endTime = performance.now();
3502
+ target.duration = target.endTime - target.startTime;
3503
+ while (this.activeStack.length > 0) {
3504
+ const popped = this.activeStack.pop();
3505
+ if (popped === target) break;
3506
+ }
3507
+ }
3508
+ measure(name, fn) {
3509
+ const marker = this.start(name);
3510
+ try {
3511
+ return fn();
3512
+ } finally {
3513
+ this.end(marker);
3854
3514
  }
3855
- messages.push(
3856
- this.buildMessage({
3857
- messageId: "",
3858
- role: "assistant",
3859
- timestampMs,
3860
- parts: [toolPart],
3861
- agent: "codex",
3862
- mode: "tool"
3863
- })
3864
- );
3865
- const newIndex = messages.length - 1;
3866
- if (callId) {
3867
- pendingToolCalls.set(callId, [newIndex, 0]);
3515
+ }
3516
+ async measureAsync(name, fn) {
3517
+ const marker = this.start(name);
3518
+ try {
3519
+ return await fn();
3520
+ } finally {
3521
+ this.end(marker);
3868
3522
  }
3869
- return { currentAssistantIndex: newIndex, latestAssistantTextIndex: null, pendingPlan: null };
3870
3523
  }
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";
3524
+ getReport() {
3525
+ if (!this.enabled) return "Performance tracing disabled";
3526
+ const lines = [];
3527
+ lines.push("\n=== Performance Report ===\n");
3528
+ for (const marker of this.rootMarkers) {
3529
+ this.formatMarker(marker, 0, lines);
3884
3530
  }
3531
+ return lines.join("\n");
3885
3532
  }
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
- };
3533
+ formatMarker(marker, depth, lines) {
3534
+ const indent = " ".repeat(depth);
3535
+ const duration = marker.duration?.toFixed(2) ?? "?";
3536
+ lines.push(`${indent}${marker.name}: ${duration}ms`);
3537
+ for (const child of marker.children) {
3538
+ this.formatMarker(child, depth + 1, lines);
3539
+ }
3540
+ }
3541
+ reset() {
3542
+ this.rootMarkers = [];
3543
+ this.activeStack = [];
3902
3544
  }
3903
3545
  };
3546
+ var perf = new PerfTracer();
3904
3547
  var CURSOR_TOOL_TITLE_MAP = {
3905
3548
  read_file_v2: "read",
3906
3549
  edit_file_v2: "edit",
@@ -4068,12 +3711,12 @@ var CursorAgent = class extends DatabaseSessionSource {
4068
3711
  try {
4069
3712
  const row = wsDb.prepare("SELECT value FROM ItemTable WHERE key = 'composer.composerData'").get();
4070
3713
  if (!row?.value) continue;
4071
- const parsed2 = JSON.parse(row.value);
3714
+ const parsed = JSON.parse(row.value);
4072
3715
  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;
3716
+ if (parsed !== null && typeof parsed === "object" && "allComposers" in parsed && Array.isArray(parsed["allComposers"])) {
3717
+ composers = parsed.allComposers;
3718
+ } else if (Array.isArray(parsed)) {
3719
+ composers = parsed;
4077
3720
  } else {
4078
3721
  continue;
4079
3722
  }
@@ -4452,10 +4095,10 @@ var CursorAgent = class extends DatabaseSessionSource {
4452
4095
  if (toolData.result !== void 0) {
4453
4096
  if (typeof toolData.result === "string") {
4454
4097
  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;
4098
+ const parsed = JSON.parse(toolData.result);
4099
+ state.output = parsed;
4100
+ if (parsed.error || parsed.message || parsed.stderr) {
4101
+ state.error = parsed.error || parsed.message || parsed.stderr;
4459
4102
  state.status = "error";
4460
4103
  }
4461
4104
  } catch {
@@ -4577,20 +4220,6 @@ function normalizeTextParts(content, timestampMs) {
4577
4220
  const text = cleanInternalText(contentToText(content));
4578
4221
  return text ? [{ type: "text", text, time_created: timestampMs }] : [];
4579
4222
  }
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
4223
  function getEntryTimestamp(entry) {
4595
4224
  return parseTimestampMs3(entry["timestamp"]);
4596
4225
  }
@@ -4634,29 +4263,6 @@ var PiAgent = class extends FileSystemSessionSource {
4634
4263
  if (!this.basePath) return false;
4635
4264
  return this.listSessionFiles().length > 0;
4636
4265
  }
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
4266
  listSessionSources(options) {
4661
4267
  if (!this.basePath) return [];
4662
4268
  return this.listSessionFiles(options).map((file) => ({
@@ -4676,9 +4282,8 @@ var PiAgent = class extends FileSystemSessionSource {
4676
4282
  const meta = this.sessionMetaMap.get(sessionId);
4677
4283
  if (!meta) throw new Error(`Session not found: ${sessionId}`);
4678
4284
  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);
4285
+ const parsed = this.parsePiFile(meta.sourcePath);
4286
+ const state = this.convertEntries(parsed.pathEntries);
4682
4287
  return {
4683
4288
  id: meta.id,
4684
4289
  title: meta.title,
@@ -4687,7 +4292,7 @@ var PiAgent = class extends FileSystemSessionSource {
4687
4292
  time_created: meta.createdAt,
4688
4293
  time_updated: meta.updatedAt,
4689
4294
  stats: {
4690
- message_count: cleanedMessages.length,
4295
+ message_count: state.messages.length,
4691
4296
  total_input_tokens: state.totalInputTokens,
4692
4297
  total_output_tokens: state.totalOutputTokens,
4693
4298
  total_cache_read_tokens: state.totalCacheReadTokens || void 0,
@@ -4695,7 +4300,7 @@ var PiAgent = class extends FileSystemSessionSource {
4695
4300
  total_cost: state.totalCost,
4696
4301
  cost_source: state.totalCost > 0 ? "recorded" : void 0
4697
4302
  },
4698
- messages: cleanedMessages
4303
+ messages: state.messages
4699
4304
  };
4700
4305
  }
4701
4306
  listSessionFiles(options) {
@@ -4739,18 +4344,18 @@ var PiAgent = class extends FileSystemSessionSource {
4739
4344
  return JSON.stringify([HEAD_INDEX_VERSION3, PARSER_VERSION2, stat.mtimeMs, stat.size]);
4740
4345
  }
4741
4346
  parseSessionHeadResult(filePath) {
4742
- const parsed2 = this.parsePiFile(filePath);
4743
- const state = this.convertEntries(parsed2.pathEntries);
4347
+ const parsed = this.parsePiFile(filePath);
4348
+ const state = this.convertEntries(parsed.pathEntries);
4744
4349
  const messageCount = state.messages.length;
4745
4350
  if (messageCount === 0) return filteredSession("no visible messages");
4746
4351
  const modelUsage = Object.keys(state.modelUsage).length > 0 ? state.modelUsage : void 0;
4747
4352
  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,
4353
+ id: parsed.sessionId,
4354
+ slug: `pi/${parsed.sessionId}`,
4355
+ title: parsed.title,
4356
+ directory: parsed.directory,
4357
+ time_created: parsed.createdAt,
4358
+ time_updated: parsed.updatedAt,
4754
4359
  stats: {
4755
4360
  message_count: messageCount,
4756
4361
  total_input_tokens: state.totalInputTokens,
@@ -4812,74 +4417,52 @@ var PiAgent = class extends FileSystemSessionSource {
4812
4417
  return null;
4813
4418
  }
4814
4419
  convertEntries(entries) {
4815
- const messages = [];
4816
- const pendingToolCalls = /* @__PURE__ */ new Map();
4420
+ const builder = new TranscriptBuilder({ messageDefaults: "sparse" });
4817
4421
  const modelUsage = {};
4818
- let totalInputTokens = 0;
4819
- let totalOutputTokens = 0;
4820
- let totalCacheReadTokens = 0;
4821
- let totalCacheCreateTokens = 0;
4822
- let totalCost = 0;
4823
4422
  for (const entry of entries) {
4824
4423
  const timestampMs = getEntryTimestamp(entry);
4825
4424
  const type = String(entry["type"] ?? "");
4826
4425
  if (type === "message") {
4827
4426
  const message = entry["message"];
4828
4427
  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;
4428
+ const result2 = this.convertAgentMessage(entry, message, timestampMs, builder);
4429
+ if (!result2) continue;
4430
+ if (result2.message) builder.appendMessage(result2.message);
4431
+ if (result2.model && result2.totalTokens > 0) {
4432
+ modelUsage[result2.model] = (modelUsage[result2.model] ?? 0) + result2.totalTokens;
4846
4433
  }
4847
4434
  continue;
4848
4435
  }
4849
4436
  const summary = this.convertSummaryEntry(entry, timestampMs);
4850
- if (summary) messages.push(summary);
4437
+ if (summary) builder.appendMessage(summary);
4851
4438
  }
4439
+ const result = builder.finish();
4852
4440
  return {
4853
- messages,
4854
- totalInputTokens,
4855
- totalOutputTokens,
4856
- totalCacheReadTokens,
4857
- totalCacheCreateTokens,
4858
- totalCost,
4441
+ messages: result.messages,
4442
+ totalInputTokens: result.stats.total_input_tokens,
4443
+ totalOutputTokens: result.stats.total_output_tokens,
4444
+ totalCacheReadTokens: result.stats.total_cache_read_tokens ?? 0,
4445
+ totalCacheCreateTokens: result.stats.total_cache_create_tokens ?? 0,
4446
+ totalCost: result.stats.total_cost,
4859
4447
  modelUsage
4860
4448
  };
4861
4449
  }
4862
- convertAgentMessage(entry, message, timestampMs, pendingToolCalls, nextMessageIndex, messages) {
4450
+ convertAgentMessage(entry, message, timestampMs, builder) {
4863
4451
  const id = String(entry["id"] ?? "");
4864
4452
  const role = String(message["role"] ?? "");
4865
4453
  if (role === "user") {
4866
4454
  const parts = normalizeTextParts(message["content"], timestampMs);
4867
4455
  if (parts.length === 0) return null;
4868
- return this.emptyUsageResult(buildMessage({ id, role: "user", timestampMs, parts }));
4456
+ return this.emptyUsageResult({ id, role: "user", timestampMs, parts });
4869
4457
  }
4870
4458
  if (role === "assistant") {
4871
- const parts = this.normalizeAssistantParts(
4872
- message["content"],
4873
- timestampMs,
4874
- pendingToolCalls,
4875
- nextMessageIndex
4876
- );
4459
+ const parts = this.normalizeAssistantParts(message["content"], timestampMs);
4877
4460
  if (parts.length === 0) return null;
4878
4461
  const usage = this.normalizeUsage(message["usage"]);
4879
4462
  const model = typeof message["model"] === "string" ? message["model"].trim() : null;
4880
4463
  const cost = usage.cost ?? estimateTokenCost(model, usage.tokens) ?? 0;
4881
4464
  return {
4882
- message: buildMessage({
4465
+ message: {
4883
4466
  id,
4884
4467
  role: "assistant",
4885
4468
  agent: "pi",
@@ -4890,18 +4473,13 @@ var PiAgent = class extends FileSystemSessionSource {
4890
4473
  tokens: usage.tokens,
4891
4474
  cost: cost || void 0,
4892
4475
  costSource: cost > 0 ? "recorded" : void 0
4893
- }),
4894
- inputTokens: usage.inputTokens,
4895
- outputTokens: usage.outputTokens,
4896
- cacheReadTokens: usage.cacheReadTokens,
4897
- cacheCreateTokens: usage.cacheCreateTokens,
4476
+ },
4898
4477
  totalTokens: usage.totalTokens,
4899
- cost,
4900
4478
  model
4901
4479
  };
4902
4480
  }
4903
4481
  if (role === "toolResult") {
4904
- this.attachToolResult(message, timestampMs, pendingToolCalls, messages);
4482
+ this.attachToolResult(message, timestampMs, builder);
4905
4483
  return this.emptyUsageResult();
4906
4484
  }
4907
4485
  if (role === "bashExecution") {
@@ -4910,24 +4488,22 @@ var PiAgent = class extends FileSystemSessionSource {
4910
4488
  if (role === "custom" && message["display"] === true) {
4911
4489
  const parts = normalizeTextParts(message["content"], timestampMs);
4912
4490
  if (parts.length === 0) return null;
4913
- return this.emptyUsageResult(buildMessage({ id, role: "user", timestampMs, parts }));
4491
+ return this.emptyUsageResult({ id, role: "user", timestampMs, parts });
4914
4492
  }
4915
4493
  if (role === "branchSummary" || role === "compactionSummary") {
4916
4494
  const summary = String(message["summary"] ?? "").trim();
4917
4495
  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
- );
4496
+ return this.emptyUsageResult({
4497
+ id,
4498
+ role: "assistant",
4499
+ agent: "pi",
4500
+ timestampMs,
4501
+ parts: [{ type: "text", text: summary, time_created: timestampMs }]
4502
+ });
4927
4503
  }
4928
4504
  return null;
4929
4505
  }
4930
- normalizeAssistantParts(content, timestampMs, pendingToolCalls, messageIndex) {
4506
+ normalizeAssistantParts(content, timestampMs) {
4931
4507
  if (!Array.isArray(content)) return [];
4932
4508
  const parts = [];
4933
4509
  for (const item of content) {
@@ -4958,29 +4534,26 @@ var PiAgent = class extends FileSystemSessionSource {
4958
4534
  }
4959
4535
  };
4960
4536
  parts.push(toolPart);
4961
- if (callId) pendingToolCalls.set(callId, [messageIndex, parts.length - 1]);
4962
4537
  }
4963
4538
  }
4964
4539
  return parts;
4965
4540
  }
4966
- attachToolResult(message, timestampMs, pendingToolCalls, messages) {
4541
+ attachToolResult(message, timestampMs, builder) {
4967
4542
  const callId = String(message["toolCallId"] ?? "").trim();
4968
4543
  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);
4544
+ if (!callId) return;
4545
+ builder.resolveToolCall(callId, {
4546
+ output,
4547
+ status: message["isError"] === true ? "error" : "completed",
4548
+ metadata: message["details"],
4549
+ consume: true
4550
+ });
4978
4551
  }
4979
4552
  convertBashExecution(id, message, timestampMs) {
4980
4553
  const command = String(message["command"] ?? "");
4981
4554
  const output = String(message["output"] ?? "");
4982
4555
  const isError = Number(message["exitCode"] ?? 0) !== 0 || message["cancelled"] === true;
4983
- return buildMessage({
4556
+ return {
4984
4557
  id,
4985
4558
  role: "tool",
4986
4559
  timestampMs,
@@ -5003,7 +4576,7 @@ var PiAgent = class extends FileSystemSessionSource {
5003
4576
  }
5004
4577
  }
5005
4578
  ]
5006
- });
4579
+ };
5007
4580
  }
5008
4581
  convertSummaryEntry(entry, timestampMs) {
5009
4582
  const type = entry["type"];
@@ -5014,13 +4587,13 @@ var PiAgent = class extends FileSystemSessionSource {
5014
4587
  const rawText = type === "custom_message" ? contentToText(entry["content"]) : String(entry["summary"] ?? "");
5015
4588
  const text = cleanInternalText(rawText);
5016
4589
  if (!text) return null;
5017
- return buildMessage({
4590
+ return {
5018
4591
  id: String(entry["id"] ?? ""),
5019
4592
  role: type === "custom_message" ? "user" : "assistant",
5020
4593
  agent: type === "custom_message" ? void 0 : "pi",
5021
4594
  timestampMs,
5022
4595
  parts: [{ type: "text", text, time_created: timestampMs }]
5023
- });
4596
+ };
5024
4597
  }
5025
4598
  normalizeUsage(raw) {
5026
4599
  const usage = isObject(raw) ? raw : {};
@@ -5050,12 +4623,7 @@ var PiAgent = class extends FileSystemSessionSource {
5050
4623
  emptyUsageResult(message) {
5051
4624
  return {
5052
4625
  message,
5053
- inputTokens: 0,
5054
- outputTokens: 0,
5055
- cacheReadTokens: 0,
5056
- cacheCreateTokens: 0,
5057
4626
  totalTokens: 0,
5058
- cost: 0,
5059
4627
  model: null
5060
4628
  };
5061
4629
  }
@@ -5076,44 +4644,30 @@ var ZCodeAgent = class extends OpenCodeSqliteAgent {
5076
4644
  }
5077
4645
  };
5078
4646
  registerAgent({
5079
- name: "claudecode",
5080
- displayName: "Claude Code",
5081
4647
  icon: "/icon/agent/claudecode.svg",
5082
4648
  create: () => new ClaudeCodeAgent()
5083
4649
  });
5084
4650
  registerAgent({
5085
- name: "opencode",
5086
- displayName: "OpenCode",
5087
4651
  icon: "/icon/agent/opencode.svg",
5088
4652
  create: () => new OpenCodeAgent()
5089
4653
  });
5090
4654
  registerAgent({
5091
- name: "zcode",
5092
- displayName: "ZCode",
5093
4655
  icon: "/icon/agent/zcode.svg",
5094
4656
  create: () => new ZCodeAgent()
5095
4657
  });
5096
4658
  registerAgent({
5097
- name: "kimi",
5098
- displayName: "Kimi-Cli",
5099
4659
  icon: "/icon/agent/kimi.svg",
5100
4660
  create: () => new KimiAgent()
5101
4661
  });
5102
4662
  registerAgent({
5103
- name: "codex",
5104
- displayName: "Codex",
5105
4663
  icon: "/icon/agent/codex.svg",
5106
4664
  create: () => new CodexAgent()
5107
4665
  });
5108
4666
  registerAgent({
5109
- name: "pi",
5110
- displayName: "Pi",
5111
4667
  icon: "/icon/agent/pi.svg",
5112
4668
  create: () => new PiAgent()
5113
4669
  });
5114
4670
  registerAgent({
5115
- name: "cursor",
5116
- displayName: "Cursor",
5117
4671
  icon: "/icon/agent/cursor.svg",
5118
4672
  create: () => new CursorAgent()
5119
4673
  });
@@ -6103,7 +5657,7 @@ function buildSessionContentFromMessages(title, messages) {
6103
5657
  }
6104
5658
  return chunks.join("\n");
6105
5659
  }
6106
- var CACHE_SCHEMA_VERSION = 13;
5660
+ var CACHE_SCHEMA_VERSION = 14;
6107
5661
  function withCacheDb(fn) {
6108
5662
  const cachePath = getCachePath2();
6109
5663
  const db = openDb(cachePath);
@@ -6376,6 +5930,7 @@ function createSearchTables(db) {
6376
5930
  activity_time INTEGER NOT NULL,
6377
5931
  content_text TEXT NOT NULL,
6378
5932
  content_hash TEXT NOT NULL,
5933
+ indexed_message_count INTEGER NOT NULL,
6379
5934
  indexed_at INTEGER NOT NULL,
6380
5935
  UNIQUE(agent_name, session_id)
6381
5936
  );
@@ -6409,6 +5964,24 @@ function createSearchTriggers(db) {
6409
5964
  END;
6410
5965
  `);
6411
5966
  }
5967
+ function addIndexedMessageCount(db) {
5968
+ if (!tableExists(db, "session_documents")) return;
5969
+ if (!columnExists(db, "session_documents", "indexed_message_count")) {
5970
+ db.exec(
5971
+ "ALTER TABLE session_documents ADD COLUMN indexed_message_count INTEGER NOT NULL DEFAULT 0"
5972
+ );
5973
+ }
5974
+ if (!tableExists(db, "messages")) return;
5975
+ db.exec(`
5976
+ UPDATE session_documents
5977
+ SET indexed_message_count = (
5978
+ SELECT COUNT(*)
5979
+ FROM messages
5980
+ WHERE messages.agent_name = session_documents.agent_name
5981
+ AND messages.session_id = session_documents.session_id
5982
+ )
5983
+ `);
5984
+ }
6412
5985
  function dropSearchTriggers(db) {
6413
5986
  db.exec(`
6414
5987
  DROP TRIGGER IF EXISTS session_documents_ai;
@@ -6514,6 +6087,9 @@ function readLegacyCacheVersion(db) {
6514
6087
  return Number(versionRow?.value ?? 0);
6515
6088
  }
6516
6089
  function inferCacheSchemaVersion(db) {
6090
+ if (columnExists(db, "session_documents", "indexed_message_count")) {
6091
+ return 14;
6092
+ }
6517
6093
  if (tableExists(db, "message_tools")) {
6518
6094
  return 11;
6519
6095
  }
@@ -6948,7 +6524,8 @@ function ensureSchema(db, dbPath) {
6948
6524
  refreshProjectIdentities(db2);
6949
6525
  }
6950
6526
  },
6951
- { version: 13, migrate: createCacheTables }
6527
+ { version: 13, migrate: createCacheTables },
6528
+ { version: 14, migrate: addIndexedMessageCount }
6952
6529
  ]
6953
6530
  });
6954
6531
  createLatestCacheSchema(db);
@@ -7110,6 +6687,9 @@ function searchIndexStateFromRows(indexedRows, messageCountRows) {
7110
6687
  contentHashBySessionId: new Map(
7111
6688
  indexedRows.map((row) => [String(row.session_id), String(row.content_hash ?? "")])
7112
6689
  ),
6690
+ indexedMessageCountBySessionId: new Map(
6691
+ indexedRows.map((row) => [String(row.session_id), Number(row.indexed_message_count ?? 0)])
6692
+ ),
7113
6693
  messageCountBySessionId: new Map(
7114
6694
  messageCountRows.map((row) => [String(row.session_id), Number(row.value ?? 0)])
7115
6695
  )
@@ -7127,19 +6707,27 @@ function readSearchIndexState(db, agentName, sessionIds) {
7127
6707
  SELECT
7128
6708
  requested.session_id,
7129
6709
  documents.content_hash,
6710
+ documents.indexed_message_count,
7130
6711
  COUNT(messages.message_index) AS value
7131
6712
  FROM requested_session_ids AS requested
7132
6713
  LEFT JOIN session_documents AS documents
7133
6714
  ON documents.agent_name = ? AND documents.session_id = requested.session_id
7134
6715
  LEFT JOIN messages
7135
6716
  ON messages.agent_name = ? AND messages.session_id = requested.session_id
7136
- GROUP BY requested.session_id, documents.content_hash
6717
+ GROUP BY
6718
+ requested.session_id,
6719
+ documents.content_hash,
6720
+ documents.indexed_message_count
7137
6721
  `
7138
6722
  ).all(...batch, agentName, agentName);
7139
6723
  rows.push(...batchRows);
7140
6724
  }
7141
6725
  return searchIndexStateFromRows(rows, rows);
7142
6726
  }
6727
+ function searchIndexEntryNeedsUpdate(state, session) {
6728
+ const sessionId = session.id;
6729
+ return state.contentHashBySessionId.get(sessionId) !== sessionContentHash(session) || state.indexedMessageCountBySessionId.get(sessionId) !== (state.messageCountBySessionId.get(sessionId) ?? 0);
6730
+ }
7143
6731
  function loadSearchIndexEntry(agentName, change, loadSessionData) {
7144
6732
  try {
7145
6733
  const data = loadSessionData(change.session.id);
@@ -7163,6 +6751,12 @@ function loadSearchIndexEntry(agentName, change, loadSessionData) {
7163
6751
  return null;
7164
6752
  }
7165
6753
  }
6754
+ function* loadSearchIndexEntries(agentName, changes, loadSessionData) {
6755
+ for (const change of changes) {
6756
+ const entry = loadSearchIndexEntry(agentName, change, loadSessionData);
6757
+ if (entry) yield entry;
6758
+ }
6759
+ }
7166
6760
  function writeSearchIndexRows(db, agentName, removedSessionIds, entries) {
7167
6761
  const deleteRow = db.prepare(
7168
6762
  "DELETE FROM session_documents WHERE agent_name = ? AND session_id = ?"
@@ -7234,8 +6828,9 @@ function writeSearchIndexRows(db, agentName, removedSessionIds, entries) {
7234
6828
  activity_time,
7235
6829
  content_text,
7236
6830
  content_hash,
6831
+ indexed_message_count,
7237
6832
  indexed_at
7238
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
6833
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
7239
6834
  ON CONFLICT(agent_name, session_id) DO UPDATE SET
7240
6835
  slug = excluded.slug,
7241
6836
  title = excluded.title,
@@ -7248,6 +6843,7 @@ function writeSearchIndexRows(db, agentName, removedSessionIds, entries) {
7248
6843
  activity_time = excluded.activity_time,
7249
6844
  content_text = excluded.content_text,
7250
6845
  content_hash = excluded.content_hash,
6846
+ indexed_message_count = excluded.indexed_message_count,
7251
6847
  indexed_at = excluded.indexed_at
7252
6848
  `);
7253
6849
  for (const sessionId of new Set(removedSessionIds)) {
@@ -7256,6 +6852,7 @@ function writeSearchIndexRows(db, agentName, removedSessionIds, entries) {
7256
6852
  deleteMessageTools.run(agentName, sessionId, 0);
7257
6853
  deleteMessages.run(agentName, sessionId, 0);
7258
6854
  }
6855
+ let indexed = 0;
7259
6856
  for (const entry of entries) {
7260
6857
  const activityTime = entry.session.time_updated ?? entry.session.time_created;
7261
6858
  upsertSessionRow(upsertIndexedSession, agentName, entry.session, null, entry.sortIndex, null);
@@ -7303,16 +6900,19 @@ function writeSearchIndexRows(db, agentName, removedSessionIds, entries) {
7303
6900
  activityTime,
7304
6901
  entry.contentText,
7305
6902
  entry.contentHash,
6903
+ entry.messages.length,
7306
6904
  Date.now()
7307
6905
  );
6906
+ indexed += 1;
7308
6907
  }
6908
+ return indexed;
7309
6909
  }
7310
6910
  function syncSessionSearchIndex(agentName, sessions, loadSessionData, options = {}) {
7311
6911
  return withCacheDb((db) => {
7312
6912
  ensureFtsConsistency(db);
7313
6913
  const startedAt = performance.now();
7314
6914
  const existingRows = db.prepare(
7315
- "SELECT session_id, content_hash FROM session_documents WHERE agent_name = ? ORDER BY id"
6915
+ "SELECT session_id, content_hash, indexed_message_count FROM session_documents WHERE agent_name = ? ORDER BY id"
7316
6916
  ).all(agentName);
7317
6917
  const sessionSortIndexMap = new Map(sessions.map((session, index) => [session.id, index]));
7318
6918
  const messageCountRows = db.prepare(
@@ -7322,20 +6922,25 @@ function syncSessionSearchIndex(agentName, sessions, loadSessionData, options =
7322
6922
  const sessionMap = new Map(sessions.map((session) => [session.id, session]));
7323
6923
  const toDelete = existingRows.map((row) => String(row.session_id)).filter((sessionId) => !sessionMap.has(sessionId));
7324
6924
  const toUpsert = sessions.filter(
7325
- (session) => searchIndexState.contentHashBySessionId.get(session.id) !== sessionContentHash(session) || searchIndexState.messageCountBySessionId.get(session.id) !== session.stats.message_count
6925
+ (session) => searchIndexEntryNeedsUpdate(searchIndexState, session)
7326
6926
  );
7327
6927
  const changedCount = toDelete.length + toUpsert.length;
7328
6928
  const isBulk = shouldBulkSyncSearchIndex(options, changedCount);
7329
- const loaded = toUpsert.map(
7330
- (session) => loadSearchIndexEntry(
6929
+ const changes = toUpsert.map((session) => ({
6930
+ session,
6931
+ sortIndex: sessionSortIndexMap.get(session.id) ?? 0
6932
+ }));
6933
+ let indexed = 0;
6934
+ const writeRows = () => {
6935
+ indexed = writeSearchIndexRows(
6936
+ db,
7331
6937
  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);
6938
+ toDelete,
6939
+ loadSearchIndexEntries(agentName, changes, loadSessionData)
6940
+ );
6941
+ };
7337
6942
  let rebuildDurationMs;
7338
- const needsRebuild = isBulk && (toDelete.length > 0 || loaded.length > 0);
6943
+ const needsRebuild = isBulk && changedCount > 0;
7339
6944
  if (needsRebuild) {
7340
6945
  db.transaction(() => {
7341
6946
  dropSearchTriggers(db);
@@ -7357,8 +6962,8 @@ function syncSessionSearchIndex(agentName, sessions, loadSessionData, options =
7357
6962
  sessions: sessions.length,
7358
6963
  changed: toUpsert.length,
7359
6964
  deleted: toDelete.length,
7360
- indexed: loaded.length,
7361
- skipped: toUpsert.length - loaded.length,
6965
+ indexed,
6966
+ skipped: toUpsert.length - indexed,
7362
6967
  durationMs: performance.now() - startedAt,
7363
6968
  rebuildDurationMs
7364
6969
  };
@@ -7386,15 +6991,22 @@ function syncSessionSearchIndexChanges(agentName, changes, removedSessionIds, lo
7386
6991
  changes.map(({ session }) => session.id)
7387
6992
  );
7388
6993
  const toUpsert = changes.filter(
7389
- ({ session }) => (searchIndexState.contentHashBySessionId.get(session.id) ?? "") !== sessionContentHash(session) || (searchIndexState.messageCountBySessionId.get(session.id) ?? 0) !== session.stats.message_count
6994
+ ({ session }) => searchIndexEntryNeedsUpdate(searchIndexState, session)
7390
6995
  );
7391
6996
  const uniqueRemovedSessionIds = Array.from(new Set(removedSessionIds));
7392
6997
  const changedCount = uniqueRemovedSessionIds.length + toUpsert.length;
7393
6998
  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);
6999
+ let indexed = 0;
7000
+ const writeRows = () => {
7001
+ indexed = writeSearchIndexRows(
7002
+ db,
7003
+ agentName,
7004
+ uniqueRemovedSessionIds,
7005
+ loadSearchIndexEntries(agentName, toUpsert, loadSessionData)
7006
+ );
7007
+ };
7396
7008
  let rebuildDurationMs;
7397
- const needsRebuild = isBulk && (uniqueRemovedSessionIds.length > 0 || loaded.length > 0);
7009
+ const needsRebuild = isBulk && changedCount > 0;
7398
7010
  if (needsRebuild) {
7399
7011
  db.transaction(() => {
7400
7012
  dropSearchTriggers(db);
@@ -7416,8 +7028,8 @@ function syncSessionSearchIndexChanges(agentName, changes, removedSessionIds, lo
7416
7028
  sessions: changes.length,
7417
7029
  changed: toUpsert.length,
7418
7030
  deleted: uniqueRemovedSessionIds.length,
7419
- indexed: loaded.length,
7420
- skipped: toUpsert.length - loaded.length,
7031
+ indexed,
7032
+ skipped: toUpsert.length - indexed,
7421
7033
  durationMs: performance.now() - startedAt,
7422
7034
  rebuildDurationMs
7423
7035
  };
@@ -7431,26 +7043,26 @@ function mergeSearchLists(left, right) {
7431
7043
  return values.length > 0 ? [...new Set(values)] : void 0;
7432
7044
  }
7433
7045
  function mergeSearchQueryOptions(query, options) {
7434
- const parsed2 = parseSearchQuery(query);
7046
+ const parsed = parseSearchQuery(query);
7435
7047
  return {
7436
- text: parsed2.text || (parsed2.hasQualifiers ? "" : query.trim()),
7048
+ text: parsed.text || (parsed.hasQualifiers ? "" : query.trim()),
7437
7049
  options: {
7438
7050
  ...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
7051
+ agent: options.agent ?? parsed.filters.agent,
7052
+ project: options.project ?? parsed.filters.project,
7053
+ projectKind: options.projectKind ?? parsed.filters.projectKind,
7054
+ projectKey: options.projectKey ?? parsed.filters.projectKey,
7055
+ cwd: options.cwd ?? parsed.filters.cwd,
7056
+ tags: mergeSearchLists(options.tags, parsed.filters.tags),
7057
+ tools: mergeSearchLists(options.tools, parsed.filters.tools),
7058
+ file: options.file ?? parsed.filters.file,
7059
+ fileKind: options.fileKind ?? parsed.filters.fileKind,
7060
+ costMin: options.costMin ?? parsed.filters.costMin,
7061
+ costMax: options.costMax ?? parsed.filters.costMax,
7062
+ costMinExclusive: options.costMinExclusive ?? parsed.filters.costMinExclusive,
7063
+ costMaxExclusive: options.costMaxExclusive ?? parsed.filters.costMaxExclusive
7452
7064
  },
7453
- parsed: parsed2
7065
+ parsed
7454
7066
  };
7455
7067
  }
7456
7068
  function sessionMatchesSearchCost(session, options) {
@@ -8504,6 +8116,47 @@ async function ensureSessionTags(agent, sessions, workerUrl) {
8504
8116
  return ensureSessionTagsSync(agent, sessions);
8505
8117
  }
8506
8118
  }
8119
+ async function finalizeAgentScan(agent, sessions, context) {
8120
+ const { finalization, options, timing, agentStart, onProgress } = context;
8121
+ const isIncremental = finalization.kind === "incremental";
8122
+ if (!isIncremental) {
8123
+ onProgress?.({ agent: agent.name, phase: "complete", newCount: sessions.length });
8124
+ }
8125
+ const identityStart = performance.now();
8126
+ const sessionsWithIdentity = attachMissingProjectIdentities(sessions);
8127
+ timing.identity = performance.now() - identityStart;
8128
+ let tagged = { sessions: sessionsWithIdentity, changed: false };
8129
+ if (finalization.kind !== "cache-only") {
8130
+ const tagsStart = performance.now();
8131
+ tagged = options.includeSmartTags === false ? tagged : await ensureSessionTags(agent, sessionsWithIdentity, options.smartTagWorkerUrl);
8132
+ timing.tags = performance.now() - tagsStart;
8133
+ }
8134
+ if (options.writeCache !== false) {
8135
+ if (finalization.kind === "incremental") {
8136
+ saveCachedSessionDiff(
8137
+ agent,
8138
+ finalization.cached.sessions,
8139
+ tagged.sessions,
8140
+ finalization.changedIds
8141
+ );
8142
+ } else if (finalization.kind === "unchanged" && tagged.changed) {
8143
+ saveCachedSessionDiff(agent, finalization.cached.sessions, tagged.sessions);
8144
+ }
8145
+ }
8146
+ if (isIncremental) {
8147
+ onProgress?.({ agent: agent.name, phase: "complete", newCount: tagged.sessions.length });
8148
+ }
8149
+ const heads = filterSessions(tagged.sessions, options);
8150
+ timing.total = performance.now() - agentStart;
8151
+ return {
8152
+ agent,
8153
+ heads,
8154
+ fromCache: true,
8155
+ ...isIncremental ? { refreshed: true } : {},
8156
+ timing,
8157
+ cacheTimestamp: isIncremental ? finalization.cacheTimestamp : finalization.cached.timestamp
8158
+ };
8159
+ }
8507
8160
  async function scanAgentSmart(agent, options, onProgress) {
8508
8161
  const agentStart = performance.now();
8509
8162
  const timing = { total: 0 };
@@ -8524,19 +8177,13 @@ async function scanAgentSmart(agent, options, onProgress) {
8524
8177
  phase: "cache",
8525
8178
  cachedCount: cached.sessions.length
8526
8179
  });
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,
8180
+ return finalizeAgentScan(agent, cached.sessions, {
8181
+ finalization: { kind: "cache-only", cached },
8182
+ options,
8537
8183
  timing,
8538
- cacheTimestamp: cached.timestamp
8539
- };
8184
+ agentStart,
8185
+ onProgress
8186
+ });
8540
8187
  }
8541
8188
  const isAvail = agent.isAvailable();
8542
8189
  if (!isAvail) {
@@ -8564,55 +8211,26 @@ async function scanAgentSmart(agent, options, onProgress) {
8564
8211
  agent.incrementalScan(cached.sessions, checkResult.changedIds || [])
8565
8212
  );
8566
8213
  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,
8214
+ return finalizeAgentScan(agent, updatedSessions, {
8215
+ finalization: {
8216
+ kind: "incremental",
8217
+ cached,
8218
+ changedIds: checkResult.changedIds ?? [],
8219
+ cacheTimestamp: checkResult.timestamp
8220
+ },
8221
+ options,
8593
8222
  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);
8223
+ agentStart,
8224
+ onProgress
8225
+ });
8606
8226
  }
8607
- const filtered2 = filterSessions(tagged.sessions, options);
8608
- timing.total = performance.now() - agentStart;
8609
- return {
8610
- agent,
8611
- heads: filtered2,
8612
- fromCache: true,
8227
+ return finalizeAgentScan(agent, cached.sessions, {
8228
+ finalization: { kind: "unchanged", cached },
8229
+ options,
8613
8230
  timing,
8614
- cacheTimestamp: cached.timestamp
8615
- };
8231
+ agentStart,
8232
+ onProgress
8233
+ });
8616
8234
  }
8617
8235
  }
8618
8236
  if (options.cacheOnly) {
@@ -8660,9 +8278,9 @@ async function scanAgentFull(agent, options, onProgress, timing = { total: 0 },
8660
8278
  markAgentFullSyncCompleted(agent.name);
8661
8279
  }
8662
8280
  onProgress?.({ agent: agent.name, phase: "complete", newCount: tagged.sessions.length });
8663
- const filtered2 = filterSessions(tagged.sessions, options);
8281
+ const filtered = filterSessions(tagged.sessions, options);
8664
8282
  timing.total = performance.now() - agentStart;
8665
- return { agent, heads: filtered2, fromCache: false, timing };
8283
+ return { agent, heads: filtered, fromCache: false, timing };
8666
8284
  } catch (err) {
8667
8285
  console.error(`Error scanning ${agent.name}:`, err);
8668
8286
  return { agent, heads: [], fromCache: false };
@@ -8710,67 +8328,35 @@ async function scanSessions(options = {}, onProgress) {
8710
8328
  async function scanSessionsAsync(options = {}, onProgress) {
8711
8329
  return scanSessions(options, onProgress);
8712
8330
  }
8713
- var BOOKMARK_DB_FILENAME = "state.db";
8714
- var BOOKMARK_SCHEMA_VERSION = 1;
8331
+ var STATE_DB_FILENAME = "state.db";
8332
+ var STATE_SCHEMA_VERSION = 2;
8715
8333
  var MEMORY_STATE_STORE = "memory";
8716
- var memoryBookmarks = /* @__PURE__ */ new Map();
8717
- var BookmarkStorageUnavailableError = class extends Error {
8334
+ var StateStorageUnavailableError = class extends Error {
8718
8335
  constructor() {
8719
8336
  super("SQLite state database is unavailable");
8720
- this.name = "BookmarkStorageUnavailableError";
8337
+ this.name = "StateStorageUnavailableError";
8721
8338
  }
8722
8339
  };
8723
8340
  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") {
8341
+ if (process.env.CODESESH_STATE_DIR) return process.env.CODESESH_STATE_DIR;
8342
+ const currentPlatform = platform2();
8343
+ if (currentPlatform === "darwin") {
8729
8344
  return join12(homedir5(), "Library", "Application Support", "codesesh");
8730
8345
  }
8731
- if (p === "win32") {
8346
+ if (currentPlatform === "win32") {
8732
8347
  const appData = process.env.APPDATA ?? process.env.LOCALAPPDATA;
8733
8348
  return join12(appData ?? join12(homedir5(), "AppData", "Roaming"), "codesesh");
8734
8349
  }
8735
8350
  return join12(process.env.XDG_DATA_HOME ?? join12(homedir5(), ".local", "share"), "codesesh");
8736
8351
  }
8737
8352
  function getStateDbPath() {
8738
- return join12(getStateDir(), BOOKMARK_DB_FILENAME);
8353
+ return join12(getStateDir(), STATE_DB_FILENAME);
8739
8354
  }
8740
8355
  function useMemoryStateStore() {
8741
8356
  return process.env.CODESESH_STATE_STORE === MEMORY_STATE_STORE;
8742
8357
  }
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) {
8358
+ function createBookmarksTable(db) {
8768
8359
  db.exec(`
8769
- CREATE TABLE IF NOT EXISTS state_meta (
8770
- key TEXT PRIMARY KEY,
8771
- value TEXT NOT NULL
8772
- );
8773
-
8774
8360
  CREATE TABLE IF NOT EXISTS bookmarks (
8775
8361
  agent_name TEXT NOT NULL,
8776
8362
  session_id TEXT NOT NULL,
@@ -8785,6 +8371,27 @@ function createStateSchema(db) {
8785
8371
  );
8786
8372
  `);
8787
8373
  }
8374
+ function createSessionAliasesTable(db) {
8375
+ db.exec(`
8376
+ CREATE TABLE IF NOT EXISTS session_aliases (
8377
+ agent_name TEXT NOT NULL,
8378
+ session_id TEXT NOT NULL,
8379
+ alias TEXT NOT NULL,
8380
+ updated_at INTEGER NOT NULL,
8381
+ PRIMARY KEY (agent_name, session_id)
8382
+ );
8383
+ `);
8384
+ }
8385
+ function createStateSchema(db) {
8386
+ db.exec(`
8387
+ CREATE TABLE IF NOT EXISTS state_meta (
8388
+ key TEXT PRIMARY KEY,
8389
+ value TEXT NOT NULL
8390
+ );
8391
+ `);
8392
+ createBookmarksTable(db);
8393
+ createSessionAliasesTable(db);
8394
+ }
8788
8395
  function readLegacyStateVersion(db) {
8789
8396
  if (!tableExists(db, "state_meta") || !columnExists(db, "state_meta", "key") || !columnExists(db, "state_meta", "value")) {
8790
8397
  return 0;
@@ -8794,13 +8401,9 @@ function readLegacyStateVersion(db) {
8794
8401
  }
8795
8402
  function getCurrentStateSchemaVersion(db) {
8796
8403
  const userVersion = getUserVersion(db);
8797
- if (userVersion > 0) {
8798
- return userVersion;
8799
- }
8404
+ if (userVersion > 0) return userVersion;
8800
8405
  const legacyVersion = readLegacyStateVersion(db);
8801
- if (legacyVersion > 0) {
8802
- return legacyVersion;
8803
- }
8406
+ if (legacyVersion > 0) return legacyVersion;
8804
8407
  return tableExists(db, "bookmarks") ? 1 : 0;
8805
8408
  }
8806
8409
  function hasAnyStateSchema(db) {
@@ -8808,14 +8411,14 @@ function hasAnyStateSchema(db) {
8808
8411
  }
8809
8412
  function setStateSchemaVersion(db) {
8810
8413
  createStateSchema(db);
8811
- setUserVersion(db, BOOKMARK_SCHEMA_VERSION);
8414
+ setUserVersion(db, STATE_SCHEMA_VERSION);
8812
8415
  db.prepare(
8813
8416
  `
8814
8417
  INSERT INTO state_meta(key, value)
8815
8418
  VALUES ('version', ?)
8816
8419
  ON CONFLICT(key) DO UPDATE SET value = excluded.value
8817
8420
  `
8818
- ).run(String(BOOKMARK_SCHEMA_VERSION));
8421
+ ).run(String(STATE_SCHEMA_VERSION));
8819
8422
  }
8820
8423
  function ensureSchema2(db, dbPath) {
8821
8424
  const currentVersion = getCurrentStateSchemaVersion(db);
@@ -8826,22 +8429,22 @@ function ensureSchema2(db, dbPath) {
8826
8429
  runSchemaMigrations(db, {
8827
8430
  dbPath,
8828
8431
  currentVersion,
8829
- targetVersion: BOOKMARK_SCHEMA_VERSION,
8432
+ targetVersion: STATE_SCHEMA_VERSION,
8830
8433
  backupLabel: "state-migration",
8831
- backupTables: ["bookmarks"],
8832
- migrations: [{ version: 1, migrate: createStateSchema }]
8434
+ backupTables: ["bookmarks", "session_aliases"],
8435
+ migrations: [
8436
+ { version: 1, migrate: createBookmarksTable },
8437
+ { version: 2, migrate: createSessionAliasesTable }
8438
+ ]
8833
8439
  });
8834
- createStateSchema(db);
8835
- if (getUserVersion(db) <= BOOKMARK_SCHEMA_VERSION) {
8440
+ if (currentVersion <= STATE_SCHEMA_VERSION) {
8836
8441
  setStateSchemaVersion(db);
8837
8442
  }
8838
8443
  }
8839
8444
  function withStateDb(fn) {
8840
8445
  const statePath = getStateDbPath();
8841
8446
  const db = openDb(statePath);
8842
- if (!db) {
8843
- throw new BookmarkStorageUnavailableError();
8844
- }
8447
+ if (!db) throw new StateStorageUnavailableError();
8845
8448
  try {
8846
8449
  ensureSchema2(db, statePath);
8847
8450
  return fn(db);
@@ -8849,6 +8452,31 @@ function withStateDb(fn) {
8849
8452
  db.close();
8850
8453
  }
8851
8454
  }
8455
+ var memoryBookmarks = /* @__PURE__ */ new Map();
8456
+ function getBookmarkKey(agentKey, sessionId) {
8457
+ return JSON.stringify([agentKey, sessionId]);
8458
+ }
8459
+ function getActivityTime(bookmark) {
8460
+ return bookmark.time_updated ?? bookmark.time_created;
8461
+ }
8462
+ function sortBookmarks(bookmarks) {
8463
+ return bookmarks.sort((a, b) => {
8464
+ const activityDelta = getActivityTime(b) - getActivityTime(a);
8465
+ return activityDelta || b.bookmarked_at - a.bookmarked_at;
8466
+ });
8467
+ }
8468
+ function listMemoryBookmarks() {
8469
+ return sortBookmarks(Array.from(memoryBookmarks.values()));
8470
+ }
8471
+ function upsertMemoryBookmark(bookmark) {
8472
+ const key = getBookmarkKey(bookmark.agentKey, bookmark.sessionId);
8473
+ const saved = {
8474
+ ...bookmark,
8475
+ bookmarked_at: memoryBookmarks.get(key)?.bookmarked_at ?? Date.now()
8476
+ };
8477
+ memoryBookmarks.set(key, saved);
8478
+ return saved;
8479
+ }
8852
8480
  function toBookmarkRecord(row) {
8853
8481
  return {
8854
8482
  agentKey: String(row.agent_name ?? ""),
@@ -9021,6 +8649,78 @@ function deleteBookmark(agentKey, sessionId) {
9021
8649
  ).run(agentKey, sessionId);
9022
8650
  });
9023
8651
  }
8652
+ var SESSION_ALIAS_MAX_LENGTH = 160;
8653
+ var memoryAliases = /* @__PURE__ */ new Map();
8654
+ function getAliasKey(agentKey, sessionId) {
8655
+ return JSON.stringify([agentKey, sessionId]);
8656
+ }
8657
+ function toSessionAlias(row) {
8658
+ return {
8659
+ agentKey: String(row.agent_name ?? ""),
8660
+ sessionId: String(row.session_id ?? ""),
8661
+ alias: String(row.alias ?? ""),
8662
+ updated_at: Number(row.updated_at ?? 0)
8663
+ };
8664
+ }
8665
+ function normalizeSessionAlias(value) {
8666
+ const alias = value.trim();
8667
+ if (!alias || alias.length > SESSION_ALIAS_MAX_LENGTH) return null;
8668
+ return alias;
8669
+ }
8670
+ function listSessionAliases() {
8671
+ if (useMemoryStateStore()) return [...memoryAliases.values()];
8672
+ return withStateDb(
8673
+ (db) => db.prepare(
8674
+ `
8675
+ SELECT agent_name, session_id, alias, updated_at
8676
+ FROM session_aliases
8677
+ ORDER BY updated_at DESC
8678
+ `
8679
+ ).all().map(toSessionAlias)
8680
+ );
8681
+ }
8682
+ function upsertSessionAlias(agentKey, sessionId, alias) {
8683
+ const normalizedAlias = normalizeSessionAlias(alias);
8684
+ if (!normalizedAlias) {
8685
+ throw new TypeError("Invalid session alias");
8686
+ }
8687
+ const saved = {
8688
+ agentKey,
8689
+ sessionId,
8690
+ alias: normalizedAlias,
8691
+ updated_at: Date.now()
8692
+ };
8693
+ if (useMemoryStateStore()) {
8694
+ memoryAliases.set(getAliasKey(agentKey, sessionId), saved);
8695
+ return saved;
8696
+ }
8697
+ return withStateDb((db) => {
8698
+ db.prepare(
8699
+ `
8700
+ INSERT INTO session_aliases(agent_name, session_id, alias, updated_at)
8701
+ VALUES (?, ?, ?, ?)
8702
+ ON CONFLICT(agent_name, session_id) DO UPDATE SET
8703
+ alias = excluded.alias,
8704
+ updated_at = excluded.updated_at
8705
+ `
8706
+ ).run(saved.agentKey, saved.sessionId, saved.alias, saved.updated_at);
8707
+ return saved;
8708
+ });
8709
+ }
8710
+ function deleteSessionAlias(agentKey, sessionId) {
8711
+ if (useMemoryStateStore()) {
8712
+ memoryAliases.delete(getAliasKey(agentKey, sessionId));
8713
+ return;
8714
+ }
8715
+ withStateDb((db) => {
8716
+ db.prepare(
8717
+ `
8718
+ DELETE FROM session_aliases
8719
+ WHERE agent_name = ? AND session_id = ?
8720
+ `
8721
+ ).run(agentKey, sessionId);
8722
+ });
8723
+ }
9024
8724
  var DASHBOARD_RECENT_LIMIT = 10;
9025
8725
  function getTotalTokens(stats) {
9026
8726
  return stats.total_tokens ?? stats.total_input_tokens + stats.total_output_tokens;
@@ -9235,8 +8935,8 @@ function searchRecentSessions(snapshot, options) {
9235
8935
  matchType: "recent"
9236
8936
  }));
9237
8937
  }
9238
- function deriveFileQuery(query, parsed2, options) {
9239
- return options.file ?? (!parsed2.text ? parsed2.filters.file : void 0) ?? (!parsed2.hasQualifiers && query ? parsed2.text || query : "");
8938
+ function deriveFileQuery(query, parsed, options) {
8939
+ return options.file ?? (!parsed.text ? parsed.filters.file : void 0) ?? (!parsed.hasQualifiers && query ? parsed.text || query : "");
9240
8940
  }
9241
8941
  function mergeSearchResultSources(results, limit) {
9242
8942
  const seen = /* @__PURE__ */ new Set();
@@ -9255,8 +8955,8 @@ function canSkipSessionsSearch(fileQuery, textQuery, options) {
9255
8955
  fileQuery && !textQuery && !options.tools?.length && !options.tags?.length && options.from == null && options.to == null
9256
8956
  );
9257
8957
  }
9258
- function searchIndexedSessions(query, textQuery, parsed2, options) {
9259
- const fileQuery = deriveFileQuery(query, parsed2, options);
8958
+ function searchIndexedSessions(query, textQuery, parsed, options) {
8959
+ const fileQuery = deriveFileQuery(query, parsed, options);
9260
8960
  const fileResults = fileQuery ? searchFileActivitySessions(fileQuery, options) : [];
9261
8961
  const sessionResults = canSkipSessionsSearch(fileQuery, textQuery, options) ? [] : searchSessions(query, options);
9262
8962
  return mergeSearchResultSources([...fileResults, ...sessionResults], options.limit ?? 50);
@@ -9286,16 +8986,12 @@ export {
9286
8986
  normalizeTitleText,
9287
8987
  basenameTitle,
9288
8988
  resolveSessionTitle,
9289
- parsed,
9290
- skipped,
9291
- filtered,
9292
8989
  cleanInternalText,
9293
8990
  cleanMessagePart,
9294
8991
  cleanMessageParts,
9295
8992
  cleanParsedMessage,
9296
8993
  cleanParsedMessages,
9297
8994
  firstUserMessageTitle,
9298
- perf,
9299
8995
  getPricingRegistry,
9300
8996
  hasBillablePricing,
9301
8997
  refreshPricingCache,
@@ -9308,6 +9004,7 @@ export {
9308
9004
  openDbReadOnly,
9309
9005
  openDb,
9310
9006
  isSqliteAvailable,
9007
+ perf,
9311
9008
  fallbackDisplayName,
9312
9009
  realFs,
9313
9010
  isProjectIdentityKind,
@@ -9329,6 +9026,7 @@ export {
9329
9026
  parseSearchQuery,
9330
9027
  syncSessionSearchIndex,
9331
9028
  syncSessionSearchIndexChanges,
9029
+ mergeSearchQueryOptions,
9332
9030
  searchSessions,
9333
9031
  listFileActivity,
9334
9032
  listSessionFileActivity,
@@ -9353,11 +9051,16 @@ export {
9353
9051
  ensureSessionTagsSync,
9354
9052
  scanSessions,
9355
9053
  scanSessionsAsync,
9356
- BookmarkStorageUnavailableError,
9054
+ StateStorageUnavailableError,
9357
9055
  listBookmarks,
9358
9056
  upsertBookmark,
9359
9057
  importBookmarks,
9360
9058
  deleteBookmark,
9059
+ SESSION_ALIAS_MAX_LENGTH,
9060
+ normalizeSessionAlias,
9061
+ listSessionAliases,
9062
+ upsertSessionAlias,
9063
+ deleteSessionAlias,
9361
9064
  DASHBOARD_RECENT_LIMIT,
9362
9065
  getTotalTokens,
9363
9066
  getSessionAgentName,
@@ -9367,4 +9070,4 @@ export {
9367
9070
  buildDashboard,
9368
9071
  executeSessionSearch
9369
9072
  };
9370
- //# sourceMappingURL=chunk-BV65IEWZ.js.map
9073
+ //# sourceMappingURL=chunk-VRVZJDNL.js.map