codesesh 0.12.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.
@@ -1,15 +1,29 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // ../core/dist/chunk-M5ISIPFR.mjs
4
+ function compareSessionActivityDesc(a, b) {
5
+ return (b.time_updated ?? b.time_created) - (a.time_updated ?? a.time_created);
6
+ }
7
+ function sortSessionsByActivity(sessions) {
8
+ for (let index = 1; index < sessions.length; index += 1) {
9
+ if (compareSessionActivityDesc(sessions[index - 1], sessions[index]) > 0) {
10
+ return [...sessions].sort(compareSessionActivityDesc);
11
+ }
12
+ }
13
+ return [...sessions];
14
+ }
15
+
3
16
  // ../core/dist/index.mjs
4
- import { existsSync as existsSync4, readdirSync, readFileSync as readFileSync3, statSync as statSync2 } from "fs";
17
+ import { existsSync as existsSync4, readdirSync, readFileSync as readFileSync2, statSync as statSync2 } from "fs";
5
18
  import { join as join3, basename as basename2, dirname } from "path";
6
19
  import { existsSync, statSync } from "fs";
7
20
  import { existsSync as existsSync2 } from "fs";
8
21
  import { homedir, platform } from "os";
9
22
  import { join } from "path";
10
- import { readFileSync } from "fs";
23
+ import { closeSync, openSync, readSync } from "fs";
24
+ import { StringDecoder } from "string_decoder";
11
25
  import { basename } from "path";
12
- import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
26
+ import { existsSync as existsSync3, mkdirSync, readFileSync, writeFileSync } from "fs";
13
27
  import { homedir as homedir2 } from "os";
14
28
  import { join as join2 } from "path";
15
29
  import { join as join5 } from "path";
@@ -17,26 +31,26 @@ import { existsSync as existsSync5, mkdirSync as mkdirSync2 } from "fs";
17
31
  import { basename as basename3, dirname as dirname2, join as join4 } from "path";
18
32
  import { createRequire } from "module";
19
33
  import { createHash } from "crypto";
20
- import { existsSync as existsSync6, readFileSync as readFileSync4, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
34
+ import { existsSync as existsSync6, readFileSync as readFileSync3, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
21
35
  import { join as join6, basename as basename4, dirname as dirname3 } from "path";
22
36
  import {
23
- closeSync,
37
+ closeSync as closeSync2,
24
38
  existsSync as existsSync7,
25
- openSync,
26
- readFileSync as readFileSync5,
27
- readSync,
39
+ openSync as openSync2,
40
+ readFileSync as readFileSync4,
41
+ readSync as readSync2,
28
42
  readdirSync as readdirSync3,
29
43
  statSync as statSync4
30
44
  } from "fs";
31
45
  import { join as join7, basename as basename5 } from "path";
32
- import { existsSync as existsSync8, readdirSync as readdirSync4, readFileSync as readFileSync6, statSync as statSync5 } from "fs";
46
+ import { existsSync as existsSync8, readdirSync as readdirSync4, readFileSync as readFileSync5, statSync as statSync5 } from "fs";
33
47
  import { join as join8, normalize } from "path";
34
- import { existsSync as existsSync9, readFileSync as readFileSync7, readdirSync as readdirSync5, statSync as statSync6 } from "fs";
48
+ import { existsSync as existsSync9, readFileSync as readFileSync6, readdirSync as readdirSync5, statSync as statSync6 } from "fs";
35
49
  import { basename as basename6, join as join9 } from "path";
36
50
  import { join as join10 } from "path";
37
51
  import { availableParallelism } from "os";
38
52
  import { Worker } from "worker_threads";
39
- import { existsSync as existsSync10, readFileSync as readFileSync8 } from "fs";
53
+ import { existsSync as existsSync10, readFileSync as readFileSync7 } from "fs";
40
54
  import { spawnSync } from "child_process";
41
55
  import * as os from "os";
42
56
  import * as path from "path";
@@ -58,15 +72,18 @@ function getRegisteredAgents() {
58
72
  return registrations;
59
73
  }
60
74
  function getAgentInfoMap(sessionsByAgent) {
61
- return registrations.map((r) => ({
62
- name: r.name,
63
- displayName: r.displayName,
64
- icon: r.icon,
65
- count: sessionsByAgent[r.name] ?? 0
66
- }));
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
+ });
67
84
  }
68
85
  function getAgentByName(name) {
69
- return registrations.find((r) => r.name === name);
86
+ return registrations.find((registration) => registration.create().name === name);
70
87
  }
71
88
  function parsedSession(session) {
72
89
  return { status: "parsed", data: session };
@@ -92,6 +109,26 @@ var BaseAgent = class {
92
109
  };
93
110
  var FileSystemSessionSource = class extends BaseAgent {
94
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
+ }
95
132
  getSessionMetaMap() {
96
133
  return this.sessionMetaMap;
97
134
  }
@@ -244,6 +281,7 @@ function getZCodeDataPath() {
244
281
  }
245
282
  return null;
246
283
  }
284
+ var READ_CHUNK_BYTES = 1 << 20;
247
285
  function* parseJsonlLines(content) {
248
286
  for (const line of content.split("\n")) {
249
287
  const trimmed = line.trim();
@@ -254,9 +292,35 @@ function* parseJsonlLines(content) {
254
292
  }
255
293
  }
256
294
  }
257
- function readJsonlFile(filePath) {
258
- const content = readFileSync(filePath, "utf-8");
259
- return parseJsonlLines(content);
295
+ function* readJsonlFileLines(filePath, chunkBytes = READ_CHUNK_BYTES) {
296
+ const fd = openSync(filePath, "r");
297
+ try {
298
+ const buffer = Buffer.alloc(chunkBytes);
299
+ const decoder = new StringDecoder("utf8");
300
+ let remainder = "";
301
+ let bytesRead = readSync(fd, buffer, 0, chunkBytes, -1);
302
+ while (bytesRead > 0) {
303
+ const lines = (remainder + decoder.write(buffer.subarray(0, bytesRead))).split("\n");
304
+ remainder = lines.pop();
305
+ for (const line of lines) {
306
+ const trimmed = line.trim();
307
+ if (trimmed) yield trimmed;
308
+ }
309
+ bytesRead = readSync(fd, buffer, 0, chunkBytes, -1);
310
+ }
311
+ const tail = (remainder + decoder.end()).trim();
312
+ if (tail) yield tail;
313
+ } finally {
314
+ closeSync(fd);
315
+ }
316
+ }
317
+ function* readJsonlFile(filePath) {
318
+ for (const line of readJsonlFileLines(filePath)) {
319
+ try {
320
+ yield JSON.parse(line);
321
+ } catch {
322
+ }
323
+ }
260
324
  }
261
325
  var INTERNAL_TAGS = [
262
326
  "command-message",
@@ -339,15 +403,6 @@ function resolveSessionTitle(explicit, message, directory) {
339
403
  }
340
404
  return UNTITLED_SESSION;
341
405
  }
342
- function parsed(data) {
343
- return { status: "parsed", data };
344
- }
345
- function skipped(reason) {
346
- return reason ? { status: "skipped", reason } : { status: "skipped" };
347
- }
348
- function filtered(reason) {
349
- return reason ? { status: "filtered", reason } : { status: "filtered" };
350
- }
351
406
  function isInternalEventType2(value) {
352
407
  return isInternalEventType(value);
353
408
  }
@@ -416,80 +471,6 @@ function firstUserMessageTitle(messages) {
416
471
  }
417
472
  return null;
418
473
  }
419
- var PerfTracer = class {
420
- rootMarkers = [];
421
- activeStack = [];
422
- enabled = false;
423
- enable() {
424
- this.enabled = true;
425
- }
426
- start(name) {
427
- const marker = {
428
- name,
429
- startTime: performance.now(),
430
- children: []
431
- };
432
- if (!this.enabled) return marker;
433
- const parent = this.activeStack[this.activeStack.length - 1];
434
- if (parent) {
435
- marker.parent = parent;
436
- parent.children.push(marker);
437
- } else {
438
- this.rootMarkers.push(marker);
439
- }
440
- this.activeStack.push(marker);
441
- return marker;
442
- }
443
- end(marker) {
444
- if (!this.enabled) return;
445
- const target = marker ?? this.activeStack[this.activeStack.length - 1];
446
- if (!target) return;
447
- target.endTime = performance.now();
448
- target.duration = target.endTime - target.startTime;
449
- while (this.activeStack.length > 0) {
450
- const popped = this.activeStack.pop();
451
- if (popped === target) break;
452
- }
453
- }
454
- measure(name, fn) {
455
- const marker = this.start(name);
456
- try {
457
- return fn();
458
- } finally {
459
- this.end(marker);
460
- }
461
- }
462
- async measureAsync(name, fn) {
463
- const marker = this.start(name);
464
- try {
465
- return await fn();
466
- } finally {
467
- this.end(marker);
468
- }
469
- }
470
- getReport() {
471
- if (!this.enabled) return "Performance tracing disabled";
472
- const lines = [];
473
- lines.push("\n=== Performance Report ===\n");
474
- for (const marker of this.rootMarkers) {
475
- this.formatMarker(marker, 0, lines);
476
- }
477
- return lines.join("\n");
478
- }
479
- formatMarker(marker, depth, lines) {
480
- const indent = " ".repeat(depth);
481
- const duration = marker.duration?.toFixed(2) ?? "?";
482
- lines.push(`${indent}${marker.name}: ${duration}ms`);
483
- for (const child of marker.children) {
484
- this.formatMarker(child, depth + 1, lines);
485
- }
486
- }
487
- reset() {
488
- this.rootMarkers = [];
489
- this.activeStack = [];
490
- }
491
- };
492
- var perf = new PerfTracer();
493
474
  var aliases_default = {
494
475
  "anthropic--claude-4.6-opus": "claude-opus-4-6",
495
476
  "anthropic--claude-4.6-sonnet": "claude-sonnet-4-6",
@@ -638,7 +619,7 @@ function loadDiskCache() {
638
619
  const path2 = getCachePath();
639
620
  if (!existsSync3(path2)) return;
640
621
  try {
641
- const cached = JSON.parse(readFileSync2(path2, "utf-8"));
622
+ const cached = JSON.parse(readFileSync(path2, "utf-8"));
642
623
  if (Date.now() - cached.timestamp <= CACHE_TTL_MS) {
643
624
  const next = loadSnapshot();
644
625
  for (const [name, rawPricing] of Object.entries(cached.data)) {
@@ -661,7 +642,7 @@ async function refreshPricingCache() {
661
642
  const path2 = getCachePath();
662
643
  if (existsSync3(path2)) {
663
644
  try {
664
- const cached = JSON.parse(readFileSync2(path2, "utf-8"));
645
+ const cached = JSON.parse(readFileSync(path2, "utf-8"));
665
646
  if (typeof cached.timestamp === "number" && Date.now() - cached.timestamp <= CACHE_TTL_MS) {
666
647
  return false;
667
648
  }
@@ -813,6 +794,179 @@ function withEstimatedSessionCost(stats, model) {
813
794
  function estimateTokenCost(model, tokens) {
814
795
  return estimateCostForTokens(model, tokens)?.cost ?? null;
815
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
+ };
816
970
  var HEAD_INDEX_VERSION = "claudecode-head-v2";
817
971
  function parseTimestampMs(data) {
818
972
  const raw = String(data["timestamp"] ?? "").trim();
@@ -867,48 +1021,6 @@ var ClaudeCodeAgent = class extends FileSystemSessionSource {
867
1021
  }
868
1022
  return false;
869
1023
  }
870
- scan(options) {
871
- if (!this.basePath) return [];
872
- const scanMarker = perf.start("claudecode:scan");
873
- const heads = [];
874
- const listMarker = perf.start("listProjectDirs");
875
- const projectDirs = this.listProjectDirs();
876
- perf.end(listMarker);
877
- const filesByProject = projectDirs.map((projectDir) => {
878
- const fileMarker = perf.start(`listJsonlFiles:${basename2(projectDir)}`);
879
- const files = this.listJsonlFiles(projectDir).filter((file) => {
880
- try {
881
- return matchesScanWindow(statSync2(file).mtimeMs, options);
882
- } catch {
883
- return false;
884
- }
885
- });
886
- perf.end(fileMarker);
887
- return { projectDir, files };
888
- });
889
- const totalFiles = filesByProject.reduce((total, item) => total + item.files.length, 0);
890
- options?.onProgress?.({ total: totalFiles, processed: 0, sessions: 0 });
891
- let processed = 0;
892
- for (const { projectDir, files } of filesByProject) {
893
- for (const file of files) {
894
- try {
895
- const parseMarker = perf.start(`parseSessionHead:${basename2(file)}`);
896
- const head = getParsedSession(this.parseSessionHeadResult(file, projectDir));
897
- perf.end(parseMarker);
898
- if (head) {
899
- heads.push(head);
900
- this.sessionMetaMap.set(head.id, this.buildSessionMeta(head, file, projectDir));
901
- }
902
- } catch {
903
- } finally {
904
- processed += 1;
905
- options?.onProgress?.({ total: totalFiles, processed, sessions: heads.length });
906
- }
907
- }
908
- }
909
- perf.end(scanMarker);
910
- return heads;
911
- }
912
1024
  listSessionSources(options) {
913
1025
  if (!this.basePath) return [];
914
1026
  const refs = [];
@@ -945,43 +1057,24 @@ var ClaudeCodeAgent = class extends FileSystemSessionSource {
945
1057
  if (!existsSync4(meta.sourcePath)) {
946
1058
  throw new Error(`Session file missing: ${meta.sourcePath}`);
947
1059
  }
948
- const content = readFileSync3(meta.sourcePath, "utf-8");
949
- const messages = [];
950
- const pendingToolCalls = /* @__PURE__ */ new Map();
1060
+ const content = readFileSync2(meta.sourcePath, "utf-8");
1061
+ const builder = new TranscriptBuilder();
951
1062
  const ignoredToolCallIds = /* @__PURE__ */ new Set();
952
1063
  const assistantUuidToToolCalls = /* @__PURE__ */ new Map();
953
1064
  const countedUsageKeys = /* @__PURE__ */ new Set();
954
- const assistantState = {
955
- currentIndex: null,
956
- latestTextIndex: null
957
- };
958
- let totalCost = 0;
959
- let totalInputTokens = 0;
960
- let totalOutputTokens = 0;
961
- let totalCacheRead = 0;
962
- let totalCacheCreate = 0;
963
1065
  for (const record of parseJsonlLines(content)) {
964
1066
  try {
965
1067
  this.convertRecord(
966
1068
  record,
967
- messages,
968
- pendingToolCalls,
1069
+ builder,
969
1070
  ignoredToolCallIds,
970
1071
  assistantUuidToToolCalls,
971
- countedUsageKeys,
972
- assistantState
1072
+ countedUsageKeys
973
1073
  );
974
1074
  } catch {
975
1075
  }
976
1076
  }
977
- const cleanedMessages = cleanParsedMessages(messages);
978
- for (const msg of cleanedMessages) {
979
- totalCost += msg.cost ?? 0;
980
- totalInputTokens += msg.tokens?.input ?? 0;
981
- totalOutputTokens += msg.tokens?.output ?? 0;
982
- totalCacheRead += msg.tokens?.cache_read ?? 0;
983
- totalCacheCreate += msg.tokens?.cache_create ?? 0;
984
- }
1077
+ const transcript = builder.finish();
985
1078
  return {
986
1079
  id: meta.id,
987
1080
  title: meta.title,
@@ -990,16 +1083,8 @@ var ClaudeCodeAgent = class extends FileSystemSessionSource {
990
1083
  version: void 0,
991
1084
  time_created: meta.createdAt,
992
1085
  time_updated: meta.updatedAt,
993
- stats: {
994
- message_count: cleanedMessages.length,
995
- total_input_tokens: totalInputTokens,
996
- total_output_tokens: totalOutputTokens,
997
- total_cost: totalCost,
998
- cost_source: totalCost > 0 ? "estimated" : void 0,
999
- total_cache_read_tokens: totalCacheRead,
1000
- total_cache_create_tokens: totalCacheCreate
1001
- },
1002
- messages: cleanedMessages
1086
+ stats: transcript.stats,
1087
+ messages: transcript.messages
1003
1088
  };
1004
1089
  }
1005
1090
  // --- Private helpers ---
@@ -1066,7 +1151,7 @@ var ClaudeCodeAgent = class extends FileSystemSessionSource {
1066
1151
  const map = /* @__PURE__ */ new Map();
1067
1152
  if (existsSync4(indexPath)) {
1068
1153
  try {
1069
- const data = JSON.parse(readFileSync3(indexPath, "utf-8"));
1154
+ const data = JSON.parse(readFileSync2(indexPath, "utf-8"));
1070
1155
  const entries = data?.entries ?? [];
1071
1156
  for (const entry of entries) {
1072
1157
  const sid = entry?.sessionId;
@@ -1085,7 +1170,7 @@ var ClaudeCodeAgent = class extends FileSystemSessionSource {
1085
1170
  return getParsedSession(this.parseSessionHeadResult(filePath, projectDir));
1086
1171
  }
1087
1172
  parseSessionHeadResult(filePath, projectDir) {
1088
- const content = readFileSync3(filePath, "utf-8");
1173
+ const content = readFileSync2(filePath, "utf-8");
1089
1174
  const lines = content.split("\n").filter((l) => l.trim());
1090
1175
  if (lines.length === 0) return skippedSession("empty file");
1091
1176
  const sessionId = basename2(filePath, ".jsonl");
@@ -1210,41 +1295,30 @@ var ClaudeCodeAgent = class extends FileSystemSessionSource {
1210
1295
  return null;
1211
1296
  }
1212
1297
  // --- Record conversion ---
1213
- convertRecord(data, messages, pendingToolCalls, ignoredToolCallIds, assistantUuidToToolCalls, countedUsageKeys, assistantState) {
1298
+ convertRecord(data, builder, ignoredToolCallIds, assistantUuidToToolCalls, countedUsageKeys) {
1214
1299
  if (data["isMeta"] === true) return;
1215
1300
  const msgType = String(data["type"] ?? "");
1216
1301
  if (isInternalEventType(msgType)) return;
1217
1302
  if (msgType === "assistant") {
1218
1303
  this.convertAssistantRecord(
1219
1304
  data,
1220
- messages,
1221
- pendingToolCalls,
1305
+ builder,
1222
1306
  ignoredToolCallIds,
1223
1307
  assistantUuidToToolCalls,
1224
- countedUsageKeys,
1225
- assistantState
1308
+ countedUsageKeys
1226
1309
  );
1227
1310
  } else if (msgType === "user") {
1228
- this.convertUserRecord(
1229
- data,
1230
- messages,
1231
- pendingToolCalls,
1232
- ignoredToolCallIds,
1233
- assistantUuidToToolCalls,
1234
- assistantState
1235
- );
1311
+ this.convertUserRecord(data, builder, ignoredToolCallIds, assistantUuidToToolCalls);
1236
1312
  } else if (msgType === "tool_result") {
1237
- this.convertToolResultRecord(data, messages, assistantState);
1313
+ this.convertToolResultRecord(data, builder);
1238
1314
  }
1239
1315
  }
1240
- convertAssistantRecord(data, messages, pendingToolCalls, ignoredToolCallIds, assistantUuidToToolCalls, countedUsageKeys, assistantState) {
1316
+ convertAssistantRecord(data, builder, ignoredToolCallIds, assistantUuidToToolCalls, countedUsageKeys) {
1241
1317
  const msg = data["message"] ?? {};
1242
1318
  const timestampMs = parseTimestampMs(data);
1243
1319
  const rawContent = msg["content"] ?? [];
1244
1320
  const uuid = String(data["uuid"] ?? "");
1245
1321
  const toolCallIds = [];
1246
- let currentAssistantIndex = assistantState.currentIndex;
1247
- let latestAssistantTextIndex = assistantState.latestTextIndex;
1248
1322
  if (Array.isArray(rawContent)) {
1249
1323
  for (const item of rawContent) {
1250
1324
  if (!item || typeof item !== "object") continue;
@@ -1253,23 +1327,28 @@ var ClaudeCodeAgent = class extends FileSystemSessionSource {
1253
1327
  if (partType === "thinking") {
1254
1328
  const text = cleanInternalText(String(part["thinking"] ?? ""));
1255
1329
  if (text) {
1256
- currentAssistantIndex = this.appendAssistantReasoning(
1257
- messages,
1258
- { messageId: uuid, data, msg, timestampMs, text, countedUsageKeys },
1259
- currentAssistantIndex
1330
+ const message2 = builder.appendAssistantPart(
1331
+ this.buildReasoningPart(text, timestampMs),
1332
+ { id: uuid, timestampMs, agent: "claude" },
1333
+ { deduplicateTail: true }
1260
1334
  );
1335
+ this.applyAssistantMetadata(message2, data, msg, countedUsageKeys);
1261
1336
  }
1262
1337
  continue;
1263
1338
  }
1264
1339
  if (partType === "text") {
1265
1340
  const text = cleanInternalText(String(part["text"] ?? ""));
1266
1341
  if (text) {
1267
- currentAssistantIndex = this.appendAssistantText(
1268
- messages,
1269
- { messageId: uuid, data, msg, timestampMs, text, countedUsageKeys },
1270
- currentAssistantIndex
1342
+ const message2 = builder.appendAssistantPart(
1343
+ this.buildTextPart(text, timestampMs),
1344
+ {
1345
+ id: uuid,
1346
+ timestampMs,
1347
+ agent: "claude"
1348
+ },
1349
+ { deduplicateTail: true }
1271
1350
  );
1272
- latestAssistantTextIndex = currentAssistantIndex;
1351
+ this.applyAssistantMetadata(message2, data, msg, countedUsageKeys);
1273
1352
  }
1274
1353
  continue;
1275
1354
  }
@@ -1281,18 +1360,13 @@ var ClaudeCodeAgent = class extends FileSystemSessionSource {
1281
1360
  continue;
1282
1361
  }
1283
1362
  const toolPart = this.buildToolPart(part, timestampMs);
1284
- const [msgIndex, partIndex] = this.attachToolCallToLatestAssistant(messages, {
1285
- messageId: uuid,
1286
- data,
1287
- msg,
1288
- timestampMs,
1363
+ const message = builder.appendToolCall(
1289
1364
  toolPart,
1290
- latestTextIndex: latestAssistantTextIndex,
1291
- countedUsageKeys
1292
- });
1293
- currentAssistantIndex = msgIndex;
1365
+ { id: uuid, timestampMs, agent: "claude" },
1366
+ { modeOnCreate: "tool" }
1367
+ );
1368
+ this.applyAssistantMetadata(message, data, msg, countedUsageKeys);
1294
1369
  if (toolCallId) {
1295
- pendingToolCalls.set(toolCallId, [msgIndex, partIndex]);
1296
1370
  toolCallIds.push(toolCallId);
1297
1371
  }
1298
1372
  }
@@ -1300,10 +1374,8 @@ var ClaudeCodeAgent = class extends FileSystemSessionSource {
1300
1374
  if (toolCallIds.length > 0) {
1301
1375
  assistantUuidToToolCalls.set(uuid, toolCallIds);
1302
1376
  }
1303
- assistantState.currentIndex = currentAssistantIndex;
1304
- assistantState.latestTextIndex = latestAssistantTextIndex;
1305
1377
  }
1306
- convertUserRecord(data, messages, pendingToolCalls, ignoredToolCallIds, assistantUuidToToolCalls, assistantState) {
1378
+ convertUserRecord(data, builder, ignoredToolCallIds, assistantUuidToToolCalls) {
1307
1379
  const msg = data["message"] ?? {};
1308
1380
  const timestampMs = parseTimestampMs(data);
1309
1381
  const content = msg["content"] ?? "";
@@ -1311,18 +1383,14 @@ var ClaudeCodeAgent = class extends FileSystemSessionSource {
1311
1383
  if (typeof content === "string") {
1312
1384
  const parts = this.normalizeUserTextParts(content, timestampMs);
1313
1385
  if (parts.length === 0) {
1314
- assistantState.currentIndex = null;
1315
- assistantState.latestTextIndex = null;
1386
+ builder.beginTurn();
1316
1387
  return;
1317
1388
  }
1318
- messages.push(this.buildMessage({ messageId: uuid, role: "user", timestampMs, parts }));
1319
- assistantState.currentIndex = null;
1320
- assistantState.latestTextIndex = null;
1389
+ builder.appendMessage({ id: uuid, role: "user", timestampMs, parts });
1321
1390
  return;
1322
1391
  }
1323
1392
  if (!Array.isArray(content)) {
1324
- assistantState.currentIndex = null;
1325
- assistantState.latestTextIndex = null;
1393
+ builder.beginTurn();
1326
1394
  return;
1327
1395
  }
1328
1396
  const visibleParts = this.normalizeUserTextParts(content, timestampMs);
@@ -1334,13 +1402,7 @@ var ClaudeCodeAgent = class extends FileSystemSessionSource {
1334
1402
  const toolCallId = this.resolveToolCallId(data, ci, assistantUuidToToolCalls);
1335
1403
  if (toolCallId && ignoredToolCallIds.has(toolCallId)) continue;
1336
1404
  const outputParts = this.normalizeClaudeToolOutput(ci["content"], timestampMs);
1337
- if (this.backfillToolOutput(
1338
- messages,
1339
- pendingToolCalls,
1340
- toolCallId,
1341
- outputParts,
1342
- toolStateUpdates
1343
- )) {
1405
+ if (this.backfillToolOutput(builder, toolCallId, outputParts, toolStateUpdates)) {
1344
1406
  continue;
1345
1407
  }
1346
1408
  const fallback = this.buildFallbackToolMessage({
@@ -1349,17 +1411,14 @@ var ClaudeCodeAgent = class extends FileSystemSessionSource {
1349
1411
  toolCallId,
1350
1412
  outputParts
1351
1413
  });
1352
- if (fallback) messages.push(fallback);
1414
+ if (fallback) builder.appendMessage(fallback);
1353
1415
  }
1354
1416
  if (visibleParts.length > 0) {
1355
- messages.push(
1356
- this.buildMessage({ messageId: uuid, role: "user", timestampMs, parts: visibleParts })
1357
- );
1417
+ builder.appendMessage({ id: uuid, role: "user", timestampMs, parts: visibleParts });
1358
1418
  }
1359
- assistantState.currentIndex = null;
1360
- assistantState.latestTextIndex = null;
1419
+ builder.beginTurn();
1361
1420
  }
1362
- convertToolResultRecord(data, messages, assistantState) {
1421
+ convertToolResultRecord(data, builder) {
1363
1422
  const timestampMs = parseTimestampMs(data);
1364
1423
  const msg = data["message"] ?? {};
1365
1424
  const outputParts = this.normalizeClaudeToolOutput(msg["content"], timestampMs);
@@ -1370,25 +1429,8 @@ var ClaudeCodeAgent = class extends FileSystemSessionSource {
1370
1429
  toolCallId: null,
1371
1430
  outputParts
1372
1431
  });
1373
- if (fallback) messages.push(fallback);
1374
- assistantState.currentIndex = null;
1375
- assistantState.latestTextIndex = null;
1376
- }
1377
- // --- Message building ---
1378
- buildMessage(opts) {
1379
- return {
1380
- id: opts.messageId,
1381
- role: opts.role,
1382
- agent: opts.agent ?? null,
1383
- time_created: opts.timestampMs,
1384
- mode: opts.mode ?? null,
1385
- model: opts.model ?? null,
1386
- provider: opts.provider ?? null,
1387
- tokens: opts.tokens ? opts.tokens : void 0,
1388
- cost: opts.cost ?? 0,
1389
- cost_source: opts.cost_source,
1390
- parts: opts.parts
1391
- };
1432
+ if (fallback) builder.appendMessage(fallback);
1433
+ builder.beginTurn();
1392
1434
  }
1393
1435
  buildTextPart(text, timestampMs) {
1394
1436
  return { type: "text", text, time_created: timestampMs };
@@ -1431,71 +1473,6 @@ var ClaudeCodeAgent = class extends FileSystemSessionSource {
1431
1473
  }
1432
1474
  }
1433
1475
  }
1434
- // --- Assistant message grouping ---
1435
- appendAssistantReasoning(messages, opts, currentIndex) {
1436
- const part = this.buildReasoningPart(opts.text, opts.timestampMs);
1437
- if (currentIndex !== null) {
1438
- const message2 = messages[currentIndex];
1439
- const hasText = message2.parts.some((p) => p.type === "text");
1440
- const hasTool = message2.parts.some((p) => p.type === "tool");
1441
- if (!hasText && !hasTool) {
1442
- this.appendPartIfNew(message2, part);
1443
- this.applyAssistantMetadata(message2, opts.data, opts.msg, opts.countedUsageKeys);
1444
- return currentIndex;
1445
- }
1446
- }
1447
- const message = this.buildMessage({
1448
- messageId: opts.messageId,
1449
- role: "assistant",
1450
- timestampMs: opts.timestampMs,
1451
- parts: [part],
1452
- agent: "claude"
1453
- });
1454
- this.applyAssistantMetadata(message, opts.data, opts.msg, opts.countedUsageKeys);
1455
- messages.push(message);
1456
- return messages.length - 1;
1457
- }
1458
- appendAssistantText(messages, opts, currentIndex) {
1459
- const part = this.buildTextPart(opts.text, opts.timestampMs);
1460
- if (currentIndex !== null) {
1461
- const message2 = messages[currentIndex];
1462
- const hasTool = message2.parts.some((p) => p.type === "tool");
1463
- if (!hasTool) {
1464
- this.appendPartIfNew(message2, part);
1465
- this.applyAssistantMetadata(message2, opts.data, opts.msg, opts.countedUsageKeys);
1466
- return currentIndex;
1467
- }
1468
- }
1469
- const message = this.buildMessage({
1470
- messageId: opts.messageId,
1471
- role: "assistant",
1472
- timestampMs: opts.timestampMs,
1473
- parts: [part],
1474
- agent: "claude"
1475
- });
1476
- this.applyAssistantMetadata(message, opts.data, opts.msg, opts.countedUsageKeys);
1477
- messages.push(message);
1478
- return messages.length - 1;
1479
- }
1480
- attachToolCallToLatestAssistant(messages, opts) {
1481
- if (opts.latestTextIndex !== null) {
1482
- const message2 = messages[opts.latestTextIndex];
1483
- message2.parts.push(opts.toolPart);
1484
- this.applyAssistantMetadata(message2, opts.data, opts.msg, opts.countedUsageKeys);
1485
- return [opts.latestTextIndex, message2.parts.length - 1];
1486
- }
1487
- const message = this.buildMessage({
1488
- messageId: opts.messageId,
1489
- role: "assistant",
1490
- timestampMs: opts.timestampMs,
1491
- parts: [opts.toolPart],
1492
- agent: "claude",
1493
- mode: "tool"
1494
- });
1495
- this.applyAssistantMetadata(message, opts.data, opts.msg, opts.countedUsageKeys);
1496
- messages.push(message);
1497
- return [messages.length - 1, 0];
1498
- }
1499
1476
  // --- User content normalization ---
1500
1477
  normalizeUserTextParts(content, timestampMs) {
1501
1478
  if (typeof content === "string") {
@@ -1543,29 +1520,19 @@ var ClaudeCodeAgent = class extends FileSystemSessionSource {
1543
1520
  return text ? [this.buildTextPart(text, timestampMs)] : [];
1544
1521
  }
1545
1522
  // --- Tool backfill ---
1546
- backfillToolOutput(messages, pendingToolCalls, callId, outputParts, stateUpdates) {
1523
+ backfillToolOutput(builder, callId, outputParts, stateUpdates) {
1547
1524
  if (!callId) return false;
1548
- const location = pendingToolCalls.get(callId);
1549
- if (location === void 0) return false;
1550
- const [msgIndex, partIndex] = location;
1551
- const state = messages[msgIndex].parts[partIndex].state ?? (messages[msgIndex].parts[partIndex].state = {});
1552
- if (outputParts.length > 0) {
1553
- const existing = state.output;
1554
- if (Array.isArray(existing)) {
1555
- existing.push(...outputParts);
1556
- } else if (existing === null || existing === void 0) {
1557
- state.output = [...outputParts];
1558
- } else {
1559
- 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];
1560
1532
  }
1561
- }
1562
- if (stateUpdates) {
1563
- Object.assign(state, stateUpdates);
1564
- }
1565
- if (outputParts.length > 0 && !state.status) {
1566
- state.status = "completed";
1567
- }
1568
- return outputParts.length > 0 || !!stateUpdates;
1533
+ if (stateUpdates) Object.assign(state, stateUpdates);
1534
+ if (outputParts.length > 0 && !state.status) state.status = "completed";
1535
+ });
1569
1536
  }
1570
1537
  resolveToolCallId(data, item, assistantUuidToToolCalls) {
1571
1538
  const directId = String(item["tool_use_id"] ?? "").trim();
@@ -1593,25 +1560,17 @@ var ClaudeCodeAgent = class extends FileSystemSessionSource {
1593
1560
  // --- Fallback ---
1594
1561
  buildFallbackToolMessage(opts) {
1595
1562
  if (opts.outputParts.length === 0) return null;
1596
- return this.buildMessage({
1597
- messageId: opts.messageId,
1563
+ return {
1564
+ id: opts.messageId,
1598
1565
  role: "tool",
1599
1566
  timestampMs: opts.timestampMs,
1600
1567
  parts: opts.outputParts
1601
- });
1568
+ };
1602
1569
  }
1603
1570
  // --- Utilities ---
1604
1571
  shouldIgnoreTool(toolName) {
1605
1572
  return toolName === "TodoWrite";
1606
1573
  }
1607
- appendPartIfNew(message, part) {
1608
- const parts = message.parts;
1609
- if (parts.length > 0 && parts[parts.length - 1].type === part.type) {
1610
- const tail = parts[parts.length - 1];
1611
- if (tail.text === part.text) return;
1612
- }
1613
- parts.push(part);
1614
- }
1615
1574
  };
1616
1575
  var DatabaseConstructor = null;
1617
1576
  try {
@@ -2142,7 +2101,7 @@ function kimiContentText(content) {
2142
2101
  }
2143
2102
  function extractFirstUserTitle(contextFile, wireFile) {
2144
2103
  if (contextFile && existsSync6(contextFile)) {
2145
- const content = readFileSync4(contextFile, "utf-8");
2104
+ const content = readFileSync3(contextFile, "utf-8");
2146
2105
  for (const record of parseJsonlLines(content)) {
2147
2106
  if (record.role !== "user") continue;
2148
2107
  const title = normalizeTitleText(kimiContentText(record.content));
@@ -2150,7 +2109,7 @@ function extractFirstUserTitle(contextFile, wireFile) {
2150
2109
  }
2151
2110
  }
2152
2111
  if (wireFile && existsSync6(wireFile)) {
2153
- const content = readFileSync4(wireFile, "utf-8");
2112
+ const content = readFileSync3(wireFile, "utf-8");
2154
2113
  for (const record of parseJsonlLines(content)) {
2155
2114
  const message = record.message ?? {};
2156
2115
  if (message.type !== "TurnBegin") continue;
@@ -2179,12 +2138,12 @@ var KimiAgent = class extends FileSystemSessionSource {
2179
2138
  const configPath = join6(roots.kimiRoot, "kimi.json");
2180
2139
  const tomlPath = join6(roots.kimiRoot, "config.toml");
2181
2140
  if (existsSync6(tomlPath)) {
2182
- const configText = readFileSync4(tomlPath, "utf-8");
2141
+ const configText = readFileSync3(tomlPath, "utf-8");
2183
2142
  this.defaultModel = configText.match(/^default_model\s*=\s*"([^"]+)"/m)?.[1] ?? null;
2184
2143
  }
2185
2144
  if (!existsSync6(configPath)) return;
2186
2145
  try {
2187
- const raw = JSON.parse(readFileSync4(configPath, "utf-8"));
2146
+ const raw = JSON.parse(readFileSync3(configPath, "utf-8"));
2188
2147
  const workDirs = raw?.work_dirs;
2189
2148
  if (!Array.isArray(workDirs)) return;
2190
2149
  for (const wd of workDirs) {
@@ -2248,12 +2207,12 @@ var KimiAgent = class extends FileSystemSessionSource {
2248
2207
  let wireMtime = null;
2249
2208
  let metaFile = "";
2250
2209
  if (existsSync6(statePath)) {
2251
- const state = JSON.parse(readFileSync4(statePath, "utf-8"));
2210
+ const state = JSON.parse(readFileSync3(statePath, "utf-8"));
2252
2211
  title = String(state.custom_title ?? "");
2253
2212
  wireMtime = typeof state.wire_mtime === "number" ? state.wire_mtime : null;
2254
2213
  metaFile = statePath;
2255
2214
  } else if (existsSync6(metaPath)) {
2256
- const meta = JSON.parse(readFileSync4(metaPath, "utf-8"));
2215
+ const meta = JSON.parse(readFileSync3(metaPath, "utf-8"));
2257
2216
  title = String(meta.title ?? "");
2258
2217
  wireMtime = typeof meta.wire_mtime === "number" ? meta.wire_mtime : null;
2259
2218
  metaFile = metaPath;
@@ -2277,50 +2236,6 @@ var KimiAgent = class extends FileSystemSessionSource {
2277
2236
  return skippedSession("malformed metadata");
2278
2237
  }
2279
2238
  }
2280
- scan(options) {
2281
- if (!this.basePath) return [];
2282
- const scanMarker = perf.start("kimi:scan");
2283
- const listMarker = perf.start("listSessionDirs");
2284
- const sessionDirs = this.listSessionDirs();
2285
- perf.end(listMarker);
2286
- const metas = [];
2287
- for (const dir of sessionDirs) {
2288
- try {
2289
- const parseMarker = perf.start(`parseSessionDir:${basename4(dir)}`);
2290
- const meta = getParsedSession(this.parseSessionDirResult(dir));
2291
- perf.end(parseMarker);
2292
- if (meta && matchesScanWindow(meta.createdAt, options)) {
2293
- metas.push(meta);
2294
- }
2295
- } catch {
2296
- }
2297
- }
2298
- options?.onProgress?.({ total: metas.length, processed: 0, sessions: 0 });
2299
- const heads = [];
2300
- let processed = 0;
2301
- for (const meta of metas) {
2302
- try {
2303
- meta.sourceFingerprint = this.sourceFingerprint(meta);
2304
- this.sessionMetaMap.set(meta.id, meta);
2305
- const stats = this.extractStats(meta.sourcePath);
2306
- heads.push({
2307
- id: meta.id,
2308
- slug: `kimi/${meta.id}`,
2309
- title: meta.title,
2310
- directory: meta.cwd,
2311
- time_created: meta.createdAt,
2312
- time_updated: meta.createdAt,
2313
- stats
2314
- });
2315
- } catch {
2316
- } finally {
2317
- processed += 1;
2318
- options?.onProgress?.({ total: metas.length, processed, sessions: heads.length });
2319
- }
2320
- }
2321
- perf.end(scanMarker);
2322
- return heads;
2323
- }
2324
2239
  listSessionSources(options) {
2325
2240
  if (!this.basePath) return [];
2326
2241
  const refs = [];
@@ -2361,9 +2276,8 @@ var KimiAgent = class extends FileSystemSessionSource {
2361
2276
  }
2362
2277
  getSessionDataFromContext(meta) {
2363
2278
  if (!meta.contextFile) throw new Error("context.jsonl is missing");
2364
- const content = readFileSync4(meta.contextFile, "utf-8");
2365
- const messages = [];
2366
- const pendingToolCalls = /* @__PURE__ */ new Map();
2279
+ const content = readFileSync3(meta.contextFile, "utf-8");
2280
+ const builder = new TranscriptBuilder();
2367
2281
  const ignoredToolCallIds = /* @__PURE__ */ new Set();
2368
2282
  let seq = 0;
2369
2283
  const fallbackTs = meta.createdAt;
@@ -2375,65 +2289,55 @@ var KimiAgent = class extends FileSystemSessionSource {
2375
2289
  if (role === "user") {
2376
2290
  const text = cleanInternalText(kimiContentText(record.content));
2377
2291
  if (text) {
2378
- messages.push(
2379
- this.buildMessage({
2380
- messageId: `context-${seq}`,
2381
- role: "user",
2382
- timestampMs: fallbackTs,
2383
- parts: [{ type: "text", text, time_created: fallbackTs }]
2384
- })
2385
- );
2292
+ builder.appendMessage({
2293
+ id: `context-${seq}`,
2294
+ role: "user",
2295
+ timestampMs: fallbackTs,
2296
+ parts: [{ type: "text", text, time_created: fallbackTs }]
2297
+ });
2386
2298
  }
2387
2299
  continue;
2388
2300
  }
2389
2301
  if (role === "assistant") {
2390
- const { message, toolIndexes } = this.buildContextAssistantMessage(
2302
+ const message = this.buildContextAssistantMessage(
2391
2303
  record,
2392
2304
  seq,
2393
2305
  ignoredToolCallIds,
2394
2306
  fallbackTs
2395
2307
  );
2396
2308
  if (!message) continue;
2397
- const msgIndex = messages.length;
2398
- messages.push(message);
2399
- for (const [callId, partIndex] of toolIndexes) {
2400
- pendingToolCalls.set(callId, [msgIndex, partIndex]);
2401
- }
2309
+ builder.appendMessage(message);
2402
2310
  continue;
2403
2311
  }
2404
2312
  if (role === "tool") {
2405
2313
  const callId = String(record.tool_call_id ?? "").trim();
2406
2314
  if (callId && ignoredToolCallIds.has(callId)) continue;
2407
2315
  const outputParts = normalizeToolOutputParts(record.content, fallbackTs);
2408
- if (callId && this.backfillToolOutput(messages, pendingToolCalls, callId, outputParts)) {
2316
+ if (callId && this.backfillToolOutput(builder, callId, outputParts)) {
2409
2317
  continue;
2410
2318
  }
2411
2319
  if (outputParts.length > 0) {
2412
- messages.push(
2413
- this.buildMessage({
2414
- messageId: `context-${seq}`,
2415
- role: "tool",
2416
- timestampMs: fallbackTs,
2417
- parts: outputParts
2418
- })
2419
- );
2320
+ builder.appendMessage({
2321
+ id: `context-${seq}`,
2322
+ role: "tool",
2323
+ timestampMs: fallbackTs,
2324
+ parts: outputParts
2325
+ });
2420
2326
  }
2421
2327
  }
2422
2328
  } catch {
2423
2329
  }
2424
2330
  }
2425
2331
  const stats = this.extractStats(meta.sourcePath);
2426
- return this.buildSessionData(meta, messages, stats);
2332
+ return this.buildSessionData(meta, builder, stats);
2427
2333
  }
2428
2334
  getSessionDataFromWire(meta) {
2429
2335
  const wirePath = meta.wireFile ?? join6(meta.sourcePath, "wire.jsonl");
2430
2336
  if (!existsSync6(wirePath)) throw new Error("wire.jsonl is missing");
2431
- const content = readFileSync4(wirePath, "utf-8");
2432
- const messages = [];
2433
- const pendingToolCalls = /* @__PURE__ */ new Map();
2337
+ const content = readFileSync3(wirePath, "utf-8");
2338
+ const builder = new TranscriptBuilder();
2434
2339
  const ignoredToolCallIds = /* @__PURE__ */ new Set();
2435
2340
  const openToolArgumentBuffer = /* @__PURE__ */ new Map();
2436
- let currentAssistantIndex = null;
2437
2341
  let openToolCallId = null;
2438
2342
  let seq = 0;
2439
2343
  for (const record of parseJsonlLines(content)) {
@@ -2450,19 +2354,13 @@ var KimiAgent = class extends FileSystemSessionSource {
2450
2354
  const inputTokens = Number(usage["input_tokens"] ?? 0);
2451
2355
  const outputTokens = Number(usage["output_tokens"] ?? 0);
2452
2356
  if (inputTokens || outputTokens) {
2453
- for (let i = messages.length - 1; i >= 0; i--) {
2454
- const msg = messages[i];
2455
- if (msg.role === "assistant" && !msg.tokens) {
2456
- msg.tokens = { input: inputTokens, output: outputTokens };
2457
- msg.model ??= this.defaultModel;
2458
- const cost = estimateTokenCost(msg.model, msg.tokens);
2459
- if (cost !== null) {
2460
- msg.cost = cost;
2461
- msg.cost_source = "estimated";
2462
- }
2463
- break;
2464
- }
2465
- }
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
+ });
2466
2364
  }
2467
2365
  }
2468
2366
  if (msgType === "TurnBegin") {
@@ -2470,37 +2368,37 @@ var KimiAgent = class extends FileSystemSessionSource {
2470
2368
  if (Array.isArray(userInput) && userInput.length > 0) {
2471
2369
  const text = cleanInternalText(kimiContentText(userInput));
2472
2370
  if (text) {
2473
- messages.push(
2474
- this.buildMessage({
2475
- messageId: `wire-${seq}`,
2476
- role: "user",
2477
- timestampMs,
2478
- parts: [{ type: "text", text, time_created: timestampMs }]
2479
- })
2480
- );
2371
+ builder.appendMessage({
2372
+ id: `wire-${seq}`,
2373
+ role: "user",
2374
+ timestampMs,
2375
+ parts: [{ type: "text", text, time_created: timestampMs }]
2376
+ });
2481
2377
  }
2482
2378
  }
2483
- currentAssistantIndex = null;
2379
+ builder.beginTurn();
2484
2380
  openToolCallId = null;
2485
2381
  continue;
2486
2382
  }
2487
2383
  if (msgType === "ContentPart") {
2488
- currentAssistantIndex = this.getOrCreateWireAssistant(
2489
- messages,
2490
- currentAssistantIndex,
2491
- `wire-${seq}`
2492
- );
2493
- const assistant = messages[currentAssistantIndex];
2494
2384
  const partType = String(payload.type ?? "");
2495
2385
  if (partType === "think") {
2496
2386
  const text = cleanInternalText(String(payload.think ?? ""));
2497
2387
  if (text) {
2498
- 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
+ );
2499
2393
  }
2500
2394
  } else if (partType === "text") {
2501
2395
  const text = cleanInternalText(String(payload.text ?? ""));
2502
2396
  if (text) {
2503
- 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
+ );
2504
2402
  }
2505
2403
  }
2506
2404
  continue;
@@ -2515,12 +2413,6 @@ var KimiAgent = class extends FileSystemSessionSource {
2515
2413
  continue;
2516
2414
  }
2517
2415
  if (!function_ || !callId || !toolName) continue;
2518
- currentAssistantIndex = this.getOrCreateWireAssistant(
2519
- messages,
2520
- currentAssistantIndex,
2521
- `wire-${seq}`
2522
- );
2523
- const assistant = messages[currentAssistantIndex];
2524
2416
  const rawArgs = function_.arguments;
2525
2417
  const normalizedArgs = normalizeToolArguments(rawArgs);
2526
2418
  const buffer = typeof rawArgs === "string" && typeof normalizedArgs !== "string" ? rawArgs : null;
@@ -2532,10 +2424,11 @@ var KimiAgent = class extends FileSystemSessionSource {
2532
2424
  state: { arguments: normalizedArgs, output: null },
2533
2425
  time_created: timestampMs
2534
2426
  };
2535
- const partIndex = assistant.parts.length;
2536
- assistant.parts.push(toolPart);
2537
- assistant.mode = "tool";
2538
- pendingToolCalls.set(callId, [currentAssistantIndex, partIndex]);
2427
+ builder.appendToolCall(
2428
+ toolPart,
2429
+ { id: `wire-${seq}`, timestampMs: 0, agent: "kimi" },
2430
+ { markModeAsTool: true, target: "current" }
2431
+ );
2539
2432
  openToolCallId = callId;
2540
2433
  if (buffer !== null) {
2541
2434
  openToolArgumentBuffer.set(callId, buffer);
@@ -2549,8 +2442,7 @@ var KimiAgent = class extends FileSystemSessionSource {
2549
2442
  argumentsPart,
2550
2443
  openToolCallId,
2551
2444
  openToolArgumentBuffer,
2552
- messages,
2553
- pendingToolCalls
2445
+ builder
2554
2446
  );
2555
2447
  continue;
2556
2448
  }
@@ -2558,27 +2450,24 @@ var KimiAgent = class extends FileSystemSessionSource {
2558
2450
  const callId = String(payload.tool_call_id ?? "").trim();
2559
2451
  if (callId && ignoredToolCallIds.has(callId)) continue;
2560
2452
  const outputParts = normalizeWireToolOutputParts(payload.return_value, timestampMs);
2561
- if (callId && this.backfillToolOutput(messages, pendingToolCalls, callId, outputParts)) {
2453
+ if (callId && this.backfillToolOutput(builder, callId, outputParts)) {
2562
2454
  continue;
2563
2455
  }
2564
2456
  if (outputParts.length > 0) {
2565
- messages.push(
2566
- this.buildMessage({
2567
- messageId: `wire-${seq}`,
2568
- role: "tool",
2569
- timestampMs,
2570
- parts: outputParts
2571
- })
2572
- );
2457
+ builder.appendMessage({
2458
+ id: `wire-${seq}`,
2459
+ role: "tool",
2460
+ timestampMs,
2461
+ parts: outputParts
2462
+ });
2573
2463
  }
2574
2464
  continue;
2575
2465
  }
2576
2466
  } catch {
2577
2467
  }
2578
2468
  }
2579
- const filteredMessages = messages.filter((m) => m.parts.length > 0);
2580
2469
  const stats = this.extractStats(meta.sourcePath);
2581
- return this.buildSessionData(meta, filteredMessages, stats);
2470
+ return this.buildSessionData(meta, builder, stats);
2582
2471
  }
2583
2472
  // --- Helpers ---
2584
2473
  sourceFingerprint(meta) {
@@ -2596,23 +2485,8 @@ var KimiAgent = class extends FileSystemSessionSource {
2596
2485
  fileMtime(meta.wireFile)
2597
2486
  ]);
2598
2487
  }
2599
- buildMessage(opts) {
2600
- return {
2601
- id: opts.messageId,
2602
- role: opts.role,
2603
- agent: opts.agent ?? null,
2604
- time_created: opts.timestampMs,
2605
- mode: opts.mode ?? null,
2606
- model: opts.model ?? null,
2607
- provider: opts.provider ?? null,
2608
- tokens: opts.tokens ? opts.tokens : void 0,
2609
- cost: opts.cost ?? 0,
2610
- parts: opts.parts
2611
- };
2612
- }
2613
2488
  buildContextAssistantMessage(record, seq, ignoredToolCallIds, fallbackTs) {
2614
2489
  const parts = [];
2615
- const toolIndexes = /* @__PURE__ */ new Map();
2616
2490
  const content = record.content;
2617
2491
  if (Array.isArray(content)) {
2618
2492
  for (const item of content) {
@@ -2650,71 +2524,41 @@ var KimiAgent = class extends FileSystemSessionSource {
2650
2524
  state: { arguments: normalizeToolArguments(function_.arguments), output: null },
2651
2525
  time_created: fallbackTs
2652
2526
  };
2653
- toolIndexes.set(callId, parts.length);
2654
2527
  parts.push(part);
2655
2528
  }
2656
2529
  }
2657
2530
  if (parts.length === 0) {
2658
- return {
2659
- message: this.buildMessage({
2660
- messageId: `context-${seq}`,
2661
- role: "assistant",
2662
- timestampMs: fallbackTs,
2663
- parts: []
2664
- }),
2665
- toolIndexes
2666
- };
2531
+ return null;
2667
2532
  }
2668
2533
  const allTools = parts.every((p) => p.type === "tool");
2669
- const message = this.buildMessage({
2670
- messageId: `context-${seq}`,
2534
+ return {
2535
+ id: `context-${seq}`,
2671
2536
  role: "assistant",
2672
2537
  timestampMs: fallbackTs,
2673
2538
  parts,
2674
2539
  agent: "kimi",
2675
2540
  mode: allTools ? "tool" : void 0
2676
- });
2677
- return { message, toolIndexes };
2678
- }
2679
- getOrCreateWireAssistant(messages, currentIndex, messageId) {
2680
- if (currentIndex !== null) return currentIndex;
2681
- messages.push(
2682
- this.buildMessage({
2683
- messageId,
2684
- role: "assistant",
2685
- timestampMs: 0,
2686
- parts: [],
2687
- agent: "kimi"
2688
- })
2689
- );
2690
- return messages.length - 1;
2541
+ };
2691
2542
  }
2692
- appendWireToolCallPart(argumentsPart, openCallId, buffer, messages, pendingToolCalls) {
2693
- if (!openCallId || !pendingToolCalls.has(openCallId)) return;
2543
+ appendWireToolCallPart(argumentsPart, openCallId, buffer, builder) {
2544
+ if (!openCallId) return;
2694
2545
  const existing = buffer.get(openCallId) ?? "";
2695
2546
  const combined = existing + argumentsPart;
2696
2547
  try {
2697
- const parsed2 = JSON.parse(combined);
2698
- const location = pendingToolCalls.get(openCallId);
2699
- if (!location) return;
2700
- const msgPart = messages[location[0]]?.parts[location[1]];
2701
- if (msgPart?.state) {
2702
- 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);
2703
2554
  }
2704
- buffer.delete(openCallId);
2705
2555
  } catch {
2706
2556
  buffer.set(openCallId, combined);
2707
2557
  }
2708
2558
  }
2709
- backfillToolOutput(messages, pendingToolCalls, callId, outputParts) {
2559
+ backfillToolOutput(builder, callId, outputParts) {
2710
2560
  if (!outputParts.length || !callId) return false;
2711
- const location = pendingToolCalls.get(callId);
2712
- if (!location) return false;
2713
- const part = messages[location[0]]?.parts[location[1]];
2714
- if (!part) return false;
2715
- if (!part.state) part.state = {};
2716
- part.state.output = [...outputParts];
2717
- return true;
2561
+ return builder.resolveToolCall(callId, { output: [...outputParts] });
2718
2562
  }
2719
2563
  extractStats(sessionDir) {
2720
2564
  let totalCost = 0;
@@ -2728,7 +2572,7 @@ var KimiAgent = class extends FileSystemSessionSource {
2728
2572
  const wirePath = join6(sessionDir, "wire.jsonl");
2729
2573
  if (!existsSync6(wirePath)) return stats;
2730
2574
  try {
2731
- const content = readFileSync4(wirePath, "utf-8");
2575
+ const content = readFileSync3(wirePath, "utf-8");
2732
2576
  for (const line of content.split("\n").filter((l) => l.trim())) {
2733
2577
  try {
2734
2578
  const data = JSON.parse(line);
@@ -2752,7 +2596,7 @@ var KimiAgent = class extends FileSystemSessionSource {
2752
2596
  const rawPath = existsSync6(contextPath) ? contextPath : wirePath;
2753
2597
  if (!existsSync6(rawPath)) return stats;
2754
2598
  try {
2755
- const rawContent = readFileSync4(rawPath, "utf-8");
2599
+ const rawContent = readFileSync3(rawPath, "utf-8");
2756
2600
  for (const line of rawContent.split("\n").filter((l) => l.trim())) {
2757
2601
  try {
2758
2602
  const data = JSON.parse(line);
@@ -2770,14 +2614,8 @@ var KimiAgent = class extends FileSystemSessionSource {
2770
2614
  }
2771
2615
  return stats;
2772
2616
  }
2773
- buildSessionData(meta, messages, stats) {
2774
- const cleanedMessages = cleanParsedMessages(messages);
2775
- stats.message_count = cleanedMessages.length;
2776
- const totalCost = cleanedMessages.reduce((sum, message) => sum + (message.cost ?? 0), 0);
2777
- if (totalCost > 0) {
2778
- stats.total_cost = Number(totalCost.toFixed(8));
2779
- stats.cost_source = "estimated";
2780
- }
2617
+ buildSessionData(meta, builder, stats) {
2618
+ const transcript = builder.finish(stats);
2781
2619
  return {
2782
2620
  id: meta.id,
2783
2621
  title: meta.title,
@@ -2785,8 +2623,8 @@ var KimiAgent = class extends FileSystemSessionSource {
2785
2623
  directory: meta.cwd,
2786
2624
  time_created: meta.createdAt,
2787
2625
  time_updated: meta.createdAt,
2788
- stats,
2789
- messages: cleanedMessages
2626
+ stats: transcript.stats,
2627
+ messages: transcript.messages
2790
2628
  };
2791
2629
  }
2792
2630
  };
@@ -2974,36 +2812,6 @@ var CodexAgent = class extends FileSystemSessionSource {
2974
2812
  return false;
2975
2813
  }
2976
2814
  }
2977
- scan(options) {
2978
- if (!this.basePath) return [];
2979
- const scanMarker = perf.start("codex:scan");
2980
- const indexMarker = perf.start("loadSessionIndex");
2981
- this.loadSessionIndex();
2982
- perf.end(indexMarker);
2983
- const heads = [];
2984
- const listMarker = perf.start("listRolloutFiles");
2985
- const files = this.listRolloutFiles(options);
2986
- perf.end(listMarker);
2987
- options?.onProgress?.({ total: files.length, processed: 0, sessions: 0 });
2988
- let processed = 0;
2989
- for (const file of files) {
2990
- try {
2991
- const parseMarker = perf.start(`parseSessionHead:${basename5(file)}`);
2992
- const head = getParsedSession(this.parseSessionHeadResult(file, options));
2993
- perf.end(parseMarker);
2994
- if (head) {
2995
- heads.push(head);
2996
- this.sessionMetaMap.set(head.id, this.buildSessionMeta(head, file));
2997
- }
2998
- } catch {
2999
- } finally {
3000
- processed += 1;
3001
- options?.onProgress?.({ total: files.length, processed, sessions: heads.length });
3002
- }
3003
- }
3004
- perf.end(scanMarker);
3005
- return heads;
3006
- }
3007
2815
  listSessionSources(options) {
3008
2816
  if (!this.basePath) return [];
3009
2817
  this.loadSessionIndex();
@@ -3013,9 +2821,9 @@ var CodexAgent = class extends FileSystemSessionSource {
3013
2821
  fingerprint: this.sourceFingerprint(file)
3014
2822
  }));
3015
2823
  }
3016
- scanSessionSource(sourcePath) {
2824
+ scanSessionSource(sourcePath, options) {
3017
2825
  this.loadSessionIndex();
3018
- const head = getParsedSession(this.parseSessionHeadResult(sourcePath));
2826
+ const head = getParsedSession(this.parseSessionHeadResult(sourcePath, options));
3019
2827
  if (head) {
3020
2828
  this.sessionMetaMap.set(head.id, this.buildSessionMeta(head, sourcePath));
3021
2829
  }
@@ -3025,15 +2833,11 @@ var CodexAgent = class extends FileSystemSessionSource {
3025
2833
  const meta = this.sessionMetaMap.get(sessionId);
3026
2834
  if (!meta) throw new Error(`Session not found: ${sessionId}`);
3027
2835
  if (!existsSync7(meta.sourcePath)) throw new Error(`Session file missing: ${meta.sourcePath}`);
3028
- const content = readFileSync5(meta.sourcePath, "utf-8");
3029
- const messages = [];
3030
- const pendingToolCalls = /* @__PURE__ */ new Map();
2836
+ const transcript = new TranscriptBuilder();
3031
2837
  let totalInputTokens = 0;
3032
2838
  let totalOutputTokens = 0;
3033
2839
  let totalCacheReadTokens = 0;
3034
2840
  let totalCost = 0;
3035
- let currentAssistantIndex = null;
3036
- let latestAssistantTextIndex = null;
3037
2841
  let pendingPlan = null;
3038
2842
  let activeModel = meta.model;
3039
2843
  let prevCumulativeTotal = 0;
@@ -3041,31 +2845,14 @@ var CodexAgent = class extends FileSystemSessionSource {
3041
2845
  let prevOutput = 0;
3042
2846
  let prevReasoning = 0;
3043
2847
  let prevCachedInput = 0;
3044
- for (const record of parseJsonlLines(content)) {
2848
+ for (const record of readJsonlFile(meta.sourcePath)) {
3045
2849
  try {
3046
2850
  const recordType = String(record["type"] ?? "");
3047
2851
  if (recordType === "turn_context") {
3048
2852
  const payload = record["payload"] ?? {};
3049
2853
  activeModel = extractModelName(payload["model"]) ?? activeModel;
3050
2854
  }
3051
- const result = this.convertRecord(
3052
- record,
3053
- messages,
3054
- pendingToolCalls,
3055
- meta.id,
3056
- currentAssistantIndex,
3057
- latestAssistantTextIndex,
3058
- pendingPlan
3059
- );
3060
- currentAssistantIndex = result.currentAssistantIndex;
3061
- latestAssistantTextIndex = result.latestAssistantTextIndex;
3062
- pendingPlan = result.pendingPlan;
3063
- if (currentAssistantIndex !== null && activeModel) {
3064
- const message = messages[currentAssistantIndex];
3065
- if (message?.role === "assistant" && !message.model) {
3066
- message.model = activeModel;
3067
- }
3068
- }
2855
+ pendingPlan = this.convertRecord(record, transcript, pendingPlan, activeModel);
3069
2856
  if (recordType === "event_msg") {
3070
2857
  const payload = record["payload"] ?? {};
3071
2858
  if (String(payload["type"] ?? "") === "token_count") {
@@ -3101,24 +2888,19 @@ var CodexAgent = class extends FileSystemSessionSource {
3101
2888
  totalInputTokens += totalInput;
3102
2889
  totalOutputTokens += outputTokens + reasoningTokens;
3103
2890
  totalCacheReadTokens += totalCacheRead;
3104
- for (let i = messages.length - 1; i >= 0; i--) {
3105
- const msg = messages[i];
3106
- if (msg.role === "assistant" && !msg.tokens) {
3107
- msg.tokens = {
3108
- input: totalInput,
3109
- output: outputTokens,
3110
- reasoning: reasoningTokens || void 0,
3111
- cache_read: totalCacheRead || void 0
3112
- };
3113
- const cost = estimateTokenCost(msg.model ?? activeModel, msg.tokens);
3114
- if (cost !== null) {
3115
- msg.cost = cost;
3116
- msg.cost_source = "estimated";
3117
- totalCost += cost;
3118
- }
3119
- break;
3120
- }
3121
- }
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;
3122
2904
  }
3123
2905
  }
3124
2906
  }
@@ -3126,10 +2908,15 @@ var CodexAgent = class extends FileSystemSessionSource {
3126
2908
  } catch {
3127
2909
  }
3128
2910
  }
3129
- if (pendingPlan && currentAssistantIndex !== null) {
3130
- messages[currentAssistantIndex].parts.push(pendingPlan);
3131
- }
3132
- 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
+ });
3133
2920
  return {
3134
2921
  id: meta.id,
3135
2922
  title: meta.title,
@@ -3137,15 +2924,8 @@ var CodexAgent = class extends FileSystemSessionSource {
3137
2924
  directory: meta.directory,
3138
2925
  time_created: meta.createdAt,
3139
2926
  time_updated: meta.updatedAt,
3140
- stats: {
3141
- message_count: cleanedMessages.length,
3142
- total_input_tokens: totalInputTokens,
3143
- total_output_tokens: totalOutputTokens,
3144
- total_cache_read_tokens: totalCacheReadTokens || void 0,
3145
- total_cost: totalCost,
3146
- cost_source: totalCost > 0 ? "estimated" : void 0
3147
- },
3148
- messages: cleanedMessages
2927
+ stats: result.stats,
2928
+ messages: result.messages
3149
2929
  };
3150
2930
  }
3151
2931
  // ---- File listing ----
@@ -3229,7 +3009,7 @@ var CodexAgent = class extends FileSystemSessionSource {
3229
3009
  this.sessionIndexMtime = mtime;
3230
3010
  if (mtime === null) return;
3231
3011
  try {
3232
- const content = readFileSync5(indexPath, "utf-8");
3012
+ const content = readFileSync4(indexPath, "utf-8");
3233
3013
  for (const record of parseJsonlLines(content)) {
3234
3014
  const sid = String(record["id"] ?? "").trim();
3235
3015
  const threadName = String(record["thread_name"] ?? "").trim();
@@ -3246,13 +3026,13 @@ var CodexAgent = class extends FileSystemSessionSource {
3246
3026
  }
3247
3027
  // ---- Session head parsing ----
3248
3028
  readFilePrefix(filePath, bytes = 64 * 1024) {
3249
- const fd = openSync(filePath, "r");
3029
+ const fd = openSync2(filePath, "r");
3250
3030
  try {
3251
3031
  const buffer = Buffer.alloc(bytes);
3252
- const bytesRead = readSync(fd, buffer, 0, bytes, 0);
3032
+ const bytesRead = readSync2(fd, buffer, 0, bytes, 0);
3253
3033
  return buffer.subarray(0, bytesRead).toString("utf-8");
3254
3034
  } finally {
3255
- closeSync(fd);
3035
+ closeSync2(fd);
3256
3036
  }
3257
3037
  }
3258
3038
  parseSessionHead(filePath, options) {
@@ -3262,23 +3042,12 @@ var CodexAgent = class extends FileSystemSessionSource {
3262
3042
  if (options?.fast) {
3263
3043
  return this.parseFastSessionHeadResult(filePath);
3264
3044
  }
3265
- const content = readFileSync5(filePath, "utf-8");
3266
- const lines = content.split("\n").filter((l) => l.trim());
3267
- if (lines.length === 0) return skippedSession("empty file");
3268
3045
  const sessionId = extractSessionId(filePath);
3269
- let firstRecord;
3270
- try {
3271
- firstRecord = JSON.parse(lines[0]);
3272
- } catch {
3273
- return skippedSession("malformed first record");
3274
- }
3275
- const payload = firstRecord["payload"] ?? {};
3276
- const createdAt = parseTimestampMs2(firstRecord) || parseTimestampMs2(payload) || statSync4(filePath).mtimeMs;
3277
- const indexTitle = this.getTitleForSession(sessionId);
3278
- const messageTitle = this.extractTitleFromLines(lines);
3279
- const directoryTitle = basenameTitle(payload["cwd"] ? String(payload["cwd"]) : null);
3280
- const title = resolveSessionTitle(indexTitle, messageTitle, directoryTitle);
3281
- let updatedAt = createdAt;
3046
+ let firstPayload = {};
3047
+ let createdAt = 0;
3048
+ let lineCount = 0;
3049
+ const titleLines = [];
3050
+ let updatedAt = 0;
3282
3051
  let messageCount = 0;
3283
3052
  let model = null;
3284
3053
  let activeModel = null;
@@ -3294,18 +3063,31 @@ var CodexAgent = class extends FileSystemSessionSource {
3294
3063
  let scanPrevCachedInput = 0;
3295
3064
  const COUNTED_TYPES = /* @__PURE__ */ new Set(["message", "function_call", "function_call_output"]);
3296
3065
  let hasNonInternalRecord = false;
3297
- for (const line of lines) {
3066
+ for (const line of readJsonlFileLines(filePath)) {
3067
+ lineCount += 1;
3068
+ if (lineCount === 1) {
3069
+ let firstRecord;
3070
+ try {
3071
+ firstRecord = JSON.parse(line);
3072
+ } catch {
3073
+ return skippedSession("malformed first record");
3074
+ }
3075
+ firstPayload = firstRecord["payload"] ?? {};
3076
+ createdAt = parseTimestampMs2(firstRecord) || parseTimestampMs2(firstPayload) || statSync4(filePath).mtimeMs;
3077
+ updatedAt = createdAt;
3078
+ }
3079
+ if (titleLines.length < 20) titleLines.push(line);
3298
3080
  try {
3299
3081
  const data = JSON.parse(line);
3300
3082
  const recordType = String(data["type"] ?? "");
3301
- const payload2 = data["payload"] ?? {};
3302
- const payloadType = String(payload2["type"] ?? "");
3083
+ const payload = data["payload"] ?? {};
3084
+ const payloadType = String(payload["type"] ?? "");
3303
3085
  if (isInternalEventType2(recordType) || isInternalEventType2(payloadType)) continue;
3304
3086
  hasNonInternalRecord = true;
3305
3087
  const recordTs = parseTimestampMs2(data) || parseTimestampMs2(data["payload"] ?? {});
3306
3088
  if (recordTs > updatedAt) updatedAt = recordTs;
3307
3089
  if (recordType === "session_meta" || recordType === "turn_context") {
3308
- const nextModel = extractModelName(payload2["model"]);
3090
+ const nextModel = extractModelName(payload["model"]);
3309
3091
  if (nextModel) {
3310
3092
  activeModel = nextModel;
3311
3093
  model ??= nextModel;
@@ -3313,7 +3095,7 @@ var CodexAgent = class extends FileSystemSessionSource {
3313
3095
  continue;
3314
3096
  }
3315
3097
  if (recordType === "response_item") {
3316
- const p = payload2;
3098
+ const p = payload;
3317
3099
  const pType = String(p["type"] ?? "");
3318
3100
  if (COUNTED_TYPES.has(pType)) {
3319
3101
  messageCount++;
@@ -3375,8 +3157,12 @@ var CodexAgent = class extends FileSystemSessionSource {
3375
3157
  } catch {
3376
3158
  }
3377
3159
  }
3160
+ if (lineCount === 0) return skippedSession("empty file");
3378
3161
  if (!hasNonInternalRecord) return filteredSession("internal events only");
3379
- const directory = payload["cwd"] ? String(payload["cwd"]) : "";
3162
+ const indexTitle = this.getTitleForSession(sessionId);
3163
+ const messageTitle = this.extractTitleFromLines(titleLines);
3164
+ const directory = firstPayload["cwd"] ? String(firstPayload["cwd"]) : "";
3165
+ const title = resolveSessionTitle(indexTitle, messageTitle, basenameTitle(directory || null));
3380
3166
  return parsedSession({
3381
3167
  id: sessionId,
3382
3168
  slug: `codex/${sessionId}`,
@@ -3458,22 +3244,16 @@ var CodexAgent = class extends FileSystemSessionSource {
3458
3244
  return null;
3459
3245
  }
3460
3246
  // ---- Record conversion ----
3461
- convertRecord(data, messages, pendingToolCalls, sessionId, currentAssistantIndex, latestAssistantTextIndex, pendingPlan) {
3247
+ convertRecord(data, transcript, pendingPlan, activeModel) {
3462
3248
  const recordType = String(data["type"] ?? "");
3463
- if (isInternalEventType2(recordType)) {
3464
- return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan };
3465
- }
3249
+ if (isInternalEventType2(recordType)) return pendingPlan;
3466
3250
  if (recordType === "session_meta" || recordType === "event_msg") {
3467
- return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan };
3468
- }
3469
- if (recordType !== "response_item") {
3470
- return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan };
3251
+ return pendingPlan;
3471
3252
  }
3253
+ if (recordType !== "response_item") return pendingPlan;
3472
3254
  const payload = data["payload"] ?? {};
3473
3255
  const payloadType = String(payload["type"] ?? "");
3474
- if (isInternalEventType2(payloadType)) {
3475
- return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan };
3476
- }
3256
+ if (isInternalEventType2(payloadType)) return pendingPlan;
3477
3257
  const timestampMs = parseTimestampMs2(data) || parseTimestampMs2(payload);
3478
3258
  switch (payloadType) {
3479
3259
  case "message": {
@@ -3481,60 +3261,39 @@ var CodexAgent = class extends FileSystemSessionSource {
3481
3261
  if (role === "assistant") {
3482
3262
  return this.convertAssistantMessage(
3483
3263
  payload,
3484
- messages,
3264
+ transcript,
3485
3265
  timestampMs,
3486
- currentAssistantIndex,
3487
- latestAssistantTextIndex,
3488
- pendingPlan
3266
+ pendingPlan,
3267
+ activeModel
3489
3268
  );
3490
3269
  }
3491
3270
  if (role === "user") {
3492
- return this.convertUserMessage(
3493
- payload,
3494
- messages,
3495
- timestampMs,
3496
- currentAssistantIndex,
3497
- latestAssistantTextIndex,
3498
- pendingPlan
3499
- );
3271
+ return this.convertUserMessage(payload, transcript, timestampMs, pendingPlan);
3500
3272
  }
3501
3273
  break;
3502
3274
  }
3503
3275
  case "reasoning":
3504
- return this.convertReasoning(payload, messages, timestampMs, currentAssistantIndex);
3276
+ this.convertReasoning(payload, transcript, timestampMs, activeModel);
3277
+ return null;
3505
3278
  case "function_call":
3506
- return this.convertFunctionCall(
3507
- payload,
3508
- messages,
3509
- pendingToolCalls,
3510
- timestampMs,
3511
- currentAssistantIndex,
3512
- latestAssistantTextIndex
3513
- );
3279
+ this.convertFunctionCall(payload, transcript, timestampMs, activeModel);
3280
+ return null;
3514
3281
  case "function_call_output":
3515
- this.convertFunctionCallOutput(payload, messages, pendingToolCalls, timestampMs);
3516
- return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan };
3282
+ this.convertToolCallOutput(payload, transcript, timestampMs);
3283
+ return pendingPlan;
3517
3284
  case "custom_tool_call":
3518
- return this.convertCustomToolCall(
3519
- payload,
3520
- messages,
3521
- pendingToolCalls,
3522
- timestampMs,
3523
- currentAssistantIndex,
3524
- latestAssistantTextIndex
3525
- );
3285
+ this.convertCustomToolCall(payload, transcript, timestampMs, activeModel);
3286
+ return null;
3526
3287
  case "custom_tool_call_output":
3527
- this.convertCustomToolCallOutput(payload, messages, pendingToolCalls, timestampMs);
3528
- return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan };
3288
+ this.convertToolCallOutput(payload, transcript, timestampMs);
3289
+ return pendingPlan;
3529
3290
  }
3530
- return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan };
3291
+ return pendingPlan;
3531
3292
  }
3532
3293
  // ---- Assistant message ----
3533
- convertAssistantMessage(payload, messages, timestampMs, currentAssistantIndex, latestAssistantTextIndex, pendingPlan) {
3294
+ convertAssistantMessage(payload, transcript, timestampMs, pendingPlan, activeModel) {
3534
3295
  const content = payload["content"];
3535
- if (!Array.isArray(content)) {
3536
- return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan };
3537
- }
3296
+ if (!Array.isArray(content)) return pendingPlan;
3538
3297
  const textParts = [];
3539
3298
  for (const item of content) {
3540
3299
  if (typeof item !== "object" || item === null) continue;
@@ -3544,9 +3303,7 @@ var CodexAgent = class extends FileSystemSessionSource {
3544
3303
  if (text.trim()) textParts.push(text);
3545
3304
  }
3546
3305
  }
3547
- if (textParts.length === 0) {
3548
- return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan };
3549
- }
3306
+ if (textParts.length === 0) return pendingPlan;
3550
3307
  const fullText = textParts.join("\n");
3551
3308
  const planMatch = fullText.match(PROPOSED_PLAN_PATTERN);
3552
3309
  if (planMatch) {
@@ -3560,61 +3317,34 @@ var CodexAgent = class extends FileSystemSessionSource {
3560
3317
  pendingPlan = planPart;
3561
3318
  }
3562
3319
  const displayText = cleanInternalText(fullText.replace(PROPOSED_PLAN_PATTERN, ""));
3563
- if (!displayText) {
3564
- return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan };
3565
- }
3320
+ if (!displayText) return pendingPlan;
3566
3321
  const textPart = { type: "text", text: displayText, time_created: timestampMs };
3567
- if (currentAssistantIndex !== null) {
3568
- const message = messages[currentAssistantIndex];
3569
- const hasTool = message.parts.some((p) => p.type === "tool");
3570
- if (!hasTool) {
3571
- message.parts.push(textPart);
3572
- latestAssistantTextIndex = currentAssistantIndex;
3573
- return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan };
3574
- }
3575
- }
3576
- messages.push(
3577
- this.buildMessage({
3578
- messageId: "",
3579
- role: "assistant",
3580
- timestampMs,
3581
- parts: [textPart],
3582
- agent: "codex"
3583
- })
3584
- );
3585
- currentAssistantIndex = messages.length - 1;
3586
- latestAssistantTextIndex = currentAssistantIndex;
3587
- return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan };
3322
+ transcript.appendAssistantPart(textPart, {
3323
+ id: "",
3324
+ timestampMs,
3325
+ agent: "codex",
3326
+ model: activeModel
3327
+ });
3328
+ return pendingPlan;
3588
3329
  }
3589
3330
  // ---- User message ----
3590
- convertUserMessage(payload, messages, timestampMs, currentAssistantIndex, latestAssistantTextIndex, pendingPlan) {
3331
+ convertUserMessage(payload, transcript, timestampMs, pendingPlan) {
3591
3332
  const content = payload["content"];
3592
3333
  const text = Array.isArray(content) ? content.map(
3593
3334
  (c) => typeof c === "object" && c !== null ? String(c["text"] ?? "") : String(c ?? "")
3594
3335
  ).join(" ") : String(content ?? "");
3595
3336
  const visibleText = cleanInternalText(text);
3596
- if (!visibleText) {
3597
- return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan };
3598
- }
3599
- if (isDeveloperLikeUserMessage(visibleText)) {
3600
- return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan };
3601
- }
3337
+ if (!visibleText) return pendingPlan;
3338
+ if (isDeveloperLikeUserMessage(visibleText)) return pendingPlan;
3602
3339
  if (visibleText.trimStart().startsWith(PLAN_APPROVAL_PREFIX)) {
3603
- if (pendingPlan && currentAssistantIndex !== null) {
3604
- messages[currentAssistantIndex].parts.push(pendingPlan);
3605
- }
3606
- pendingPlan = null;
3607
- messages.push(
3608
- this.buildMessage({
3609
- messageId: "",
3610
- role: "user",
3611
- timestampMs,
3612
- parts: [{ type: "text", text: visibleText, time_created: timestampMs }]
3613
- })
3614
- );
3615
- currentAssistantIndex = null;
3616
- latestAssistantTextIndex = null;
3617
- 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;
3618
3348
  }
3619
3349
  const subagentMatch = visibleText.match(SUBAGENT_NOTIFICATION_PATTERN);
3620
3350
  if (subagentMatch) {
@@ -3628,41 +3358,32 @@ var CodexAgent = class extends FileSystemSessionSource {
3628
3358
  text: completedText || `Subagent ${nickname} completed`,
3629
3359
  time_created: timestampMs
3630
3360
  };
3631
- messages.push(
3632
- this.buildMessage({
3633
- messageId: "",
3634
- role: "assistant",
3635
- timestampMs,
3636
- parts: [textPart],
3637
- agent: "codex",
3638
- subagent_id: agentId || void 0,
3639
- nickname: nickname || void 0
3640
- })
3641
- );
3642
- currentAssistantIndex = null;
3643
- latestAssistantTextIndex = null;
3644
- 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;
3645
3372
  } catch {
3646
3373
  }
3647
3374
  }
3648
- messages.push(
3649
- this.buildMessage({
3650
- messageId: "",
3651
- role: "user",
3652
- timestampMs,
3653
- parts: [{ type: "text", text: visibleText, time_created: timestampMs }]
3654
- })
3655
- );
3656
- currentAssistantIndex = null;
3657
- latestAssistantTextIndex = null;
3658
- 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;
3659
3382
  }
3660
3383
  // ---- Reasoning ----
3661
- convertReasoning(payload, messages, timestampMs, currentAssistantIndex) {
3384
+ convertReasoning(payload, transcript, timestampMs, activeModel) {
3662
3385
  const summary = payload["summary"];
3663
- if (!Array.isArray(summary)) {
3664
- return { currentAssistantIndex, latestAssistantTextIndex: null, pendingPlan: null };
3665
- }
3386
+ if (!Array.isArray(summary)) return;
3666
3387
  const texts = [];
3667
3388
  for (const item of summary) {
3668
3389
  if (typeof item === "object" && item !== null) {
@@ -3673,42 +3394,25 @@ var CodexAgent = class extends FileSystemSessionSource {
3673
3394
  }
3674
3395
  }
3675
3396
  }
3676
- if (texts.length === 0) {
3677
- return { currentAssistantIndex, latestAssistantTextIndex: null, pendingPlan: null };
3678
- }
3397
+ if (texts.length === 0) return;
3679
3398
  const reasoningText = texts.join("\n");
3680
3399
  const part = { type: "reasoning", text: reasoningText, time_created: timestampMs };
3681
- if (currentAssistantIndex !== null) {
3682
- const message = messages[currentAssistantIndex];
3683
- const hasText = message.parts.some((p) => p.type === "text");
3684
- const hasTool = message.parts.some((p) => p.type === "tool");
3685
- if (!hasText && !hasTool) {
3686
- message.parts.push(part);
3687
- return { currentAssistantIndex, latestAssistantTextIndex: null, pendingPlan: null };
3688
- }
3689
- }
3690
- messages.push(
3691
- this.buildMessage({
3692
- messageId: "",
3693
- role: "assistant",
3400
+ transcript.appendAssistantPart(
3401
+ part,
3402
+ {
3403
+ id: "",
3694
3404
  timestampMs,
3695
- parts: [part],
3696
- agent: "codex"
3697
- })
3405
+ agent: "codex",
3406
+ model: activeModel
3407
+ },
3408
+ { resetLatestText: true }
3698
3409
  );
3699
- return {
3700
- currentAssistantIndex: messages.length - 1,
3701
- latestAssistantTextIndex: null,
3702
- pendingPlan: null
3703
- };
3704
3410
  }
3705
3411
  // ---- Function call ----
3706
- convertFunctionCall(payload, messages, pendingToolCalls, timestampMs, currentAssistantIndex, latestAssistantTextIndex) {
3412
+ convertFunctionCall(payload, transcript, timestampMs, activeModel) {
3707
3413
  const callId = String(payload["call_id"] ?? "").trim();
3708
3414
  const name = String(payload["name"] ?? "").trim();
3709
- if (!name) {
3710
- return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan: null };
3711
- }
3415
+ if (!name) return;
3712
3416
  const toolIdentity = resolveToolIdentity(name, payload["namespace"]);
3713
3417
  const arguments_ = normalizeToolArguments2(payload["arguments"]);
3714
3418
  const toolPart = {
@@ -3723,59 +3427,27 @@ var CodexAgent = class extends FileSystemSessionSource {
3723
3427
  },
3724
3428
  time_created: timestampMs
3725
3429
  };
3726
- const targetIndex = latestAssistantTextIndex ?? currentAssistantIndex;
3727
- if (targetIndex !== null) {
3728
- const message = messages[targetIndex];
3729
- const partIndex = message.parts.length;
3730
- message.parts.push(toolPart);
3731
- message.mode = "tool";
3732
- if (callId) {
3733
- pendingToolCalls.set(callId, [targetIndex, partIndex]);
3734
- }
3735
- return {
3736
- currentAssistantIndex: targetIndex,
3737
- latestAssistantTextIndex: targetIndex,
3738
- pendingPlan: null
3739
- };
3740
- }
3741
- messages.push(
3742
- this.buildMessage({
3743
- messageId: "",
3744
- role: "assistant",
3745
- timestampMs,
3746
- parts: [toolPart],
3747
- agent: "codex",
3748
- mode: "tool"
3749
- })
3430
+ transcript.appendToolCall(
3431
+ toolPart,
3432
+ { id: "", timestampMs, agent: "codex", model: activeModel },
3433
+ { markModeAsTool: true }
3750
3434
  );
3751
- const newIndex = messages.length - 1;
3752
- if (callId) {
3753
- pendingToolCalls.set(callId, [newIndex, 0]);
3754
- }
3755
- return { currentAssistantIndex: newIndex, latestAssistantTextIndex: null, pendingPlan: null };
3756
3435
  }
3757
3436
  // ---- Function call output ----
3758
- convertFunctionCallOutput(payload, messages, pendingToolCalls, timestampMs) {
3437
+ convertToolCallOutput(payload, transcript, timestampMs) {
3759
3438
  const callId = String(payload["call_id"] ?? "").trim();
3760
3439
  if (!callId) return;
3761
- const location = pendingToolCalls.get(callId);
3762
- if (!location) return;
3763
3440
  const outputText = cleanInternalText(String(payload["output"] ?? ""));
3764
3441
  const outputParts = outputText ? [{ type: "text", text: outputText, time_created: timestampMs }] : [];
3765
- const [msgIndex, partIndex] = location;
3766
- const state = messages[msgIndex].parts[partIndex].state ?? (messages[msgIndex].parts[partIndex].state = {});
3767
3442
  if (outputParts.length > 0) {
3768
- state.output = [...outputParts];
3769
- state.status = "completed";
3443
+ transcript.resolveToolCall(callId, { output: outputParts, status: "completed" });
3770
3444
  }
3771
3445
  }
3772
3446
  // ---- Custom tool call ----
3773
- convertCustomToolCall(payload, messages, pendingToolCalls, timestampMs, currentAssistantIndex, latestAssistantTextIndex) {
3447
+ convertCustomToolCall(payload, transcript, timestampMs, activeModel) {
3774
3448
  const callId = String(payload["call_id"] ?? "").trim();
3775
3449
  const name = String(payload["name"] ?? "").trim();
3776
- if (!name) {
3777
- return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan: null };
3778
- }
3450
+ if (!name) return;
3779
3451
  const toolIdentity = resolveToolIdentity(name, payload["namespace"]);
3780
3452
  const rawInput = payload["input"];
3781
3453
  const normalizedInput = normalizeCustomToolArguments(name, rawInput);
@@ -3791,70 +3463,87 @@ var CodexAgent = class extends FileSystemSessionSource {
3791
3463
  },
3792
3464
  time_created: timestampMs
3793
3465
  };
3794
- const targetIndex = latestAssistantTextIndex ?? currentAssistantIndex;
3795
- if (targetIndex !== null) {
3796
- const message = messages[targetIndex];
3797
- const partIndex = message.parts.length;
3798
- message.parts.push(toolPart);
3799
- message.mode = "tool";
3800
- if (callId) {
3801
- pendingToolCalls.set(callId, [targetIndex, partIndex]);
3802
- }
3803
- return {
3804
- currentAssistantIndex: targetIndex,
3805
- latestAssistantTextIndex: targetIndex,
3806
- pendingPlan: null
3807
- };
3808
- }
3809
- messages.push(
3810
- this.buildMessage({
3811
- messageId: "",
3812
- role: "assistant",
3813
- timestampMs,
3814
- parts: [toolPart],
3815
- agent: "codex",
3816
- mode: "tool"
3817
- })
3466
+ transcript.appendToolCall(
3467
+ toolPart,
3468
+ { id: "", timestampMs, agent: "codex", model: activeModel },
3469
+ { markModeAsTool: true }
3818
3470
  );
3819
- const newIndex = messages.length - 1;
3820
- if (callId) {
3821
- pendingToolCalls.set(callId, [newIndex, 0]);
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);
3822
3493
  }
3823
- return { currentAssistantIndex: newIndex, latestAssistantTextIndex: null, pendingPlan: null };
3494
+ this.activeStack.push(marker);
3495
+ return marker;
3824
3496
  }
3825
- // ---- Custom tool call output ----
3826
- convertCustomToolCallOutput(payload, messages, pendingToolCalls, timestampMs) {
3827
- const callId = String(payload["call_id"] ?? "").trim();
3828
- if (!callId) return;
3829
- const location = pendingToolCalls.get(callId);
3830
- if (!location) return;
3831
- const outputText = cleanInternalText(String(payload["output"] ?? ""));
3832
- const outputParts = outputText ? [{ type: "text", text: outputText, time_created: timestampMs }] : [];
3833
- const [msgIndex, partIndex] = location;
3834
- const state = messages[msgIndex].parts[partIndex].state ?? (messages[msgIndex].parts[partIndex].state = {});
3835
- if (outputParts.length > 0) {
3836
- state.output = [...outputParts];
3837
- state.status = "completed";
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;
3838
3506
  }
3839
3507
  }
3840
- // ---- Message builder ----
3841
- buildMessage(opts) {
3842
- return {
3843
- id: opts.messageId,
3844
- role: opts.role,
3845
- agent: opts.agent ?? null,
3846
- time_created: opts.timestampMs,
3847
- mode: opts.mode ?? null,
3848
- model: opts.model ?? null,
3849
- provider: opts.provider ?? null,
3850
- tokens: opts.tokens ? opts.tokens : void 0,
3851
- cost: opts.cost ?? 0,
3852
- parts: opts.parts,
3853
- subagent_id: opts.subagent_id,
3854
- nickname: opts.nickname
3855
- };
3508
+ measure(name, fn) {
3509
+ const marker = this.start(name);
3510
+ try {
3511
+ return fn();
3512
+ } finally {
3513
+ this.end(marker);
3514
+ }
3515
+ }
3516
+ async measureAsync(name, fn) {
3517
+ const marker = this.start(name);
3518
+ try {
3519
+ return await fn();
3520
+ } finally {
3521
+ this.end(marker);
3522
+ }
3523
+ }
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);
3530
+ }
3531
+ return lines.join("\n");
3532
+ }
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 = [];
3856
3544
  }
3857
3545
  };
3546
+ var perf = new PerfTracer();
3858
3547
  var CURSOR_TOOL_TITLE_MAP = {
3859
3548
  read_file_v2: "read",
3860
3549
  edit_file_v2: "edit",
@@ -4008,7 +3697,7 @@ var CursorAgent = class extends DatabaseSessionSource {
4008
3697
  if (!existsSync8(wsJsonPath)) continue;
4009
3698
  let workspacePath;
4010
3699
  try {
4011
- const data = JSON.parse(readFileSync6(wsJsonPath, "utf-8"));
3700
+ const data = JSON.parse(readFileSync5(wsJsonPath, "utf-8"));
4012
3701
  const uri = data.folder ?? data.workspace ?? "";
4013
3702
  if (!uri) continue;
4014
3703
  workspacePath = normalize(decodeURIComponent(uri.replace(/^file:\/\//, "")));
@@ -4022,12 +3711,12 @@ var CursorAgent = class extends DatabaseSessionSource {
4022
3711
  try {
4023
3712
  const row = wsDb.prepare("SELECT value FROM ItemTable WHERE key = 'composer.composerData'").get();
4024
3713
  if (!row?.value) continue;
4025
- const parsed2 = JSON.parse(row.value);
3714
+ const parsed = JSON.parse(row.value);
4026
3715
  let composers;
4027
- if (parsed2 !== null && typeof parsed2 === "object" && "allComposers" in parsed2 && Array.isArray(parsed2["allComposers"])) {
4028
- composers = parsed2.allComposers;
4029
- } else if (Array.isArray(parsed2)) {
4030
- 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;
4031
3720
  } else {
4032
3721
  continue;
4033
3722
  }
@@ -4406,10 +4095,10 @@ var CursorAgent = class extends DatabaseSessionSource {
4406
4095
  if (toolData.result !== void 0) {
4407
4096
  if (typeof toolData.result === "string") {
4408
4097
  try {
4409
- const parsed2 = JSON.parse(toolData.result);
4410
- state.output = parsed2;
4411
- if (parsed2.error || parsed2.message || parsed2.stderr) {
4412
- 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;
4413
4102
  state.status = "error";
4414
4103
  }
4415
4104
  } catch {
@@ -4531,20 +4220,6 @@ function normalizeTextParts(content, timestampMs) {
4531
4220
  const text = cleanInternalText(contentToText(content));
4532
4221
  return text ? [{ type: "text", text, time_created: timestampMs }] : [];
4533
4222
  }
4534
- function buildMessage(params) {
4535
- return {
4536
- id: params.id,
4537
- role: params.role,
4538
- agent: params.agent,
4539
- time_created: params.timestampMs,
4540
- provider: params.provider,
4541
- model: params.model,
4542
- tokens: params.tokens,
4543
- cost: params.cost,
4544
- cost_source: params.costSource,
4545
- parts: params.parts
4546
- };
4547
- }
4548
4223
  function getEntryTimestamp(entry) {
4549
4224
  return parseTimestampMs3(entry["timestamp"]);
4550
4225
  }
@@ -4588,29 +4263,6 @@ var PiAgent = class extends FileSystemSessionSource {
4588
4263
  if (!this.basePath) return false;
4589
4264
  return this.listSessionFiles().length > 0;
4590
4265
  }
4591
- scan(options) {
4592
- if (!this.basePath) return [];
4593
- const scanMarker = perf.start("pi:scan");
4594
- const files = this.listSessionFiles(options);
4595
- options?.onProgress?.({ total: files.length, processed: 0, sessions: 0 });
4596
- const heads = [];
4597
- let processed = 0;
4598
- for (const file of files) {
4599
- try {
4600
- const head = getParsedSession(this.parseSessionHeadResult(file));
4601
- if (head) {
4602
- heads.push(head);
4603
- this.sessionMetaMap.set(head.id, this.buildSessionMeta(head, file));
4604
- }
4605
- } catch {
4606
- } finally {
4607
- processed += 1;
4608
- options?.onProgress?.({ total: files.length, processed, sessions: heads.length });
4609
- }
4610
- }
4611
- perf.end(scanMarker);
4612
- return heads;
4613
- }
4614
4266
  listSessionSources(options) {
4615
4267
  if (!this.basePath) return [];
4616
4268
  return this.listSessionFiles(options).map((file) => ({
@@ -4630,9 +4282,8 @@ var PiAgent = class extends FileSystemSessionSource {
4630
4282
  const meta = this.sessionMetaMap.get(sessionId);
4631
4283
  if (!meta) throw new Error(`Session not found: ${sessionId}`);
4632
4284
  if (!existsSync9(meta.sourcePath)) throw new Error(`Session file missing: ${meta.sourcePath}`);
4633
- const parsed2 = this.parsePiFile(meta.sourcePath);
4634
- const state = this.convertEntries(parsed2.pathEntries);
4635
- const cleanedMessages = cleanParsedMessages(state.messages);
4285
+ const parsed = this.parsePiFile(meta.sourcePath);
4286
+ const state = this.convertEntries(parsed.pathEntries);
4636
4287
  return {
4637
4288
  id: meta.id,
4638
4289
  title: meta.title,
@@ -4641,7 +4292,7 @@ var PiAgent = class extends FileSystemSessionSource {
4641
4292
  time_created: meta.createdAt,
4642
4293
  time_updated: meta.updatedAt,
4643
4294
  stats: {
4644
- message_count: cleanedMessages.length,
4295
+ message_count: state.messages.length,
4645
4296
  total_input_tokens: state.totalInputTokens,
4646
4297
  total_output_tokens: state.totalOutputTokens,
4647
4298
  total_cache_read_tokens: state.totalCacheReadTokens || void 0,
@@ -4649,7 +4300,7 @@ var PiAgent = class extends FileSystemSessionSource {
4649
4300
  total_cost: state.totalCost,
4650
4301
  cost_source: state.totalCost > 0 ? "recorded" : void 0
4651
4302
  },
4652
- messages: cleanedMessages
4303
+ messages: state.messages
4653
4304
  };
4654
4305
  }
4655
4306
  listSessionFiles(options) {
@@ -4693,18 +4344,18 @@ var PiAgent = class extends FileSystemSessionSource {
4693
4344
  return JSON.stringify([HEAD_INDEX_VERSION3, PARSER_VERSION2, stat.mtimeMs, stat.size]);
4694
4345
  }
4695
4346
  parseSessionHeadResult(filePath) {
4696
- const parsed2 = this.parsePiFile(filePath);
4697
- const state = this.convertEntries(parsed2.pathEntries);
4347
+ const parsed = this.parsePiFile(filePath);
4348
+ const state = this.convertEntries(parsed.pathEntries);
4698
4349
  const messageCount = state.messages.length;
4699
4350
  if (messageCount === 0) return filteredSession("no visible messages");
4700
4351
  const modelUsage = Object.keys(state.modelUsage).length > 0 ? state.modelUsage : void 0;
4701
4352
  return parsedSession({
4702
- id: parsed2.sessionId,
4703
- slug: `pi/${parsed2.sessionId}`,
4704
- title: parsed2.title,
4705
- directory: parsed2.directory,
4706
- time_created: parsed2.createdAt,
4707
- 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,
4708
4359
  stats: {
4709
4360
  message_count: messageCount,
4710
4361
  total_input_tokens: state.totalInputTokens,
@@ -4718,7 +4369,7 @@ var PiAgent = class extends FileSystemSessionSource {
4718
4369
  });
4719
4370
  }
4720
4371
  parsePiFile(filePath) {
4721
- const records = Array.from(parseJsonlLines(readFileSync7(filePath, "utf-8")));
4372
+ const records = Array.from(parseJsonlLines(readFileSync6(filePath, "utf-8")));
4722
4373
  if (records.length === 0) throw new Error("empty file");
4723
4374
  const header = records.find((record) => record["type"] === "session");
4724
4375
  if (!header) throw new Error("missing session header");
@@ -4766,74 +4417,52 @@ var PiAgent = class extends FileSystemSessionSource {
4766
4417
  return null;
4767
4418
  }
4768
4419
  convertEntries(entries) {
4769
- const messages = [];
4770
- const pendingToolCalls = /* @__PURE__ */ new Map();
4420
+ const builder = new TranscriptBuilder({ messageDefaults: "sparse" });
4771
4421
  const modelUsage = {};
4772
- let totalInputTokens = 0;
4773
- let totalOutputTokens = 0;
4774
- let totalCacheReadTokens = 0;
4775
- let totalCacheCreateTokens = 0;
4776
- let totalCost = 0;
4777
4422
  for (const entry of entries) {
4778
4423
  const timestampMs = getEntryTimestamp(entry);
4779
4424
  const type = String(entry["type"] ?? "");
4780
4425
  if (type === "message") {
4781
4426
  const message = entry["message"];
4782
4427
  if (!isObject(message)) continue;
4783
- const result = this.convertAgentMessage(
4784
- entry,
4785
- message,
4786
- timestampMs,
4787
- pendingToolCalls,
4788
- messages.length,
4789
- messages
4790
- );
4791
- if (!result) continue;
4792
- if (result.message) messages.push(result.message);
4793
- totalInputTokens += result.inputTokens;
4794
- totalOutputTokens += result.outputTokens;
4795
- totalCacheReadTokens += result.cacheReadTokens;
4796
- totalCacheCreateTokens += result.cacheCreateTokens;
4797
- totalCost += result.cost;
4798
- if (result.model && result.totalTokens > 0) {
4799
- 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;
4800
4433
  }
4801
4434
  continue;
4802
4435
  }
4803
4436
  const summary = this.convertSummaryEntry(entry, timestampMs);
4804
- if (summary) messages.push(summary);
4437
+ if (summary) builder.appendMessage(summary);
4805
4438
  }
4439
+ const result = builder.finish();
4806
4440
  return {
4807
- messages,
4808
- totalInputTokens,
4809
- totalOutputTokens,
4810
- totalCacheReadTokens,
4811
- totalCacheCreateTokens,
4812
- 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,
4813
4447
  modelUsage
4814
4448
  };
4815
4449
  }
4816
- convertAgentMessage(entry, message, timestampMs, pendingToolCalls, nextMessageIndex, messages) {
4450
+ convertAgentMessage(entry, message, timestampMs, builder) {
4817
4451
  const id = String(entry["id"] ?? "");
4818
4452
  const role = String(message["role"] ?? "");
4819
4453
  if (role === "user") {
4820
4454
  const parts = normalizeTextParts(message["content"], timestampMs);
4821
4455
  if (parts.length === 0) return null;
4822
- return this.emptyUsageResult(buildMessage({ id, role: "user", timestampMs, parts }));
4456
+ return this.emptyUsageResult({ id, role: "user", timestampMs, parts });
4823
4457
  }
4824
4458
  if (role === "assistant") {
4825
- const parts = this.normalizeAssistantParts(
4826
- message["content"],
4827
- timestampMs,
4828
- pendingToolCalls,
4829
- nextMessageIndex
4830
- );
4459
+ const parts = this.normalizeAssistantParts(message["content"], timestampMs);
4831
4460
  if (parts.length === 0) return null;
4832
4461
  const usage = this.normalizeUsage(message["usage"]);
4833
4462
  const model = typeof message["model"] === "string" ? message["model"].trim() : null;
4834
4463
  const cost = usage.cost ?? estimateTokenCost(model, usage.tokens) ?? 0;
4835
4464
  return {
4836
- message: buildMessage({
4465
+ message: {
4837
4466
  id,
4838
4467
  role: "assistant",
4839
4468
  agent: "pi",
@@ -4844,18 +4473,13 @@ var PiAgent = class extends FileSystemSessionSource {
4844
4473
  tokens: usage.tokens,
4845
4474
  cost: cost || void 0,
4846
4475
  costSource: cost > 0 ? "recorded" : void 0
4847
- }),
4848
- inputTokens: usage.inputTokens,
4849
- outputTokens: usage.outputTokens,
4850
- cacheReadTokens: usage.cacheReadTokens,
4851
- cacheCreateTokens: usage.cacheCreateTokens,
4476
+ },
4852
4477
  totalTokens: usage.totalTokens,
4853
- cost,
4854
4478
  model
4855
4479
  };
4856
4480
  }
4857
4481
  if (role === "toolResult") {
4858
- this.attachToolResult(message, timestampMs, pendingToolCalls, messages);
4482
+ this.attachToolResult(message, timestampMs, builder);
4859
4483
  return this.emptyUsageResult();
4860
4484
  }
4861
4485
  if (role === "bashExecution") {
@@ -4864,24 +4488,22 @@ var PiAgent = class extends FileSystemSessionSource {
4864
4488
  if (role === "custom" && message["display"] === true) {
4865
4489
  const parts = normalizeTextParts(message["content"], timestampMs);
4866
4490
  if (parts.length === 0) return null;
4867
- return this.emptyUsageResult(buildMessage({ id, role: "user", timestampMs, parts }));
4491
+ return this.emptyUsageResult({ id, role: "user", timestampMs, parts });
4868
4492
  }
4869
4493
  if (role === "branchSummary" || role === "compactionSummary") {
4870
4494
  const summary = String(message["summary"] ?? "").trim();
4871
4495
  if (!summary) return null;
4872
- return this.emptyUsageResult(
4873
- buildMessage({
4874
- id,
4875
- role: "assistant",
4876
- agent: "pi",
4877
- timestampMs,
4878
- parts: [{ type: "text", text: summary, time_created: timestampMs }]
4879
- })
4880
- );
4496
+ return this.emptyUsageResult({
4497
+ id,
4498
+ role: "assistant",
4499
+ agent: "pi",
4500
+ timestampMs,
4501
+ parts: [{ type: "text", text: summary, time_created: timestampMs }]
4502
+ });
4881
4503
  }
4882
4504
  return null;
4883
4505
  }
4884
- normalizeAssistantParts(content, timestampMs, pendingToolCalls, messageIndex) {
4506
+ normalizeAssistantParts(content, timestampMs) {
4885
4507
  if (!Array.isArray(content)) return [];
4886
4508
  const parts = [];
4887
4509
  for (const item of content) {
@@ -4912,29 +4534,26 @@ var PiAgent = class extends FileSystemSessionSource {
4912
4534
  }
4913
4535
  };
4914
4536
  parts.push(toolPart);
4915
- if (callId) pendingToolCalls.set(callId, [messageIndex, parts.length - 1]);
4916
4537
  }
4917
4538
  }
4918
4539
  return parts;
4919
4540
  }
4920
- attachToolResult(message, timestampMs, pendingToolCalls, messages) {
4541
+ attachToolResult(message, timestampMs, builder) {
4921
4542
  const callId = String(message["toolCallId"] ?? "").trim();
4922
4543
  const output = normalizeTextParts(message["content"], timestampMs);
4923
- const location = callId ? pendingToolCalls.get(callId) : void 0;
4924
- if (!location) return;
4925
- const [messageIndex, partIndex] = location;
4926
- const target = messages[messageIndex]?.parts[partIndex];
4927
- if (!target?.state) return;
4928
- target.state.output = output;
4929
- target.state.status = message["isError"] === true ? "error" : "completed";
4930
- target.state.metadata = message["details"];
4931
- 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
+ });
4932
4551
  }
4933
4552
  convertBashExecution(id, message, timestampMs) {
4934
4553
  const command = String(message["command"] ?? "");
4935
4554
  const output = String(message["output"] ?? "");
4936
4555
  const isError = Number(message["exitCode"] ?? 0) !== 0 || message["cancelled"] === true;
4937
- return buildMessage({
4556
+ return {
4938
4557
  id,
4939
4558
  role: "tool",
4940
4559
  timestampMs,
@@ -4957,7 +4576,7 @@ var PiAgent = class extends FileSystemSessionSource {
4957
4576
  }
4958
4577
  }
4959
4578
  ]
4960
- });
4579
+ };
4961
4580
  }
4962
4581
  convertSummaryEntry(entry, timestampMs) {
4963
4582
  const type = entry["type"];
@@ -4968,13 +4587,13 @@ var PiAgent = class extends FileSystemSessionSource {
4968
4587
  const rawText = type === "custom_message" ? contentToText(entry["content"]) : String(entry["summary"] ?? "");
4969
4588
  const text = cleanInternalText(rawText);
4970
4589
  if (!text) return null;
4971
- return buildMessage({
4590
+ return {
4972
4591
  id: String(entry["id"] ?? ""),
4973
4592
  role: type === "custom_message" ? "user" : "assistant",
4974
4593
  agent: type === "custom_message" ? void 0 : "pi",
4975
4594
  timestampMs,
4976
4595
  parts: [{ type: "text", text, time_created: timestampMs }]
4977
- });
4596
+ };
4978
4597
  }
4979
4598
  normalizeUsage(raw) {
4980
4599
  const usage = isObject(raw) ? raw : {};
@@ -5004,12 +4623,7 @@ var PiAgent = class extends FileSystemSessionSource {
5004
4623
  emptyUsageResult(message) {
5005
4624
  return {
5006
4625
  message,
5007
- inputTokens: 0,
5008
- outputTokens: 0,
5009
- cacheReadTokens: 0,
5010
- cacheCreateTokens: 0,
5011
4626
  totalTokens: 0,
5012
- cost: 0,
5013
4627
  model: null
5014
4628
  };
5015
4629
  }
@@ -5030,44 +4644,30 @@ var ZCodeAgent = class extends OpenCodeSqliteAgent {
5030
4644
  }
5031
4645
  };
5032
4646
  registerAgent({
5033
- name: "claudecode",
5034
- displayName: "Claude Code",
5035
4647
  icon: "/icon/agent/claudecode.svg",
5036
4648
  create: () => new ClaudeCodeAgent()
5037
4649
  });
5038
4650
  registerAgent({
5039
- name: "opencode",
5040
- displayName: "OpenCode",
5041
4651
  icon: "/icon/agent/opencode.svg",
5042
4652
  create: () => new OpenCodeAgent()
5043
4653
  });
5044
4654
  registerAgent({
5045
- name: "zcode",
5046
- displayName: "ZCode",
5047
4655
  icon: "/icon/agent/zcode.svg",
5048
4656
  create: () => new ZCodeAgent()
5049
4657
  });
5050
4658
  registerAgent({
5051
- name: "kimi",
5052
- displayName: "Kimi-Cli",
5053
4659
  icon: "/icon/agent/kimi.svg",
5054
4660
  create: () => new KimiAgent()
5055
4661
  });
5056
4662
  registerAgent({
5057
- name: "codex",
5058
- displayName: "Codex",
5059
4663
  icon: "/icon/agent/codex.svg",
5060
4664
  create: () => new CodexAgent()
5061
4665
  });
5062
4666
  registerAgent({
5063
- name: "pi",
5064
- displayName: "Pi",
5065
4667
  icon: "/icon/agent/pi.svg",
5066
4668
  create: () => new PiAgent()
5067
4669
  });
5068
4670
  registerAgent({
5069
- name: "cursor",
5070
- displayName: "Cursor",
5071
4671
  icon: "/icon/agent/cursor.svg",
5072
4672
  create: () => new CursorAgent()
5073
4673
  });
@@ -5083,7 +4683,7 @@ var realFs = {
5083
4683
  },
5084
4684
  readText(path2) {
5085
4685
  try {
5086
- return readFileSync8(path2, "utf8");
4686
+ return readFileSync7(path2, "utf8");
5087
4687
  } catch {
5088
4688
  return null;
5089
4689
  }
@@ -5096,43 +4696,6 @@ var realFs = {
5096
4696
  };
5097
4697
  }
5098
4698
  };
5099
- function getAgentName(session) {
5100
- return session.slug.split("/")[0]?.toLowerCase() || "unknown";
5101
- }
5102
- function buildProjectGroups(sessions) {
5103
- const groups = /* @__PURE__ */ new Map();
5104
- for (const session of sessions) {
5105
- const identity = session.project_identity;
5106
- if (!identity) continue;
5107
- const activity = session.time_updated ?? session.time_created;
5108
- const groupKey = `${identity.kind}:${identity.key}`;
5109
- const current = groups.get(groupKey);
5110
- if (current) {
5111
- current.sources.add(getAgentName(session));
5112
- current.sessionCount += 1;
5113
- current.lastActivity = Math.max(current.lastActivity, activity);
5114
- } else {
5115
- groups.set(groupKey, {
5116
- identity,
5117
- sources: /* @__PURE__ */ new Set([getAgentName(session)]),
5118
- sessionCount: 1,
5119
- lastActivity: activity
5120
- });
5121
- }
5122
- }
5123
- return [...groups.values()].map((group) => ({
5124
- identityKind: group.identity.kind,
5125
- identityKey: group.identity.key,
5126
- displayName: group.identity.displayName,
5127
- sources: [...group.sources].sort(),
5128
- sessionCount: group.sessionCount,
5129
- lastActivity: group.lastActivity || null
5130
- })).sort((a, b) => {
5131
- if (a.identityKind === "loose" && b.identityKind !== "loose") return 1;
5132
- if (b.identityKind === "loose" && a.identityKind !== "loose") return -1;
5133
- return (b.lastActivity ?? 0) - (a.lastActivity ?? 0);
5134
- });
5135
- }
5136
4699
  var MANIFESTS = [
5137
4700
  "package.json",
5138
4701
  "Cargo.toml",
@@ -5145,6 +4708,23 @@ var MANIFESTS = [
5145
4708
  var PARSEABLE_MANIFESTS = ["package.json", "Cargo.toml", "pyproject.toml"];
5146
4709
  var LOOSE_DIRS = /* @__PURE__ */ new Set(["/tmp", "/private/tmp"]);
5147
4710
  var LOOSE_HOME_DIRS = ["Desktop", "Downloads", "Documents"];
4711
+ var PROJECT_IDENTITY_KINDS = /* @__PURE__ */ new Set([
4712
+ "git_remote",
4713
+ "git_common_dir",
4714
+ "manifest_path",
4715
+ "synthetic",
4716
+ "path",
4717
+ "loose"
4718
+ ]);
4719
+ function isProjectIdentityKind(value) {
4720
+ return PROJECT_IDENTITY_KINDS.has(value);
4721
+ }
4722
+ function getProjectIdentityKey(identity) {
4723
+ return `${identity.kind}:${identity.key}`;
4724
+ }
4725
+ function matchesProjectIdentity(identity, expected) {
4726
+ return identity?.kind === expected.kind && identity.key === expected.key;
4727
+ }
5148
4728
  function normalizeGitRemote(url) {
5149
4729
  if (!url) return null;
5150
4730
  let value = url.trim().replace(/\.git$/, "");
@@ -5275,15 +4855,53 @@ function parseManifestName(file, text) {
5275
4855
  }
5276
4856
  return null;
5277
4857
  }
4858
+ function getAgentName(session) {
4859
+ return session.slug.split("/")[0]?.toLowerCase() || "unknown";
4860
+ }
4861
+ function buildProjectGroups(sessions) {
4862
+ const groups = /* @__PURE__ */ new Map();
4863
+ for (const session of sessions) {
4864
+ const identity = session.project_identity;
4865
+ if (!identity) continue;
4866
+ const activity = session.time_updated ?? session.time_created;
4867
+ const groupKey = getProjectIdentityKey(identity);
4868
+ const current = groups.get(groupKey);
4869
+ if (current) {
4870
+ current.sources.add(getAgentName(session));
4871
+ current.sessionCount += 1;
4872
+ current.lastActivity = Math.max(current.lastActivity, activity);
4873
+ } else {
4874
+ groups.set(groupKey, {
4875
+ identity,
4876
+ sources: /* @__PURE__ */ new Set([getAgentName(session)]),
4877
+ sessionCount: 1,
4878
+ lastActivity: activity
4879
+ });
4880
+ }
4881
+ }
4882
+ return [...groups.values()].map((group) => ({
4883
+ identityKind: group.identity.kind,
4884
+ identityKey: group.identity.key,
4885
+ displayName: group.identity.displayName,
4886
+ sources: [...group.sources].sort(),
4887
+ sessionCount: group.sessionCount,
4888
+ lastActivity: group.lastActivity || null
4889
+ })).sort((a, b) => {
4890
+ if (a.identityKind === "loose" && b.identityKind !== "loose") return 1;
4891
+ if (b.identityKind === "loose" && a.identityKind !== "loose") return -1;
4892
+ return (b.lastActivity ?? 0) - (a.lastActivity ?? 0);
4893
+ });
4894
+ }
5278
4895
  function createProjectScopeMatcher(queryPath, fs = realFs) {
4896
+ const identity = computeIdentity(queryPath, fs);
5279
4897
  return {
5280
- identityKey: computeIdentity(queryPath, fs).key,
4898
+ identity: { kind: identity.kind, key: identity.key },
5281
4899
  path: normalizeScopePath(queryPath)
5282
4900
  };
5283
4901
  }
5284
4902
  function matchesProjectScope(session, scope) {
5285
4903
  if (!session.directory) return false;
5286
- if (session.project_identity?.key === scope.identityKey) return true;
4904
+ if (matchesProjectIdentity(session.project_identity, scope.identity)) return true;
5287
4905
  return isPathScopeMatch(scope.path, session.directory);
5288
4906
  }
5289
4907
  function filterSessionsByProjectScope(sessions, queryPath, fs) {
@@ -6039,7 +5657,7 @@ function buildSessionContentFromMessages(title, messages) {
6039
5657
  }
6040
5658
  return chunks.join("\n");
6041
5659
  }
6042
- var CACHE_SCHEMA_VERSION = 13;
5660
+ var CACHE_SCHEMA_VERSION = 14;
6043
5661
  function withCacheDb(fn) {
6044
5662
  const cachePath = getCachePath2();
6045
5663
  const db = openDb(cachePath);
@@ -6312,6 +5930,7 @@ function createSearchTables(db) {
6312
5930
  activity_time INTEGER NOT NULL,
6313
5931
  content_text TEXT NOT NULL,
6314
5932
  content_hash TEXT NOT NULL,
5933
+ indexed_message_count INTEGER NOT NULL,
6315
5934
  indexed_at INTEGER NOT NULL,
6316
5935
  UNIQUE(agent_name, session_id)
6317
5936
  );
@@ -6345,6 +5964,24 @@ function createSearchTriggers(db) {
6345
5964
  END;
6346
5965
  `);
6347
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
+ }
6348
5985
  function dropSearchTriggers(db) {
6349
5986
  db.exec(`
6350
5987
  DROP TRIGGER IF EXISTS session_documents_ai;
@@ -6450,6 +6087,9 @@ function readLegacyCacheVersion(db) {
6450
6087
  return Number(versionRow?.value ?? 0);
6451
6088
  }
6452
6089
  function inferCacheSchemaVersion(db) {
6090
+ if (columnExists(db, "session_documents", "indexed_message_count")) {
6091
+ return 14;
6092
+ }
6453
6093
  if (tableExists(db, "message_tools")) {
6454
6094
  return 11;
6455
6095
  }
@@ -6882,39 +6522,16 @@ function ensureSchema(db, dbPath) {
6882
6522
  version: 12,
6883
6523
  migrate(db2) {
6884
6524
  refreshProjectIdentities(db2);
6885
- }
6886
- },
6887
- { version: 13, migrate: createCacheTables }
6888
- ]
6889
- });
6890
- createLatestCacheSchema(db);
6891
- if (getUserVersion(db) <= CACHE_SCHEMA_VERSION) {
6892
- setCacheSchemaVersion(db);
6893
- }
6894
- }
6895
- function shouldBulkSyncSearchIndex(options, changedCount) {
6896
- if (options.isBulk != null) {
6897
- return options.isBulk;
6898
- }
6899
- const threshold = options.bulkThreshold ?? SEARCH_INDEX_BULK_SYNC_THRESHOLD;
6900
- return threshold > 0 && changedCount >= threshold;
6901
- }
6902
- function sessionContentHash(session) {
6903
- return JSON.stringify([
6904
- session.slug,
6905
- session.title,
6906
- session.directory,
6907
- session.time_created,
6908
- session.time_updated ?? session.time_created,
6909
- session.stats.message_count,
6910
- session.stats.total_input_tokens,
6911
- session.stats.total_output_tokens,
6912
- session.stats.total_cache_read_tokens ?? 0,
6913
- session.stats.total_cache_create_tokens ?? 0,
6914
- session.stats.total_cost,
6915
- session.stats.cost_source ?? "",
6916
- session.stats.total_tokens ?? 0
6917
- ]);
6525
+ }
6526
+ },
6527
+ { version: 13, migrate: createCacheTables },
6528
+ { version: 14, migrate: addIndexedMessageCount }
6529
+ ]
6530
+ });
6531
+ createLatestCacheSchema(db);
6532
+ if (getUserVersion(db) <= CACHE_SCHEMA_VERSION) {
6533
+ setCacheSchemaVersion(db);
6534
+ }
6918
6535
  }
6919
6536
  function escapeFtsTerm(value) {
6920
6537
  return value.replaceAll('"', '""');
@@ -6938,9 +6555,7 @@ function splitSearchTokens(input) {
6938
6555
  }
6939
6556
  token += char;
6940
6557
  }
6941
- if (token) {
6942
- tokens.push(token);
6943
- }
6558
+ if (token) tokens.push(token);
6944
6559
  return tokens;
6945
6560
  }
6946
6561
  function unwrapSearchValue(value) {
@@ -7000,7 +6615,10 @@ function parseSearchQuery(input) {
7000
6615
  if (key === "agent") filters.agent = value.toLowerCase();
7001
6616
  else if (key === "project") filters.project = value;
7002
6617
  else if (key === "projectkey" || key === "project-key") filters.projectKey = value;
7003
- else if (key === "cwd") filters.cwd = value;
6618
+ else if (key === "projectkind" || key === "project-kind") {
6619
+ if (isProjectIdentityKind(value)) filters.projectKind = value;
6620
+ else consumed = false;
6621
+ } else if (key === "cwd") filters.cwd = value;
7004
6622
  else if (key === "tool") filters.tools = appendUnique(filters.tools, value.toLowerCase());
7005
6623
  else if (key === "file" || key === "path") filters.file = value;
7006
6624
  else if (key === "kind" || key === "filekind" || key === "file-kind") {
@@ -7011,21 +6629,15 @@ function parseSearchQuery(input) {
7011
6629
  }
7012
6630
  } else if (key === "tag" || key === "signal") {
7013
6631
  const tag = value.toLowerCase();
7014
- if (isSmartTag(tag)) {
7015
- filters.tags = appendUnique(filters.tags, tag);
7016
- } else {
7017
- consumed = false;
7018
- }
6632
+ if (isSmartTag(tag)) filters.tags = appendUnique(filters.tags, tag);
6633
+ else consumed = false;
7019
6634
  } else if (key === "cost") {
7020
6635
  parseCostQualifier(value, filters);
7021
6636
  } else {
7022
6637
  consumed = false;
7023
6638
  }
7024
- if (consumed) {
7025
- hasQualifiers = true;
7026
- } else {
7027
- textTokens.push(token);
7028
- }
6639
+ if (consumed) hasQualifiers = true;
6640
+ else textTokens.push(token);
7029
6641
  }
7030
6642
  return {
7031
6643
  text: textTokens.join(" ").trim(),
@@ -7035,18 +6647,86 @@ function parseSearchQuery(input) {
7035
6647
  }
7036
6648
  function toFtsQuery(input) {
7037
6649
  const tokens = splitSearchTokens(input);
7038
- const mapped = tokens.map((token) => {
7039
- if (/^OR$/i.test(token)) {
7040
- return "OR";
7041
- }
6650
+ return tokens.map((token) => {
6651
+ if (/^OR$/i.test(token)) return "OR";
7042
6652
  if (token.startsWith('"') && token.endsWith('"')) {
7043
6653
  return `"${escapeFtsTerm(token.slice(1, -1))}"`;
7044
6654
  }
7045
6655
  return `"${escapeFtsTerm(token)}"`;
7046
6656
  }).filter(
7047
6657
  (token, index, values) => token !== "OR" || index > 0 && index < values.length - 1 && values[index - 1] !== "OR" && values[index + 1] !== "OR"
7048
- );
7049
- return mapped.join(" ");
6658
+ ).join(" ");
6659
+ }
6660
+ var SEARCH_INDEX_STATE_BATCH_SIZE = 900;
6661
+ function shouldBulkSyncSearchIndex(options, changedCount) {
6662
+ if (options.isBulk != null) {
6663
+ return options.isBulk;
6664
+ }
6665
+ const threshold = options.bulkThreshold ?? SEARCH_INDEX_BULK_SYNC_THRESHOLD;
6666
+ return threshold > 0 && changedCount >= threshold;
6667
+ }
6668
+ function sessionContentHash(session) {
6669
+ return JSON.stringify([
6670
+ session.slug,
6671
+ session.title,
6672
+ session.directory,
6673
+ session.time_created,
6674
+ session.time_updated ?? session.time_created,
6675
+ session.stats.message_count,
6676
+ session.stats.total_input_tokens,
6677
+ session.stats.total_output_tokens,
6678
+ session.stats.total_cache_read_tokens ?? 0,
6679
+ session.stats.total_cache_create_tokens ?? 0,
6680
+ session.stats.total_cost,
6681
+ session.stats.cost_source ?? "",
6682
+ session.stats.total_tokens ?? 0
6683
+ ]);
6684
+ }
6685
+ function searchIndexStateFromRows(indexedRows, messageCountRows) {
6686
+ return {
6687
+ contentHashBySessionId: new Map(
6688
+ indexedRows.map((row) => [String(row.session_id), String(row.content_hash ?? "")])
6689
+ ),
6690
+ indexedMessageCountBySessionId: new Map(
6691
+ indexedRows.map((row) => [String(row.session_id), Number(row.indexed_message_count ?? 0)])
6692
+ ),
6693
+ messageCountBySessionId: new Map(
6694
+ messageCountRows.map((row) => [String(row.session_id), Number(row.value ?? 0)])
6695
+ )
6696
+ };
6697
+ }
6698
+ function readSearchIndexState(db, agentName, sessionIds) {
6699
+ const rows = [];
6700
+ const uniqueSessionIds = [...new Set(sessionIds)];
6701
+ for (let offset = 0; offset < uniqueSessionIds.length; offset += SEARCH_INDEX_STATE_BATCH_SIZE) {
6702
+ const batch = uniqueSessionIds.slice(offset, offset + SEARCH_INDEX_STATE_BATCH_SIZE);
6703
+ const requestedRows = batch.map(() => "(?)").join(", ");
6704
+ const batchRows = db.prepare(
6705
+ `
6706
+ WITH requested_session_ids(session_id) AS (VALUES ${requestedRows})
6707
+ SELECT
6708
+ requested.session_id,
6709
+ documents.content_hash,
6710
+ documents.indexed_message_count,
6711
+ COUNT(messages.message_index) AS value
6712
+ FROM requested_session_ids AS requested
6713
+ LEFT JOIN session_documents AS documents
6714
+ ON documents.agent_name = ? AND documents.session_id = requested.session_id
6715
+ LEFT JOIN messages
6716
+ ON messages.agent_name = ? AND messages.session_id = requested.session_id
6717
+ GROUP BY
6718
+ requested.session_id,
6719
+ documents.content_hash,
6720
+ documents.indexed_message_count
6721
+ `
6722
+ ).all(...batch, agentName, agentName);
6723
+ rows.push(...batchRows);
6724
+ }
6725
+ return searchIndexStateFromRows(rows, rows);
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);
7050
6730
  }
7051
6731
  function loadSearchIndexEntry(agentName, change, loadSessionData) {
7052
6732
  try {
@@ -7071,6 +6751,12 @@ function loadSearchIndexEntry(agentName, change, loadSessionData) {
7071
6751
  return null;
7072
6752
  }
7073
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
+ }
7074
6760
  function writeSearchIndexRows(db, agentName, removedSessionIds, entries) {
7075
6761
  const deleteRow = db.prepare(
7076
6762
  "DELETE FROM session_documents WHERE agent_name = ? AND session_id = ?"
@@ -7142,8 +6828,9 @@ function writeSearchIndexRows(db, agentName, removedSessionIds, entries) {
7142
6828
  activity_time,
7143
6829
  content_text,
7144
6830
  content_hash,
6831
+ indexed_message_count,
7145
6832
  indexed_at
7146
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
6833
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
7147
6834
  ON CONFLICT(agent_name, session_id) DO UPDATE SET
7148
6835
  slug = excluded.slug,
7149
6836
  title = excluded.title,
@@ -7156,6 +6843,7 @@ function writeSearchIndexRows(db, agentName, removedSessionIds, entries) {
7156
6843
  activity_time = excluded.activity_time,
7157
6844
  content_text = excluded.content_text,
7158
6845
  content_hash = excluded.content_hash,
6846
+ indexed_message_count = excluded.indexed_message_count,
7159
6847
  indexed_at = excluded.indexed_at
7160
6848
  `);
7161
6849
  for (const sessionId of new Set(removedSessionIds)) {
@@ -7164,6 +6852,7 @@ function writeSearchIndexRows(db, agentName, removedSessionIds, entries) {
7164
6852
  deleteMessageTools.run(agentName, sessionId, 0);
7165
6853
  deleteMessages.run(agentName, sessionId, 0);
7166
6854
  }
6855
+ let indexed = 0;
7167
6856
  for (const entry of entries) {
7168
6857
  const activityTime = entry.session.time_updated ?? entry.session.time_created;
7169
6858
  upsertSessionRow(upsertIndexedSession, agentName, entry.session, null, entry.sortIndex, null);
@@ -7211,44 +6900,47 @@ function writeSearchIndexRows(db, agentName, removedSessionIds, entries) {
7211
6900
  activityTime,
7212
6901
  entry.contentText,
7213
6902
  entry.contentHash,
6903
+ entry.messages.length,
7214
6904
  Date.now()
7215
6905
  );
6906
+ indexed += 1;
7216
6907
  }
6908
+ return indexed;
7217
6909
  }
7218
6910
  function syncSessionSearchIndex(agentName, sessions, loadSessionData, options = {}) {
7219
6911
  return withCacheDb((db) => {
7220
6912
  ensureFtsConsistency(db);
7221
6913
  const startedAt = performance.now();
7222
6914
  const existingRows = db.prepare(
7223
- "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"
7224
6916
  ).all(agentName);
7225
- const existingMap = new Map(
7226
- existingRows.map((row) => [String(row.session_id), String(row.content_hash ?? "")])
7227
- );
7228
6917
  const sessionSortIndexMap = new Map(sessions.map((session, index) => [session.id, index]));
7229
6918
  const messageCountRows = db.prepare(
7230
6919
  "SELECT session_id, COUNT(*) AS value FROM messages WHERE agent_name = ? GROUP BY session_id"
7231
6920
  ).all(agentName);
7232
- const messageCountMap = new Map(
7233
- messageCountRows.map((row) => [String(row.session_id), Number(row.value ?? 0)])
7234
- );
6921
+ const searchIndexState = searchIndexStateFromRows(existingRows, messageCountRows);
7235
6922
  const sessionMap = new Map(sessions.map((session) => [session.id, session]));
7236
6923
  const toDelete = existingRows.map((row) => String(row.session_id)).filter((sessionId) => !sessionMap.has(sessionId));
7237
6924
  const toUpsert = sessions.filter(
7238
- (session) => existingMap.get(session.id) !== sessionContentHash(session) || messageCountMap.get(session.id) !== session.stats.message_count
6925
+ (session) => searchIndexEntryNeedsUpdate(searchIndexState, session)
7239
6926
  );
7240
6927
  const changedCount = toDelete.length + toUpsert.length;
7241
6928
  const isBulk = shouldBulkSyncSearchIndex(options, changedCount);
7242
- const loaded = toUpsert.map(
7243
- (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,
7244
6937
  agentName,
7245
- { session, sortIndex: sessionSortIndexMap.get(session.id) ?? 0 },
7246
- loadSessionData
7247
- )
7248
- ).filter((entry) => entry !== null);
7249
- const writeRows = () => writeSearchIndexRows(db, agentName, toDelete, loaded);
6938
+ toDelete,
6939
+ loadSearchIndexEntries(agentName, changes, loadSessionData)
6940
+ );
6941
+ };
7250
6942
  let rebuildDurationMs;
7251
- const needsRebuild = isBulk && (toDelete.length > 0 || loaded.length > 0);
6943
+ const needsRebuild = isBulk && changedCount > 0;
7252
6944
  if (needsRebuild) {
7253
6945
  db.transaction(() => {
7254
6946
  dropSearchTriggers(db);
@@ -7270,8 +6962,8 @@ function syncSessionSearchIndex(agentName, sessions, loadSessionData, options =
7270
6962
  sessions: sessions.length,
7271
6963
  changed: toUpsert.length,
7272
6964
  deleted: toDelete.length,
7273
- indexed: loaded.length,
7274
- skipped: toUpsert.length - loaded.length,
6965
+ indexed,
6966
+ skipped: toUpsert.length - indexed,
7275
6967
  durationMs: performance.now() - startedAt,
7276
6968
  rebuildDurationMs
7277
6969
  };
@@ -7293,24 +6985,28 @@ function syncSessionSearchIndexChanges(agentName, changes, removedSessionIds, lo
7293
6985
  return withCacheDb((db) => {
7294
6986
  ensureFtsConsistency(db);
7295
6987
  const startedAt = performance.now();
7296
- const getIndexedRow = db.prepare(
7297
- "SELECT content_hash FROM session_documents WHERE agent_name = ? AND session_id = ?"
6988
+ const searchIndexState = readSearchIndexState(
6989
+ db,
6990
+ agentName,
6991
+ changes.map(({ session }) => session.id)
7298
6992
  );
7299
- const getMessageCount = db.prepare(
7300
- "SELECT COUNT(*) AS value FROM messages WHERE agent_name = ? AND session_id = ?"
6993
+ const toUpsert = changes.filter(
6994
+ ({ session }) => searchIndexEntryNeedsUpdate(searchIndexState, session)
7301
6995
  );
7302
- const toUpsert = changes.filter(({ session }) => {
7303
- const indexed = getIndexedRow.get(agentName, session.id);
7304
- const messageCount = getMessageCount.get(agentName, session.id);
7305
- return String(indexed?.content_hash ?? "") !== sessionContentHash(session) || Number(messageCount?.value ?? 0) !== session.stats.message_count;
7306
- });
7307
6996
  const uniqueRemovedSessionIds = Array.from(new Set(removedSessionIds));
7308
6997
  const changedCount = uniqueRemovedSessionIds.length + toUpsert.length;
7309
6998
  const isBulk = shouldBulkSyncSearchIndex(options, changedCount);
7310
- const loaded = toUpsert.map((change) => loadSearchIndexEntry(agentName, change, loadSessionData)).filter((entry) => entry !== null);
7311
- 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
+ };
7312
7008
  let rebuildDurationMs;
7313
- const needsRebuild = isBulk && (uniqueRemovedSessionIds.length > 0 || loaded.length > 0);
7009
+ const needsRebuild = isBulk && changedCount > 0;
7314
7010
  if (needsRebuild) {
7315
7011
  db.transaction(() => {
7316
7012
  dropSearchTriggers(db);
@@ -7332,8 +7028,8 @@ function syncSessionSearchIndexChanges(agentName, changes, removedSessionIds, lo
7332
7028
  sessions: changes.length,
7333
7029
  changed: toUpsert.length,
7334
7030
  deleted: uniqueRemovedSessionIds.length,
7335
- indexed: loaded.length,
7336
- skipped: toUpsert.length - loaded.length,
7031
+ indexed,
7032
+ skipped: toUpsert.length - indexed,
7337
7033
  durationMs: performance.now() - startedAt,
7338
7034
  rebuildDurationMs
7339
7035
  };
@@ -7347,25 +7043,26 @@ function mergeSearchLists(left, right) {
7347
7043
  return values.length > 0 ? [...new Set(values)] : void 0;
7348
7044
  }
7349
7045
  function mergeSearchQueryOptions(query, options) {
7350
- const parsed2 = parseSearchQuery(query);
7046
+ const parsed = parseSearchQuery(query);
7351
7047
  return {
7352
- text: parsed2.text || (parsed2.hasQualifiers ? "" : query.trim()),
7048
+ text: parsed.text || (parsed.hasQualifiers ? "" : query.trim()),
7353
7049
  options: {
7354
7050
  ...options,
7355
- agent: options.agent ?? parsed2.filters.agent,
7356
- project: options.project ?? parsed2.filters.project,
7357
- projectKey: options.projectKey ?? parsed2.filters.projectKey,
7358
- cwd: options.cwd ?? parsed2.filters.cwd,
7359
- tags: mergeSearchLists(options.tags, parsed2.filters.tags),
7360
- tools: mergeSearchLists(options.tools, parsed2.filters.tools),
7361
- file: options.file ?? parsed2.filters.file,
7362
- fileKind: options.fileKind ?? parsed2.filters.fileKind,
7363
- costMin: options.costMin ?? parsed2.filters.costMin,
7364
- costMax: options.costMax ?? parsed2.filters.costMax,
7365
- costMinExclusive: options.costMinExclusive ?? parsed2.filters.costMinExclusive,
7366
- 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
7367
7064
  },
7368
- parsed: parsed2
7065
+ parsed
7369
7066
  };
7370
7067
  }
7371
7068
  function sessionMatchesSearchCost(session, options) {
@@ -7389,13 +7086,20 @@ function buildSessionSearchFilters(options) {
7389
7086
  clauses.push("s.agent_name = ?");
7390
7087
  params.push(options.agent);
7391
7088
  }
7392
- if (options.projectKey) {
7393
- clauses.push("s.project_identity_key = ?");
7394
- params.push(options.projectKey);
7089
+ if (options.projectKind || options.projectKey) {
7090
+ if (options.projectKind && options.projectKey) {
7091
+ clauses.push("s.project_identity_kind = ? AND s.project_identity_key = ?");
7092
+ params.push(options.projectKind, options.projectKey);
7093
+ } else {
7094
+ clauses.push("0");
7095
+ }
7395
7096
  }
7396
7097
  if (options.cwd) {
7397
- clauses.push("(s.project_identity_key = ? OR LOWER(s.directory) LIKE ? ESCAPE '\\')");
7398
- params.push(computeIdentity(options.cwd, realFs).key, likePattern(options.cwd));
7098
+ const identity = computeIdentity(options.cwd, realFs);
7099
+ clauses.push(
7100
+ "((s.project_identity_kind = ? AND s.project_identity_key = ?) OR LOWER(s.directory) LIKE ? ESCAPE '\\')"
7101
+ );
7102
+ params.push(identity.kind, identity.key, likePattern(options.cwd));
7399
7103
  }
7400
7104
  if (options.project) {
7401
7105
  clauses.push(
@@ -7640,10 +7344,13 @@ function searchSessions(query, options = {}) {
7640
7344
  }
7641
7345
  function fileActivityFilters(options) {
7642
7346
  const path2 = options.path ? normalizeFilePathSearch(options.path) : "";
7347
+ const cwdIdentity = options.cwd ? computeIdentity(options.cwd, realFs) : null;
7643
7348
  return {
7349
+ projectKind: options.projectKind ?? null,
7644
7350
  projectKey: options.projectKey ?? null,
7645
7351
  projectLike: options.project ? likePattern(options.project) : null,
7646
- cwdKey: options.cwd ? computeIdentity(options.cwd, realFs).key : null,
7352
+ cwdKind: cwdIdentity?.kind ?? null,
7353
+ cwdKey: cwdIdentity?.key ?? null,
7647
7354
  cwdLike: options.cwd ? likePattern(options.cwd) : null,
7648
7355
  path: path2,
7649
7356
  pathLike: path2 ? likePattern(path2) : null
@@ -7672,9 +7379,13 @@ function buildFileActivityWhere(options) {
7672
7379
  clauses.push("fa.session_id = ?");
7673
7380
  params.push(options.sessionId);
7674
7381
  }
7675
- if (filters.projectKey != null) {
7676
- clauses.push("fa.project_identity_key = ?");
7677
- params.push(filters.projectKey);
7382
+ if (filters.projectKind != null || filters.projectKey != null) {
7383
+ if (filters.projectKind != null && filters.projectKey != null) {
7384
+ clauses.push("s.project_identity_kind = ? AND fa.project_identity_key = ?");
7385
+ params.push(filters.projectKind, filters.projectKey);
7386
+ } else {
7387
+ clauses.push("0");
7388
+ }
7678
7389
  }
7679
7390
  if (filters.projectLike != null) {
7680
7391
  clauses.push(
@@ -7683,8 +7394,10 @@ function buildFileActivityWhere(options) {
7683
7394
  params.push(filters.projectLike, filters.projectLike, filters.projectLike);
7684
7395
  }
7685
7396
  if (filters.cwdKey != null) {
7686
- clauses.push("(s.project_identity_key = ? OR LOWER(s.directory) LIKE ? ESCAPE '\\')");
7687
- params.push(filters.cwdKey, filters.cwdLike);
7397
+ clauses.push(
7398
+ "((s.project_identity_kind = ? AND s.project_identity_key = ?) OR LOWER(s.directory) LIKE ? ESCAPE '\\')"
7399
+ );
7400
+ params.push(filters.cwdKind, filters.cwdKey, filters.cwdLike);
7688
7401
  }
7689
7402
  if (filters.pathLike != null) {
7690
7403
  const pathQuery = filePathFtsQuery(filters.path);
@@ -7782,6 +7495,7 @@ function searchFileActivitySessions(query, options = {}) {
7782
7495
  if (!path2) return [];
7783
7496
  const rows = listFileActivity({
7784
7497
  agent: search.options.agent,
7498
+ projectKind: search.options.projectKind,
7785
7499
  projectKey: search.options.projectKey,
7786
7500
  project: search.options.project,
7787
7501
  cwd: search.options.cwd,
@@ -8258,9 +7972,7 @@ function sessionSignature(session) {
8258
7972
  ]);
8259
7973
  }
8260
7974
  function sortSessions(sessions) {
8261
- return [...sessions].sort(
8262
- (a, b) => (b.time_updated ?? b.time_created) - (a.time_updated ?? a.time_created)
8263
- );
7975
+ return sortSessionsByActivity(sessions);
8264
7976
  }
8265
7977
  function computeSessionDiff(cachedSessions, updatedSessions, changedIds = [], signature = sessionSignature) {
8266
7978
  const cachedMap = new Map(cachedSessions.map((session) => [session.id, session]));
@@ -8404,6 +8116,47 @@ async function ensureSessionTags(agent, sessions, workerUrl) {
8404
8116
  return ensureSessionTagsSync(agent, sessions);
8405
8117
  }
8406
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
+ }
8407
8160
  async function scanAgentSmart(agent, options, onProgress) {
8408
8161
  const agentStart = performance.now();
8409
8162
  const timing = { total: 0 };
@@ -8424,19 +8177,13 @@ async function scanAgentSmart(agent, options, onProgress) {
8424
8177
  phase: "cache",
8425
8178
  cachedCount: cached.sessions.length
8426
8179
  });
8427
- onProgress?.({ agent: agent.name, phase: "complete", newCount: cached.sessions.length });
8428
- const t32 = performance.now();
8429
- const cachedWithIdentity2 = attachMissingProjectIdentities(cached.sessions);
8430
- timing.identity = performance.now() - t32;
8431
- const filtered3 = filterSessions(cachedWithIdentity2, options);
8432
- timing.total = performance.now() - agentStart;
8433
- return {
8434
- agent,
8435
- heads: filtered3,
8436
- fromCache: true,
8180
+ return finalizeAgentScan(agent, cached.sessions, {
8181
+ finalization: { kind: "cache-only", cached },
8182
+ options,
8437
8183
  timing,
8438
- cacheTimestamp: cached.timestamp
8439
- };
8184
+ agentStart,
8185
+ onProgress
8186
+ });
8440
8187
  }
8441
8188
  const isAvail = agent.isAvailable();
8442
8189
  if (!isAvail) {
@@ -8464,55 +8211,26 @@ async function scanAgentSmart(agent, options, onProgress) {
8464
8211
  agent.incrementalScan(cached.sessions, checkResult.changedIds || [])
8465
8212
  );
8466
8213
  timing.scan = performance.now() - t2;
8467
- const t32 = performance.now();
8468
- const sessionsWithIdentity = attachMissingProjectIdentities(updatedSessions);
8469
- timing.identity = performance.now() - t32;
8470
- const t42 = performance.now();
8471
- const tagged2 = options.includeSmartTags === false ? { sessions: sessionsWithIdentity, changed: false } : await ensureSessionTags(agent, sessionsWithIdentity, options.smartTagWorkerUrl);
8472
- timing.tags = performance.now() - t42;
8473
- if (options.writeCache !== false) {
8474
- saveCachedSessionDiff(
8475
- agent,
8476
- cached.sessions,
8477
- tagged2.sessions,
8478
- checkResult.changedIds ?? []
8479
- );
8480
- }
8481
- onProgress?.({
8482
- agent: agent.name,
8483
- phase: "complete",
8484
- newCount: tagged2.sessions.length
8485
- });
8486
- const filtered3 = filterSessions(tagged2.sessions, options);
8487
- timing.total = performance.now() - agentStart;
8488
- return {
8489
- agent,
8490
- heads: filtered3,
8491
- fromCache: true,
8492
- 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,
8493
8222
  timing,
8494
- cacheTimestamp: checkResult.timestamp
8495
- };
8496
- }
8497
- onProgress?.({ agent: agent.name, phase: "complete", newCount: cached.sessions.length });
8498
- const t3 = performance.now();
8499
- const cachedWithIdentity = attachMissingProjectIdentities(cached.sessions);
8500
- timing.identity = performance.now() - t3;
8501
- const t4 = performance.now();
8502
- const tagged = options.includeSmartTags === false ? { sessions: cachedWithIdentity, changed: false } : await ensureSessionTags(agent, cachedWithIdentity, options.smartTagWorkerUrl);
8503
- timing.tags = performance.now() - t4;
8504
- if (tagged.changed && options.writeCache !== false) {
8505
- saveCachedSessionDiff(agent, cached.sessions, tagged.sessions);
8223
+ agentStart,
8224
+ onProgress
8225
+ });
8506
8226
  }
8507
- const filtered2 = filterSessions(tagged.sessions, options);
8508
- timing.total = performance.now() - agentStart;
8509
- return {
8510
- agent,
8511
- heads: filtered2,
8512
- fromCache: true,
8227
+ return finalizeAgentScan(agent, cached.sessions, {
8228
+ finalization: { kind: "unchanged", cached },
8229
+ options,
8513
8230
  timing,
8514
- cacheTimestamp: cached.timestamp
8515
- };
8231
+ agentStart,
8232
+ onProgress
8233
+ });
8516
8234
  }
8517
8235
  }
8518
8236
  if (options.cacheOnly) {
@@ -8560,9 +8278,9 @@ async function scanAgentFull(agent, options, onProgress, timing = { total: 0 },
8560
8278
  markAgentFullSyncCompleted(agent.name);
8561
8279
  }
8562
8280
  onProgress?.({ agent: agent.name, phase: "complete", newCount: tagged.sessions.length });
8563
- const filtered2 = filterSessions(tagged.sessions, options);
8281
+ const filtered = filterSessions(tagged.sessions, options);
8564
8282
  timing.total = performance.now() - agentStart;
8565
- return { agent, heads: filtered2, fromCache: false, timing };
8283
+ return { agent, heads: filtered, fromCache: false, timing };
8566
8284
  } catch (err) {
8567
8285
  console.error(`Error scanning ${agent.name}:`, err);
8568
8286
  return { agent, heads: [], fromCache: false };
@@ -8610,67 +8328,35 @@ async function scanSessions(options = {}, onProgress) {
8610
8328
  async function scanSessionsAsync(options = {}, onProgress) {
8611
8329
  return scanSessions(options, onProgress);
8612
8330
  }
8613
- var BOOKMARK_DB_FILENAME = "state.db";
8614
- var BOOKMARK_SCHEMA_VERSION = 1;
8331
+ var STATE_DB_FILENAME = "state.db";
8332
+ var STATE_SCHEMA_VERSION = 2;
8615
8333
  var MEMORY_STATE_STORE = "memory";
8616
- var memoryBookmarks = /* @__PURE__ */ new Map();
8617
- var BookmarkStorageUnavailableError = class extends Error {
8334
+ var StateStorageUnavailableError = class extends Error {
8618
8335
  constructor() {
8619
8336
  super("SQLite state database is unavailable");
8620
- this.name = "BookmarkStorageUnavailableError";
8337
+ this.name = "StateStorageUnavailableError";
8621
8338
  }
8622
8339
  };
8623
8340
  function getStateDir() {
8624
- if (process.env.CODESESH_STATE_DIR) {
8625
- return process.env.CODESESH_STATE_DIR;
8626
- }
8627
- const p = platform2();
8628
- if (p === "darwin") {
8341
+ if (process.env.CODESESH_STATE_DIR) return process.env.CODESESH_STATE_DIR;
8342
+ const currentPlatform = platform2();
8343
+ if (currentPlatform === "darwin") {
8629
8344
  return join12(homedir5(), "Library", "Application Support", "codesesh");
8630
8345
  }
8631
- if (p === "win32") {
8346
+ if (currentPlatform === "win32") {
8632
8347
  const appData = process.env.APPDATA ?? process.env.LOCALAPPDATA;
8633
8348
  return join12(appData ?? join12(homedir5(), "AppData", "Roaming"), "codesesh");
8634
8349
  }
8635
8350
  return join12(process.env.XDG_DATA_HOME ?? join12(homedir5(), ".local", "share"), "codesesh");
8636
8351
  }
8637
8352
  function getStateDbPath() {
8638
- return join12(getStateDir(), BOOKMARK_DB_FILENAME);
8353
+ return join12(getStateDir(), STATE_DB_FILENAME);
8639
8354
  }
8640
8355
  function useMemoryStateStore() {
8641
8356
  return process.env.CODESESH_STATE_STORE === MEMORY_STATE_STORE;
8642
8357
  }
8643
- function getBookmarkKey(agentKey, sessionId) {
8644
- return JSON.stringify([agentKey, sessionId]);
8645
- }
8646
- function getActivityTime(bookmark) {
8647
- return bookmark.time_updated ?? bookmark.time_created;
8648
- }
8649
- function sortBookmarks(bookmarks) {
8650
- return bookmarks.sort((a, b) => {
8651
- const activityDelta = getActivityTime(b) - getActivityTime(a);
8652
- return activityDelta || b.bookmarked_at - a.bookmarked_at;
8653
- });
8654
- }
8655
- function listMemoryBookmarks() {
8656
- return sortBookmarks(Array.from(memoryBookmarks.values()));
8657
- }
8658
- function upsertMemoryBookmark(bookmark) {
8659
- const key = getBookmarkKey(bookmark.agentKey, bookmark.sessionId);
8660
- const saved = {
8661
- ...bookmark,
8662
- bookmarked_at: memoryBookmarks.get(key)?.bookmarked_at ?? Date.now()
8663
- };
8664
- memoryBookmarks.set(key, saved);
8665
- return saved;
8666
- }
8667
- function createStateSchema(db) {
8358
+ function createBookmarksTable(db) {
8668
8359
  db.exec(`
8669
- CREATE TABLE IF NOT EXISTS state_meta (
8670
- key TEXT PRIMARY KEY,
8671
- value TEXT NOT NULL
8672
- );
8673
-
8674
8360
  CREATE TABLE IF NOT EXISTS bookmarks (
8675
8361
  agent_name TEXT NOT NULL,
8676
8362
  session_id TEXT NOT NULL,
@@ -8685,6 +8371,27 @@ function createStateSchema(db) {
8685
8371
  );
8686
8372
  `);
8687
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
+ }
8688
8395
  function readLegacyStateVersion(db) {
8689
8396
  if (!tableExists(db, "state_meta") || !columnExists(db, "state_meta", "key") || !columnExists(db, "state_meta", "value")) {
8690
8397
  return 0;
@@ -8694,13 +8401,9 @@ function readLegacyStateVersion(db) {
8694
8401
  }
8695
8402
  function getCurrentStateSchemaVersion(db) {
8696
8403
  const userVersion = getUserVersion(db);
8697
- if (userVersion > 0) {
8698
- return userVersion;
8699
- }
8404
+ if (userVersion > 0) return userVersion;
8700
8405
  const legacyVersion = readLegacyStateVersion(db);
8701
- if (legacyVersion > 0) {
8702
- return legacyVersion;
8703
- }
8406
+ if (legacyVersion > 0) return legacyVersion;
8704
8407
  return tableExists(db, "bookmarks") ? 1 : 0;
8705
8408
  }
8706
8409
  function hasAnyStateSchema(db) {
@@ -8708,14 +8411,14 @@ function hasAnyStateSchema(db) {
8708
8411
  }
8709
8412
  function setStateSchemaVersion(db) {
8710
8413
  createStateSchema(db);
8711
- setUserVersion(db, BOOKMARK_SCHEMA_VERSION);
8414
+ setUserVersion(db, STATE_SCHEMA_VERSION);
8712
8415
  db.prepare(
8713
8416
  `
8714
8417
  INSERT INTO state_meta(key, value)
8715
8418
  VALUES ('version', ?)
8716
8419
  ON CONFLICT(key) DO UPDATE SET value = excluded.value
8717
8420
  `
8718
- ).run(String(BOOKMARK_SCHEMA_VERSION));
8421
+ ).run(String(STATE_SCHEMA_VERSION));
8719
8422
  }
8720
8423
  function ensureSchema2(db, dbPath) {
8721
8424
  const currentVersion = getCurrentStateSchemaVersion(db);
@@ -8726,22 +8429,22 @@ function ensureSchema2(db, dbPath) {
8726
8429
  runSchemaMigrations(db, {
8727
8430
  dbPath,
8728
8431
  currentVersion,
8729
- targetVersion: BOOKMARK_SCHEMA_VERSION,
8432
+ targetVersion: STATE_SCHEMA_VERSION,
8730
8433
  backupLabel: "state-migration",
8731
- backupTables: ["bookmarks"],
8732
- migrations: [{ version: 1, migrate: createStateSchema }]
8434
+ backupTables: ["bookmarks", "session_aliases"],
8435
+ migrations: [
8436
+ { version: 1, migrate: createBookmarksTable },
8437
+ { version: 2, migrate: createSessionAliasesTable }
8438
+ ]
8733
8439
  });
8734
- createStateSchema(db);
8735
- if (getUserVersion(db) <= BOOKMARK_SCHEMA_VERSION) {
8440
+ if (currentVersion <= STATE_SCHEMA_VERSION) {
8736
8441
  setStateSchemaVersion(db);
8737
8442
  }
8738
8443
  }
8739
8444
  function withStateDb(fn) {
8740
8445
  const statePath = getStateDbPath();
8741
8446
  const db = openDb(statePath);
8742
- if (!db) {
8743
- throw new BookmarkStorageUnavailableError();
8744
- }
8447
+ if (!db) throw new StateStorageUnavailableError();
8745
8448
  try {
8746
8449
  ensureSchema2(db, statePath);
8747
8450
  return fn(db);
@@ -8749,6 +8452,31 @@ function withStateDb(fn) {
8749
8452
  db.close();
8750
8453
  }
8751
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
+ }
8752
8480
  function toBookmarkRecord(row) {
8753
8481
  return {
8754
8482
  agentKey: String(row.agent_name ?? ""),
@@ -8921,6 +8649,78 @@ function deleteBookmark(agentKey, sessionId) {
8921
8649
  ).run(agentKey, sessionId);
8922
8650
  });
8923
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
+ }
8924
8724
  var DASHBOARD_RECENT_LIMIT = 10;
8925
8725
  function getTotalTokens(stats) {
8926
8726
  return stats.total_tokens ?? stats.total_input_tokens + stats.total_output_tokens;
@@ -8974,10 +8774,11 @@ function buildDashboard(sessions, options) {
8974
8774
  for (const session of sessions) {
8975
8775
  const agentName = getSessionAgentName(session);
8976
8776
  if (scope.agent && agentName !== scope.agent) continue;
8977
- if (scope.projectKey) {
8777
+ if (scope.projectKind || scope.projectKey) {
8978
8778
  const identity = session.project_identity;
8979
- if (!identity || identity.key !== scope.projectKey) continue;
8980
- if (scope.projectKind && identity.kind !== scope.projectKind) continue;
8779
+ if (!identity || !scope.projectKind || !scope.projectKey || identity.kind !== scope.projectKind || identity.key !== scope.projectKey) {
8780
+ continue;
8781
+ }
8981
8782
  }
8982
8783
  const activity = getSessionActivityTime(session);
8983
8784
  if (from != null && activity < from) continue;
@@ -9076,6 +8877,90 @@ function buildDashboard(sessions, options) {
9076
8877
  recentSessions
9077
8878
  };
9078
8879
  }
8880
+ function executeSessionSearch(query, options, snapshot) {
8881
+ const merged = mergeSearchQueryOptions(query, options);
8882
+ if (!needsIndexedSearch(merged.text, merged.options)) {
8883
+ return searchRecentSessions(snapshot, merged.options);
8884
+ }
8885
+ return searchIndexedSessions(query, merged.text, merged.parsed, merged.options);
8886
+ }
8887
+ function needsIndexedSearch(textQuery, options) {
8888
+ return Boolean(textQuery || options.file || options.fileKind || options.tools?.length);
8889
+ }
8890
+ function filterSessionsByActivityWindow(sessions, from, to) {
8891
+ if (from == null && to == null) return sessions;
8892
+ return sessions.filter((session) => {
8893
+ const activity = getSessionActivityTime(session);
8894
+ if (from != null && activity < from) return false;
8895
+ if (to != null && activity > to) return false;
8896
+ return true;
8897
+ });
8898
+ }
8899
+ function matchesRecentSearchFilters(session, options, projectScope) {
8900
+ if (options.projectKind || options.projectKey) {
8901
+ if (!options.projectKind || !options.projectKey || !matchesProjectIdentity(session.project_identity, {
8902
+ kind: options.projectKind,
8903
+ key: options.projectKey
8904
+ })) {
8905
+ return false;
8906
+ }
8907
+ }
8908
+ if (projectScope && !matchesProjectScope(session, projectScope)) return false;
8909
+ if (options.project) {
8910
+ const projectNeedle = options.project.toLowerCase();
8911
+ const projectText = [
8912
+ session.project_identity?.key,
8913
+ session.project_identity?.displayName,
8914
+ session.directory
8915
+ ].filter(Boolean).join("\n").toLowerCase();
8916
+ if (!projectText.includes(projectNeedle)) return false;
8917
+ }
8918
+ if (options.tags?.length && !options.tags.every((tag) => session.smart_tags?.includes(tag))) {
8919
+ return false;
8920
+ }
8921
+ if (!sessionMatchesSearchCost(session, options)) return false;
8922
+ return true;
8923
+ }
8924
+ function searchRecentSessions(snapshot, options) {
8925
+ const projectScope = options.cwd ? createProjectScopeMatcher(options.cwd) : null;
8926
+ const entries = options.agent ? [[options.agent, snapshot.byAgent[options.agent] ?? []]] : Object.entries(snapshot.byAgent);
8927
+ return entries.flatMap(
8928
+ ([agentName, sessions]) => filterSessionsByActivityWindow(sessions, options.from, options.to).filter((session) => matchesRecentSearchFilters(session, options, projectScope)).map((session) => ({ agentName, session }))
8929
+ ).sort(
8930
+ (a, b) => (b.session.time_updated ?? b.session.time_created) - (a.session.time_updated ?? a.session.time_created)
8931
+ ).slice(0, options.limit ?? 50).map(({ agentName, session }) => ({
8932
+ agentName,
8933
+ session,
8934
+ snippet: `Recent session \xB7 ${session.directory}`,
8935
+ matchType: "recent"
8936
+ }));
8937
+ }
8938
+ function deriveFileQuery(query, parsed, options) {
8939
+ return options.file ?? (!parsed.text ? parsed.filters.file : void 0) ?? (!parsed.hasQualifiers && query ? parsed.text || query : "");
8940
+ }
8941
+ function mergeSearchResultSources(results, limit) {
8942
+ const seen = /* @__PURE__ */ new Set();
8943
+ const merged = [];
8944
+ for (const result of results) {
8945
+ const key = `${result.agentName}/${result.session.id}`;
8946
+ if (seen.has(key)) continue;
8947
+ seen.add(key);
8948
+ merged.push(result);
8949
+ if (merged.length >= limit) break;
8950
+ }
8951
+ return merged;
8952
+ }
8953
+ function canSkipSessionsSearch(fileQuery, textQuery, options) {
8954
+ return Boolean(
8955
+ fileQuery && !textQuery && !options.tools?.length && !options.tags?.length && options.from == null && options.to == null
8956
+ );
8957
+ }
8958
+ function searchIndexedSessions(query, textQuery, parsed, options) {
8959
+ const fileQuery = deriveFileQuery(query, parsed, options);
8960
+ const fileResults = fileQuery ? searchFileActivitySessions(fileQuery, options) : [];
8961
+ const sessionResults = canSkipSessionsSearch(fileQuery, textQuery, options) ? [] : searchSessions(query, options);
8962
+ return mergeSearchResultSources([...fileResults, ...sessionResults], options.limit ?? 50);
8963
+ }
9079
8964
 
9080
8965
  export {
9081
8966
  registerAgent,
@@ -9101,16 +8986,12 @@ export {
9101
8986
  normalizeTitleText,
9102
8987
  basenameTitle,
9103
8988
  resolveSessionTitle,
9104
- parsed,
9105
- skipped,
9106
- filtered,
9107
8989
  cleanInternalText,
9108
8990
  cleanMessagePart,
9109
8991
  cleanMessageParts,
9110
8992
  cleanParsedMessage,
9111
8993
  cleanParsedMessages,
9112
8994
  firstUserMessageTitle,
9113
- perf,
9114
8995
  getPricingRegistry,
9115
8996
  hasBillablePricing,
9116
8997
  refreshPricingCache,
@@ -9123,11 +9004,15 @@ export {
9123
9004
  openDbReadOnly,
9124
9005
  openDb,
9125
9006
  isSqliteAvailable,
9007
+ perf,
9126
9008
  fallbackDisplayName,
9127
9009
  realFs,
9128
- buildProjectGroups,
9010
+ isProjectIdentityKind,
9011
+ getProjectIdentityKey,
9012
+ matchesProjectIdentity,
9129
9013
  normalizeGitRemote,
9130
9014
  computeIdentity,
9015
+ buildProjectGroups,
9131
9016
  createProjectScopeMatcher,
9132
9017
  matchesProjectScope,
9133
9018
  filterSessionsByProjectScope,
@@ -9141,6 +9026,7 @@ export {
9141
9026
  parseSearchQuery,
9142
9027
  syncSessionSearchIndex,
9143
9028
  syncSessionSearchIndexChanges,
9029
+ mergeSearchQueryOptions,
9144
9030
  searchSessions,
9145
9031
  listFileActivity,
9146
9032
  listSessionFileActivity,
@@ -9165,17 +9051,23 @@ export {
9165
9051
  ensureSessionTagsSync,
9166
9052
  scanSessions,
9167
9053
  scanSessionsAsync,
9168
- BookmarkStorageUnavailableError,
9054
+ StateStorageUnavailableError,
9169
9055
  listBookmarks,
9170
9056
  upsertBookmark,
9171
9057
  importBookmarks,
9172
9058
  deleteBookmark,
9059
+ SESSION_ALIAS_MAX_LENGTH,
9060
+ normalizeSessionAlias,
9061
+ listSessionAliases,
9062
+ upsertSessionAlias,
9063
+ deleteSessionAlias,
9173
9064
  DASHBOARD_RECENT_LIMIT,
9174
9065
  getTotalTokens,
9175
9066
  getSessionAgentName,
9176
9067
  getSessionActivityTime,
9177
9068
  toLocalDateKey,
9178
9069
  startOfLocalDay,
9179
- buildDashboard
9070
+ buildDashboard,
9071
+ executeSessionSearch
9180
9072
  };
9181
- //# sourceMappingURL=chunk-GCOAE7KI.js.map
9073
+ //# sourceMappingURL=chunk-VRVZJDNL.js.map