codesesh 0.9.1 → 0.11.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,19 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // ../core/dist/index.mjs
4
- import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync3, statSync } from "fs";
4
+ import { existsSync as existsSync4, readdirSync, readFileSync as readFileSync3, statSync as statSync2 } from "fs";
5
5
  import { join as join3, basename as basename2, dirname } from "path";
6
- import { existsSync } from "fs";
6
+ import { existsSync, statSync } from "fs";
7
+ import { existsSync as existsSync2 } from "fs";
7
8
  import { homedir, platform } from "os";
8
9
  import { join } from "path";
9
10
  import { readFileSync } from "fs";
10
11
  import { basename } from "path";
11
- import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
12
+ import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
12
13
  import { homedir as homedir2 } from "os";
13
14
  import { join as join2 } from "path";
14
- import { existsSync as existsSync5, statSync as statSync2 } from "fs";
15
15
  import { join as join5 } from "path";
16
- import { existsSync as existsSync4, mkdirSync as mkdirSync2 } from "fs";
16
+ import { existsSync as existsSync5, mkdirSync as mkdirSync2 } from "fs";
17
17
  import { basename as basename3, dirname as dirname2, join as join4 } from "path";
18
18
  import { createRequire } from "module";
19
19
  import { createHash } from "crypto";
@@ -33,6 +33,7 @@ import { existsSync as existsSync8, readdirSync as readdirSync4, readFileSync as
33
33
  import { join as join8, normalize } from "path";
34
34
  import { existsSync as existsSync9, readFileSync as readFileSync7, readdirSync as readdirSync5, statSync as statSync6 } from "fs";
35
35
  import { basename as basename6, join as join9 } from "path";
36
+ import { join as join10 } from "path";
36
37
  import { availableParallelism } from "os";
37
38
  import { Worker } from "worker_threads";
38
39
  import { existsSync as existsSync10, readFileSync as readFileSync8 } from "fs";
@@ -40,11 +41,12 @@ import { spawnSync } from "child_process";
40
41
  import * as os from "os";
41
42
  import * as path from "path";
42
43
  import { resolve, sep } from "path";
43
- import { existsSync as existsSync11, rmSync, unlinkSync } from "fs";
44
- import { join as join10 } from "path";
44
+ import { existsSync as existsSync11 } from "fs";
45
+ import { join as join11 } from "path";
45
46
  import { homedir as homedir4 } from "os";
47
+ import { existsSync as existsSync12, rmSync, unlinkSync } from "fs";
46
48
  import { homedir as homedir5, platform as platform2 } from "os";
47
- import { join as join11 } from "path";
49
+ import { join as join12 } from "path";
48
50
  var registrations = [];
49
51
  function registerAgent(reg) {
50
52
  registrations.push(reg);
@@ -88,6 +90,105 @@ var BaseAgent = class {
88
90
  return `${this.name}://${sessionId}`;
89
91
  }
90
92
  };
93
+ var FileSystemSessionSource = class extends BaseAgent {
94
+ sessionMetaMap = /* @__PURE__ */ new Map();
95
+ getSessionMetaMap() {
96
+ return this.sessionMetaMap;
97
+ }
98
+ setSessionMetaMap(meta) {
99
+ this.sessionMetaMap = meta;
100
+ }
101
+ /**
102
+ * 变更检测:枚举当前源 → 与缓存 metaMap 的指纹/路径比对。
103
+ * 新增、变更、删除三类统一产出 changedIds。
104
+ */
105
+ checkForChanges(_sinceTimestamp, cachedSessions) {
106
+ const currentRefs = this.listSessionSources();
107
+ const currentIds = new Set(currentRefs.map((ref) => ref.sessionId));
108
+ const changedIds = /* @__PURE__ */ new Set();
109
+ for (const ref of currentRefs) {
110
+ const meta = this.sessionMetaMap.get(ref.sessionId);
111
+ const samePath = meta?.sourcePath === ref.sourcePath;
112
+ const sameFingerprint = typeof meta?.sourceFingerprint === "string" && meta.sourceFingerprint === ref.fingerprint;
113
+ if (!samePath || !sameFingerprint) changedIds.add(ref.sessionId);
114
+ }
115
+ for (const session of cachedSessions) {
116
+ if (!currentIds.has(session.id)) changedIds.add(session.id);
117
+ }
118
+ const changedIdList = [...changedIds];
119
+ return {
120
+ hasChanges: changedIdList.length > 0,
121
+ changedIds: changedIdList,
122
+ timestamp: Date.now()
123
+ };
124
+ }
125
+ /**
126
+ * 增量扫描:对变更/新增源调用 scanSessionSource 重解析,
127
+ * 删除已消失的源,合并回 cachedSessions。
128
+ */
129
+ incrementalScan(cachedSessions, changedIds) {
130
+ const sessionMap = new Map(cachedSessions.map((session) => [session.id, session]));
131
+ const changedSet = new Set(changedIds);
132
+ const currentIds = /* @__PURE__ */ new Set();
133
+ for (const ref of this.listSessionSources()) {
134
+ currentIds.add(ref.sessionId);
135
+ if (!changedSet.has(ref.sessionId)) continue;
136
+ const head = this.scanSessionSource(ref.sourcePath);
137
+ if (head) {
138
+ sessionMap.set(head.id, head);
139
+ } else {
140
+ sessionMap.delete(ref.sessionId);
141
+ this.sessionMetaMap.delete(ref.sessionId);
142
+ }
143
+ }
144
+ for (const id of changedSet) {
145
+ if (!currentIds.has(id)) {
146
+ sessionMap.delete(id);
147
+ this.sessionMetaMap.delete(id);
148
+ }
149
+ }
150
+ return [...sessionMap.values()];
151
+ }
152
+ };
153
+ var DatabaseSessionSource = class extends BaseAgent {
154
+ sessionMetaMap = /* @__PURE__ */ new Map();
155
+ /** 记录单个会话的缓存 meta(sourcePath = dbPath)。 */
156
+ rememberSession(sessionId) {
157
+ const dbPath = this.getDatabasePath();
158
+ if (!dbPath) return;
159
+ this.sessionMetaMap.set(sessionId, { id: sessionId, sourcePath: dbPath });
160
+ }
161
+ getSessionMetaMap() {
162
+ return this.sessionMetaMap;
163
+ }
164
+ setSessionMetaMap(meta) {
165
+ this.sessionMetaMap = meta;
166
+ }
167
+ /**
168
+ * 变更检测:数据库内部变更难以按行定位,简单起见按库文件 mtime 判定。
169
+ * 库有变更则标记全部缓存会话刷新。
170
+ */
171
+ checkForChanges(sinceTimestamp, cachedSessions) {
172
+ const dbPath = this.getDatabasePath();
173
+ if (!dbPath || !existsSync(dbPath)) {
174
+ return { hasChanges: false, timestamp: Date.now() };
175
+ }
176
+ try {
177
+ const hasChanges = statSync(dbPath).mtimeMs > sinceTimestamp;
178
+ return {
179
+ hasChanges,
180
+ changedIds: hasChanges ? cachedSessions.map((session) => session.id) : [],
181
+ timestamp: Date.now()
182
+ };
183
+ } catch {
184
+ return { hasChanges: false, timestamp: Date.now() };
185
+ }
186
+ }
187
+ /** 增量扫描:数据库型无法增量,直接全量重扫。 */
188
+ incrementalScan(_cachedSessions, _changedIds) {
189
+ return this.scan();
190
+ }
191
+ };
91
192
  function envPath(name) {
92
193
  const value = process.env[name];
93
194
  if (!value) return null;
@@ -95,7 +196,7 @@ function envPath(name) {
95
196
  }
96
197
  function firstExisting(...paths) {
97
198
  for (const p of paths) {
98
- if (existsSync(p)) return p;
199
+ if (existsSync2(p)) return p;
99
200
  }
100
201
  return null;
101
202
  }
@@ -115,7 +216,8 @@ function resolveProviderRoots() {
115
216
  claudeRoot: envPath("CLAUDE_CONFIG_DIR") ?? join(home, ".claude"),
116
217
  kimiRoot: envPath("KIMI_SHARE_DIR") ?? join(home, ".kimi"),
117
218
  opencodeRoot: join(getDataHome(), "opencode"),
118
- piRoot: envPath("PI_HOME") ?? join(home, ".pi")
219
+ piRoot: envPath("PI_HOME") ?? join(home, ".pi"),
220
+ zcodeRoot: getZCodeDataPath()
119
221
  };
120
222
  }
121
223
  function getCursorDataPath() {
@@ -135,6 +237,13 @@ function getCursorDataPath() {
135
237
  }
136
238
  return null;
137
239
  }
240
+ function getZCodeDataPath() {
241
+ const p = platform();
242
+ if (p === "darwin" || p === "win32") {
243
+ return join(homedir(), ".zcode");
244
+ }
245
+ return null;
246
+ }
138
247
  function* parseJsonlLines(content) {
139
248
  for (const line of content.split("\n")) {
140
249
  const trimmed = line.trim();
@@ -527,7 +636,7 @@ function parseLiteLLMData(data) {
527
636
  }
528
637
  function loadDiskCache() {
529
638
  const path2 = getCachePath();
530
- if (!existsSync2(path2)) return;
639
+ if (!existsSync3(path2)) return;
531
640
  try {
532
641
  const cached = JSON.parse(readFileSync2(path2, "utf-8"));
533
642
  if (Date.now() - cached.timestamp <= CACHE_TTL_MS) {
@@ -550,7 +659,7 @@ function hasBillablePricing(pricing) {
550
659
  }
551
660
  async function refreshPricingCache() {
552
661
  const path2 = getCachePath();
553
- if (existsSync2(path2)) {
662
+ if (existsSync3(path2)) {
554
663
  try {
555
664
  const cached = JSON.parse(readFileSync2(path2, "utf-8"));
556
665
  if (typeof cached.timestamp === "number" && Date.now() - cached.timestamp <= CACHE_TTL_MS) {
@@ -733,13 +842,13 @@ function extractClaudeUsage(data, msg) {
733
842
  cacheCreate: numericUsage(u["cache_creation_input_tokens"])
734
843
  };
735
844
  }
736
- var ClaudeCodeAgent = class extends BaseAgent {
845
+ var ClaudeCodeAgent = class extends FileSystemSessionSource {
737
846
  name = "claudecode";
738
847
  displayName = "Claude Code";
739
848
  basePath = null;
740
849
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
741
850
  sessionsIndexCache = {};
742
- sessionMetaMap = /* @__PURE__ */ new Map();
851
+ sessionsIndexMtime = {};
743
852
  findBasePath() {
744
853
  const roots = resolveProviderRoots();
745
854
  return firstExisting(join3(roots.claudeRoot, "projects"), "data/claudecode");
@@ -750,7 +859,7 @@ var ClaudeCodeAgent = class extends BaseAgent {
750
859
  try {
751
860
  for (const entry of readdirSync(this.basePath)) {
752
861
  const dir = join3(this.basePath, entry);
753
- if (existsSync3(dir) && readdirSync(dir).some((f) => f.endsWith(".jsonl"))) {
862
+ if (existsSync4(dir) && readdirSync(dir).some((f) => f.endsWith(".jsonl"))) {
754
863
  return true;
755
864
  }
756
865
  }
@@ -769,7 +878,7 @@ var ClaudeCodeAgent = class extends BaseAgent {
769
878
  const fileMarker = perf.start(`listJsonlFiles:${basename2(projectDir)}`);
770
879
  const files = this.listJsonlFiles(projectDir).filter((file) => {
771
880
  try {
772
- return matchesScanWindow(statSync(file).mtimeMs, options);
881
+ return matchesScanWindow(statSync2(file).mtimeMs, options);
773
882
  } catch {
774
883
  return false;
775
884
  }
@@ -823,18 +932,12 @@ var ClaudeCodeAgent = class extends BaseAgent {
823
932
  }
824
933
  return head;
825
934
  }
826
- getSessionMetaMap() {
827
- return this.sessionMetaMap;
828
- }
829
- setSessionMetaMap(meta) {
830
- this.sessionMetaMap = meta;
831
- }
832
935
  getSessionData(sessionId) {
833
936
  const meta = this.sessionMetaMap.get(sessionId);
834
937
  if (!meta) {
835
938
  throw new Error(`Session not found: ${sessionId}`);
836
939
  }
837
- if (!existsSync3(meta.sourcePath)) {
940
+ if (!existsSync4(meta.sourcePath)) {
838
941
  throw new Error(`Session file missing: ${meta.sourcePath}`);
839
942
  }
840
943
  const content = readFileSync3(meta.sourcePath, "utf-8");
@@ -894,83 +997,11 @@ var ClaudeCodeAgent = class extends BaseAgent {
894
997
  messages: cleanedMessages
895
998
  };
896
999
  }
897
- /**
898
- * 检测文件系统变更
899
- * - 已有 session:statSync 检测文件修改(APFS 写文件不更新 dir mtime,必须 file-level)
900
- * - 新 session 检测:readdirSync 文件列表比对,比 dir statSync 快 ~10x
901
- */
902
- checkForChanges(_sinceTimestamp, cachedSessions) {
903
- if (!this.basePath) {
904
- return { hasChanges: false, timestamp: Date.now() };
905
- }
906
- const changedIds = /* @__PURE__ */ new Set();
907
- for (const session of cachedSessions) {
908
- const meta = this.sessionMetaMap.get(session.id);
909
- if (!meta) {
910
- changedIds.add(session.id);
911
- continue;
912
- }
913
- try {
914
- if (this.hasMetaChanged(meta)) {
915
- changedIds.add(session.id);
916
- delete this.sessionsIndexCache[basename2(dirname(meta.sourcePath))];
917
- }
918
- } catch {
919
- changedIds.add(session.id);
920
- }
921
- }
922
- const cachedIdSet = new Set(cachedSessions.map((s) => s.id));
923
- let hasNewFiles = false;
924
- try {
925
- outer: for (const dir of this.listProjectDirs()) {
926
- try {
927
- for (const file of this.listJsonlFiles(dir)) {
928
- if (!cachedIdSet.has(basename2(file, ".jsonl"))) {
929
- hasNewFiles = true;
930
- delete this.sessionsIndexCache[basename2(dir)];
931
- break outer;
932
- }
933
- }
934
- } catch {
935
- }
936
- }
937
- } catch {
938
- }
939
- return {
940
- hasChanges: changedIds.size > 0 || hasNewFiles,
941
- changedIds: Array.from(changedIds),
942
- timestamp: Date.now()
943
- };
944
- }
945
- /**
946
- * 增量扫描 - 只扫描变更的会话
947
- */
948
- incrementalScan(cachedSessions, changedIds) {
949
- if (!this.basePath) return cachedSessions;
950
- const sessionMap = new Map(cachedSessions.map((s) => [s.id, s]));
951
- const changedSet = new Set(changedIds);
952
- for (const projectDir of this.listProjectDirs()) {
953
- for (const file of this.listJsonlFiles(projectDir)) {
954
- try {
955
- const sessionId = basename2(file, ".jsonl");
956
- if (changedSet.has(sessionId) || !sessionMap.has(sessionId)) {
957
- const head = getParsedSession(this.parseSessionHeadResult(file, projectDir));
958
- if (head) {
959
- sessionMap.set(head.id, head);
960
- this.sessionMetaMap.set(head.id, this.buildSessionMeta(head, file, projectDir));
961
- }
962
- }
963
- } catch {
964
- }
965
- }
966
- }
967
- return Array.from(sessionMap.values());
968
- }
969
1000
  // --- Private helpers ---
970
1001
  listProjectDirs() {
971
1002
  if (!this.basePath) return [];
972
1003
  try {
973
- return readdirSync(this.basePath).map((e) => join3(this.basePath, e)).filter((p) => existsSync3(p));
1004
+ return readdirSync(this.basePath).map((e) => join3(this.basePath, e)).filter((p) => existsSync4(p));
974
1005
  } catch {
975
1006
  return [];
976
1007
  }
@@ -989,8 +1020,8 @@ var ClaudeCodeAgent = class extends BaseAgent {
989
1020
  title: head.title,
990
1021
  sourcePath: file,
991
1022
  sourceFingerprint: this.sourceFingerprint(file, projectDir),
992
- sourceMtimeMs: statSync(file).mtimeMs,
993
- indexPath: existsSync3(indexPath) ? indexPath : null,
1023
+ sourceMtimeMs: statSync2(file).mtimeMs,
1024
+ indexPath: existsSync4(indexPath) ? indexPath : null,
994
1025
  indexMtimeMs: this.getFileMtimeMs(indexPath),
995
1026
  headIndexVersion: HEAD_INDEX_VERSION,
996
1027
  directory: head.directory,
@@ -1000,15 +1031,8 @@ var ClaudeCodeAgent = class extends BaseAgent {
1000
1031
  updatedAt: head.time_updated ?? head.time_created
1001
1032
  };
1002
1033
  }
1003
- hasMetaChanged(meta) {
1004
- if (meta.headIndexVersion !== HEAD_INDEX_VERSION) return true;
1005
- if (typeof meta.sourceMtimeMs !== "number") return true;
1006
- if (statSync(meta.sourcePath).mtimeMs !== meta.sourceMtimeMs) return true;
1007
- const indexPath = meta.indexPath ?? this.getSessionsIndexPath(dirname(meta.sourcePath));
1008
- return this.getFileMtimeMs(indexPath) !== (meta.indexMtimeMs ?? null);
1009
- }
1010
1034
  sourceFingerprint(file, projectDir) {
1011
- const stat = statSync(file);
1035
+ const stat = statSync2(file);
1012
1036
  const indexPath = this.getSessionsIndexPath(projectDir);
1013
1037
  return JSON.stringify([
1014
1038
  HEAD_INDEX_VERSION,
@@ -1022,19 +1046,20 @@ var ClaudeCodeAgent = class extends BaseAgent {
1022
1046
  }
1023
1047
  getFileMtimeMs(filePath) {
1024
1048
  try {
1025
- return statSync(filePath).mtimeMs;
1049
+ return statSync2(filePath).mtimeMs;
1026
1050
  } catch {
1027
1051
  return null;
1028
1052
  }
1029
1053
  }
1030
1054
  loadSessionsIndex(projectDir) {
1031
1055
  const cacheKey = basename2(projectDir);
1032
- if (cacheKey in this.sessionsIndexCache) {
1056
+ const indexPath = this.getSessionsIndexPath(projectDir);
1057
+ const mtime = this.getFileMtimeMs(indexPath);
1058
+ if (cacheKey in this.sessionsIndexCache && this.sessionsIndexMtime[cacheKey] === mtime) {
1033
1059
  return this.sessionsIndexCache[cacheKey];
1034
1060
  }
1035
- const indexPath = this.getSessionsIndexPath(projectDir);
1036
1061
  const map = /* @__PURE__ */ new Map();
1037
- if (existsSync3(indexPath)) {
1062
+ if (existsSync4(indexPath)) {
1038
1063
  try {
1039
1064
  const data = JSON.parse(readFileSync3(indexPath, "utf-8"));
1040
1065
  const entries = data?.entries ?? [];
@@ -1048,6 +1073,7 @@ var ClaudeCodeAgent = class extends BaseAgent {
1048
1073
  }
1049
1074
  }
1050
1075
  this.sessionsIndexCache[cacheKey] = map;
1076
+ this.sessionsIndexMtime[cacheKey] = mtime;
1051
1077
  return map;
1052
1078
  }
1053
1079
  parseSessionHead(filePath, projectDir) {
@@ -1064,7 +1090,7 @@ var ClaudeCodeAgent = class extends BaseAgent {
1064
1090
  } catch {
1065
1091
  return skippedSession("malformed first record");
1066
1092
  }
1067
- const createdAt = parseTimestampMs(firstRecord) || statSync(filePath).mtimeMs;
1093
+ const createdAt = parseTimestampMs(firstRecord) || statSync2(filePath).mtimeMs;
1068
1094
  const index = this.loadSessionsIndex(projectDir);
1069
1095
  const indexEntry = index.get(sessionId);
1070
1096
  const explicitTitle = indexEntry?.summary ? String(indexEntry.summary) : null;
@@ -1636,7 +1662,7 @@ function backupDatabase(db, dbPath, label) {
1636
1662
  }
1637
1663
  const timestamp = new Date(Date.now()).toISOString().replaceAll(":", "").replaceAll(".", "-");
1638
1664
  let backupPath = join4(dirname2(dbPath), `${basename3(dbPath)}.${timestamp}.${label}.bak`);
1639
- for (let counter = 1; existsSync4(backupPath); counter += 1) {
1665
+ for (let counter = 1; existsSync5(backupPath); counter += 1) {
1640
1666
  backupPath = join4(dirname2(dbPath), `${basename3(dbPath)}.${timestamp}.${label}.${counter}.bak`);
1641
1667
  }
1642
1668
  db.exec(`VACUUM INTO ${quoteSqlString(backupPath)}`);
@@ -1709,16 +1735,25 @@ function openDb(dbPath) {
1709
1735
  function isSqliteAvailable() {
1710
1736
  return DatabaseConstructor !== null;
1711
1737
  }
1712
- var OpenCodeAgent = class extends BaseAgent {
1713
- name = "opencode";
1714
- displayName = "OpenCode";
1738
+ var OpenCodeSqliteAgent = class extends DatabaseSessionSource {
1739
+ constructor(config) {
1740
+ super();
1741
+ this.config = config;
1742
+ this.name = config.name;
1743
+ this.displayName = config.displayName;
1744
+ }
1745
+ config;
1746
+ name;
1747
+ displayName;
1715
1748
  dbPath = null;
1716
- // Session metadata for caching
1717
- sessionMetaMap = /* @__PURE__ */ new Map();
1749
+ getDatabasePath() {
1750
+ if (!this.dbPath) {
1751
+ this.dbPath = this.findDbPath();
1752
+ }
1753
+ return this.dbPath;
1754
+ }
1718
1755
  findDbPath() {
1719
- if (!isSqliteAvailable()) return null;
1720
- const roots = resolveProviderRoots();
1721
- return firstExisting(join5(roots.opencodeRoot, "opencode.db"), "data/opencode/opencode.db");
1756
+ return this.config.findDbPath();
1722
1757
  }
1723
1758
  isAvailable() {
1724
1759
  this.dbPath = this.findDbPath();
@@ -1793,7 +1828,7 @@ var OpenCodeAgent = class extends BaseAgent {
1793
1828
  const messageTitle = context?.messageTitle ?? null;
1794
1829
  return parsedSession({
1795
1830
  id,
1796
- slug: `opencode/${id}`,
1831
+ slug: `${this.name}/${id}`,
1797
1832
  title: resolveSessionTitle(String(row.title ?? ""), messageTitle, null),
1798
1833
  directory: String(row.directory ?? ""),
1799
1834
  time_created: timeCreated,
@@ -1830,42 +1865,6 @@ var OpenCodeAgent = class extends BaseAgent {
1830
1865
  `
1831
1866
  ).all(cutoffTime);
1832
1867
  }
1833
- getSessionMetaMap() {
1834
- return this.sessionMetaMap;
1835
- }
1836
- setSessionMetaMap(meta) {
1837
- this.sessionMetaMap = meta;
1838
- }
1839
- /**
1840
- * 检测数据库变更
1841
- * 对于 SQLite,检测数据库文件修改时间
1842
- */
1843
- checkForChanges(sinceTimestamp, cachedSessions) {
1844
- if (!this.dbPath) {
1845
- this.dbPath = this.findDbPath();
1846
- }
1847
- if (!this.dbPath || !existsSync5(this.dbPath)) {
1848
- return { hasChanges: false, timestamp: Date.now() };
1849
- }
1850
- try {
1851
- const stat = statSync2(this.dbPath);
1852
- const hasChanges = stat.mtimeMs > sinceTimestamp;
1853
- const changedIds = hasChanges ? cachedSessions.map((s) => s.id) : [];
1854
- return {
1855
- hasChanges,
1856
- changedIds,
1857
- timestamp: Date.now()
1858
- };
1859
- } catch {
1860
- return { hasChanges: false, timestamp: Date.now() };
1861
- }
1862
- }
1863
- /**
1864
- * 增量扫描 - 重新查询数据库
1865
- */
1866
- incrementalScan(_cachedSessions, _changedIds) {
1867
- return this.scan();
1868
- }
1869
1868
  parsePartRow(partRow) {
1870
1869
  const partData = JSON.parse(String(partRow.data ?? "{}"));
1871
1870
  const partType = String(partData.type ?? "");
@@ -1969,11 +1968,11 @@ var OpenCodeAgent = class extends BaseAgent {
1969
1968
  this.dbPath = this.findDbPath();
1970
1969
  }
1971
1970
  if (!this.dbPath) {
1972
- throw new Error("OpenCode database is missing");
1971
+ throw new Error(`${this.displayName} database is missing`);
1973
1972
  }
1974
1973
  const db = openDbReadOnly(this.dbPath);
1975
1974
  if (!db) {
1976
- throw new Error("OpenCode database is missing");
1975
+ throw new Error(`${this.displayName} database is missing`);
1977
1976
  }
1978
1977
  try {
1979
1978
  const sessionRow = db.prepare("SELECT * FROM session WHERE id = ?").get(sessionId);
@@ -1981,7 +1980,7 @@ var OpenCodeAgent = class extends BaseAgent {
1981
1980
  throw new Error(`Session not found: ${sessionId}`);
1982
1981
  }
1983
1982
  const id = String(sessionRow.id ?? sessionId);
1984
- const slug = `opencode/${id}`;
1983
+ const slug = `${this.name}/${id}`;
1985
1984
  const directory = String(sessionRow.directory ?? "");
1986
1985
  const timeCreated = Number(sessionRow.time_created ?? 0);
1987
1986
  const timeUpdated = Number(sessionRow.time_updated ?? timeCreated);
@@ -2052,6 +2051,20 @@ var OpenCodeAgent = class extends BaseAgent {
2052
2051
  }
2053
2052
  }
2054
2053
  };
2054
+ function findOpenCodeDbPath() {
2055
+ if (!isSqliteAvailable()) return null;
2056
+ const roots = resolveProviderRoots();
2057
+ return firstExisting(join5(roots.opencodeRoot, "opencode.db"), "data/opencode/opencode.db");
2058
+ }
2059
+ var OpenCodeAgent = class extends OpenCodeSqliteAgent {
2060
+ constructor() {
2061
+ super({
2062
+ name: "opencode",
2063
+ displayName: "OpenCode",
2064
+ findDbPath: findOpenCodeDbPath
2065
+ });
2066
+ }
2067
+ };
2055
2068
  var KIMI_TOOL_TITLE_MAP = {
2056
2069
  ReadFile: "read",
2057
2070
  Glob: "glob",
@@ -2145,11 +2158,10 @@ function extractFirstUserTitle(contextFile, wireFile) {
2145
2158
  }
2146
2159
  return null;
2147
2160
  }
2148
- var KimiAgent = class extends BaseAgent {
2161
+ var KimiAgent = class extends FileSystemSessionSource {
2149
2162
  name = "kimi";
2150
2163
  displayName = "Kimi-Cli";
2151
2164
  basePath = null;
2152
- sessionMetaMap = /* @__PURE__ */ new Map();
2153
2165
  projectMap = /* @__PURE__ */ new Map();
2154
2166
  defaultModel = null;
2155
2167
  findBasePath() {
@@ -2310,8 +2322,6 @@ var KimiAgent = class extends BaseAgent {
2310
2322
  for (const dir of this.listSessionDirs()) {
2311
2323
  const meta = getParsedSession(this.parseSessionDirResult(dir));
2312
2324
  if (!meta) continue;
2313
- meta.sourceFingerprint = this.sourceFingerprint(meta);
2314
- this.sessionMetaMap.set(meta.id, meta);
2315
2325
  refs.push({
2316
2326
  sessionId: meta.id,
2317
2327
  sourcePath: meta.sourcePath,
@@ -2336,87 +2346,6 @@ var KimiAgent = class extends BaseAgent {
2336
2346
  stats
2337
2347
  };
2338
2348
  }
2339
- getSessionMetaMap() {
2340
- return this.sessionMetaMap;
2341
- }
2342
- setSessionMetaMap(meta) {
2343
- this.sessionMetaMap = meta;
2344
- }
2345
- /**
2346
- * 检测文件系统变更
2347
- */
2348
- checkForChanges(sinceTimestamp, cachedSessions) {
2349
- const changedIds = /* @__PURE__ */ new Set();
2350
- const cachedIds = new Set(cachedSessions.map((session) => session.id));
2351
- const currentIds = /* @__PURE__ */ new Set();
2352
- for (const dir of this.listSessionDirs()) {
2353
- const meta = getParsedSession(this.parseSessionDirResult(dir));
2354
- if (!meta) continue;
2355
- currentIds.add(meta.id);
2356
- this.sessionMetaMap.set(meta.id, meta);
2357
- if (!cachedIds.has(meta.id)) {
2358
- changedIds.add(meta.id);
2359
- continue;
2360
- }
2361
- const dataFile = meta.wireFile || meta.contextFile;
2362
- try {
2363
- const metaStat = statSync3(meta.metaFile);
2364
- if (metaStat.mtimeMs > sinceTimestamp) {
2365
- changedIds.add(meta.id);
2366
- continue;
2367
- }
2368
- if (dataFile) {
2369
- const dataStat = statSync3(dataFile);
2370
- if (dataStat.mtimeMs > sinceTimestamp) {
2371
- changedIds.add(meta.id);
2372
- }
2373
- }
2374
- } catch {
2375
- changedIds.add(meta.id);
2376
- }
2377
- }
2378
- for (const session of cachedSessions) {
2379
- if (!currentIds.has(session.id)) changedIds.add(session.id);
2380
- }
2381
- const changedIdList = Array.from(changedIds);
2382
- return {
2383
- hasChanges: changedIdList.length > 0,
2384
- changedIds: changedIdList,
2385
- timestamp: Date.now()
2386
- };
2387
- }
2388
- /**
2389
- * 增量扫描
2390
- */
2391
- incrementalScan(cachedSessions, changedIds) {
2392
- const sessionMap = new Map(cachedSessions.map((s) => [s.id, s]));
2393
- const changedIdSet = new Set(changedIds);
2394
- for (const id of changedIdSet) {
2395
- sessionMap.delete(id);
2396
- this.sessionMetaMap.delete(id);
2397
- }
2398
- for (const dir of this.listSessionDirs()) {
2399
- try {
2400
- const meta = getParsedSession(this.parseSessionDirResult(dir));
2401
- if (!meta) continue;
2402
- if (changedIdSet.has(meta.id)) {
2403
- this.sessionMetaMap.set(meta.id, meta);
2404
- const stats = this.extractStats(meta.sourcePath);
2405
- sessionMap.set(meta.id, {
2406
- id: meta.id,
2407
- slug: `kimi/${meta.id}`,
2408
- title: meta.title,
2409
- directory: meta.cwd,
2410
- time_created: meta.createdAt,
2411
- time_updated: meta.createdAt,
2412
- stats
2413
- });
2414
- }
2415
- } catch {
2416
- }
2417
- }
2418
- return Array.from(sessionMap.values());
2419
- }
2420
2349
  getSessionData(sessionId) {
2421
2350
  const meta = this.sessionMetaMap.get(sessionId);
2422
2351
  if (!meta) throw new Error(`Session not found: ${sessionId}`);
@@ -3019,12 +2948,12 @@ function extractPatchContent(lines, startIndex) {
3019
2948
  }
3020
2949
  return { text: contentLines.join("\n"), nextLineIndex: i };
3021
2950
  }
3022
- var CodexAgent = class extends BaseAgent {
2951
+ var CodexAgent = class extends FileSystemSessionSource {
3023
2952
  name = "codex";
3024
2953
  displayName = "Codex";
3025
2954
  basePath = null;
3026
2955
  sessionIndexCache = /* @__PURE__ */ new Map();
3027
- sessionMetaMap = /* @__PURE__ */ new Map();
2956
+ sessionIndexMtime = null;
3028
2957
  // ---- BaseAgent implementation ----
3029
2958
  findBasePath() {
3030
2959
  const roots = resolveProviderRoots();
@@ -3087,120 +3016,32 @@ var CodexAgent = class extends BaseAgent {
3087
3016
  }
3088
3017
  return head;
3089
3018
  }
3090
- getSessionMetaMap() {
3091
- return this.sessionMetaMap;
3092
- }
3093
- setSessionMetaMap(meta) {
3094
- this.sessionMetaMap = meta;
3095
- }
3096
- /**
3097
- * 检测文件系统变更
3098
- */
3099
- checkForChanges(_sinceTimestamp, cachedSessions) {
3100
- if (!this.basePath) {
3101
- return { hasChanges: false, timestamp: Date.now() };
3102
- }
3103
- const currentFiles = this.listRolloutFiles();
3104
- const currentIds = new Set(currentFiles.map((file) => extractSessionId(file)));
3105
- const cachedIds = new Set(cachedSessions.map((session) => session.id));
3106
- const changedIds = /* @__PURE__ */ new Set();
3107
- for (const session of cachedSessions) {
3108
- if (!currentIds.has(session.id)) {
3109
- changedIds.add(session.id);
3110
- continue;
3111
- }
3112
- const meta = this.sessionMetaMap.get(session.id);
3113
- if (!meta) {
3114
- changedIds.add(session.id);
3115
- continue;
3116
- }
3019
+ getSessionData(sessionId) {
3020
+ const meta = this.sessionMetaMap.get(sessionId);
3021
+ if (!meta) throw new Error(`Session not found: ${sessionId}`);
3022
+ if (!existsSync7(meta.sourcePath)) throw new Error(`Session file missing: ${meta.sourcePath}`);
3023
+ const content = readFileSync5(meta.sourcePath, "utf-8");
3024
+ const messages = [];
3025
+ const pendingToolCalls = /* @__PURE__ */ new Map();
3026
+ let totalInputTokens = 0;
3027
+ let totalOutputTokens = 0;
3028
+ let totalCacheReadTokens = 0;
3029
+ let totalCost = 0;
3030
+ let currentAssistantIndex = null;
3031
+ let latestAssistantTextIndex = null;
3032
+ let pendingPlan = null;
3033
+ let activeModel = meta.model;
3034
+ let prevCumulativeTotal = 0;
3035
+ let prevInput = 0;
3036
+ let prevOutput = 0;
3037
+ let prevReasoning = 0;
3038
+ let prevCachedInput = 0;
3039
+ for (const record of parseJsonlLines(content)) {
3117
3040
  try {
3118
- if (this.hasMetaChanged(meta)) {
3119
- changedIds.add(session.id);
3120
- }
3121
- } catch {
3122
- changedIds.add(session.id);
3123
- }
3124
- }
3125
- const hasAddedSessions = currentFiles.some((file) => !cachedIds.has(extractSessionId(file)));
3126
- if (hasAddedSessions || changedIds.size > 0) {
3127
- this.sessionIndexCache.clear();
3128
- }
3129
- return {
3130
- hasChanges: changedIds.size > 0 || hasAddedSessions,
3131
- changedIds: Array.from(changedIds),
3132
- timestamp: Date.now()
3133
- };
3134
- }
3135
- /**
3136
- * 增量扫描
3137
- */
3138
- incrementalScan(cachedSessions, changedIds) {
3139
- if (!this.basePath) return cachedSessions;
3140
- const sessionMap = new Map(cachedSessions.map((s) => [s.id, s]));
3141
- const changedSet = new Set(changedIds);
3142
- const currentFiles = this.listRolloutFiles();
3143
- const currentIds = new Set(currentFiles.map((file) => extractSessionId(file)));
3144
- for (const session of cachedSessions) {
3145
- if (!currentIds.has(session.id)) {
3146
- sessionMap.delete(session.id);
3147
- this.sessionMetaMap.delete(session.id);
3148
- }
3149
- }
3150
- for (const file of currentFiles) {
3151
- try {
3152
- const sessionId = extractSessionId(file);
3153
- if (changedSet.has(sessionId)) {
3154
- const head = this.parseSessionHead(file);
3155
- if (head) {
3156
- sessionMap.set(head.id, head);
3157
- this.sessionMetaMap.set(head.id, this.buildSessionMeta(head, file));
3158
- }
3159
- }
3160
- } catch {
3161
- }
3162
- }
3163
- for (const file of currentFiles) {
3164
- try {
3165
- const sessionId = extractSessionId(file);
3166
- if (!sessionMap.has(sessionId)) {
3167
- const head = this.parseSessionHead(file);
3168
- if (head) {
3169
- sessionMap.set(head.id, head);
3170
- this.sessionMetaMap.set(head.id, this.buildSessionMeta(head, file));
3171
- }
3172
- }
3173
- } catch {
3174
- }
3175
- }
3176
- return Array.from(sessionMap.values());
3177
- }
3178
- getSessionData(sessionId) {
3179
- const meta = this.sessionMetaMap.get(sessionId);
3180
- if (!meta) throw new Error(`Session not found: ${sessionId}`);
3181
- if (!existsSync7(meta.sourcePath)) throw new Error(`Session file missing: ${meta.sourcePath}`);
3182
- const content = readFileSync5(meta.sourcePath, "utf-8");
3183
- const messages = [];
3184
- const pendingToolCalls = /* @__PURE__ */ new Map();
3185
- let totalInputTokens = 0;
3186
- let totalOutputTokens = 0;
3187
- let totalCacheReadTokens = 0;
3188
- let totalCost = 0;
3189
- let currentAssistantIndex = null;
3190
- let latestAssistantTextIndex = null;
3191
- let pendingPlan = null;
3192
- let activeModel = meta.model;
3193
- let prevCumulativeTotal = 0;
3194
- let prevInput = 0;
3195
- let prevOutput = 0;
3196
- let prevReasoning = 0;
3197
- let prevCachedInput = 0;
3198
- for (const record of parseJsonlLines(content)) {
3199
- try {
3200
- const recordType = String(record["type"] ?? "");
3201
- if (recordType === "turn_context") {
3202
- const payload = record["payload"] ?? {};
3203
- activeModel = extractModelName(payload["model"]) ?? activeModel;
3041
+ const recordType = String(record["type"] ?? "");
3042
+ if (recordType === "turn_context") {
3043
+ const payload = record["payload"] ?? {};
3044
+ activeModel = extractModelName(payload["model"]) ?? activeModel;
3204
3045
  }
3205
3046
  const result = this.convertRecord(
3206
3047
  record,
@@ -3352,14 +3193,6 @@ var CodexAgent = class extends BaseAgent {
3352
3193
  updatedAt: head.time_updated ?? head.time_created
3353
3194
  };
3354
3195
  }
3355
- hasMetaChanged(meta) {
3356
- if (meta.headIndexVersion !== HEAD_INDEX_VERSION2) return true;
3357
- if (meta.parserVersion !== PARSER_VERSION) return true;
3358
- if (typeof meta.sourceMtimeMs !== "number") return true;
3359
- if (statSync4(meta.sourcePath).mtimeMs !== meta.sourceMtimeMs) return true;
3360
- const indexTitle = this.getTitleForSession(meta.id);
3361
- return indexTitle !== null && indexTitle !== meta.title;
3362
- }
3363
3196
  sourceFingerprint(file) {
3364
3197
  const stat = statSync4(file);
3365
3198
  const sessionId = extractSessionId(file);
@@ -3384,9 +3217,12 @@ var CodexAgent = class extends BaseAgent {
3384
3217
  }
3385
3218
  // ---- Session index ----
3386
3219
  loadSessionIndex() {
3387
- if (this.sessionIndexCache.size > 0) return;
3388
3220
  const indexPath = this.getSessionIndexPath();
3389
- if (!existsSync7(indexPath)) return;
3221
+ const mtime = this.getFileMtimeMs(indexPath);
3222
+ if (this.sessionIndexCache.size > 0 && this.sessionIndexMtime === mtime) return;
3223
+ this.sessionIndexCache.clear();
3224
+ this.sessionIndexMtime = mtime;
3225
+ if (mtime === null) return;
3390
3226
  try {
3391
3227
  const content = readFileSync5(indexPath, "utf-8");
3392
3228
  for (const record of parseJsonlLines(content)) {
@@ -4122,14 +3958,18 @@ function convertActionToPart(action, timestampMs) {
4122
3958
  }
4123
3959
  return null;
4124
3960
  }
4125
- var CursorAgent = class extends BaseAgent {
3961
+ var CursorAgent = class extends DatabaseSessionSource {
4126
3962
  name = "cursor";
4127
3963
  displayName = "Cursor";
4128
3964
  dbPath = null;
4129
3965
  // Cache composer data from scan so getSessionData can reuse it
4130
3966
  composerCache = /* @__PURE__ */ new Map();
4131
- // Session metadata for caching
4132
- sessionMetaMap = /* @__PURE__ */ new Map();
3967
+ getDatabasePath() {
3968
+ if (!this.dbPath) {
3969
+ this.dbPath = this.findDbPath();
3970
+ }
3971
+ return this.dbPath;
3972
+ }
4133
3973
  findDbPath() {
4134
3974
  if (!isSqliteAvailable()) return null;
4135
3975
  const dataPath = getCursorDataPath();
@@ -4332,42 +4172,6 @@ var CursorAgent = class extends BaseAgent {
4332
4172
  db.close();
4333
4173
  }
4334
4174
  }
4335
- getSessionMetaMap() {
4336
- return this.sessionMetaMap;
4337
- }
4338
- setSessionMetaMap(meta) {
4339
- this.sessionMetaMap = meta;
4340
- }
4341
- /**
4342
- * 检测数据库变更
4343
- * 对于 SQLite,检测数据库文件修改时间
4344
- */
4345
- checkForChanges(sinceTimestamp, cachedSessions) {
4346
- if (!this.dbPath) {
4347
- this.dbPath = this.findDbPath();
4348
- }
4349
- if (!this.dbPath || !existsSync8(this.dbPath)) {
4350
- return { hasChanges: false, timestamp: Date.now() };
4351
- }
4352
- try {
4353
- const stat = statSync5(this.dbPath);
4354
- const hasChanges = stat.mtimeMs > sinceTimestamp;
4355
- const changedIds = hasChanges ? cachedSessions.map((s) => s.id) : [];
4356
- return {
4357
- hasChanges,
4358
- changedIds,
4359
- timestamp: Date.now()
4360
- };
4361
- } catch {
4362
- return { hasChanges: false, timestamp: Date.now() };
4363
- }
4364
- }
4365
- /**
4366
- * 增量扫描 - 重新查询数据库
4367
- */
4368
- incrementalScan(_cachedSessions, _changedIds) {
4369
- return this.scan();
4370
- }
4371
4175
  getSessionData(sessionId) {
4372
4176
  if (!this.dbPath) {
4373
4177
  this.dbPath = this.findDbPath();
@@ -4766,11 +4570,10 @@ function buildCurrentPathEntries(entries) {
4766
4570
  }
4767
4571
  return path2.reverse();
4768
4572
  }
4769
- var PiAgent = class extends BaseAgent {
4573
+ var PiAgent = class extends FileSystemSessionSource {
4770
4574
  name = "pi";
4771
4575
  displayName = "Pi";
4772
4576
  basePath = null;
4773
- sessionMetaMap = /* @__PURE__ */ new Map();
4774
4577
  findBasePath() {
4775
4578
  const roots = resolveProviderRoots();
4776
4579
  return firstExisting(join9(roots.piRoot, "agent", "sessions"), "data/pi");
@@ -4818,69 +4621,6 @@ var PiAgent = class extends BaseAgent {
4818
4621
  }
4819
4622
  return head;
4820
4623
  }
4821
- getSessionMetaMap() {
4822
- return this.sessionMetaMap;
4823
- }
4824
- setSessionMetaMap(meta) {
4825
- this.sessionMetaMap = meta;
4826
- }
4827
- checkForChanges(_sinceTimestamp, cachedSessions) {
4828
- if (!this.basePath) return { hasChanges: false, timestamp: Date.now() };
4829
- const currentFiles = this.listSessionFiles();
4830
- const currentIds = new Set(currentFiles.map((file) => extractSessionIdFromFilename(file)));
4831
- const cachedIds = new Set(cachedSessions.map((session) => session.id));
4832
- const changedIds = /* @__PURE__ */ new Set();
4833
- for (const session of cachedSessions) {
4834
- if (!currentIds.has(session.id)) {
4835
- changedIds.add(session.id);
4836
- continue;
4837
- }
4838
- const meta = this.sessionMetaMap.get(session.id);
4839
- if (!meta) {
4840
- changedIds.add(session.id);
4841
- continue;
4842
- }
4843
- try {
4844
- if (this.hasMetaChanged(meta)) changedIds.add(session.id);
4845
- } catch {
4846
- changedIds.add(session.id);
4847
- }
4848
- }
4849
- const hasAddedSessions = currentFiles.some(
4850
- (file) => !cachedIds.has(extractSessionIdFromFilename(file))
4851
- );
4852
- return {
4853
- hasChanges: changedIds.size > 0 || hasAddedSessions,
4854
- changedIds: [...changedIds],
4855
- timestamp: Date.now()
4856
- };
4857
- }
4858
- incrementalScan(cachedSessions, changedIds) {
4859
- if (!this.basePath) return cachedSessions;
4860
- const sessionMap = new Map(cachedSessions.map((session) => [session.id, session]));
4861
- const changedSet = new Set(changedIds);
4862
- const currentFiles = this.listSessionFiles();
4863
- const currentIds = new Set(currentFiles.map((file) => extractSessionIdFromFilename(file)));
4864
- for (const session of cachedSessions) {
4865
- if (!currentIds.has(session.id)) {
4866
- sessionMap.delete(session.id);
4867
- this.sessionMetaMap.delete(session.id);
4868
- }
4869
- }
4870
- for (const file of currentFiles) {
4871
- try {
4872
- const sessionId = extractSessionIdFromFilename(file);
4873
- if (!changedSet.has(sessionId) && sessionMap.has(sessionId)) continue;
4874
- const head = getParsedSession(this.parseSessionHeadResult(file));
4875
- if (head) {
4876
- sessionMap.set(head.id, head);
4877
- this.sessionMetaMap.set(head.id, this.buildSessionMeta(head, file));
4878
- }
4879
- } catch {
4880
- }
4881
- }
4882
- return [...sessionMap.values()];
4883
- }
4884
4624
  getSessionData(sessionId) {
4885
4625
  const meta = this.sessionMetaMap.get(sessionId);
4886
4626
  if (!meta) throw new Error(`Session not found: ${sessionId}`);
@@ -4943,11 +4683,6 @@ var PiAgent = class extends BaseAgent {
4943
4683
  updatedAt: head.time_updated ?? head.time_created
4944
4684
  };
4945
4685
  }
4946
- hasMetaChanged(meta) {
4947
- if (meta.headIndexVersion !== HEAD_INDEX_VERSION3) return true;
4948
- if (meta.parserVersion !== PARSER_VERSION2) return true;
4949
- return statSync6(meta.sourcePath).mtimeMs !== meta.sourceMtimeMs;
4950
- }
4951
4686
  sourceFingerprint(file) {
4952
4687
  const stat = statSync6(file);
4953
4688
  return JSON.stringify([HEAD_INDEX_VERSION3, PARSER_VERSION2, stat.mtimeMs, stat.size]);
@@ -5274,6 +5009,21 @@ var PiAgent = class extends BaseAgent {
5274
5009
  };
5275
5010
  }
5276
5011
  };
5012
+ function findZCodeDbPath() {
5013
+ if (!isSqliteAvailable()) return null;
5014
+ const roots = resolveProviderRoots();
5015
+ if (!roots.zcodeRoot) return null;
5016
+ return firstExisting(join10(roots.zcodeRoot, "cli", "db", "db.sqlite"));
5017
+ }
5018
+ var ZCodeAgent = class extends OpenCodeSqliteAgent {
5019
+ constructor() {
5020
+ super({
5021
+ name: "zcode",
5022
+ displayName: "ZCode",
5023
+ findDbPath: findZCodeDbPath
5024
+ });
5025
+ }
5026
+ };
5277
5027
  registerAgent({
5278
5028
  name: "claudecode",
5279
5029
  displayName: "Claude Code",
@@ -5286,6 +5036,12 @@ registerAgent({
5286
5036
  icon: "/icon/agent/opencode.svg",
5287
5037
  create: () => new OpenCodeAgent()
5288
5038
  });
5039
+ registerAgent({
5040
+ name: "zcode",
5041
+ displayName: "ZCode",
5042
+ icon: "/icon/agent/zcode.svg",
5043
+ create: () => new ZCodeAgent()
5044
+ });
5289
5045
  registerAgent({
5290
5046
  name: "kimi",
5291
5047
  displayName: "Kimi-Cli",
@@ -5805,688 +5561,861 @@ function extractSessionFileActivity(agentName, sessionId, projectIdentityKey, me
5805
5561
  extractFileActivityOccurrences(messages)
5806
5562
  );
5807
5563
  }
5808
- var CACHE_SCHEMA_VERSION = 13;
5809
- var CACHE_INITIALIZATION_VERSION = "session-cache-v2";
5810
5564
  var CACHE_FILENAME = "codesesh.db";
5811
5565
  var LEGACY_CACHE_FILENAME = "scan-cache.json";
5812
5566
  var SEARCH_INDEX_BULK_SYNC_THRESHOLD = 100;
5813
5567
  var ftsIntegrityCheckedPath = null;
5568
+ function getFtsIntegrityCheckedPath() {
5569
+ return ftsIntegrityCheckedPath;
5570
+ }
5571
+ function setFtsIntegrityCheckedPath(path2) {
5572
+ ftsIntegrityCheckedPath = path2;
5573
+ }
5814
5574
  function getCacheDir2() {
5815
- return join10(homedir4(), ".cache", "codesesh");
5575
+ return join11(homedir4(), ".cache", "codesesh");
5816
5576
  }
5817
5577
  function getCachePath2() {
5818
- return join10(getCacheDir2(), CACHE_FILENAME);
5578
+ return join11(getCacheDir2(), CACHE_FILENAME);
5819
5579
  }
5820
5580
  function getLegacyCachePath() {
5821
- return join10(getCacheDir2(), LEGACY_CACHE_FILENAME);
5581
+ return join11(getCacheDir2(), LEGACY_CACHE_FILENAME);
5822
5582
  }
5823
5583
  function hasCacheStorage() {
5824
5584
  return existsSync11(getCachePath2());
5825
5585
  }
5826
- function withCacheDb(fn) {
5827
- const cachePath = getCachePath2();
5828
- const db = openDb(cachePath);
5829
- if (!db) return null;
5830
- try {
5831
- ensureSchema(db, cachePath);
5832
- return fn(db);
5833
- } catch {
5834
- return null;
5835
- } finally {
5836
- db.close();
5837
- }
5586
+ function likePattern(value) {
5587
+ return `%${value.trim().toLowerCase().replace(/[\\%_]/g, "\\$&")}%`;
5838
5588
  }
5839
- function withCacheDbReadOnly(fn) {
5840
- const db = openDbReadOnly(getCachePath2());
5841
- if (!db) return null;
5842
- try {
5843
- return fn(db);
5844
- } catch {
5845
- return null;
5846
- } finally {
5847
- db.close();
5848
- }
5589
+ function filePathFtsQuery(value) {
5590
+ const path2 = normalizeFilePathSearch(value);
5591
+ if (path2.length < 3) return null;
5592
+ return `"${path2.replaceAll('"', '""')}"`;
5849
5593
  }
5850
- function createCacheTables(db) {
5851
- db.exec(`
5852
- CREATE TABLE IF NOT EXISTS cache_meta (
5853
- key TEXT PRIMARY KEY,
5854
- value TEXT NOT NULL
5855
- );
5856
-
5857
- CREATE TABLE IF NOT EXISTS agent_cache (
5858
- agent_name TEXT PRIMARY KEY,
5859
- timestamp INTEGER NOT NULL
5860
- );
5861
-
5862
- CREATE TABLE IF NOT EXISTS cached_sessions (
5863
- agent_name TEXT NOT NULL,
5864
- session_id TEXT NOT NULL,
5865
- session_json TEXT NOT NULL,
5866
- meta_json TEXT,
5867
- PRIMARY KEY (agent_name, session_id)
5868
- );
5869
-
5870
- CREATE TABLE IF NOT EXISTS cache_initialization (
5871
- agent_name TEXT PRIMARY KEY,
5872
- initialized_at INTEGER NOT NULL,
5873
- index_version TEXT NOT NULL,
5874
- last_sync_at INTEGER NOT NULL
5875
- );
5594
+ function escapeRegExp(value) {
5595
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5596
+ }
5597
+ function normalizeFilePathSearch(value) {
5598
+ return value.trim().replace(/^"|"$/g, "");
5599
+ }
5600
+ function stringifyOptionalJson(value) {
5601
+ return value == null ? null : JSON.stringify(value);
5602
+ }
5603
+ function parseOptionalJson(value) {
5604
+ return value == null ? void 0 : JSON.parse(String(value));
5605
+ }
5606
+ function sourcePathFromMeta(meta) {
5607
+ return typeof meta?.sourcePath === "string" ? meta.sourcePath : null;
5608
+ }
5609
+ function sourcePathFromMetaJson(metaJson) {
5610
+ if (!metaJson) return null;
5611
+ const meta = JSON.parse(metaJson);
5612
+ return sourcePathFromMeta(meta);
5613
+ }
5614
+ function prepareUpsertCachedSession(db) {
5615
+ return db.prepare(`
5616
+ INSERT INTO cached_sessions(agent_name, session_id, session_json, meta_json)
5617
+ VALUES (?, ?, ?, ?)
5618
+ ON CONFLICT(agent_name, session_id) DO UPDATE SET
5619
+ session_json = excluded.session_json,
5620
+ meta_json = excluded.meta_json
5876
5621
  `);
5877
5622
  }
5878
- function createSessionTables(db) {
5879
- db.exec(`
5880
- CREATE TABLE IF NOT EXISTS sessions (
5881
- agent_name TEXT NOT NULL,
5882
- session_id TEXT NOT NULL,
5883
- sort_index INTEGER NOT NULL DEFAULT 0,
5884
- slug TEXT NOT NULL,
5885
- title TEXT NOT NULL,
5886
- source_path TEXT,
5887
- directory TEXT NOT NULL,
5888
- project_identity_kind TEXT NOT NULL,
5889
- project_identity_key TEXT NOT NULL,
5890
- project_display_name TEXT NOT NULL,
5891
- time_created INTEGER NOT NULL,
5892
- time_updated INTEGER,
5893
- activity_time INTEGER NOT NULL,
5894
- message_count INTEGER NOT NULL,
5895
- total_input_tokens INTEGER NOT NULL,
5896
- total_output_tokens INTEGER NOT NULL,
5897
- total_cache_read_tokens INTEGER,
5898
- total_cache_create_tokens INTEGER,
5899
- total_cost REAL NOT NULL,
5900
- cost_source TEXT,
5901
- total_tokens INTEGER,
5902
- model_usage_json TEXT,
5903
- smart_tags_json TEXT,
5904
- smart_tags_source_updated_at INTEGER,
5905
- meta_json TEXT,
5906
- PRIMARY KEY (agent_name, session_id)
5907
- );
5908
-
5909
- CREATE INDEX IF NOT EXISTS idx_sessions_agent_activity
5910
- ON sessions(agent_name, activity_time);
5911
-
5912
- CREATE INDEX IF NOT EXISTS idx_sessions_project
5913
- ON sessions(project_identity_kind, project_identity_key, activity_time);
5914
-
5915
- CREATE TABLE IF NOT EXISTS messages (
5916
- agent_name TEXT NOT NULL,
5917
- session_id TEXT NOT NULL,
5918
- message_index INTEGER NOT NULL,
5919
- message_id TEXT NOT NULL,
5920
- role TEXT NOT NULL,
5921
- time_created INTEGER NOT NULL,
5922
- time_completed INTEGER,
5923
- agent TEXT,
5924
- mode TEXT,
5925
- model TEXT,
5926
- provider TEXT,
5927
- tokens_json TEXT,
5928
- cost REAL,
5929
- cost_source TEXT,
5930
- parts_json TEXT NOT NULL,
5931
- subagent_id TEXT,
5932
- nickname TEXT,
5933
- content_text TEXT NOT NULL,
5934
- tool_metadata_json TEXT,
5935
- PRIMARY KEY (agent_name, session_id, message_index),
5936
- FOREIGN KEY (agent_name, session_id)
5937
- REFERENCES sessions(agent_name, session_id)
5938
- ON DELETE CASCADE
5939
- );
5940
-
5941
- CREATE INDEX IF NOT EXISTS idx_messages_session
5942
- ON messages(agent_name, session_id, message_index);
5623
+ function prepareUpsertProjectSession(db) {
5624
+ return db.prepare(`
5625
+ INSERT INTO project_sessions(
5626
+ agent_name,
5627
+ session_id,
5628
+ identity_kind,
5629
+ identity_key,
5630
+ display_name,
5631
+ directory,
5632
+ activity_time
5633
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
5634
+ ON CONFLICT(agent_name, session_id) DO UPDATE SET
5635
+ identity_kind = excluded.identity_kind,
5636
+ identity_key = excluded.identity_key,
5637
+ display_name = excluded.display_name,
5638
+ directory = excluded.directory,
5639
+ activity_time = excluded.activity_time
5943
5640
  `);
5944
- createMessageToolTables(db);
5945
5641
  }
5946
- function createMessageToolTables(db) {
5947
- db.exec(`
5948
- CREATE TABLE IF NOT EXISTS message_tools (
5949
- agent_name TEXT NOT NULL,
5950
- session_id TEXT NOT NULL,
5951
- message_index INTEGER NOT NULL,
5952
- tool_name TEXT NOT NULL,
5953
- PRIMARY KEY (agent_name, session_id, message_index, tool_name),
5954
- FOREIGN KEY (agent_name, session_id, message_index)
5955
- REFERENCES messages(agent_name, session_id, message_index)
5956
- ON DELETE CASCADE
5957
- );
5958
-
5959
- CREATE INDEX IF NOT EXISTS idx_message_tools_filter
5960
- ON message_tools(tool_name, agent_name, session_id);
5642
+ function prepareUpsertSession(db) {
5643
+ return db.prepare(`
5644
+ INSERT INTO sessions(
5645
+ agent_name,
5646
+ session_id,
5647
+ sort_index,
5648
+ slug,
5649
+ title,
5650
+ source_path,
5651
+ directory,
5652
+ project_identity_kind,
5653
+ project_identity_key,
5654
+ project_display_name,
5655
+ time_created,
5656
+ time_updated,
5657
+ activity_time,
5658
+ message_count,
5659
+ total_input_tokens,
5660
+ total_output_tokens,
5661
+ total_cache_read_tokens,
5662
+ total_cache_create_tokens,
5663
+ total_cost,
5664
+ cost_source,
5665
+ total_tokens,
5666
+ model_usage_json,
5667
+ smart_tags_json,
5668
+ smart_tags_source_updated_at,
5669
+ meta_json
5670
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
5671
+ ON CONFLICT(agent_name, session_id) DO UPDATE SET
5672
+ sort_index = excluded.sort_index,
5673
+ slug = excluded.slug,
5674
+ title = excluded.title,
5675
+ source_path = excluded.source_path,
5676
+ directory = excluded.directory,
5677
+ project_identity_kind = excluded.project_identity_kind,
5678
+ project_identity_key = excluded.project_identity_key,
5679
+ project_display_name = excluded.project_display_name,
5680
+ time_created = excluded.time_created,
5681
+ time_updated = excluded.time_updated,
5682
+ activity_time = excluded.activity_time,
5683
+ message_count = excluded.message_count,
5684
+ total_input_tokens = excluded.total_input_tokens,
5685
+ total_output_tokens = excluded.total_output_tokens,
5686
+ total_cache_read_tokens = excluded.total_cache_read_tokens,
5687
+ total_cache_create_tokens = excluded.total_cache_create_tokens,
5688
+ total_cost = excluded.total_cost,
5689
+ cost_source = excluded.cost_source,
5690
+ total_tokens = excluded.total_tokens,
5691
+ model_usage_json = excluded.model_usage_json,
5692
+ smart_tags_json = excluded.smart_tags_json,
5693
+ smart_tags_source_updated_at = excluded.smart_tags_source_updated_at,
5694
+ meta_json = excluded.meta_json
5961
5695
  `);
5962
5696
  }
5963
- function createMessageSearchTables(db) {
5964
- if (!tableExists(db, "messages")) {
5965
- createSessionTables(db);
5966
- }
5967
- db.exec(`
5968
- CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
5969
- content_text,
5970
- content='messages',
5971
- content_rowid='rowid'
5972
- );
5697
+ function prepareUpsertIndexedSession(db) {
5698
+ return db.prepare(`
5699
+ INSERT INTO sessions(
5700
+ agent_name,
5701
+ session_id,
5702
+ sort_index,
5703
+ slug,
5704
+ title,
5705
+ source_path,
5706
+ directory,
5707
+ project_identity_kind,
5708
+ project_identity_key,
5709
+ project_display_name,
5710
+ time_created,
5711
+ time_updated,
5712
+ activity_time,
5713
+ message_count,
5714
+ total_input_tokens,
5715
+ total_output_tokens,
5716
+ total_cache_read_tokens,
5717
+ total_cache_create_tokens,
5718
+ total_cost,
5719
+ cost_source,
5720
+ total_tokens,
5721
+ model_usage_json,
5722
+ smart_tags_json,
5723
+ smart_tags_source_updated_at,
5724
+ meta_json
5725
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
5726
+ ON CONFLICT(agent_name, session_id) DO UPDATE SET
5727
+ slug = excluded.slug,
5728
+ title = excluded.title,
5729
+ directory = excluded.directory,
5730
+ project_identity_kind = excluded.project_identity_kind,
5731
+ project_identity_key = excluded.project_identity_key,
5732
+ project_display_name = excluded.project_display_name,
5733
+ time_created = excluded.time_created,
5734
+ time_updated = excluded.time_updated,
5735
+ activity_time = excluded.activity_time,
5736
+ message_count = excluded.message_count,
5737
+ total_input_tokens = excluded.total_input_tokens,
5738
+ total_output_tokens = excluded.total_output_tokens,
5739
+ total_cache_read_tokens = excluded.total_cache_read_tokens,
5740
+ total_cache_create_tokens = excluded.total_cache_create_tokens,
5741
+ total_cost = excluded.total_cost,
5742
+ cost_source = excluded.cost_source,
5743
+ total_tokens = excluded.total_tokens,
5744
+ model_usage_json = excluded.model_usage_json,
5745
+ smart_tags_json = excluded.smart_tags_json,
5746
+ smart_tags_source_updated_at = excluded.smart_tags_source_updated_at
5973
5747
  `);
5974
- createMessageSearchTriggers(db);
5975
5748
  }
5976
- function createMessageSearchTriggers(db) {
5977
- db.exec(`
5978
- CREATE TRIGGER IF NOT EXISTS messages_ai AFTER INSERT ON messages BEGIN
5979
- INSERT INTO messages_fts(rowid, content_text)
5980
- VALUES (new.rowid, new.content_text);
5981
- END;
5982
-
5983
- CREATE TRIGGER IF NOT EXISTS messages_ad AFTER DELETE ON messages BEGIN
5984
- INSERT INTO messages_fts(messages_fts, rowid, content_text)
5985
- VALUES ('delete', old.rowid, old.content_text);
5986
- END;
5987
-
5988
- CREATE TRIGGER IF NOT EXISTS messages_au AFTER UPDATE ON messages BEGIN
5989
- INSERT INTO messages_fts(messages_fts, rowid, content_text)
5990
- VALUES ('delete', old.rowid, old.content_text);
5991
- INSERT INTO messages_fts(rowid, content_text)
5992
- VALUES (new.rowid, new.content_text);
5993
- END;
5749
+ function upsertSessionRow(statement, agentName, session, metaJson, sortIndex, sourcePath) {
5750
+ const identity = session.project_identity ?? computeIdentity(session.directory, realFs);
5751
+ const activityTime = session.time_updated ?? session.time_created;
5752
+ statement.run(
5753
+ agentName,
5754
+ session.id,
5755
+ sortIndex,
5756
+ session.slug,
5757
+ session.title,
5758
+ sourcePath,
5759
+ session.directory,
5760
+ identity.kind,
5761
+ identity.key,
5762
+ identity.displayName,
5763
+ session.time_created,
5764
+ session.time_updated ?? null,
5765
+ activityTime,
5766
+ session.stats.message_count,
5767
+ session.stats.total_input_tokens,
5768
+ session.stats.total_output_tokens,
5769
+ session.stats.total_cache_read_tokens ?? null,
5770
+ session.stats.total_cache_create_tokens ?? null,
5771
+ session.stats.total_cost,
5772
+ session.stats.cost_source ?? null,
5773
+ session.stats.total_tokens ?? null,
5774
+ stringifyOptionalJson(session.model_usage),
5775
+ stringifyOptionalJson(session.smart_tags),
5776
+ session.smart_tags_source_updated_at ?? null,
5777
+ metaJson
5778
+ );
5779
+ }
5780
+ function prepareInsertFileActivity(db) {
5781
+ return db.prepare(`
5782
+ INSERT INTO session_file_activity(
5783
+ agent_name,
5784
+ session_id,
5785
+ project_identity_key,
5786
+ path,
5787
+ kind,
5788
+ count,
5789
+ latest_time
5790
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
5994
5791
  `);
5995
5792
  }
5996
- function dropMessageSearchTriggers(db) {
5997
- db.exec(`
5998
- DROP TRIGGER IF EXISTS messages_ai;
5999
- DROP TRIGGER IF EXISTS messages_ad;
6000
- DROP TRIGGER IF EXISTS messages_au;
5793
+ function prepareInsertMessageTool(db) {
5794
+ return db.prepare(`
5795
+ INSERT OR IGNORE INTO message_tools(
5796
+ agent_name,
5797
+ session_id,
5798
+ message_index,
5799
+ tool_name
5800
+ ) VALUES (?, ?, ?, ?)
6001
5801
  `);
6002
5802
  }
6003
- function createFileActivityTables(db) {
6004
- db.exec(`
6005
- CREATE TABLE IF NOT EXISTS session_file_activity (
6006
- agent_name TEXT NOT NULL,
6007
- session_id TEXT NOT NULL,
6008
- project_identity_key TEXT NOT NULL,
6009
- path TEXT NOT NULL,
6010
- kind TEXT NOT NULL,
6011
- count INTEGER NOT NULL,
6012
- latest_time INTEGER NOT NULL,
6013
- PRIMARY KEY (agent_name, session_id, project_identity_key, path, kind),
6014
- FOREIGN KEY (agent_name, session_id)
6015
- REFERENCES sessions(agent_name, session_id)
6016
- ON DELETE CASCADE
6017
- );
6018
-
6019
- CREATE INDEX IF NOT EXISTS idx_file_activity_project_latest
6020
- ON session_file_activity(project_identity_key, latest_time);
6021
-
6022
- CREATE INDEX IF NOT EXISTS idx_file_activity_latest
6023
- ON session_file_activity(latest_time DESC, count DESC, path);
6024
-
6025
- CREATE INDEX IF NOT EXISTS idx_file_activity_agent_latest
6026
- ON session_file_activity(agent_name, latest_time DESC, count DESC, path);
6027
-
6028
- CREATE INDEX IF NOT EXISTS idx_file_activity_project_latest_ordered
6029
- ON session_file_activity(project_identity_key, latest_time DESC, count DESC, path);
6030
-
6031
- CREATE INDEX IF NOT EXISTS idx_file_activity_path
6032
- ON session_file_activity(path);
6033
-
6034
- CREATE INDEX IF NOT EXISTS idx_file_activity_kind
6035
- ON session_file_activity(kind);
6036
- `);
6037
- createFileActivityPathSearchTables(db);
6038
- }
6039
- function createFileActivityPathSearchTables(db) {
6040
- db.exec(`
6041
- CREATE VIRTUAL TABLE IF NOT EXISTS session_file_activity_path_fts USING fts5(
6042
- path,
6043
- content='session_file_activity',
6044
- content_rowid='rowid',
6045
- tokenize='trigram'
5803
+ function writeFileActivityRows(statement, activities) {
5804
+ for (const activity of activities) {
5805
+ statement.run(
5806
+ activity.agent_name,
5807
+ activity.session_id,
5808
+ activity.project_identity_key,
5809
+ activity.path,
5810
+ activity.kind,
5811
+ activity.count,
5812
+ activity.latest_time
6046
5813
  );
6047
- `);
6048
- createFileActivityPathSearchTriggers(db);
6049
- }
6050
- function createFileActivityPathSearchTriggers(db) {
6051
- db.exec(`
6052
- CREATE TRIGGER IF NOT EXISTS session_file_activity_path_ai
6053
- AFTER INSERT ON session_file_activity BEGIN
6054
- INSERT INTO session_file_activity_path_fts(rowid, path)
6055
- VALUES (new.rowid, new.path);
6056
- END;
6057
-
6058
- CREATE TRIGGER IF NOT EXISTS session_file_activity_path_ad
6059
- AFTER DELETE ON session_file_activity BEGIN
6060
- INSERT INTO session_file_activity_path_fts(session_file_activity_path_fts, rowid, path)
6061
- VALUES ('delete', old.rowid, old.path);
6062
- END;
6063
-
6064
- CREATE TRIGGER IF NOT EXISTS session_file_activity_path_au
6065
- AFTER UPDATE ON session_file_activity BEGIN
6066
- INSERT INTO session_file_activity_path_fts(session_file_activity_path_fts, rowid, path)
6067
- VALUES ('delete', old.rowid, old.path);
6068
- INSERT INTO session_file_activity_path_fts(rowid, path)
6069
- VALUES (new.rowid, new.path);
6070
- END;
6071
- `);
6072
- }
6073
- function rebuildFileActivityPathIndex(db) {
6074
- if (!tableExists(db, "session_file_activity_path_fts")) {
6075
- return;
6076
5814
  }
6077
- db.exec(
6078
- "INSERT INTO session_file_activity_path_fts(session_file_activity_path_fts) VALUES ('rebuild')"
5815
+ }
5816
+ function writeProjectSessionRow(statement, agentName, session, identity) {
5817
+ statement.run(
5818
+ agentName,
5819
+ session.id,
5820
+ identity.kind,
5821
+ identity.key,
5822
+ identity.displayName,
5823
+ session.directory,
5824
+ session.time_updated ?? session.time_created
6079
5825
  );
6080
5826
  }
6081
- function createSearchTables(db) {
6082
- db.exec(`
6083
- CREATE TABLE IF NOT EXISTS session_documents (
6084
- id INTEGER PRIMARY KEY AUTOINCREMENT,
6085
- agent_name TEXT NOT NULL,
6086
- session_id TEXT NOT NULL,
6087
- slug TEXT NOT NULL,
6088
- title TEXT NOT NULL,
6089
- directory TEXT NOT NULL,
6090
- time_created INTEGER NOT NULL,
6091
- time_updated INTEGER,
6092
- activity_time INTEGER NOT NULL,
6093
- content_text TEXT NOT NULL,
6094
- content_hash TEXT NOT NULL,
6095
- indexed_at INTEGER NOT NULL,
6096
- UNIQUE(agent_name, session_id)
6097
- );
6098
-
6099
- CREATE VIRTUAL TABLE IF NOT EXISTS session_documents_fts USING fts5(
6100
- title,
6101
- content_text,
6102
- content='session_documents',
6103
- content_rowid='id'
6104
- );
6105
- `);
6106
- createSearchTriggers(db);
5827
+ function sessionFromRow(row) {
5828
+ const session = {
5829
+ id: String(row.session_id),
5830
+ slug: String(row.slug),
5831
+ title: String(row.title),
5832
+ directory: String(row.directory),
5833
+ time_created: Number(row.time_created),
5834
+ stats: {
5835
+ message_count: Number(row.message_count ?? 0),
5836
+ total_input_tokens: Number(row.total_input_tokens ?? 0),
5837
+ total_output_tokens: Number(row.total_output_tokens ?? 0),
5838
+ total_cost: Number(row.total_cost ?? 0)
5839
+ }
5840
+ };
5841
+ if (row.project_identity_key) {
5842
+ session.project_identity = {
5843
+ kind: row.project_identity_kind ?? "path",
5844
+ key: String(row.project_identity_key),
5845
+ displayName: String(row.project_display_name ?? "")
5846
+ };
5847
+ }
5848
+ if (row.time_updated != null) {
5849
+ session.time_updated = Number(row.time_updated);
5850
+ }
5851
+ if (row.total_cache_read_tokens != null) {
5852
+ session.stats.total_cache_read_tokens = Number(row.total_cache_read_tokens);
5853
+ }
5854
+ if (row.total_cache_create_tokens != null) {
5855
+ session.stats.total_cache_create_tokens = Number(row.total_cache_create_tokens);
5856
+ }
5857
+ if (row.cost_source) {
5858
+ session.stats.cost_source = row.cost_source;
5859
+ }
5860
+ if (row.total_tokens != null) {
5861
+ session.stats.total_tokens = Number(row.total_tokens);
5862
+ }
5863
+ const modelUsage = parseOptionalJson(row.model_usage_json);
5864
+ if (modelUsage) {
5865
+ session.model_usage = modelUsage;
5866
+ }
5867
+ const smartTags = parseOptionalJson(row.smart_tags_json);
5868
+ if (smartTags) {
5869
+ session.smart_tags = smartTags;
5870
+ }
5871
+ if (row.smart_tags_source_updated_at != null) {
5872
+ session.smart_tags_source_updated_at = Number(row.smart_tags_source_updated_at);
5873
+ }
5874
+ return session;
6107
5875
  }
6108
- function createSearchTriggers(db) {
6109
- db.exec(`
6110
- CREATE TRIGGER IF NOT EXISTS session_documents_ai AFTER INSERT ON session_documents BEGIN
6111
- INSERT INTO session_documents_fts(rowid, title, content_text)
6112
- VALUES (new.id, new.title, new.content_text);
6113
- END;
6114
-
6115
- CREATE TRIGGER IF NOT EXISTS session_documents_ad AFTER DELETE ON session_documents BEGIN
6116
- INSERT INTO session_documents_fts(session_documents_fts, rowid, title, content_text)
6117
- VALUES ('delete', old.id, old.title, old.content_text);
6118
- END;
6119
-
6120
- CREATE TRIGGER IF NOT EXISTS session_documents_au AFTER UPDATE ON session_documents BEGIN
6121
- INSERT INTO session_documents_fts(session_documents_fts, rowid, title, content_text)
6122
- VALUES ('delete', old.id, old.title, old.content_text);
6123
- INSERT INTO session_documents_fts(rowid, title, content_text)
6124
- VALUES (new.id, new.title, new.content_text);
6125
- END;
6126
- `);
5876
+ function messageFromBackfillRow(row) {
5877
+ const role = row.role === "assistant" || row.role === "tool" ? row.role : "user";
5878
+ return {
5879
+ id: String(row.message_id ?? ""),
5880
+ role,
5881
+ agent: row.agent ?? null,
5882
+ time_created: Number(row.time_created ?? 0),
5883
+ time_completed: row.time_completed == null ? null : Number(row.time_completed),
5884
+ mode: row.mode ?? null,
5885
+ model: row.model ?? null,
5886
+ provider: row.provider ?? null,
5887
+ parts: JSON.parse(String(row.parts_json ?? "[]")),
5888
+ subagent_id: row.subagent_id ?? void 0,
5889
+ nickname: row.nickname ?? void 0
5890
+ };
6127
5891
  }
6128
- function dropSearchTriggers(db) {
6129
- db.exec(`
6130
- DROP TRIGGER IF EXISTS session_documents_ai;
6131
- DROP TRIGGER IF EXISTS session_documents_ad;
6132
- DROP TRIGGER IF EXISTS session_documents_au;
6133
- `);
5892
+ function messageFromCachedRow(row) {
5893
+ const message = messageFromBackfillRow(row);
5894
+ const tokens = parseOptionalJson(row.tokens_json);
5895
+ if (tokens) {
5896
+ message.tokens = tokens;
5897
+ }
5898
+ if (row.cost != null) {
5899
+ message.cost = Number(row.cost);
5900
+ }
5901
+ if (row.cost_source) {
5902
+ message.cost_source = row.cost_source;
5903
+ }
5904
+ return message;
6134
5905
  }
6135
- function ensureProjectColumns(db) {
6136
- if (!tableExists(db, "session_documents")) {
5906
+ function appendPlainText(value, chunks) {
5907
+ if (value == null) return;
5908
+ if (typeof value === "string") {
5909
+ const normalized = value.trim();
5910
+ if (normalized) {
5911
+ chunks.push(normalized);
5912
+ }
6137
5913
  return;
6138
5914
  }
6139
- if (!columnExists(db, "session_documents", "project_identity_kind")) {
6140
- db.exec(
6141
- "ALTER TABLE session_documents ADD COLUMN project_identity_kind TEXT NOT NULL DEFAULT 'path'"
6142
- );
5915
+ if (typeof value === "number" || typeof value === "boolean") {
5916
+ chunks.push(String(value));
5917
+ return;
6143
5918
  }
6144
- if (!columnExists(db, "session_documents", "project_identity_key")) {
6145
- db.exec(
6146
- "ALTER TABLE session_documents ADD COLUMN project_identity_key TEXT NOT NULL DEFAULT ''"
6147
- );
5919
+ if (Array.isArray(value)) {
5920
+ for (const item of value) {
5921
+ appendPlainText(item, chunks);
5922
+ }
5923
+ return;
6148
5924
  }
6149
- if (!columnExists(db, "session_documents", "project_display_name")) {
6150
- db.exec(
6151
- "ALTER TABLE session_documents ADD COLUMN project_display_name TEXT NOT NULL DEFAULT ''"
6152
- );
5925
+ if (typeof value === "object") {
5926
+ for (const nested of Object.values(value)) {
5927
+ appendPlainText(nested, chunks);
5928
+ }
6153
5929
  }
6154
5930
  }
6155
- function createProjectTables(db) {
6156
- ensureProjectColumns(db);
6157
- db.exec(`
6158
- CREATE TABLE IF NOT EXISTS project_sessions (
6159
- agent_name TEXT NOT NULL,
6160
- session_id TEXT NOT NULL,
6161
- identity_kind TEXT NOT NULL,
6162
- identity_key TEXT NOT NULL,
6163
- display_name TEXT NOT NULL,
6164
- directory TEXT NOT NULL,
6165
- activity_time INTEGER NOT NULL,
6166
- PRIMARY KEY (agent_name, session_id)
6167
- );
6168
-
6169
- CREATE INDEX IF NOT EXISTS idx_project_sessions_identity
6170
- ON project_sessions(identity_kind, identity_key);
6171
- `);
6172
- createProjectGroupsView(db);
5931
+ function compactRecord(record) {
5932
+ return Object.fromEntries(Object.entries(record).filter(([, value]) => value != null));
6173
5933
  }
6174
- function createProjectGroupsView(db) {
6175
- if (!tableExists(db, "sessions")) {
6176
- db.exec(`
6177
- CREATE VIEW IF NOT EXISTS project_groups_v AS
6178
- SELECT
6179
- identity_kind,
6180
- identity_key,
6181
- MIN(display_name) AS display_name,
6182
- GROUP_CONCAT(DISTINCT agent_name) AS sources_csv,
6183
- COUNT(*) AS session_count,
6184
- MAX(activity_time) AS last_activity
6185
- FROM project_sessions
6186
- GROUP BY identity_kind, identity_key;
6187
- `);
6188
- return;
5934
+ function normalizeToolName2(value) {
5935
+ if (typeof value !== "string") return null;
5936
+ const name = value.trim().toLowerCase();
5937
+ return name || null;
5938
+ }
5939
+ function toolNamesFromMetadataJson(value) {
5940
+ if (!value) return [];
5941
+ try {
5942
+ const metadata = JSON.parse(String(value));
5943
+ if (!Array.isArray(metadata)) return [];
5944
+ const tools = /* @__PURE__ */ new Set();
5945
+ for (const item of metadata) {
5946
+ if (item == null || typeof item !== "object") continue;
5947
+ const toolName = normalizeToolName2(item.tool);
5948
+ if (toolName) tools.add(toolName);
5949
+ }
5950
+ return [...tools];
5951
+ } catch {
5952
+ return [];
6189
5953
  }
6190
- db.exec(`
6191
- CREATE VIEW IF NOT EXISTS project_groups_v AS
6192
- SELECT
6193
- project_identity_kind AS identity_kind,
6194
- project_identity_key AS identity_key,
6195
- MIN(project_display_name) AS display_name,
6196
- GROUP_CONCAT(DISTINCT agent_name) AS sources_csv,
6197
- COUNT(*) AS session_count,
6198
- MAX(activity_time) AS last_activity
6199
- FROM sessions
6200
- GROUP BY project_identity_kind, project_identity_key;
6201
- `);
6202
5954
  }
6203
- function recreateProjectGroupsView(db) {
6204
- db.exec("DROP VIEW IF EXISTS project_groups_v");
6205
- createProjectGroupsView(db);
5955
+ function toolNamesFromMessage(message) {
5956
+ const tools = /* @__PURE__ */ new Set();
5957
+ for (const part of message.parts) {
5958
+ if (part.type !== "tool") continue;
5959
+ const toolName = normalizeToolName2(part.tool);
5960
+ if (toolName) tools.add(toolName);
5961
+ }
5962
+ return [...tools];
6206
5963
  }
6207
- function createLatestCacheSchema(db) {
6208
- createCacheTables(db);
6209
- createSessionTables(db);
6210
- createMessageSearchTables(db);
6211
- createFileActivityTables(db);
6212
- createSearchTables(db);
6213
- createProjectTables(db);
5964
+ function summarizeToolPart(part) {
5965
+ const state = part.state == null ? void 0 : compactRecord({
5966
+ status: part.state.status,
5967
+ error: part.state.error,
5968
+ metadata: part.state.metadata
5969
+ });
5970
+ return compactRecord({
5971
+ type: part.type,
5972
+ tool: part.tool,
5973
+ title: part.title,
5974
+ nickname: part.nickname,
5975
+ callID: part.callID,
5976
+ approval_status: part.approval_status,
5977
+ state
5978
+ });
6214
5979
  }
6215
- function stringifyOptionalJson(value) {
6216
- return value == null ? null : JSON.stringify(value);
5980
+ function buildMessageText(message) {
5981
+ const chunks = [];
5982
+ chunks.push(message.role);
5983
+ appendPlainText(message.agent, chunks);
5984
+ appendPlainText(message.model, chunks);
5985
+ for (const part of message.parts) {
5986
+ appendPlainText(part.type, chunks);
5987
+ appendPlainText(part.title, chunks);
5988
+ appendPlainText(part.nickname, chunks);
5989
+ appendPlainText(part.tool, chunks);
5990
+ appendPlainText(part.text, chunks);
5991
+ appendPlainText(part.input, chunks);
5992
+ appendPlainText(part.output, chunks);
5993
+ appendPlainText(part.state, chunks);
5994
+ }
5995
+ return chunks.join("\n");
6217
5996
  }
6218
- function parseOptionalJson(value) {
6219
- return value == null ? void 0 : JSON.parse(String(value));
5997
+ function normalizeMessages(session) {
5998
+ return session.messages.map((message, index) => {
5999
+ const toolMetadata = message.parts.filter((part) => part.type === "tool").map((part) => summarizeToolPart(part));
6000
+ return {
6001
+ index,
6002
+ id: message.id || `${session.id}:${index}`,
6003
+ role: message.role,
6004
+ timeCreated: message.time_created,
6005
+ timeCompleted: message.time_completed ?? null,
6006
+ agent: message.agent ?? null,
6007
+ mode: message.mode ?? null,
6008
+ model: message.model ?? null,
6009
+ provider: message.provider ?? null,
6010
+ tokensJson: stringifyOptionalJson(message.tokens),
6011
+ cost: message.cost ?? null,
6012
+ costSource: message.cost_source ?? null,
6013
+ partsJson: JSON.stringify(message.parts),
6014
+ subagentId: message.subagent_id ?? null,
6015
+ nickname: message.nickname ?? null,
6016
+ contentText: buildMessageText(message),
6017
+ toolMetadataJson: toolMetadata.length > 0 ? JSON.stringify(toolMetadata) : null,
6018
+ toolNames: toolNamesFromMessage(message)
6019
+ };
6020
+ });
6220
6021
  }
6221
- function sourcePathFromMeta(meta) {
6222
- return typeof meta?.sourcePath === "string" ? meta.sourcePath : null;
6022
+ function buildSessionContentFromMessages(title, messages) {
6023
+ const chunks = [];
6024
+ appendPlainText(title, chunks);
6025
+ for (const message of messages) {
6026
+ appendPlainText(message.contentText, chunks);
6027
+ }
6028
+ return chunks.join("\n");
6223
6029
  }
6224
- function sourcePathFromMetaJson(metaJson) {
6225
- if (!metaJson) return null;
6226
- const meta = JSON.parse(metaJson);
6227
- return sourcePathFromMeta(meta);
6030
+ var CACHE_SCHEMA_VERSION = 13;
6031
+ function withCacheDb(fn) {
6032
+ const cachePath = getCachePath2();
6033
+ const db = openDb(cachePath);
6034
+ if (!db) return null;
6035
+ try {
6036
+ ensureSchema(db, cachePath);
6037
+ return fn(db);
6038
+ } catch {
6039
+ return null;
6040
+ } finally {
6041
+ db.close();
6042
+ }
6228
6043
  }
6229
- function prepareUpsertCachedSession(db) {
6230
- return db.prepare(`
6231
- INSERT INTO cached_sessions(agent_name, session_id, session_json, meta_json)
6232
- VALUES (?, ?, ?, ?)
6233
- ON CONFLICT(agent_name, session_id) DO UPDATE SET
6234
- session_json = excluded.session_json,
6235
- meta_json = excluded.meta_json
6236
- `);
6044
+ function withCacheDbReadOnly(fn) {
6045
+ const db = openDbReadOnly(getCachePath2());
6046
+ if (!db) return null;
6047
+ try {
6048
+ return fn(db);
6049
+ } catch {
6050
+ return null;
6051
+ } finally {
6052
+ db.close();
6053
+ }
6237
6054
  }
6238
- function prepareUpsertProjectSession(db) {
6239
- return db.prepare(`
6240
- INSERT INTO project_sessions(
6241
- agent_name,
6242
- session_id,
6243
- identity_kind,
6244
- identity_key,
6245
- display_name,
6246
- directory,
6247
- activity_time
6248
- ) VALUES (?, ?, ?, ?, ?, ?, ?)
6249
- ON CONFLICT(agent_name, session_id) DO UPDATE SET
6250
- identity_kind = excluded.identity_kind,
6251
- identity_key = excluded.identity_key,
6252
- display_name = excluded.display_name,
6253
- directory = excluded.directory,
6254
- activity_time = excluded.activity_time
6055
+ function createCacheTables(db) {
6056
+ db.exec(`
6057
+ CREATE TABLE IF NOT EXISTS cache_meta (
6058
+ key TEXT PRIMARY KEY,
6059
+ value TEXT NOT NULL
6060
+ );
6061
+
6062
+ CREATE TABLE IF NOT EXISTS agent_cache (
6063
+ agent_name TEXT PRIMARY KEY,
6064
+ timestamp INTEGER NOT NULL
6065
+ );
6066
+
6067
+ CREATE TABLE IF NOT EXISTS cached_sessions (
6068
+ agent_name TEXT NOT NULL,
6069
+ session_id TEXT NOT NULL,
6070
+ session_json TEXT NOT NULL,
6071
+ meta_json TEXT,
6072
+ PRIMARY KEY (agent_name, session_id)
6073
+ );
6074
+
6075
+ CREATE TABLE IF NOT EXISTS cache_initialization (
6076
+ agent_name TEXT PRIMARY KEY,
6077
+ initialized_at INTEGER NOT NULL,
6078
+ index_version TEXT NOT NULL,
6079
+ last_sync_at INTEGER NOT NULL
6080
+ );
6255
6081
  `);
6256
6082
  }
6257
- function prepareUpsertSession(db) {
6258
- return db.prepare(`
6259
- INSERT INTO sessions(
6260
- agent_name,
6261
- session_id,
6262
- sort_index,
6263
- slug,
6264
- title,
6265
- source_path,
6266
- directory,
6267
- project_identity_kind,
6268
- project_identity_key,
6269
- project_display_name,
6270
- time_created,
6271
- time_updated,
6272
- activity_time,
6273
- message_count,
6274
- total_input_tokens,
6275
- total_output_tokens,
6276
- total_cache_read_tokens,
6277
- total_cache_create_tokens,
6278
- total_cost,
6279
- cost_source,
6280
- total_tokens,
6281
- model_usage_json,
6282
- smart_tags_json,
6283
- smart_tags_source_updated_at,
6284
- meta_json
6285
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
6286
- ON CONFLICT(agent_name, session_id) DO UPDATE SET
6287
- sort_index = excluded.sort_index,
6288
- slug = excluded.slug,
6289
- title = excluded.title,
6290
- source_path = excluded.source_path,
6291
- directory = excluded.directory,
6292
- project_identity_kind = excluded.project_identity_kind,
6293
- project_identity_key = excluded.project_identity_key,
6294
- project_display_name = excluded.project_display_name,
6295
- time_created = excluded.time_created,
6296
- time_updated = excluded.time_updated,
6297
- activity_time = excluded.activity_time,
6298
- message_count = excluded.message_count,
6299
- total_input_tokens = excluded.total_input_tokens,
6300
- total_output_tokens = excluded.total_output_tokens,
6301
- total_cache_read_tokens = excluded.total_cache_read_tokens,
6302
- total_cache_create_tokens = excluded.total_cache_create_tokens,
6303
- total_cost = excluded.total_cost,
6304
- cost_source = excluded.cost_source,
6305
- total_tokens = excluded.total_tokens,
6306
- model_usage_json = excluded.model_usage_json,
6307
- smart_tags_json = excluded.smart_tags_json,
6308
- smart_tags_source_updated_at = excluded.smart_tags_source_updated_at,
6309
- meta_json = excluded.meta_json
6083
+ function createSessionTables(db) {
6084
+ db.exec(`
6085
+ CREATE TABLE IF NOT EXISTS sessions (
6086
+ agent_name TEXT NOT NULL,
6087
+ session_id TEXT NOT NULL,
6088
+ sort_index INTEGER NOT NULL DEFAULT 0,
6089
+ slug TEXT NOT NULL,
6090
+ title TEXT NOT NULL,
6091
+ source_path TEXT,
6092
+ directory TEXT NOT NULL,
6093
+ project_identity_kind TEXT NOT NULL,
6094
+ project_identity_key TEXT NOT NULL,
6095
+ project_display_name TEXT NOT NULL,
6096
+ time_created INTEGER NOT NULL,
6097
+ time_updated INTEGER,
6098
+ activity_time INTEGER NOT NULL,
6099
+ message_count INTEGER NOT NULL,
6100
+ total_input_tokens INTEGER NOT NULL,
6101
+ total_output_tokens INTEGER NOT NULL,
6102
+ total_cache_read_tokens INTEGER,
6103
+ total_cache_create_tokens INTEGER,
6104
+ total_cost REAL NOT NULL,
6105
+ cost_source TEXT,
6106
+ total_tokens INTEGER,
6107
+ model_usage_json TEXT,
6108
+ smart_tags_json TEXT,
6109
+ smart_tags_source_updated_at INTEGER,
6110
+ meta_json TEXT,
6111
+ PRIMARY KEY (agent_name, session_id)
6112
+ );
6113
+
6114
+ CREATE INDEX IF NOT EXISTS idx_sessions_agent_activity
6115
+ ON sessions(agent_name, activity_time);
6116
+
6117
+ CREATE INDEX IF NOT EXISTS idx_sessions_project
6118
+ ON sessions(project_identity_kind, project_identity_key, activity_time);
6119
+
6120
+ CREATE TABLE IF NOT EXISTS messages (
6121
+ agent_name TEXT NOT NULL,
6122
+ session_id TEXT NOT NULL,
6123
+ message_index INTEGER NOT NULL,
6124
+ message_id TEXT NOT NULL,
6125
+ role TEXT NOT NULL,
6126
+ time_created INTEGER NOT NULL,
6127
+ time_completed INTEGER,
6128
+ agent TEXT,
6129
+ mode TEXT,
6130
+ model TEXT,
6131
+ provider TEXT,
6132
+ tokens_json TEXT,
6133
+ cost REAL,
6134
+ cost_source TEXT,
6135
+ parts_json TEXT NOT NULL,
6136
+ subagent_id TEXT,
6137
+ nickname TEXT,
6138
+ content_text TEXT NOT NULL,
6139
+ tool_metadata_json TEXT,
6140
+ PRIMARY KEY (agent_name, session_id, message_index),
6141
+ FOREIGN KEY (agent_name, session_id)
6142
+ REFERENCES sessions(agent_name, session_id)
6143
+ ON DELETE CASCADE
6144
+ );
6145
+
6146
+ CREATE INDEX IF NOT EXISTS idx_messages_session
6147
+ ON messages(agent_name, session_id, message_index);
6310
6148
  `);
6149
+ createMessageToolTables(db);
6311
6150
  }
6312
- function prepareUpsertIndexedSession(db) {
6313
- return db.prepare(`
6314
- INSERT INTO sessions(
6315
- agent_name,
6316
- session_id,
6317
- sort_index,
6318
- slug,
6319
- title,
6320
- source_path,
6321
- directory,
6322
- project_identity_kind,
6323
- project_identity_key,
6324
- project_display_name,
6325
- time_created,
6326
- time_updated,
6327
- activity_time,
6328
- message_count,
6329
- total_input_tokens,
6330
- total_output_tokens,
6331
- total_cache_read_tokens,
6332
- total_cache_create_tokens,
6333
- total_cost,
6334
- cost_source,
6335
- total_tokens,
6336
- model_usage_json,
6337
- smart_tags_json,
6338
- smart_tags_source_updated_at,
6339
- meta_json
6340
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
6341
- ON CONFLICT(agent_name, session_id) DO UPDATE SET
6342
- slug = excluded.slug,
6343
- title = excluded.title,
6344
- directory = excluded.directory,
6345
- project_identity_kind = excluded.project_identity_kind,
6346
- project_identity_key = excluded.project_identity_key,
6347
- project_display_name = excluded.project_display_name,
6348
- time_created = excluded.time_created,
6349
- time_updated = excluded.time_updated,
6350
- activity_time = excluded.activity_time,
6351
- message_count = excluded.message_count,
6352
- total_input_tokens = excluded.total_input_tokens,
6353
- total_output_tokens = excluded.total_output_tokens,
6354
- total_cache_read_tokens = excluded.total_cache_read_tokens,
6355
- total_cache_create_tokens = excluded.total_cache_create_tokens,
6356
- total_cost = excluded.total_cost,
6357
- cost_source = excluded.cost_source,
6358
- total_tokens = excluded.total_tokens,
6359
- model_usage_json = excluded.model_usage_json,
6360
- smart_tags_json = excluded.smart_tags_json,
6361
- smart_tags_source_updated_at = excluded.smart_tags_source_updated_at
6151
+ function createMessageToolTables(db) {
6152
+ db.exec(`
6153
+ CREATE TABLE IF NOT EXISTS message_tools (
6154
+ agent_name TEXT NOT NULL,
6155
+ session_id TEXT NOT NULL,
6156
+ message_index INTEGER NOT NULL,
6157
+ tool_name TEXT NOT NULL,
6158
+ PRIMARY KEY (agent_name, session_id, message_index, tool_name),
6159
+ FOREIGN KEY (agent_name, session_id, message_index)
6160
+ REFERENCES messages(agent_name, session_id, message_index)
6161
+ ON DELETE CASCADE
6162
+ );
6163
+
6164
+ CREATE INDEX IF NOT EXISTS idx_message_tools_filter
6165
+ ON message_tools(tool_name, agent_name, session_id);
6362
6166
  `);
6363
6167
  }
6364
- function upsertSessionRow(statement, agentName, session, metaJson, sortIndex, sourcePath) {
6365
- const identity = session.project_identity ?? computeIdentity(session.directory, realFs);
6366
- const activityTime = session.time_updated ?? session.time_created;
6367
- statement.run(
6368
- agentName,
6369
- session.id,
6370
- sortIndex,
6371
- session.slug,
6372
- session.title,
6373
- sourcePath,
6374
- session.directory,
6375
- identity.kind,
6376
- identity.key,
6377
- identity.displayName,
6378
- session.time_created,
6379
- session.time_updated ?? null,
6380
- activityTime,
6381
- session.stats.message_count,
6382
- session.stats.total_input_tokens,
6383
- session.stats.total_output_tokens,
6384
- session.stats.total_cache_read_tokens ?? null,
6385
- session.stats.total_cache_create_tokens ?? null,
6386
- session.stats.total_cost,
6387
- session.stats.cost_source ?? null,
6388
- session.stats.total_tokens ?? null,
6389
- stringifyOptionalJson(session.model_usage),
6390
- stringifyOptionalJson(session.smart_tags),
6391
- session.smart_tags_source_updated_at ?? null,
6392
- metaJson
6393
- );
6168
+ function createMessageSearchTables(db) {
6169
+ if (!tableExists(db, "messages")) {
6170
+ createSessionTables(db);
6171
+ }
6172
+ db.exec(`
6173
+ CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
6174
+ content_text,
6175
+ content='messages',
6176
+ content_rowid='rowid'
6177
+ );
6178
+ `);
6179
+ createMessageSearchTriggers(db);
6394
6180
  }
6395
- function prepareInsertFileActivity(db) {
6396
- return db.prepare(`
6397
- INSERT INTO session_file_activity(
6398
- agent_name,
6399
- session_id,
6400
- project_identity_key,
6401
- path,
6402
- kind,
6403
- count,
6404
- latest_time
6405
- ) VALUES (?, ?, ?, ?, ?, ?, ?)
6181
+ function createMessageSearchTriggers(db) {
6182
+ db.exec(`
6183
+ CREATE TRIGGER IF NOT EXISTS messages_ai AFTER INSERT ON messages BEGIN
6184
+ INSERT INTO messages_fts(rowid, content_text)
6185
+ VALUES (new.rowid, new.content_text);
6186
+ END;
6187
+
6188
+ CREATE TRIGGER IF NOT EXISTS messages_ad AFTER DELETE ON messages BEGIN
6189
+ INSERT INTO messages_fts(messages_fts, rowid, content_text)
6190
+ VALUES ('delete', old.rowid, old.content_text);
6191
+ END;
6192
+
6193
+ CREATE TRIGGER IF NOT EXISTS messages_au AFTER UPDATE ON messages BEGIN
6194
+ INSERT INTO messages_fts(messages_fts, rowid, content_text)
6195
+ VALUES ('delete', old.rowid, old.content_text);
6196
+ INSERT INTO messages_fts(rowid, content_text)
6197
+ VALUES (new.rowid, new.content_text);
6198
+ END;
6406
6199
  `);
6407
6200
  }
6408
- function prepareInsertMessageTool(db) {
6409
- return db.prepare(`
6410
- INSERT OR IGNORE INTO message_tools(
6411
- agent_name,
6412
- session_id,
6413
- message_index,
6414
- tool_name
6415
- ) VALUES (?, ?, ?, ?)
6201
+ function dropMessageSearchTriggers(db) {
6202
+ db.exec(`
6203
+ DROP TRIGGER IF EXISTS messages_ai;
6204
+ DROP TRIGGER IF EXISTS messages_ad;
6205
+ DROP TRIGGER IF EXISTS messages_au;
6416
6206
  `);
6417
6207
  }
6418
- function writeFileActivityRows(statement, activities) {
6419
- for (const activity of activities) {
6420
- statement.run(
6421
- activity.agent_name,
6422
- activity.session_id,
6423
- activity.project_identity_key,
6424
- activity.path,
6425
- activity.kind,
6426
- activity.count,
6427
- activity.latest_time
6208
+ function createFileActivityTables(db) {
6209
+ db.exec(`
6210
+ CREATE TABLE IF NOT EXISTS session_file_activity (
6211
+ agent_name TEXT NOT NULL,
6212
+ session_id TEXT NOT NULL,
6213
+ project_identity_key TEXT NOT NULL,
6214
+ path TEXT NOT NULL,
6215
+ kind TEXT NOT NULL,
6216
+ count INTEGER NOT NULL,
6217
+ latest_time INTEGER NOT NULL,
6218
+ PRIMARY KEY (agent_name, session_id, project_identity_key, path, kind),
6219
+ FOREIGN KEY (agent_name, session_id)
6220
+ REFERENCES sessions(agent_name, session_id)
6221
+ ON DELETE CASCADE
6428
6222
  );
6429
- }
6223
+
6224
+ CREATE INDEX IF NOT EXISTS idx_file_activity_project_latest
6225
+ ON session_file_activity(project_identity_key, latest_time);
6226
+
6227
+ CREATE INDEX IF NOT EXISTS idx_file_activity_latest
6228
+ ON session_file_activity(latest_time DESC, count DESC, path);
6229
+
6230
+ CREATE INDEX IF NOT EXISTS idx_file_activity_agent_latest
6231
+ ON session_file_activity(agent_name, latest_time DESC, count DESC, path);
6232
+
6233
+ CREATE INDEX IF NOT EXISTS idx_file_activity_project_latest_ordered
6234
+ ON session_file_activity(project_identity_key, latest_time DESC, count DESC, path);
6235
+
6236
+ CREATE INDEX IF NOT EXISTS idx_file_activity_path
6237
+ ON session_file_activity(path);
6238
+
6239
+ CREATE INDEX IF NOT EXISTS idx_file_activity_kind
6240
+ ON session_file_activity(kind);
6241
+ `);
6242
+ createFileActivityPathSearchTables(db);
6430
6243
  }
6431
- function writeProjectSessionRow(statement, agentName, session, identity) {
6432
- statement.run(
6433
- agentName,
6434
- session.id,
6435
- identity.kind,
6436
- identity.key,
6437
- identity.displayName,
6438
- session.directory,
6439
- session.time_updated ?? session.time_created
6440
- );
6244
+ function createFileActivityPathSearchTables(db) {
6245
+ db.exec(`
6246
+ CREATE VIRTUAL TABLE IF NOT EXISTS session_file_activity_path_fts USING fts5(
6247
+ path,
6248
+ content='session_file_activity',
6249
+ content_rowid='rowid',
6250
+ tokenize='trigram'
6251
+ );
6252
+ `);
6253
+ createFileActivityPathSearchTriggers(db);
6441
6254
  }
6442
- function sessionFromRow(row) {
6443
- const session = {
6444
- id: String(row.session_id),
6445
- slug: String(row.slug),
6446
- title: String(row.title),
6447
- directory: String(row.directory),
6448
- time_created: Number(row.time_created),
6449
- stats: {
6450
- message_count: Number(row.message_count ?? 0),
6451
- total_input_tokens: Number(row.total_input_tokens ?? 0),
6452
- total_output_tokens: Number(row.total_output_tokens ?? 0),
6453
- total_cost: Number(row.total_cost ?? 0)
6454
- }
6455
- };
6456
- if (row.project_identity_key) {
6457
- session.project_identity = {
6458
- kind: row.project_identity_kind ?? "path",
6459
- key: String(row.project_identity_key),
6460
- displayName: String(row.project_display_name ?? "")
6461
- };
6462
- }
6463
- if (row.time_updated != null) {
6464
- session.time_updated = Number(row.time_updated);
6465
- }
6466
- if (row.total_cache_read_tokens != null) {
6467
- session.stats.total_cache_read_tokens = Number(row.total_cache_read_tokens);
6468
- }
6469
- if (row.total_cache_create_tokens != null) {
6470
- session.stats.total_cache_create_tokens = Number(row.total_cache_create_tokens);
6255
+ function createFileActivityPathSearchTriggers(db) {
6256
+ db.exec(`
6257
+ CREATE TRIGGER IF NOT EXISTS session_file_activity_path_ai
6258
+ AFTER INSERT ON session_file_activity BEGIN
6259
+ INSERT INTO session_file_activity_path_fts(rowid, path)
6260
+ VALUES (new.rowid, new.path);
6261
+ END;
6262
+
6263
+ CREATE TRIGGER IF NOT EXISTS session_file_activity_path_ad
6264
+ AFTER DELETE ON session_file_activity BEGIN
6265
+ INSERT INTO session_file_activity_path_fts(session_file_activity_path_fts, rowid, path)
6266
+ VALUES ('delete', old.rowid, old.path);
6267
+ END;
6268
+
6269
+ CREATE TRIGGER IF NOT EXISTS session_file_activity_path_au
6270
+ AFTER UPDATE ON session_file_activity BEGIN
6271
+ INSERT INTO session_file_activity_path_fts(session_file_activity_path_fts, rowid, path)
6272
+ VALUES ('delete', old.rowid, old.path);
6273
+ INSERT INTO session_file_activity_path_fts(rowid, path)
6274
+ VALUES (new.rowid, new.path);
6275
+ END;
6276
+ `);
6277
+ }
6278
+ function rebuildFileActivityPathIndex(db) {
6279
+ if (!tableExists(db, "session_file_activity_path_fts")) {
6280
+ return;
6471
6281
  }
6472
- if (row.cost_source) {
6473
- session.stats.cost_source = row.cost_source;
6282
+ db.exec(
6283
+ "INSERT INTO session_file_activity_path_fts(session_file_activity_path_fts) VALUES ('rebuild')"
6284
+ );
6285
+ }
6286
+ function createSearchTables(db) {
6287
+ db.exec(`
6288
+ CREATE TABLE IF NOT EXISTS session_documents (
6289
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
6290
+ agent_name TEXT NOT NULL,
6291
+ session_id TEXT NOT NULL,
6292
+ slug TEXT NOT NULL,
6293
+ title TEXT NOT NULL,
6294
+ directory TEXT NOT NULL,
6295
+ time_created INTEGER NOT NULL,
6296
+ time_updated INTEGER,
6297
+ activity_time INTEGER NOT NULL,
6298
+ content_text TEXT NOT NULL,
6299
+ content_hash TEXT NOT NULL,
6300
+ indexed_at INTEGER NOT NULL,
6301
+ UNIQUE(agent_name, session_id)
6302
+ );
6303
+
6304
+ CREATE VIRTUAL TABLE IF NOT EXISTS session_documents_fts USING fts5(
6305
+ title,
6306
+ content_text,
6307
+ content='session_documents',
6308
+ content_rowid='id'
6309
+ );
6310
+ `);
6311
+ createSearchTriggers(db);
6312
+ }
6313
+ function createSearchTriggers(db) {
6314
+ db.exec(`
6315
+ CREATE TRIGGER IF NOT EXISTS session_documents_ai AFTER INSERT ON session_documents BEGIN
6316
+ INSERT INTO session_documents_fts(rowid, title, content_text)
6317
+ VALUES (new.id, new.title, new.content_text);
6318
+ END;
6319
+
6320
+ CREATE TRIGGER IF NOT EXISTS session_documents_ad AFTER DELETE ON session_documents BEGIN
6321
+ INSERT INTO session_documents_fts(session_documents_fts, rowid, title, content_text)
6322
+ VALUES ('delete', old.id, old.title, old.content_text);
6323
+ END;
6324
+
6325
+ CREATE TRIGGER IF NOT EXISTS session_documents_au AFTER UPDATE ON session_documents BEGIN
6326
+ INSERT INTO session_documents_fts(session_documents_fts, rowid, title, content_text)
6327
+ VALUES ('delete', old.id, old.title, old.content_text);
6328
+ INSERT INTO session_documents_fts(rowid, title, content_text)
6329
+ VALUES (new.id, new.title, new.content_text);
6330
+ END;
6331
+ `);
6332
+ }
6333
+ function dropSearchTriggers(db) {
6334
+ db.exec(`
6335
+ DROP TRIGGER IF EXISTS session_documents_ai;
6336
+ DROP TRIGGER IF EXISTS session_documents_ad;
6337
+ DROP TRIGGER IF EXISTS session_documents_au;
6338
+ `);
6339
+ }
6340
+ function ensureProjectColumns(db) {
6341
+ if (!tableExists(db, "session_documents")) {
6342
+ return;
6474
6343
  }
6475
- if (row.total_tokens != null) {
6476
- session.stats.total_tokens = Number(row.total_tokens);
6344
+ if (!columnExists(db, "session_documents", "project_identity_kind")) {
6345
+ db.exec(
6346
+ "ALTER TABLE session_documents ADD COLUMN project_identity_kind TEXT NOT NULL DEFAULT 'path'"
6347
+ );
6477
6348
  }
6478
- const modelUsage = parseOptionalJson(row.model_usage_json);
6479
- if (modelUsage) {
6480
- session.model_usage = modelUsage;
6349
+ if (!columnExists(db, "session_documents", "project_identity_key")) {
6350
+ db.exec(
6351
+ "ALTER TABLE session_documents ADD COLUMN project_identity_key TEXT NOT NULL DEFAULT ''"
6352
+ );
6481
6353
  }
6482
- const smartTags = parseOptionalJson(row.smart_tags_json);
6483
- if (smartTags) {
6484
- session.smart_tags = smartTags;
6354
+ if (!columnExists(db, "session_documents", "project_display_name")) {
6355
+ db.exec(
6356
+ "ALTER TABLE session_documents ADD COLUMN project_display_name TEXT NOT NULL DEFAULT ''"
6357
+ );
6485
6358
  }
6486
- if (row.smart_tags_source_updated_at != null) {
6487
- session.smart_tags_source_updated_at = Number(row.smart_tags_source_updated_at);
6359
+ }
6360
+ function createProjectTables(db) {
6361
+ ensureProjectColumns(db);
6362
+ db.exec(`
6363
+ CREATE TABLE IF NOT EXISTS project_sessions (
6364
+ agent_name TEXT NOT NULL,
6365
+ session_id TEXT NOT NULL,
6366
+ identity_kind TEXT NOT NULL,
6367
+ identity_key TEXT NOT NULL,
6368
+ display_name TEXT NOT NULL,
6369
+ directory TEXT NOT NULL,
6370
+ activity_time INTEGER NOT NULL,
6371
+ PRIMARY KEY (agent_name, session_id)
6372
+ );
6373
+
6374
+ CREATE INDEX IF NOT EXISTS idx_project_sessions_identity
6375
+ ON project_sessions(identity_kind, identity_key);
6376
+ `);
6377
+ createProjectGroupsView(db);
6378
+ }
6379
+ function createProjectGroupsView(db) {
6380
+ if (!tableExists(db, "sessions")) {
6381
+ db.exec(`
6382
+ CREATE VIEW IF NOT EXISTS project_groups_v AS
6383
+ SELECT
6384
+ identity_kind,
6385
+ identity_key,
6386
+ MIN(display_name) AS display_name,
6387
+ GROUP_CONCAT(DISTINCT agent_name) AS sources_csv,
6388
+ COUNT(*) AS session_count,
6389
+ MAX(activity_time) AS last_activity
6390
+ FROM project_sessions
6391
+ GROUP BY identity_kind, identity_key;
6392
+ `);
6393
+ return;
6488
6394
  }
6489
- return session;
6395
+ db.exec(`
6396
+ CREATE VIEW IF NOT EXISTS project_groups_v AS
6397
+ SELECT
6398
+ project_identity_kind AS identity_kind,
6399
+ project_identity_key AS identity_key,
6400
+ MIN(project_display_name) AS display_name,
6401
+ GROUP_CONCAT(DISTINCT agent_name) AS sources_csv,
6402
+ COUNT(*) AS session_count,
6403
+ MAX(activity_time) AS last_activity
6404
+ FROM sessions
6405
+ GROUP BY project_identity_kind, project_identity_key;
6406
+ `);
6407
+ }
6408
+ function recreateProjectGroupsView(db) {
6409
+ db.exec("DROP VIEW IF EXISTS project_groups_v");
6410
+ createProjectGroupsView(db);
6411
+ }
6412
+ function createLatestCacheSchema(db) {
6413
+ createCacheTables(db);
6414
+ createSessionTables(db);
6415
+ createMessageSearchTables(db);
6416
+ createFileActivityTables(db);
6417
+ createSearchTables(db);
6418
+ createProjectTables(db);
6490
6419
  }
6491
6420
  function recreateSearchIndexSchema(db) {
6492
6421
  db.exec(`
@@ -6743,36 +6672,6 @@ function backfillStructuredSessions(db) {
6743
6672
  );
6744
6673
  }
6745
6674
  }
6746
- function messageFromBackfillRow(row) {
6747
- const role = row.role === "assistant" || row.role === "tool" ? row.role : "user";
6748
- return {
6749
- id: String(row.message_id ?? ""),
6750
- role,
6751
- agent: row.agent ?? null,
6752
- time_created: Number(row.time_created ?? 0),
6753
- time_completed: row.time_completed == null ? null : Number(row.time_completed),
6754
- mode: row.mode ?? null,
6755
- model: row.model ?? null,
6756
- provider: row.provider ?? null,
6757
- parts: JSON.parse(String(row.parts_json ?? "[]")),
6758
- subagent_id: row.subagent_id ?? void 0,
6759
- nickname: row.nickname ?? void 0
6760
- };
6761
- }
6762
- function messageFromCachedRow(row) {
6763
- const message = messageFromBackfillRow(row);
6764
- const tokens = parseOptionalJson(row.tokens_json);
6765
- if (tokens) {
6766
- message.tokens = tokens;
6767
- }
6768
- if (row.cost != null) {
6769
- message.cost = Number(row.cost);
6770
- }
6771
- if (row.cost_source) {
6772
- message.cost_source = row.cost_source;
6773
- }
6774
- return message;
6775
- }
6776
6675
  function backfillMessageTools(db) {
6777
6676
  createMessageToolTables(db);
6778
6677
  if (!tableExists(db, "messages")) {
@@ -6866,13 +6765,6 @@ function rebuildMessageSearchIndex(db) {
6866
6765
  }
6867
6766
  db.exec("INSERT INTO messages_fts(messages_fts) VALUES ('rebuild')");
6868
6767
  }
6869
- function shouldBulkSyncSearchIndex(options, changedCount) {
6870
- if (options.isBulk != null) {
6871
- return options.isBulk;
6872
- }
6873
- const threshold = options.bulkThreshold ?? SEARCH_INDEX_BULK_SYNC_THRESHOLD;
6874
- return threshold > 0 && changedCount >= threshold;
6875
- }
6876
6768
  function ensureFtsReady(db) {
6877
6769
  if (!tableExists(db, "session_documents_fts")) {
6878
6770
  createSearchTables(db);
@@ -6887,7 +6779,7 @@ function ensureFtsReady(db) {
6887
6779
  function ensureFtsConsistency(db) {
6888
6780
  ensureFtsReady(db);
6889
6781
  const cachePath = getCachePath2();
6890
- if (ftsIntegrityCheckedPath === cachePath) {
6782
+ if (getFtsIntegrityCheckedPath() === cachePath) {
6891
6783
  return;
6892
6784
  }
6893
6785
  try {
@@ -6895,11 +6787,11 @@ function ensureFtsConsistency(db) {
6895
6787
  "INSERT INTO session_documents_fts(session_documents_fts, rank) VALUES ('integrity-check', 1)"
6896
6788
  );
6897
6789
  db.exec("INSERT INTO messages_fts(messages_fts, rank) VALUES ('integrity-check', 1)");
6898
- ftsIntegrityCheckedPath = cachePath;
6790
+ setFtsIntegrityCheckedPath(cachePath);
6899
6791
  } catch {
6900
6792
  rebuildSearchIndex(db);
6901
6793
  rebuildMessageSearchIndex(db);
6902
- ftsIntegrityCheckedPath = cachePath;
6794
+ setFtsIntegrityCheckedPath(cachePath);
6903
6795
  }
6904
6796
  }
6905
6797
  function setCacheSchemaVersion(db) {
@@ -6959,655 +6851,187 @@ function ensureSchema(db, dbPath) {
6959
6851
  }
6960
6852
  },
6961
6853
  {
6962
- version: 10,
6963
- migrate(db2) {
6964
- createFileActivityPathSearchTables(db2);
6965
- rebuildFileActivityPathIndex(db2);
6966
- }
6967
- },
6968
- {
6969
- version: 11,
6970
- migrate(db2) {
6971
- backfillMessageTools(db2);
6972
- }
6973
- },
6974
- {
6975
- version: 12,
6976
- migrate(db2) {
6977
- refreshProjectIdentities(db2);
6978
- }
6979
- },
6980
- { version: 13, migrate: createCacheTables }
6981
- ]
6982
- });
6983
- createLatestCacheSchema(db);
6984
- if (getUserVersion(db) <= CACHE_SCHEMA_VERSION) {
6985
- setCacheSchemaVersion(db);
6986
- }
6987
- }
6988
- function sessionContentHash(session) {
6989
- return JSON.stringify([
6990
- session.slug,
6991
- session.title,
6992
- session.directory,
6993
- session.time_created,
6994
- session.time_updated ?? session.time_created,
6995
- session.stats.message_count,
6996
- session.stats.total_input_tokens,
6997
- session.stats.total_output_tokens,
6998
- session.stats.total_cache_read_tokens ?? 0,
6999
- session.stats.total_cache_create_tokens ?? 0,
7000
- session.stats.total_cost,
7001
- session.stats.cost_source ?? "",
7002
- session.stats.total_tokens ?? 0
7003
- ]);
7004
- }
7005
- function escapeFtsTerm(value) {
7006
- return value.replaceAll('"', '""');
7007
- }
7008
- function splitSearchTokens(input) {
7009
- const tokens = [];
7010
- let token = "";
7011
- let inQuote = false;
7012
- for (const char of input) {
7013
- if (char === '"') {
7014
- inQuote = !inQuote;
7015
- token += char;
7016
- continue;
7017
- }
7018
- if (/\s/.test(char) && !inQuote) {
7019
- if (token) {
7020
- tokens.push(token);
7021
- token = "";
7022
- }
7023
- continue;
7024
- }
7025
- token += char;
7026
- }
7027
- if (token) {
7028
- tokens.push(token);
7029
- }
7030
- return tokens;
7031
- }
7032
- function unwrapSearchValue(value) {
7033
- const trimmed = value.trim();
7034
- if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
7035
- return trimmed.slice(1, -1).trim();
7036
- }
7037
- return trimmed;
7038
- }
7039
- function parseCostQualifier(value, filters) {
7040
- const raw = unwrapSearchValue(value);
7041
- const range = raw.match(/^(\d+(?:\.\d+)?)\.\.(\d+(?:\.\d+)?)$/);
7042
- if (range) {
7043
- filters.costMin = Number(range[1]);
7044
- filters.costMax = Number(range[2]);
7045
- return;
7046
- }
7047
- const comparison = raw.match(/^(>=|>|<=|<)(\d+(?:\.\d+)?)$/);
7048
- if (comparison) {
7049
- const amount2 = Number(comparison[2]);
7050
- if (comparison[1]?.includes(">")) {
7051
- filters.costMin = amount2;
7052
- filters.costMinExclusive = comparison[1] === ">";
7053
- } else {
7054
- filters.costMax = amount2;
7055
- filters.costMaxExclusive = comparison[1] === "<";
7056
- }
7057
- return;
7058
- }
7059
- const amount = Number(raw);
7060
- if (!Number.isNaN(amount)) {
7061
- filters.costMin = amount;
7062
- filters.costMax = amount;
7063
- }
7064
- }
7065
- function appendUnique(values, value) {
7066
- if (values?.includes(value)) return values;
7067
- return [...values ?? [], value];
7068
- }
7069
- function isSmartTag(value) {
7070
- return value === "bugfix" || value === "refactoring" || value === "feature-dev" || value === "testing" || value === "docs" || value === "git-ops" || value === "build-deploy" || value === "exploration" || value === "planning";
7071
- }
7072
- function parseSearchQuery(input) {
7073
- const filters = {};
7074
- const textTokens = [];
7075
- let hasQualifiers = false;
7076
- for (const token of splitSearchTokens(input)) {
7077
- const match = token.match(/^([a-zA-Z][a-zA-Z_-]*):(.+)$/);
7078
- if (!match) {
7079
- textTokens.push(token);
7080
- continue;
7081
- }
7082
- const key = match[1].toLowerCase();
7083
- const value = unwrapSearchValue(match[2]);
7084
- if (!value) continue;
7085
- let consumed = true;
7086
- if (key === "agent") filters.agent = value.toLowerCase();
7087
- else if (key === "project") filters.project = value;
7088
- else if (key === "projectkey" || key === "project-key") filters.projectKey = value;
7089
- else if (key === "cwd") filters.cwd = value;
7090
- else if (key === "tool") filters.tools = appendUnique(filters.tools, value.toLowerCase());
7091
- else if (key === "file" || key === "path") filters.file = value;
7092
- else if (key === "kind" || key === "filekind" || key === "file-kind") {
7093
- if (value === "read" || value === "edit" || value === "write" || value === "delete") {
7094
- filters.fileKind = value;
7095
- } else {
7096
- consumed = false;
7097
- }
7098
- } else if (key === "tag" || key === "signal") {
7099
- const tag = value.toLowerCase();
7100
- if (isSmartTag(tag)) {
7101
- filters.tags = appendUnique(filters.tags, tag);
7102
- } else {
7103
- consumed = false;
7104
- }
7105
- } else if (key === "cost") {
7106
- parseCostQualifier(value, filters);
7107
- } else {
7108
- consumed = false;
7109
- }
7110
- if (consumed) {
7111
- hasQualifiers = true;
7112
- } else {
7113
- textTokens.push(token);
7114
- }
7115
- }
7116
- return {
7117
- text: textTokens.join(" ").trim(),
7118
- filters,
7119
- hasQualifiers
7120
- };
7121
- }
7122
- function toFtsQuery(input) {
7123
- const tokens = splitSearchTokens(input);
7124
- const mapped = tokens.map((token) => {
7125
- if (/^OR$/i.test(token)) {
7126
- return "OR";
7127
- }
7128
- if (token.startsWith('"') && token.endsWith('"')) {
7129
- return `"${escapeFtsTerm(token.slice(1, -1))}"`;
7130
- }
7131
- return `"${escapeFtsTerm(token)}"`;
7132
- }).filter(
7133
- (token, index, values) => token !== "OR" || index > 0 && index < values.length - 1 && values[index - 1] !== "OR" && values[index + 1] !== "OR"
7134
- );
7135
- return mapped.join(" ");
7136
- }
7137
- function appendPlainText(value, chunks) {
7138
- if (value == null) return;
7139
- if (typeof value === "string") {
7140
- const normalized = value.trim();
7141
- if (normalized) {
7142
- chunks.push(normalized);
7143
- }
7144
- return;
7145
- }
7146
- if (typeof value === "number" || typeof value === "boolean") {
7147
- chunks.push(String(value));
7148
- return;
7149
- }
7150
- if (Array.isArray(value)) {
7151
- for (const item of value) {
7152
- appendPlainText(item, chunks);
7153
- }
7154
- return;
7155
- }
7156
- if (typeof value === "object") {
7157
- for (const nested of Object.values(value)) {
7158
- appendPlainText(nested, chunks);
7159
- }
7160
- }
7161
- }
7162
- function compactRecord(record) {
7163
- return Object.fromEntries(Object.entries(record).filter(([, value]) => value != null));
7164
- }
7165
- function normalizeToolName2(value) {
7166
- if (typeof value !== "string") return null;
7167
- const name = value.trim().toLowerCase();
7168
- return name || null;
7169
- }
7170
- function toolNamesFromMetadataJson(value) {
7171
- if (!value) return [];
7172
- try {
7173
- const metadata = JSON.parse(String(value));
7174
- if (!Array.isArray(metadata)) return [];
7175
- const tools = /* @__PURE__ */ new Set();
7176
- for (const item of metadata) {
7177
- if (item == null || typeof item !== "object") continue;
7178
- const toolName = normalizeToolName2(item.tool);
7179
- if (toolName) tools.add(toolName);
7180
- }
7181
- return [...tools];
7182
- } catch {
7183
- return [];
7184
- }
7185
- }
7186
- function toolNamesFromMessage(message) {
7187
- const tools = /* @__PURE__ */ new Set();
7188
- for (const part of message.parts) {
7189
- if (part.type !== "tool") continue;
7190
- const toolName = normalizeToolName2(part.tool);
7191
- if (toolName) tools.add(toolName);
7192
- }
7193
- return [...tools];
7194
- }
7195
- function summarizeToolPart(part) {
7196
- const state = part.state == null ? void 0 : compactRecord({
7197
- status: part.state.status,
7198
- error: part.state.error,
7199
- metadata: part.state.metadata
7200
- });
7201
- return compactRecord({
7202
- type: part.type,
7203
- tool: part.tool,
7204
- title: part.title,
7205
- nickname: part.nickname,
7206
- callID: part.callID,
7207
- approval_status: part.approval_status,
7208
- state
7209
- });
7210
- }
7211
- function buildMessageText(message) {
7212
- const chunks = [];
7213
- chunks.push(message.role);
7214
- appendPlainText(message.agent, chunks);
7215
- appendPlainText(message.model, chunks);
7216
- for (const part of message.parts) {
7217
- appendPlainText(part.type, chunks);
7218
- appendPlainText(part.title, chunks);
7219
- appendPlainText(part.nickname, chunks);
7220
- appendPlainText(part.tool, chunks);
7221
- appendPlainText(part.text, chunks);
7222
- appendPlainText(part.input, chunks);
7223
- appendPlainText(part.output, chunks);
7224
- appendPlainText(part.state, chunks);
7225
- }
7226
- return chunks.join("\n");
7227
- }
7228
- function normalizeMessages(session) {
7229
- return session.messages.map((message, index) => {
7230
- const toolMetadata = message.parts.filter((part) => part.type === "tool").map((part) => summarizeToolPart(part));
7231
- return {
7232
- index,
7233
- id: message.id || `${session.id}:${index}`,
7234
- role: message.role,
7235
- timeCreated: message.time_created,
7236
- timeCompleted: message.time_completed ?? null,
7237
- agent: message.agent ?? null,
7238
- mode: message.mode ?? null,
7239
- model: message.model ?? null,
7240
- provider: message.provider ?? null,
7241
- tokensJson: stringifyOptionalJson(message.tokens),
7242
- cost: message.cost ?? null,
7243
- costSource: message.cost_source ?? null,
7244
- partsJson: JSON.stringify(message.parts),
7245
- subagentId: message.subagent_id ?? null,
7246
- nickname: message.nickname ?? null,
7247
- contentText: buildMessageText(message),
7248
- toolMetadataJson: toolMetadata.length > 0 ? JSON.stringify(toolMetadata) : null,
7249
- toolNames: toolNamesFromMessage(message)
7250
- };
7251
- });
7252
- }
7253
- function buildSessionContentFromMessages(title, messages) {
7254
- const chunks = [];
7255
- appendPlainText(title, chunks);
7256
- for (const message of messages) {
7257
- appendPlainText(message.contentText, chunks);
7258
- }
7259
- return chunks.join("\n");
7260
- }
7261
- function deleteLegacyCacheFile() {
7262
- const legacyPath = getLegacyCachePath();
7263
- if (!existsSync11(legacyPath)) {
7264
- return;
7265
- }
7266
- try {
7267
- unlinkSync(legacyPath);
7268
- } catch {
7269
- }
7270
- }
7271
- function loadCachedSessions(agentName) {
7272
- if (!hasCacheStorage()) {
7273
- return null;
7274
- }
7275
- return withCacheDb((db) => {
7276
- const timestampRow = db.prepare("SELECT timestamp AS value FROM agent_cache WHERE agent_name = ?").get(agentName);
7277
- const timestamp = Number(timestampRow?.value ?? 0);
7278
- if (!timestamp) {
7279
- return null;
7280
- }
7281
- const rows = db.prepare(
7282
- `
7283
- SELECT
7284
- session_id,
7285
- sort_index,
7286
- slug,
7287
- title,
7288
- source_path,
7289
- directory,
7290
- project_identity_kind,
7291
- project_identity_key,
7292
- project_display_name,
7293
- time_created,
7294
- time_updated,
7295
- message_count,
7296
- total_input_tokens,
7297
- total_output_tokens,
7298
- total_cache_read_tokens,
7299
- total_cache_create_tokens,
7300
- total_cost,
7301
- cost_source,
7302
- total_tokens,
7303
- model_usage_json,
7304
- smart_tags_json,
7305
- smart_tags_source_updated_at,
7306
- meta_json
7307
- FROM sessions
7308
- WHERE agent_name = ?
7309
- ORDER BY sort_index, activity_time DESC
7310
- `
7311
- ).all(agentName);
7312
- const sessions = [];
7313
- const meta = {};
7314
- for (const row of rows) {
7315
- const session = sessionFromRow(row);
7316
- sessions.push(session);
7317
- if (row.meta_json) {
7318
- meta[session.id] = JSON.parse(row.meta_json);
7319
- }
7320
- }
7321
- return { sessions, meta, timestamp };
7322
- });
7323
- }
7324
- function isAgentCacheInitialized(agentName, indexVersion = CACHE_INITIALIZATION_VERSION) {
7325
- if (!hasCacheStorage()) {
7326
- return false;
7327
- }
7328
- return withCacheDbReadOnly((db) => {
7329
- if (!tableExists(db, "cache_initialization")) return false;
7330
- const row = db.prepare(
7331
- `
7332
- SELECT index_version
7333
- FROM cache_initialization
7334
- WHERE agent_name = ?
7335
- `
7336
- ).get(agentName);
7337
- return row?.index_version === indexVersion;
7338
- }) ?? false;
7339
- }
7340
- function markAgentCacheInitialized(agentName, indexVersion = CACHE_INITIALIZATION_VERSION) {
7341
- withCacheDb((db) => {
7342
- const now = Date.now();
7343
- db.prepare(
7344
- `
7345
- INSERT INTO cache_initialization(agent_name, initialized_at, index_version, last_sync_at)
7346
- VALUES (?, ?, ?, ?)
7347
- ON CONFLICT(agent_name) DO UPDATE SET
7348
- index_version = excluded.index_version,
7349
- last_sync_at = excluded.last_sync_at
7350
- `
7351
- ).run(agentName, now, indexVersion, now);
7352
- });
7353
- }
7354
- function loadCachedSessionData(agentName, sessionId) {
7355
- if (!hasCacheStorage()) {
7356
- return null;
7357
- }
7358
- return withCacheDbReadOnly((db) => {
7359
- const row = db.prepare(
7360
- `
7361
- SELECT
7362
- session_id,
7363
- sort_index,
7364
- slug,
7365
- title,
7366
- source_path,
7367
- directory,
7368
- project_identity_kind,
7369
- project_identity_key,
7370
- project_display_name,
7371
- time_created,
7372
- time_updated,
7373
- message_count,
7374
- total_input_tokens,
7375
- total_output_tokens,
7376
- total_cache_read_tokens,
7377
- total_cache_create_tokens,
7378
- total_cost,
7379
- cost_source,
7380
- total_tokens,
7381
- model_usage_json,
7382
- smart_tags_json,
7383
- smart_tags_source_updated_at,
7384
- meta_json
7385
- FROM sessions
7386
- WHERE agent_name = ? AND session_id = ?
7387
- `
7388
- ).get(agentName, sessionId);
7389
- if (!row) {
7390
- return null;
7391
- }
7392
- const messageRows = db.prepare(
7393
- `
7394
- SELECT
7395
- message_id,
7396
- role,
7397
- time_created,
7398
- time_completed,
7399
- agent,
7400
- mode,
7401
- model,
7402
- provider,
7403
- tokens_json,
7404
- cost,
7405
- cost_source,
7406
- parts_json,
7407
- subagent_id,
7408
- nickname
7409
- FROM messages
7410
- WHERE agent_name = ? AND session_id = ?
7411
- ORDER BY message_index
7412
- `
7413
- ).all(agentName, sessionId);
7414
- const head = sessionFromRow(row);
7415
- const fileActivityRows = db.prepare(
7416
- `
7417
- SELECT agent_name, session_id, project_identity_key, path, kind, count, latest_time
7418
- FROM session_file_activity
7419
- WHERE agent_name = ? AND session_id = ?
7420
- ORDER BY latest_time DESC, count DESC, path
7421
- LIMIT 500
7422
- `
7423
- ).all(agentName, sessionId);
7424
- return {
7425
- ...head,
7426
- messages: messageRows.map((messageRow) => messageFromCachedRow(messageRow)),
7427
- file_activity: fileActivityRows.map((activityRow) => fileActivityFromRow(activityRow))
7428
- };
7429
- });
7430
- }
7431
- function saveCachedSessions(agentName, sessions, meta = {}) {
7432
- withCacheDb((db) => {
7433
- const deleteAgent = db.prepare("DELETE FROM agent_cache WHERE agent_name = ?");
7434
- const deleteLegacySessions = db.prepare("DELETE FROM cached_sessions WHERE agent_name = ?");
7435
- const deleteSession = db.prepare(
7436
- "DELETE FROM sessions WHERE agent_name = ? AND session_id = ?"
7437
- );
7438
- const deleteSearchDocument = db.prepare(
7439
- "DELETE FROM session_documents WHERE agent_name = ? AND session_id = ?"
7440
- );
7441
- const deleteMessages = db.prepare(
7442
- "DELETE FROM messages WHERE agent_name = ? AND session_id = ?"
7443
- );
7444
- const deleteMessageTools = db.prepare(
7445
- "DELETE FROM message_tools WHERE agent_name = ? AND session_id = ?"
7446
- );
7447
- const deleteFileActivity = db.prepare(
7448
- "DELETE FROM session_file_activity WHERE agent_name = ? AND session_id = ?"
7449
- );
7450
- const deleteProjectSession = db.prepare(
7451
- "DELETE FROM project_sessions WHERE agent_name = ? AND session_id = ?"
7452
- );
7453
- const deleteProjectSessions = db.prepare("DELETE FROM project_sessions WHERE agent_name = ?");
7454
- const upsertAgent = db.prepare(`
7455
- INSERT INTO agent_cache(agent_name, timestamp)
7456
- VALUES (?, ?)
7457
- ON CONFLICT(agent_name) DO UPDATE SET timestamp = excluded.timestamp
7458
- `);
7459
- const upsertCachedSession = prepareUpsertCachedSession(db);
7460
- const upsertSession = prepareUpsertSession(db);
7461
- const upsertProjectSession = prepareUpsertProjectSession(db);
7462
- const write = db.transaction(() => {
7463
- const timestamp = Date.now();
7464
- const sessionIds = new Set(sessions.map((session) => session.id));
7465
- const existingSessionIds = db.prepare("SELECT session_id FROM sessions WHERE agent_name = ?").all(agentName);
7466
- deleteAgent.run(agentName);
7467
- deleteLegacySessions.run(agentName);
7468
- deleteProjectSessions.run(agentName);
7469
- upsertAgent.run(agentName, timestamp);
7470
- for (const row of existingSessionIds) {
7471
- const sessionId = String(row.session_id);
7472
- if (!sessionIds.has(sessionId)) {
7473
- deleteSearchDocument.run(agentName, sessionId);
7474
- deleteMessageTools.run(agentName, sessionId);
7475
- deleteMessages.run(agentName, sessionId);
7476
- deleteFileActivity.run(agentName, sessionId);
7477
- deleteProjectSession.run(agentName, sessionId);
7478
- deleteSession.run(agentName, sessionId);
7479
- }
7480
- }
7481
- sessions.forEach((session, index) => {
7482
- const identity = session.project_identity ?? computeIdentity(session.directory, realFs);
7483
- const sessionMeta = meta[session.id];
7484
- const metaJson = sessionMeta ? JSON.stringify(sessionMeta) : null;
7485
- upsertCachedSession.run(agentName, session.id, JSON.stringify(session), metaJson);
7486
- upsertSessionRow(
7487
- upsertSession,
7488
- agentName,
7489
- session,
7490
- metaJson,
7491
- index,
7492
- sourcePathFromMeta(sessionMeta)
7493
- );
7494
- writeProjectSessionRow(upsertProjectSession, agentName, session, identity);
7495
- });
7496
- });
7497
- write();
7498
- deleteLegacyCacheFile();
6854
+ version: 10,
6855
+ migrate(db2) {
6856
+ createFileActivityPathSearchTables(db2);
6857
+ rebuildFileActivityPathIndex(db2);
6858
+ }
6859
+ },
6860
+ {
6861
+ version: 11,
6862
+ migrate(db2) {
6863
+ backfillMessageTools(db2);
6864
+ }
6865
+ },
6866
+ {
6867
+ version: 12,
6868
+ migrate(db2) {
6869
+ refreshProjectIdentities(db2);
6870
+ }
6871
+ },
6872
+ { version: 13, migrate: createCacheTables }
6873
+ ]
7499
6874
  });
6875
+ createLatestCacheSchema(db);
6876
+ if (getUserVersion(db) <= CACHE_SCHEMA_VERSION) {
6877
+ setCacheSchemaVersion(db);
6878
+ }
7500
6879
  }
7501
- function saveCachedSessionChanges(agentName, changes, removedSessionIds, meta = {}) {
7502
- if (changes.length === 0 && removedSessionIds.length === 0) {
7503
- return;
6880
+ function shouldBulkSyncSearchIndex(options, changedCount) {
6881
+ if (options.isBulk != null) {
6882
+ return options.isBulk;
7504
6883
  }
7505
- withCacheDb((db) => {
7506
- const deleteLegacySession = db.prepare(
7507
- "DELETE FROM cached_sessions WHERE agent_name = ? AND session_id = ?"
7508
- );
7509
- const deleteSession = db.prepare(
7510
- "DELETE FROM sessions WHERE agent_name = ? AND session_id = ?"
7511
- );
7512
- const deleteSearchDocument = db.prepare(
7513
- "DELETE FROM session_documents WHERE agent_name = ? AND session_id = ?"
7514
- );
7515
- const deleteMessages = db.prepare(
7516
- "DELETE FROM messages WHERE agent_name = ? AND session_id = ?"
7517
- );
7518
- const deleteMessageTools = db.prepare(
7519
- "DELETE FROM message_tools WHERE agent_name = ? AND session_id = ?"
7520
- );
7521
- const deleteFileActivity = db.prepare(
7522
- "DELETE FROM session_file_activity WHERE agent_name = ? AND session_id = ?"
7523
- );
7524
- const deleteProjectSession = db.prepare(
7525
- "DELETE FROM project_sessions WHERE agent_name = ? AND session_id = ?"
7526
- );
7527
- const upsertAgent = db.prepare(`
7528
- INSERT INTO agent_cache(agent_name, timestamp)
7529
- VALUES (?, ?)
7530
- ON CONFLICT(agent_name) DO UPDATE SET timestamp = excluded.timestamp
7531
- `);
7532
- const upsertCachedSession = prepareUpsertCachedSession(db);
7533
- const upsertSession = prepareUpsertSession(db);
7534
- const upsertProjectSession = prepareUpsertProjectSession(db);
7535
- const write = db.transaction(() => {
7536
- upsertAgent.run(agentName, Date.now());
7537
- for (const sessionId of new Set(removedSessionIds)) {
7538
- deleteLegacySession.run(agentName, sessionId);
7539
- deleteSearchDocument.run(agentName, sessionId);
7540
- deleteMessageTools.run(agentName, sessionId);
7541
- deleteMessages.run(agentName, sessionId);
7542
- deleteFileActivity.run(agentName, sessionId);
7543
- deleteProjectSession.run(agentName, sessionId);
7544
- deleteSession.run(agentName, sessionId);
7545
- }
7546
- for (const { session, sortIndex } of changes) {
7547
- const identity = session.project_identity ?? computeIdentity(session.directory, realFs);
7548
- const sessionMeta = meta[session.id];
7549
- const metaJson = sessionMeta ? JSON.stringify(sessionMeta) : null;
7550
- upsertCachedSession.run(agentName, session.id, JSON.stringify(session), metaJson);
7551
- upsertSessionRow(
7552
- upsertSession,
7553
- agentName,
7554
- session,
7555
- metaJson,
7556
- sortIndex,
7557
- sourcePathFromMeta(sessionMeta)
7558
- );
7559
- writeProjectSessionRow(upsertProjectSession, agentName, session, identity);
6884
+ const threshold = options.bulkThreshold ?? SEARCH_INDEX_BULK_SYNC_THRESHOLD;
6885
+ return threshold > 0 && changedCount >= threshold;
6886
+ }
6887
+ function sessionContentHash(session) {
6888
+ return JSON.stringify([
6889
+ session.slug,
6890
+ session.title,
6891
+ session.directory,
6892
+ session.time_created,
6893
+ session.time_updated ?? session.time_created,
6894
+ session.stats.message_count,
6895
+ session.stats.total_input_tokens,
6896
+ session.stats.total_output_tokens,
6897
+ session.stats.total_cache_read_tokens ?? 0,
6898
+ session.stats.total_cache_create_tokens ?? 0,
6899
+ session.stats.total_cost,
6900
+ session.stats.cost_source ?? "",
6901
+ session.stats.total_tokens ?? 0
6902
+ ]);
6903
+ }
6904
+ function escapeFtsTerm(value) {
6905
+ return value.replaceAll('"', '""');
6906
+ }
6907
+ function splitSearchTokens(input) {
6908
+ const tokens = [];
6909
+ let token = "";
6910
+ let inQuote = false;
6911
+ for (const char of input) {
6912
+ if (char === '"') {
6913
+ inQuote = !inQuote;
6914
+ token += char;
6915
+ continue;
6916
+ }
6917
+ if (/\s/.test(char) && !inQuote) {
6918
+ if (token) {
6919
+ tokens.push(token);
6920
+ token = "";
7560
6921
  }
7561
- });
7562
- write();
7563
- deleteLegacyCacheFile();
7564
- });
6922
+ continue;
6923
+ }
6924
+ token += char;
6925
+ }
6926
+ if (token) {
6927
+ tokens.push(token);
6928
+ }
6929
+ return tokens;
7565
6930
  }
7566
- function clearCache() {
7567
- ftsIntegrityCheckedPath = null;
7568
- if (!hasCacheStorage()) {
7569
- deleteLegacyCacheFile();
6931
+ function unwrapSearchValue(value) {
6932
+ const trimmed = value.trim();
6933
+ if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
6934
+ return trimmed.slice(1, -1).trim();
6935
+ }
6936
+ return trimmed;
6937
+ }
6938
+ function parseCostQualifier(value, filters) {
6939
+ const raw = unwrapSearchValue(value);
6940
+ const range = raw.match(/^(\d+(?:\.\d+)?)\.\.(\d+(?:\.\d+)?)$/);
6941
+ if (range) {
6942
+ filters.costMin = Number(range[1]);
6943
+ filters.costMax = Number(range[2]);
7570
6944
  return;
7571
6945
  }
7572
- withCacheDb((db) => {
7573
- db.exec(`
7574
- DELETE FROM agent_cache;
7575
- DELETE FROM cache_initialization;
7576
- DELETE FROM cached_sessions;
7577
- DELETE FROM session_documents;
7578
- DELETE FROM session_file_activity;
7579
- DELETE FROM message_tools;
7580
- DELETE FROM messages;
7581
- DELETE FROM sessions;
7582
- DELETE FROM project_sessions;
7583
- `);
7584
- });
7585
- deleteLegacyCacheFile();
7586
- const cachePath = getCachePath2();
7587
- const walPath = `${cachePath}-wal`;
7588
- const shmPath = `${cachePath}-shm`;
7589
- for (const filePath of [walPath, shmPath]) {
7590
- if (!existsSync11(filePath)) {
6946
+ const comparison = raw.match(/^(>=|>|<=|<)(\d+(?:\.\d+)?)$/);
6947
+ if (comparison) {
6948
+ const amount2 = Number(comparison[2]);
6949
+ if (comparison[1]?.includes(">")) {
6950
+ filters.costMin = amount2;
6951
+ filters.costMinExclusive = comparison[1] === ">";
6952
+ } else {
6953
+ filters.costMax = amount2;
6954
+ filters.costMaxExclusive = comparison[1] === "<";
6955
+ }
6956
+ return;
6957
+ }
6958
+ const amount = Number(raw);
6959
+ if (!Number.isNaN(amount)) {
6960
+ filters.costMin = amount;
6961
+ filters.costMax = amount;
6962
+ }
6963
+ }
6964
+ function appendUnique(values, value) {
6965
+ if (values?.includes(value)) return values;
6966
+ return [...values ?? [], value];
6967
+ }
6968
+ function isSmartTag(value) {
6969
+ return value === "bugfix" || value === "refactoring" || value === "feature-dev" || value === "testing" || value === "docs" || value === "git-ops" || value === "build-deploy" || value === "exploration" || value === "planning";
6970
+ }
6971
+ function parseSearchQuery(input) {
6972
+ const filters = {};
6973
+ const textTokens = [];
6974
+ let hasQualifiers = false;
6975
+ for (const token of splitSearchTokens(input)) {
6976
+ const match = token.match(/^([a-zA-Z][a-zA-Z_-]*):(.+)$/);
6977
+ if (!match) {
6978
+ textTokens.push(token);
7591
6979
  continue;
7592
6980
  }
7593
- try {
7594
- rmSync(filePath, { force: true });
7595
- } catch {
6981
+ const key = match[1].toLowerCase();
6982
+ const value = unwrapSearchValue(match[2]);
6983
+ if (!value) continue;
6984
+ let consumed = true;
6985
+ if (key === "agent") filters.agent = value.toLowerCase();
6986
+ else if (key === "project") filters.project = value;
6987
+ else if (key === "projectkey" || key === "project-key") filters.projectKey = value;
6988
+ else if (key === "cwd") filters.cwd = value;
6989
+ else if (key === "tool") filters.tools = appendUnique(filters.tools, value.toLowerCase());
6990
+ else if (key === "file" || key === "path") filters.file = value;
6991
+ else if (key === "kind" || key === "filekind" || key === "file-kind") {
6992
+ if (value === "read" || value === "edit" || value === "write" || value === "delete") {
6993
+ filters.fileKind = value;
6994
+ } else {
6995
+ consumed = false;
6996
+ }
6997
+ } else if (key === "tag" || key === "signal") {
6998
+ const tag = value.toLowerCase();
6999
+ if (isSmartTag(tag)) {
7000
+ filters.tags = appendUnique(filters.tags, tag);
7001
+ } else {
7002
+ consumed = false;
7003
+ }
7004
+ } else if (key === "cost") {
7005
+ parseCostQualifier(value, filters);
7006
+ } else {
7007
+ consumed = false;
7008
+ }
7009
+ if (consumed) {
7010
+ hasQualifiers = true;
7011
+ } else {
7012
+ textTokens.push(token);
7596
7013
  }
7597
7014
  }
7015
+ return {
7016
+ text: textTokens.join(" ").trim(),
7017
+ filters,
7018
+ hasQualifiers
7019
+ };
7598
7020
  }
7599
- function getCacheInfo() {
7600
- if (!hasCacheStorage()) {
7601
- return { lastScanTime: null, size: 0 };
7602
- }
7603
- const info = withCacheDb((db) => {
7604
- const timestampRow = db.prepare("SELECT MAX(timestamp) AS value FROM agent_cache").get();
7605
- const sizeRow = db.prepare("SELECT COUNT(*) AS value FROM sessions").get();
7606
- const lastScanTime = Number(timestampRow?.value ?? 0) || null;
7607
- const size = Number(sizeRow?.value ?? 0);
7608
- return { lastScanTime, size };
7609
- });
7610
- return info ?? { lastScanTime: null, size: 0 };
7021
+ function toFtsQuery(input) {
7022
+ const tokens = splitSearchTokens(input);
7023
+ const mapped = tokens.map((token) => {
7024
+ if (/^OR$/i.test(token)) {
7025
+ return "OR";
7026
+ }
7027
+ if (token.startsWith('"') && token.endsWith('"')) {
7028
+ return `"${escapeFtsTerm(token.slice(1, -1))}"`;
7029
+ }
7030
+ return `"${escapeFtsTerm(token)}"`;
7031
+ }).filter(
7032
+ (token, index, values) => token !== "OR" || index > 0 && index < values.length - 1 && values[index - 1] !== "OR" && values[index + 1] !== "OR"
7033
+ );
7034
+ return mapped.join(" ");
7611
7035
  }
7612
7036
  function loadSearchIndexEntry(agentName, change, loadSessionData) {
7613
7037
  try {
@@ -7943,17 +7367,6 @@ function sessionMatchesSearchCost(session, options) {
7943
7367
  }
7944
7368
  return true;
7945
7369
  }
7946
- function likePattern(value) {
7947
- return `%${value.trim().toLowerCase().replace(/[\\%_]/g, "\\$&")}%`;
7948
- }
7949
- function filePathFtsQuery(value) {
7950
- const path2 = normalizeFilePathSearch(value);
7951
- if (path2.length < 3) return null;
7952
- return `"${path2.replaceAll('"', '""')}"`;
7953
- }
7954
- function escapeRegExp(value) {
7955
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7956
- }
7957
7370
  function buildSessionSearchFilters(options) {
7958
7371
  const clauses = [];
7959
7372
  const params = [];
@@ -8081,307 +7494,656 @@ function buildTermSnippet(text, terms) {
8081
7494
  const end = Math.min(text.length, index + term.length + 80);
8082
7495
  return `${start > 0 ? "\u2026 " : ""}${highlightTerm(text.slice(start, end), term)}${end < text.length ? " \u2026" : ""}`;
8083
7496
  }
8084
- function messageMatchType(row) {
8085
- if (row.role === "user") return "user_message";
8086
- if (row.role === "tool" || row.mode === "tool" || row.tool_metadata_json) return "tool_output";
8087
- return "assistant_reply";
7497
+ function messageMatchType(row) {
7498
+ if (row.role === "user") return "user_message";
7499
+ if (row.role === "tool" || row.mode === "tool" || row.tool_metadata_json) return "tool_output";
7500
+ return "assistant_reply";
7501
+ }
7502
+ function searchResultRowKey(row) {
7503
+ return `${String(row.agent_name)}\0${String(row.session_id)}`;
7504
+ }
7505
+ function fetchMessageSearchMatches(db, rows, ftsQuery, terms) {
7506
+ const candidates = rows.filter((row) => !textMatchesTerms(String(row.title ?? ""), terms));
7507
+ if (candidates.length === 0) {
7508
+ return /* @__PURE__ */ new Map();
7509
+ }
7510
+ const clauses = [];
7511
+ const params = [ftsQuery];
7512
+ for (const row of candidates) {
7513
+ clauses.push("(m.agent_name = ? AND m.session_id = ?)");
7514
+ params.push(String(row.agent_name), String(row.session_id));
7515
+ }
7516
+ const messageRows = db.prepare(
7517
+ `
7518
+ SELECT
7519
+ m.agent_name,
7520
+ m.session_id,
7521
+ m.message_index,
7522
+ m.role,
7523
+ m.mode,
7524
+ m.content_text,
7525
+ m.tool_metadata_json
7526
+ FROM messages_fts
7527
+ JOIN messages m ON m.rowid = messages_fts.rowid
7528
+ WHERE messages_fts MATCH ?
7529
+ AND (${clauses.join(" OR ")})
7530
+ ORDER BY m.message_index
7531
+ `
7532
+ ).all(...params);
7533
+ const matches = /* @__PURE__ */ new Map();
7534
+ for (const message of messageRows) {
7535
+ const key = searchResultRowKey(message);
7536
+ if (matches.has(key)) continue;
7537
+ const text = String(message.content_text ?? "");
7538
+ if (!textMatchesTerms(text, terms)) continue;
7539
+ matches.set(key, {
7540
+ snippet: buildTermSnippet(text, terms),
7541
+ matchType: messageMatchType(message)
7542
+ });
7543
+ }
7544
+ return matches;
7545
+ }
7546
+ function resolveSearchMatch(row, terms, messageMatches) {
7547
+ const title = String(row.title ?? "");
7548
+ if (terms.terms.length === 0) {
7549
+ return {
7550
+ snippet: `Recent session \xB7 ${String(row.directory ?? "")}`,
7551
+ matchType: "recent"
7552
+ };
7553
+ }
7554
+ if (textMatchesTerms(title, terms)) {
7555
+ return { snippet: buildTermSnippet(title, terms), matchType: "title" };
7556
+ }
7557
+ const messageMatch = messageMatches.get(searchResultRowKey(row));
7558
+ if (messageMatch) {
7559
+ return messageMatch;
7560
+ }
7561
+ return {
7562
+ snippet: String(row.snippet ?? ""),
7563
+ matchType: "assistant_reply"
7564
+ };
7565
+ }
7566
+ function rowsToSearchResults(db, rows, textQuery, ftsQuery = toFtsQuery(textQuery)) {
7567
+ const terms = parseTextTerms(textQuery);
7568
+ const messageMatches = terms.terms.length > 0 && ftsQuery ? fetchMessageSearchMatches(db, rows, ftsQuery, terms) : /* @__PURE__ */ new Map();
7569
+ return rows.map((row) => {
7570
+ const match = resolveSearchMatch(row, terms, messageMatches);
7571
+ return {
7572
+ agentName: String(row.agent_name),
7573
+ session: sessionHeadFromSearchRow(row),
7574
+ snippet: match.snippet,
7575
+ matchType: match.matchType
7576
+ };
7577
+ });
7578
+ }
7579
+ function searchSessions(query, options = {}) {
7580
+ const search = mergeSearchQueryOptions(query, options);
7581
+ const normalizedQuery = search.text.trim();
7582
+ if (!hasCacheStorage()) {
7583
+ return [];
7584
+ }
7585
+ const results = withCacheDb((db) => {
7586
+ ensureFtsReady(db);
7587
+ const filters = buildSessionSearchFilters(search.options);
7588
+ if (!normalizedQuery) {
7589
+ const rows2 = db.prepare(
7590
+ `
7591
+ SELECT
7592
+ ${searchSessionColumns()},
7593
+ '' AS snippet
7594
+ FROM sessions s
7595
+ WHERE 1 = 1
7596
+ ${filters.where}
7597
+ ORDER BY s.activity_time DESC
7598
+ LIMIT ?
7599
+ `
7600
+ ).all(...filters.params, search.options.limit ?? 50);
7601
+ return rowsToSearchResults(db, rows2, "");
7602
+ }
7603
+ const ftsQuery = toFtsQuery(normalizedQuery);
7604
+ if (!ftsQuery) return [];
7605
+ const rows = db.prepare(
7606
+ `
7607
+ SELECT
7608
+ ${searchSessionColumns()},
7609
+ COALESCE(
7610
+ NULLIF(snippet(session_documents_fts, 1, '<mark>', '</mark>', ' \u2026 ', 18), ''),
7611
+ highlight(session_documents_fts, 0, '<mark>', '</mark>')
7612
+ ) AS snippet
7613
+ FROM session_documents_fts
7614
+ JOIN session_documents d ON d.id = session_documents_fts.rowid
7615
+ JOIN sessions s ON s.agent_name = d.agent_name AND s.session_id = d.session_id
7616
+ WHERE session_documents_fts MATCH ?
7617
+ ${filters.where}
7618
+ ORDER BY bm25(session_documents_fts, 8.0, 1.0), s.activity_time DESC
7619
+ LIMIT ?
7620
+ `
7621
+ ).all(ftsQuery, ...filters.params, search.options.limit ?? 50);
7622
+ return rowsToSearchResults(db, rows, normalizedQuery, ftsQuery);
7623
+ });
7624
+ return results ?? [];
7625
+ }
7626
+ function fileActivityFilters(options) {
7627
+ const path2 = options.path ? normalizeFilePathSearch(options.path) : "";
7628
+ return {
7629
+ projectKey: options.projectKey ?? null,
7630
+ projectLike: options.project ? likePattern(options.project) : null,
7631
+ cwdKey: options.cwd ? computeIdentity(options.cwd, realFs).key : null,
7632
+ cwdLike: options.cwd ? likePattern(options.cwd) : null,
7633
+ path: path2,
7634
+ pathLike: path2 ? likePattern(path2) : null
7635
+ };
7636
+ }
7637
+ function fileActivityFromRow(row) {
7638
+ return {
7639
+ agent_name: String(row.agent_name),
7640
+ session_id: String(row.session_id),
7641
+ project_identity_key: String(row.project_identity_key ?? ""),
7642
+ path: String(row.path ?? ""),
7643
+ kind: row.kind ?? "read",
7644
+ count: Number(row.count ?? 0),
7645
+ latest_time: Number(row.latest_time ?? 0)
7646
+ };
7647
+ }
7648
+ function buildFileActivityWhere(options) {
7649
+ const filters = fileActivityFilters(options);
7650
+ const clauses = [];
7651
+ const params = [];
7652
+ if (options.agent != null) {
7653
+ clauses.push("fa.agent_name = ?");
7654
+ params.push(options.agent);
7655
+ }
7656
+ if (options.sessionId != null) {
7657
+ clauses.push("fa.session_id = ?");
7658
+ params.push(options.sessionId);
7659
+ }
7660
+ if (filters.projectKey != null) {
7661
+ clauses.push("fa.project_identity_key = ?");
7662
+ params.push(filters.projectKey);
7663
+ }
7664
+ if (filters.projectLike != null) {
7665
+ clauses.push(
7666
+ "(LOWER(fa.project_identity_key) LIKE ? ESCAPE '\\' OR LOWER(s.project_display_name) LIKE ? ESCAPE '\\' OR LOWER(s.directory) LIKE ? ESCAPE '\\')"
7667
+ );
7668
+ params.push(filters.projectLike, filters.projectLike, filters.projectLike);
7669
+ }
7670
+ if (filters.cwdKey != null) {
7671
+ clauses.push("(s.project_identity_key = ? OR LOWER(s.directory) LIKE ? ESCAPE '\\')");
7672
+ params.push(filters.cwdKey, filters.cwdLike);
7673
+ }
7674
+ if (filters.pathLike != null) {
7675
+ const pathQuery = filePathFtsQuery(filters.path);
7676
+ if (pathQuery) {
7677
+ clauses.push(
7678
+ "fa.rowid IN (SELECT rowid FROM session_file_activity_path_fts WHERE path MATCH ?)"
7679
+ );
7680
+ params.push(pathQuery);
7681
+ } else {
7682
+ clauses.push("LOWER(fa.path) LIKE ? ESCAPE '\\'");
7683
+ params.push(filters.pathLike);
7684
+ }
7685
+ }
7686
+ if (options.kind != null) {
7687
+ clauses.push("fa.kind = ?");
7688
+ params.push(options.kind);
7689
+ }
7690
+ if (options.from != null) {
7691
+ clauses.push("fa.latest_time >= ?");
7692
+ params.push(options.from);
7693
+ }
7694
+ if (options.to != null) {
7695
+ clauses.push("fa.latest_time <= ?");
7696
+ params.push(options.to);
7697
+ }
7698
+ return {
7699
+ where: clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "",
7700
+ params
7701
+ };
7702
+ }
7703
+ function listFileActivity(options = {}) {
7704
+ if (!hasCacheStorage()) {
7705
+ return [];
7706
+ }
7707
+ const filters = buildFileActivityWhere(options);
7708
+ const queryRows = (db) => db.prepare(
7709
+ `
7710
+ SELECT
7711
+ fa.agent_name,
7712
+ fa.session_id,
7713
+ fa.project_identity_key,
7714
+ fa.path,
7715
+ fa.kind,
7716
+ fa.count,
7717
+ fa.latest_time,
7718
+ s.slug,
7719
+ s.title,
7720
+ s.directory,
7721
+ s.project_identity_kind,
7722
+ s.project_display_name,
7723
+ s.time_created,
7724
+ s.time_updated,
7725
+ s.message_count,
7726
+ s.total_input_tokens,
7727
+ s.total_output_tokens,
7728
+ s.total_cache_read_tokens,
7729
+ s.total_cache_create_tokens,
7730
+ s.total_cost,
7731
+ s.cost_source,
7732
+ s.total_tokens
7733
+ FROM session_file_activity fa
7734
+ JOIN sessions s ON s.agent_name = fa.agent_name AND s.session_id = fa.session_id
7735
+ ${filters.where}
7736
+ ORDER BY fa.latest_time DESC, fa.count DESC, fa.path
7737
+ LIMIT ?
7738
+ `
7739
+ ).all(...filters.params, options.limit ?? 50);
7740
+ let rows = withCacheDbReadOnly(queryRows);
7741
+ if (rows == null && options.path) {
7742
+ rows = withCacheDb(queryRows);
7743
+ }
7744
+ return (rows ?? []).map((row) => ({
7745
+ ...fileActivityFromRow(row),
7746
+ session: sessionHeadFromSearchRow(row)
7747
+ }));
7748
+ }
7749
+ function listSessionFileActivity(agentName, sessionId) {
7750
+ return listFileActivity({ agent: agentName, sessionId, limit: 500 }).map(
7751
+ ({ session: _session, ...activity }) => activity
7752
+ );
8088
7753
  }
8089
- function searchResultRowKey(row) {
8090
- return `${String(row.agent_name)}\0${String(row.session_id)}`;
7754
+ function highlightFilePath(path2, query) {
7755
+ const needle = normalizeFilePathSearch(query);
7756
+ if (!needle) return path2;
7757
+ const lower = path2.toLowerCase();
7758
+ const index = lower.indexOf(needle.toLowerCase());
7759
+ if (index < 0) return path2;
7760
+ return `${path2.slice(0, index)}<mark>${path2.slice(index, index + needle.length)}</mark>${path2.slice(
7761
+ index + needle.length
7762
+ )}`;
8091
7763
  }
8092
- function fetchMessageSearchMatches(db, rows, ftsQuery, terms) {
8093
- const candidates = rows.filter((row) => !textMatchesTerms(String(row.title ?? ""), terms));
8094
- if (candidates.length === 0) {
8095
- return /* @__PURE__ */ new Map();
8096
- }
8097
- const clauses = [];
8098
- const params = [ftsQuery];
8099
- for (const row of candidates) {
8100
- clauses.push("(m.agent_name = ? AND m.session_id = ?)");
8101
- params.push(String(row.agent_name), String(row.session_id));
8102
- }
8103
- const messageRows = db.prepare(
8104
- `
8105
- SELECT
8106
- m.agent_name,
8107
- m.session_id,
8108
- m.message_index,
8109
- m.role,
8110
- m.mode,
8111
- m.content_text,
8112
- m.tool_metadata_json
8113
- FROM messages_fts
8114
- JOIN messages m ON m.rowid = messages_fts.rowid
8115
- WHERE messages_fts MATCH ?
8116
- AND (${clauses.join(" OR ")})
8117
- ORDER BY m.message_index
8118
- `
8119
- ).all(...params);
8120
- const matches = /* @__PURE__ */ new Map();
8121
- for (const message of messageRows) {
8122
- const key = searchResultRowKey(message);
8123
- if (matches.has(key)) continue;
8124
- const text = String(message.content_text ?? "");
8125
- if (!textMatchesTerms(text, terms)) continue;
8126
- matches.set(key, {
8127
- snippet: buildTermSnippet(text, terms),
8128
- matchType: messageMatchType(message)
7764
+ function searchFileActivitySessions(query, options = {}) {
7765
+ const search = mergeSearchQueryOptions(query, options);
7766
+ const path2 = normalizeFilePathSearch(search.options.file ?? search.text);
7767
+ if (!path2) return [];
7768
+ const rows = listFileActivity({
7769
+ agent: search.options.agent,
7770
+ projectKey: search.options.projectKey,
7771
+ project: search.options.project,
7772
+ cwd: search.options.cwd,
7773
+ path: path2,
7774
+ kind: search.options.fileKind,
7775
+ from: search.options.from,
7776
+ to: search.options.to,
7777
+ limit: (search.options.limit ?? 50) * 3
7778
+ });
7779
+ const seen = /* @__PURE__ */ new Set();
7780
+ const results = [];
7781
+ for (const row of rows) {
7782
+ const key = `${row.agent_name}/${row.session_id}`;
7783
+ if (seen.has(key)) continue;
7784
+ if (!sessionMatchesSearchCost(row.session, search.options)) continue;
7785
+ seen.add(key);
7786
+ results.push({
7787
+ agentName: row.agent_name,
7788
+ session: row.session,
7789
+ snippet: `${row.kind} ${highlightFilePath(row.path, path2)} \xB7 ${row.count} events`,
7790
+ matchType: "file_path"
8129
7791
  });
7792
+ if (results.length >= (search.options.limit ?? 50)) break;
8130
7793
  }
8131
- return matches;
7794
+ return results;
8132
7795
  }
8133
- function resolveSearchMatch(row, terms, messageMatches) {
8134
- const title = String(row.title ?? "");
8135
- if (terms.terms.length === 0) {
8136
- return {
8137
- snippet: `Recent session \xB7 ${String(row.directory ?? "")}`,
8138
- matchType: "recent"
8139
- };
7796
+ var CACHE_INITIALIZATION_VERSION = "session-cache-v2";
7797
+ function deleteLegacyCacheFile() {
7798
+ const legacyPath = getLegacyCachePath();
7799
+ if (!existsSync12(legacyPath)) {
7800
+ return;
8140
7801
  }
8141
- if (textMatchesTerms(title, terms)) {
8142
- return { snippet: buildTermSnippet(title, terms), matchType: "title" };
7802
+ try {
7803
+ unlinkSync(legacyPath);
7804
+ } catch {
8143
7805
  }
8144
- const messageMatch = messageMatches.get(searchResultRowKey(row));
8145
- if (messageMatch) {
8146
- return messageMatch;
7806
+ }
7807
+ function loadCachedSessions(agentName) {
7808
+ if (!hasCacheStorage()) {
7809
+ return null;
8147
7810
  }
8148
- return {
8149
- snippet: String(row.snippet ?? ""),
8150
- matchType: "assistant_reply"
8151
- };
7811
+ return withCacheDb((db) => {
7812
+ const timestampRow = db.prepare("SELECT timestamp AS value FROM agent_cache WHERE agent_name = ?").get(agentName);
7813
+ const timestamp = Number(timestampRow?.value ?? 0);
7814
+ if (!timestamp) {
7815
+ return null;
7816
+ }
7817
+ const rows = db.prepare(
7818
+ `
7819
+ SELECT
7820
+ session_id,
7821
+ sort_index,
7822
+ slug,
7823
+ title,
7824
+ source_path,
7825
+ directory,
7826
+ project_identity_kind,
7827
+ project_identity_key,
7828
+ project_display_name,
7829
+ time_created,
7830
+ time_updated,
7831
+ message_count,
7832
+ total_input_tokens,
7833
+ total_output_tokens,
7834
+ total_cache_read_tokens,
7835
+ total_cache_create_tokens,
7836
+ total_cost,
7837
+ cost_source,
7838
+ total_tokens,
7839
+ model_usage_json,
7840
+ smart_tags_json,
7841
+ smart_tags_source_updated_at,
7842
+ meta_json
7843
+ FROM sessions
7844
+ WHERE agent_name = ?
7845
+ ORDER BY sort_index, activity_time DESC
7846
+ `
7847
+ ).all(agentName);
7848
+ const sessions = [];
7849
+ const meta = {};
7850
+ for (const row of rows) {
7851
+ const session = sessionFromRow(row);
7852
+ sessions.push(session);
7853
+ if (row.meta_json) {
7854
+ meta[session.id] = JSON.parse(row.meta_json);
7855
+ }
7856
+ }
7857
+ return { sessions, meta, timestamp };
7858
+ });
8152
7859
  }
8153
- function rowsToSearchResults(db, rows, textQuery, ftsQuery = toFtsQuery(textQuery)) {
8154
- const terms = parseTextTerms(textQuery);
8155
- const messageMatches = terms.terms.length > 0 && ftsQuery ? fetchMessageSearchMatches(db, rows, ftsQuery, terms) : /* @__PURE__ */ new Map();
8156
- return rows.map((row) => {
8157
- const match = resolveSearchMatch(row, terms, messageMatches);
8158
- return {
8159
- agentName: String(row.agent_name),
8160
- session: sessionHeadFromSearchRow(row),
8161
- snippet: match.snippet,
8162
- matchType: match.matchType
8163
- };
7860
+ function isAgentCacheInitialized(agentName, indexVersion = CACHE_INITIALIZATION_VERSION) {
7861
+ if (!hasCacheStorage()) {
7862
+ return false;
7863
+ }
7864
+ return withCacheDbReadOnly((db) => {
7865
+ if (!tableExists(db, "cache_initialization")) return false;
7866
+ const row = db.prepare(
7867
+ `
7868
+ SELECT index_version
7869
+ FROM cache_initialization
7870
+ WHERE agent_name = ?
7871
+ `
7872
+ ).get(agentName);
7873
+ return row?.index_version === indexVersion;
7874
+ }) ?? false;
7875
+ }
7876
+ function markAgentCacheInitialized(agentName, indexVersion = CACHE_INITIALIZATION_VERSION) {
7877
+ withCacheDb((db) => {
7878
+ const now = Date.now();
7879
+ db.prepare(
7880
+ `
7881
+ INSERT INTO cache_initialization(agent_name, initialized_at, index_version, last_sync_at)
7882
+ VALUES (?, ?, ?, ?)
7883
+ ON CONFLICT(agent_name) DO UPDATE SET
7884
+ index_version = excluded.index_version,
7885
+ last_sync_at = excluded.last_sync_at
7886
+ `
7887
+ ).run(agentName, now, indexVersion, now);
8164
7888
  });
8165
7889
  }
8166
- function searchSessions(query, options = {}) {
8167
- const search = mergeSearchQueryOptions(query, options);
8168
- const normalizedQuery = search.text.trim();
7890
+ function loadCachedSessionData(agentName, sessionId) {
8169
7891
  if (!hasCacheStorage()) {
8170
- return [];
7892
+ return null;
8171
7893
  }
8172
- const results = withCacheDb((db) => {
8173
- ensureFtsReady(db);
8174
- const filters = buildSessionSearchFilters(search.options);
8175
- if (!normalizedQuery) {
8176
- const rows2 = db.prepare(
7894
+ return withCacheDbReadOnly((db) => {
7895
+ const row = db.prepare(
7896
+ `
7897
+ SELECT
7898
+ session_id,
7899
+ sort_index,
7900
+ slug,
7901
+ title,
7902
+ source_path,
7903
+ directory,
7904
+ project_identity_kind,
7905
+ project_identity_key,
7906
+ project_display_name,
7907
+ time_created,
7908
+ time_updated,
7909
+ message_count,
7910
+ total_input_tokens,
7911
+ total_output_tokens,
7912
+ total_cache_read_tokens,
7913
+ total_cache_create_tokens,
7914
+ total_cost,
7915
+ cost_source,
7916
+ total_tokens,
7917
+ model_usage_json,
7918
+ smart_tags_json,
7919
+ smart_tags_source_updated_at,
7920
+ meta_json
7921
+ FROM sessions
7922
+ WHERE agent_name = ? AND session_id = ?
8177
7923
  `
8178
- SELECT
8179
- ${searchSessionColumns()},
8180
- '' AS snippet
8181
- FROM sessions s
8182
- WHERE 1 = 1
8183
- ${filters.where}
8184
- ORDER BY s.activity_time DESC
8185
- LIMIT ?
8186
- `
8187
- ).all(...filters.params, search.options.limit ?? 50);
8188
- return rowsToSearchResults(db, rows2, "");
7924
+ ).get(agentName, sessionId);
7925
+ if (!row) {
7926
+ return null;
8189
7927
  }
8190
- const ftsQuery = toFtsQuery(normalizedQuery);
8191
- if (!ftsQuery) return [];
8192
- const rows = db.prepare(
7928
+ const messageRows = db.prepare(
8193
7929
  `
8194
7930
  SELECT
8195
- ${searchSessionColumns()},
8196
- COALESCE(
8197
- NULLIF(snippet(session_documents_fts, 1, '<mark>', '</mark>', ' \u2026 ', 18), ''),
8198
- highlight(session_documents_fts, 0, '<mark>', '</mark>')
8199
- ) AS snippet
8200
- FROM session_documents_fts
8201
- JOIN session_documents d ON d.id = session_documents_fts.rowid
8202
- JOIN sessions s ON s.agent_name = d.agent_name AND s.session_id = d.session_id
8203
- WHERE session_documents_fts MATCH ?
8204
- ${filters.where}
8205
- ORDER BY bm25(session_documents_fts, 8.0, 1.0), s.activity_time DESC
8206
- LIMIT ?
7931
+ message_id,
7932
+ role,
7933
+ time_created,
7934
+ time_completed,
7935
+ agent,
7936
+ mode,
7937
+ model,
7938
+ provider,
7939
+ tokens_json,
7940
+ cost,
7941
+ cost_source,
7942
+ parts_json,
7943
+ subagent_id,
7944
+ nickname
7945
+ FROM messages
7946
+ WHERE agent_name = ? AND session_id = ?
7947
+ ORDER BY message_index
8207
7948
  `
8208
- ).all(ftsQuery, ...filters.params, search.options.limit ?? 50);
8209
- return rowsToSearchResults(db, rows, normalizedQuery, ftsQuery);
7949
+ ).all(agentName, sessionId);
7950
+ const head = sessionFromRow(row);
7951
+ const fileActivityRows = db.prepare(
7952
+ `
7953
+ SELECT agent_name, session_id, project_identity_key, path, kind, count, latest_time
7954
+ FROM session_file_activity
7955
+ WHERE agent_name = ? AND session_id = ?
7956
+ ORDER BY latest_time DESC, count DESC, path
7957
+ LIMIT 500
7958
+ `
7959
+ ).all(agentName, sessionId);
7960
+ return {
7961
+ ...head,
7962
+ messages: messageRows.map((messageRow) => messageFromCachedRow(messageRow)),
7963
+ file_activity: fileActivityRows.map((activityRow) => fileActivityFromRow(activityRow))
7964
+ };
8210
7965
  });
8211
- return results ?? [];
8212
- }
8213
- function normalizeFilePathSearch(value) {
8214
- return value.trim().replace(/^"|"$/g, "");
8215
- }
8216
- function fileActivityFilters(options) {
8217
- const path2 = options.path ? normalizeFilePathSearch(options.path) : "";
8218
- return {
8219
- projectKey: options.projectKey ?? null,
8220
- projectLike: options.project ? likePattern(options.project) : null,
8221
- cwdKey: options.cwd ? computeIdentity(options.cwd, realFs).key : null,
8222
- cwdLike: options.cwd ? likePattern(options.cwd) : null,
8223
- path: path2,
8224
- pathLike: path2 ? likePattern(path2) : null
8225
- };
8226
7966
  }
8227
- function fileActivityFromRow(row) {
8228
- return {
8229
- agent_name: String(row.agent_name),
8230
- session_id: String(row.session_id),
8231
- project_identity_key: String(row.project_identity_key ?? ""),
8232
- path: String(row.path ?? ""),
8233
- kind: row.kind ?? "read",
8234
- count: Number(row.count ?? 0),
8235
- latest_time: Number(row.latest_time ?? 0)
8236
- };
7967
+ function saveCachedSessions(agentName, sessions, meta = {}) {
7968
+ withCacheDb((db) => {
7969
+ const deleteAgent = db.prepare("DELETE FROM agent_cache WHERE agent_name = ?");
7970
+ const deleteLegacySessions = db.prepare("DELETE FROM cached_sessions WHERE agent_name = ?");
7971
+ const deleteSession = db.prepare(
7972
+ "DELETE FROM sessions WHERE agent_name = ? AND session_id = ?"
7973
+ );
7974
+ const deleteSearchDocument = db.prepare(
7975
+ "DELETE FROM session_documents WHERE agent_name = ? AND session_id = ?"
7976
+ );
7977
+ const deleteMessages = db.prepare(
7978
+ "DELETE FROM messages WHERE agent_name = ? AND session_id = ?"
7979
+ );
7980
+ const deleteMessageTools = db.prepare(
7981
+ "DELETE FROM message_tools WHERE agent_name = ? AND session_id = ?"
7982
+ );
7983
+ const deleteFileActivity = db.prepare(
7984
+ "DELETE FROM session_file_activity WHERE agent_name = ? AND session_id = ?"
7985
+ );
7986
+ const deleteProjectSession = db.prepare(
7987
+ "DELETE FROM project_sessions WHERE agent_name = ? AND session_id = ?"
7988
+ );
7989
+ const deleteProjectSessions = db.prepare("DELETE FROM project_sessions WHERE agent_name = ?");
7990
+ const upsertAgent = db.prepare(`
7991
+ INSERT INTO agent_cache(agent_name, timestamp)
7992
+ VALUES (?, ?)
7993
+ ON CONFLICT(agent_name) DO UPDATE SET timestamp = excluded.timestamp
7994
+ `);
7995
+ const upsertCachedSession = prepareUpsertCachedSession(db);
7996
+ const upsertSession = prepareUpsertSession(db);
7997
+ const upsertProjectSession = prepareUpsertProjectSession(db);
7998
+ const write = db.transaction(() => {
7999
+ const timestamp = Date.now();
8000
+ const sessionIds = new Set(sessions.map((session) => session.id));
8001
+ const existingSessionIds = db.prepare("SELECT session_id FROM sessions WHERE agent_name = ?").all(agentName);
8002
+ deleteAgent.run(agentName);
8003
+ deleteLegacySessions.run(agentName);
8004
+ deleteProjectSessions.run(agentName);
8005
+ upsertAgent.run(agentName, timestamp);
8006
+ for (const row of existingSessionIds) {
8007
+ const sessionId = String(row.session_id);
8008
+ if (!sessionIds.has(sessionId)) {
8009
+ deleteSearchDocument.run(agentName, sessionId);
8010
+ deleteMessageTools.run(agentName, sessionId);
8011
+ deleteMessages.run(agentName, sessionId);
8012
+ deleteFileActivity.run(agentName, sessionId);
8013
+ deleteProjectSession.run(agentName, sessionId);
8014
+ deleteSession.run(agentName, sessionId);
8015
+ }
8016
+ }
8017
+ sessions.forEach((session, index) => {
8018
+ const identity = session.project_identity ?? computeIdentity(session.directory, realFs);
8019
+ const sessionMeta = meta[session.id];
8020
+ const metaJson = sessionMeta ? JSON.stringify(sessionMeta) : null;
8021
+ upsertCachedSession.run(agentName, session.id, JSON.stringify(session), metaJson);
8022
+ upsertSessionRow(
8023
+ upsertSession,
8024
+ agentName,
8025
+ session,
8026
+ metaJson,
8027
+ index,
8028
+ sourcePathFromMeta(sessionMeta)
8029
+ );
8030
+ writeProjectSessionRow(upsertProjectSession, agentName, session, identity);
8031
+ });
8032
+ });
8033
+ write();
8034
+ deleteLegacyCacheFile();
8035
+ });
8237
8036
  }
8238
- function buildFileActivityWhere(options) {
8239
- const filters = fileActivityFilters(options);
8240
- const clauses = [];
8241
- const params = [];
8242
- if (options.agent != null) {
8243
- clauses.push("fa.agent_name = ?");
8244
- params.push(options.agent);
8245
- }
8246
- if (options.sessionId != null) {
8247
- clauses.push("fa.session_id = ?");
8248
- params.push(options.sessionId);
8249
- }
8250
- if (filters.projectKey != null) {
8251
- clauses.push("fa.project_identity_key = ?");
8252
- params.push(filters.projectKey);
8037
+ function saveCachedSessionChanges(agentName, changes, removedSessionIds, meta = {}) {
8038
+ if (changes.length === 0 && removedSessionIds.length === 0) {
8039
+ return;
8253
8040
  }
8254
- if (filters.projectLike != null) {
8255
- clauses.push(
8256
- "(LOWER(fa.project_identity_key) LIKE ? ESCAPE '\\' OR LOWER(s.project_display_name) LIKE ? ESCAPE '\\' OR LOWER(s.directory) LIKE ? ESCAPE '\\')"
8041
+ withCacheDb((db) => {
8042
+ const deleteLegacySession = db.prepare(
8043
+ "DELETE FROM cached_sessions WHERE agent_name = ? AND session_id = ?"
8257
8044
  );
8258
- params.push(filters.projectLike, filters.projectLike, filters.projectLike);
8259
- }
8260
- if (filters.cwdKey != null) {
8261
- clauses.push("(s.project_identity_key = ? OR LOWER(s.directory) LIKE ? ESCAPE '\\')");
8262
- params.push(filters.cwdKey, filters.cwdLike);
8263
- }
8264
- if (filters.pathLike != null) {
8265
- const pathQuery = filePathFtsQuery(filters.path);
8266
- if (pathQuery) {
8267
- clauses.push(
8268
- "fa.rowid IN (SELECT rowid FROM session_file_activity_path_fts WHERE path MATCH ?)"
8269
- );
8270
- params.push(pathQuery);
8271
- } else {
8272
- clauses.push("LOWER(fa.path) LIKE ? ESCAPE '\\'");
8273
- params.push(filters.pathLike);
8274
- }
8275
- }
8276
- if (options.kind != null) {
8277
- clauses.push("fa.kind = ?");
8278
- params.push(options.kind);
8279
- }
8280
- if (options.from != null) {
8281
- clauses.push("fa.latest_time >= ?");
8282
- params.push(options.from);
8283
- }
8284
- if (options.to != null) {
8285
- clauses.push("fa.latest_time <= ?");
8286
- params.push(options.to);
8287
- }
8288
- return {
8289
- where: clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "",
8290
- params
8291
- };
8045
+ const deleteSession = db.prepare(
8046
+ "DELETE FROM sessions WHERE agent_name = ? AND session_id = ?"
8047
+ );
8048
+ const deleteSearchDocument = db.prepare(
8049
+ "DELETE FROM session_documents WHERE agent_name = ? AND session_id = ?"
8050
+ );
8051
+ const deleteMessages = db.prepare(
8052
+ "DELETE FROM messages WHERE agent_name = ? AND session_id = ?"
8053
+ );
8054
+ const deleteMessageTools = db.prepare(
8055
+ "DELETE FROM message_tools WHERE agent_name = ? AND session_id = ?"
8056
+ );
8057
+ const deleteFileActivity = db.prepare(
8058
+ "DELETE FROM session_file_activity WHERE agent_name = ? AND session_id = ?"
8059
+ );
8060
+ const deleteProjectSession = db.prepare(
8061
+ "DELETE FROM project_sessions WHERE agent_name = ? AND session_id = ?"
8062
+ );
8063
+ const upsertAgent = db.prepare(`
8064
+ INSERT INTO agent_cache(agent_name, timestamp)
8065
+ VALUES (?, ?)
8066
+ ON CONFLICT(agent_name) DO UPDATE SET timestamp = excluded.timestamp
8067
+ `);
8068
+ const upsertCachedSession = prepareUpsertCachedSession(db);
8069
+ const upsertSession = prepareUpsertSession(db);
8070
+ const upsertProjectSession = prepareUpsertProjectSession(db);
8071
+ const write = db.transaction(() => {
8072
+ upsertAgent.run(agentName, Date.now());
8073
+ for (const sessionId of new Set(removedSessionIds)) {
8074
+ deleteLegacySession.run(agentName, sessionId);
8075
+ deleteSearchDocument.run(agentName, sessionId);
8076
+ deleteMessageTools.run(agentName, sessionId);
8077
+ deleteMessages.run(agentName, sessionId);
8078
+ deleteFileActivity.run(agentName, sessionId);
8079
+ deleteProjectSession.run(agentName, sessionId);
8080
+ deleteSession.run(agentName, sessionId);
8081
+ }
8082
+ for (const { session, sortIndex } of changes) {
8083
+ const identity = session.project_identity ?? computeIdentity(session.directory, realFs);
8084
+ const sessionMeta = meta[session.id];
8085
+ const metaJson = sessionMeta ? JSON.stringify(sessionMeta) : null;
8086
+ upsertCachedSession.run(agentName, session.id, JSON.stringify(session), metaJson);
8087
+ upsertSessionRow(
8088
+ upsertSession,
8089
+ agentName,
8090
+ session,
8091
+ metaJson,
8092
+ sortIndex,
8093
+ sourcePathFromMeta(sessionMeta)
8094
+ );
8095
+ writeProjectSessionRow(upsertProjectSession, agentName, session, identity);
8096
+ }
8097
+ });
8098
+ write();
8099
+ deleteLegacyCacheFile();
8100
+ });
8292
8101
  }
8293
- function listFileActivity(options = {}) {
8102
+ function clearCache() {
8103
+ setFtsIntegrityCheckedPath(null);
8294
8104
  if (!hasCacheStorage()) {
8295
- return [];
8105
+ deleteLegacyCacheFile();
8106
+ return;
8296
8107
  }
8297
- const filters = buildFileActivityWhere(options);
8298
- const queryRows = (db) => db.prepare(
8299
- `
8300
- SELECT
8301
- fa.agent_name,
8302
- fa.session_id,
8303
- fa.project_identity_key,
8304
- fa.path,
8305
- fa.kind,
8306
- fa.count,
8307
- fa.latest_time,
8308
- s.slug,
8309
- s.title,
8310
- s.directory,
8311
- s.project_identity_kind,
8312
- s.project_display_name,
8313
- s.time_created,
8314
- s.time_updated,
8315
- s.message_count,
8316
- s.total_input_tokens,
8317
- s.total_output_tokens,
8318
- s.total_cache_read_tokens,
8319
- s.total_cache_create_tokens,
8320
- s.total_cost,
8321
- s.cost_source,
8322
- s.total_tokens
8323
- FROM session_file_activity fa
8324
- JOIN sessions s ON s.agent_name = fa.agent_name AND s.session_id = fa.session_id
8325
- ${filters.where}
8326
- ORDER BY fa.latest_time DESC, fa.count DESC, fa.path
8327
- LIMIT ?
8328
- `
8329
- ).all(...filters.params, options.limit ?? 50);
8330
- let rows = withCacheDbReadOnly(queryRows);
8331
- if (rows == null && options.path) {
8332
- rows = withCacheDb(queryRows);
8108
+ withCacheDb((db) => {
8109
+ db.exec(`
8110
+ DELETE FROM agent_cache;
8111
+ DELETE FROM cache_initialization;
8112
+ DELETE FROM cached_sessions;
8113
+ DELETE FROM session_documents;
8114
+ DELETE FROM session_file_activity;
8115
+ DELETE FROM message_tools;
8116
+ DELETE FROM messages;
8117
+ DELETE FROM sessions;
8118
+ DELETE FROM project_sessions;
8119
+ `);
8120
+ });
8121
+ deleteLegacyCacheFile();
8122
+ const cachePath = getCachePath2();
8123
+ const walPath = `${cachePath}-wal`;
8124
+ const shmPath = `${cachePath}-shm`;
8125
+ for (const filePath of [walPath, shmPath]) {
8126
+ if (!existsSync12(filePath)) {
8127
+ continue;
8128
+ }
8129
+ try {
8130
+ rmSync(filePath, { force: true });
8131
+ } catch {
8132
+ }
8333
8133
  }
8334
- return (rows ?? []).map((row) => ({
8335
- ...fileActivityFromRow(row),
8336
- session: sessionHeadFromSearchRow(row)
8337
- }));
8338
- }
8339
- function listSessionFileActivity(agentName, sessionId) {
8340
- return listFileActivity({ agent: agentName, sessionId, limit: 500 }).map(
8341
- ({ session: _session, ...activity }) => activity
8342
- );
8343
- }
8344
- function highlightFilePath(path2, query) {
8345
- const needle = normalizeFilePathSearch(query);
8346
- if (!needle) return path2;
8347
- const lower = path2.toLowerCase();
8348
- const index = lower.indexOf(needle.toLowerCase());
8349
- if (index < 0) return path2;
8350
- return `${path2.slice(0, index)}<mark>${path2.slice(index, index + needle.length)}</mark>${path2.slice(
8351
- index + needle.length
8352
- )}`;
8353
8134
  }
8354
- function searchFileActivitySessions(query, options = {}) {
8355
- const search = mergeSearchQueryOptions(query, options);
8356
- const path2 = normalizeFilePathSearch(search.options.file ?? search.text);
8357
- if (!path2) return [];
8358
- const rows = listFileActivity({
8359
- agent: search.options.agent,
8360
- projectKey: search.options.projectKey,
8361
- project: search.options.project,
8362
- cwd: search.options.cwd,
8363
- path: path2,
8364
- kind: search.options.fileKind,
8365
- from: search.options.from,
8366
- to: search.options.to,
8367
- limit: (search.options.limit ?? 50) * 3
8368
- });
8369
- const seen = /* @__PURE__ */ new Set();
8370
- const results = [];
8371
- for (const row of rows) {
8372
- const key = `${row.agent_name}/${row.session_id}`;
8373
- if (seen.has(key)) continue;
8374
- if (!sessionMatchesSearchCost(row.session, search.options)) continue;
8375
- seen.add(key);
8376
- results.push({
8377
- agentName: row.agent_name,
8378
- session: row.session,
8379
- snippet: `${row.kind} ${highlightFilePath(row.path, path2)} \xB7 ${row.count} events`,
8380
- matchType: "file_path"
8381
- });
8382
- if (results.length >= (search.options.limit ?? 50)) break;
8135
+ function getCacheInfo() {
8136
+ if (!hasCacheStorage()) {
8137
+ return { lastScanTime: null, size: 0 };
8383
8138
  }
8384
- return results;
8139
+ const info = withCacheDb((db) => {
8140
+ const timestampRow = db.prepare("SELECT MAX(timestamp) AS value FROM agent_cache").get();
8141
+ const sizeRow = db.prepare("SELECT COUNT(*) AS value FROM sessions").get();
8142
+ const lastScanTime = Number(timestampRow?.value ?? 0) || null;
8143
+ const size = Number(sizeRow?.value ?? 0);
8144
+ return { lastScanTime, size };
8145
+ });
8146
+ return info ?? { lastScanTime: null, size: 0 };
8385
8147
  }
8386
8148
  function listCachedProjectGroups(sessions) {
8387
8149
  if (sessions) {
@@ -8423,57 +8185,89 @@ function createIdentityResolver() {
8423
8185
  return identity;
8424
8186
  };
8425
8187
  }
8426
- function attachProjectIdentities(sessions) {
8188
+ function attachMissingProjectIdentities(sessions) {
8427
8189
  const resolveIdentity = createIdentityResolver();
8428
8190
  return sessions.map((session) => {
8429
8191
  if (session.project_identity) return session;
8430
- return {
8431
- ...session,
8432
- project_identity: resolveIdentity(session.directory)
8433
- };
8192
+ return { ...session, project_identity: resolveIdentity(session.directory) };
8434
8193
  });
8435
8194
  }
8436
- function filterSessions(sessions, options) {
8437
- let result = sessions;
8438
- if (options.cwd) {
8439
- result = filterSessionsByProjectScope(result, options.cwd);
8440
- }
8441
- if (options.from != null) {
8442
- result = result.filter((s) => (s.time_updated ?? s.time_created) >= options.from);
8443
- }
8444
- if (options.to != null) {
8445
- result = result.filter((s) => (s.time_updated ?? s.time_created) <= options.to);
8446
- }
8447
- return result;
8448
- }
8449
- function buildAgentCacheMeta(agent) {
8195
+ function buildAgentCacheMeta(agent, sessionIds) {
8450
8196
  const metaMap = agent.getSessionMetaMap?.();
8451
8197
  const meta = {};
8452
8198
  if (!metaMap) return meta;
8453
8199
  for (const [id, data] of metaMap.entries()) {
8200
+ if (sessionIds && !sessionIds.has(id)) continue;
8454
8201
  meta[id] = { id, ...data };
8455
8202
  }
8456
8203
  return meta;
8457
8204
  }
8458
- function sessionCacheValue(session) {
8459
- return JSON.stringify(session);
8205
+ function sessionSignature(session) {
8206
+ return JSON.stringify([
8207
+ session.title,
8208
+ session.directory,
8209
+ session.time_created,
8210
+ session.time_updated ?? session.time_created,
8211
+ session.stats.message_count,
8212
+ session.stats.total_input_tokens,
8213
+ session.stats.total_output_tokens,
8214
+ session.stats.total_cost,
8215
+ session.stats.total_tokens ?? 0,
8216
+ session.smart_tags_source_updated_at ?? null
8217
+ ]);
8218
+ }
8219
+ function sortSessions(sessions) {
8220
+ return [...sessions].sort(
8221
+ (a, b) => (b.time_updated ?? b.time_created) - (a.time_updated ?? a.time_created)
8222
+ );
8460
8223
  }
8461
- function buildCacheChanges(cachedSessions, updatedSessions, changedIds = []) {
8224
+ function computeSessionDiff(cachedSessions, updatedSessions, changedIds = [], signature = sessionSignature) {
8462
8225
  const cachedMap = new Map(cachedSessions.map((session) => [session.id, session]));
8463
8226
  const updatedIds = new Set(updatedSessions.map((session) => session.id));
8464
8227
  const changedIdSet = new Set(changedIds);
8465
- const removedSessionIds = cachedSessions.filter((session) => !updatedIds.has(session.id)).map((session) => session.id);
8466
8228
  const changes = [];
8229
+ const removedSessionIds = [];
8230
+ let newCount = 0;
8231
+ let updatedCount = 0;
8467
8232
  updatedSessions.forEach((session, sortIndex) => {
8468
8233
  const cached = cachedMap.get(session.id);
8469
- if (!cached || changedIdSet.has(session.id) || cached !== session && sessionCacheValue(cached) !== sessionCacheValue(session)) {
8234
+ if (!cached) {
8235
+ newCount += 1;
8236
+ changes.push({ session, sortIndex });
8237
+ return;
8238
+ }
8239
+ const hasSignatureChange = signature(cached) !== signature(session);
8240
+ if (changedIdSet.has(session.id) || hasSignatureChange) {
8241
+ updatedCount += 1;
8470
8242
  changes.push({ session, sortIndex });
8471
8243
  }
8472
8244
  });
8473
- return { changes, removedSessionIds };
8245
+ for (const session of cachedSessions) {
8246
+ if (!updatedIds.has(session.id)) {
8247
+ removedSessionIds.push(session.id);
8248
+ }
8249
+ }
8250
+ return {
8251
+ changes,
8252
+ removedSessionIds,
8253
+ counts: { new: newCount, updated: updatedCount, removed: removedSessionIds.length }
8254
+ };
8255
+ }
8256
+ function filterSessions(sessions, options) {
8257
+ let result = sessions;
8258
+ if (options.cwd) {
8259
+ result = filterSessionsByProjectScope(result, options.cwd);
8260
+ }
8261
+ if (options.from != null) {
8262
+ result = result.filter((s) => (s.time_updated ?? s.time_created) >= options.from);
8263
+ }
8264
+ if (options.to != null) {
8265
+ result = result.filter((s) => (s.time_updated ?? s.time_created) <= options.to);
8266
+ }
8267
+ return result;
8474
8268
  }
8475
8269
  function saveCachedSessionDiff(agent, cachedSessions, updatedSessions, changedIds = []) {
8476
- const diff = buildCacheChanges(cachedSessions, updatedSessions, changedIds);
8270
+ const diff = computeSessionDiff(cachedSessions, updatedSessions, changedIds, sessionSignature);
8477
8271
  saveCachedSessionChanges(
8478
8272
  agent.name,
8479
8273
  diff.changes,
@@ -8573,19 +8367,16 @@ async function scanAgentSmart(agent, options, onProgress) {
8573
8367
  const agentStart = performance.now();
8574
8368
  const timing = { total: 0 };
8575
8369
  const useCache = options.useCache ?? true;
8576
- const canValidateCache = Boolean(agent.checkForChanges && agent.incrementalScan);
8577
8370
  if (useCache) {
8578
8371
  const t0 = performance.now();
8579
8372
  const cached = loadCachedSessions(agent.name);
8580
8373
  timing.cacheLoad = performance.now() - t0;
8581
8374
  if (cached !== null) {
8582
- if (agent.setSessionMetaMap) {
8583
- const metaMap = /* @__PURE__ */ new Map();
8584
- for (const [id, meta] of Object.entries(cached.meta)) {
8585
- metaMap.set(id, meta);
8586
- }
8587
- agent.setSessionMetaMap(metaMap);
8375
+ const metaMap = /* @__PURE__ */ new Map();
8376
+ for (const [id, meta] of Object.entries(cached.meta)) {
8377
+ metaMap.set(id, meta);
8588
8378
  }
8379
+ agent.setSessionMetaMap(metaMap);
8589
8380
  if (options.cacheOnly) {
8590
8381
  onProgress?.({
8591
8382
  agent: agent.name,
@@ -8594,7 +8385,7 @@ async function scanAgentSmart(agent, options, onProgress) {
8594
8385
  });
8595
8386
  onProgress?.({ agent: agent.name, phase: "complete", newCount: cached.sessions.length });
8596
8387
  const t32 = performance.now();
8597
- const cachedWithIdentity2 = attachProjectIdentities(cached.sessions);
8388
+ const cachedWithIdentity2 = attachMissingProjectIdentities(cached.sessions);
8598
8389
  timing.identity = performance.now() - t32;
8599
8390
  const filtered3 = filterSessions(cachedWithIdentity2, options);
8600
8391
  timing.total = performance.now() - agentStart;
@@ -8615,58 +8406,56 @@ async function scanAgentSmart(agent, options, onProgress) {
8615
8406
  phase: "cache",
8616
8407
  cachedCount: cached.sessions.length
8617
8408
  });
8618
- if (canValidateCache) {
8619
- onProgress?.({ agent: agent.name, phase: "checking" });
8620
- const t1 = performance.now();
8621
- const checkResult = await Promise.resolve(
8622
- agent.checkForChanges(cached.timestamp, cached.sessions)
8409
+ onProgress?.({ agent: agent.name, phase: "checking" });
8410
+ const t1 = performance.now();
8411
+ const checkResult = await Promise.resolve(
8412
+ agent.checkForChanges(cached.timestamp, cached.sessions)
8413
+ );
8414
+ timing.checkChanges = performance.now() - t1;
8415
+ if (checkResult.hasChanges) {
8416
+ onProgress?.({
8417
+ agent: agent.name,
8418
+ phase: "incremental",
8419
+ changedCount: checkResult.changedIds?.length
8420
+ });
8421
+ const t2 = performance.now();
8422
+ const updatedSessions = await Promise.resolve(
8423
+ agent.incrementalScan(cached.sessions, checkResult.changedIds || [])
8623
8424
  );
8624
- timing.checkChanges = performance.now() - t1;
8625
- if (checkResult.hasChanges) {
8626
- onProgress?.({
8627
- agent: agent.name,
8628
- phase: "incremental",
8629
- changedCount: checkResult.changedIds?.length
8630
- });
8631
- const t2 = performance.now();
8632
- const updatedSessions = await Promise.resolve(
8633
- agent.incrementalScan(cached.sessions, checkResult.changedIds || [])
8634
- );
8635
- timing.scan = performance.now() - t2;
8636
- const t32 = performance.now();
8637
- const sessionsWithIdentity = attachProjectIdentities(updatedSessions);
8638
- timing.identity = performance.now() - t32;
8639
- const t42 = performance.now();
8640
- const tagged2 = options.includeSmartTags === false ? { sessions: sessionsWithIdentity, changed: false } : await ensureSessionTags(agent, sessionsWithIdentity, options.smartTagWorkerUrl);
8641
- timing.tags = performance.now() - t42;
8642
- if (options.writeCache !== false) {
8643
- saveCachedSessionDiff(
8644
- agent,
8645
- cached.sessions,
8646
- tagged2.sessions,
8647
- checkResult.changedIds ?? []
8648
- );
8649
- }
8650
- onProgress?.({
8651
- agent: agent.name,
8652
- phase: "complete",
8653
- newCount: tagged2.sessions.length
8654
- });
8655
- const filtered3 = filterSessions(tagged2.sessions, options);
8656
- timing.total = performance.now() - agentStart;
8657
- return {
8425
+ timing.scan = performance.now() - t2;
8426
+ const t32 = performance.now();
8427
+ const sessionsWithIdentity = attachMissingProjectIdentities(updatedSessions);
8428
+ timing.identity = performance.now() - t32;
8429
+ const t42 = performance.now();
8430
+ const tagged2 = options.includeSmartTags === false ? { sessions: sessionsWithIdentity, changed: false } : await ensureSessionTags(agent, sessionsWithIdentity, options.smartTagWorkerUrl);
8431
+ timing.tags = performance.now() - t42;
8432
+ if (options.writeCache !== false) {
8433
+ saveCachedSessionDiff(
8658
8434
  agent,
8659
- heads: filtered3,
8660
- fromCache: true,
8661
- refreshed: true,
8662
- timing,
8663
- cacheTimestamp: checkResult.timestamp
8664
- };
8435
+ cached.sessions,
8436
+ tagged2.sessions,
8437
+ checkResult.changedIds ?? []
8438
+ );
8665
8439
  }
8666
- onProgress?.({ agent: agent.name, phase: "complete", newCount: cached.sessions.length });
8440
+ onProgress?.({
8441
+ agent: agent.name,
8442
+ phase: "complete",
8443
+ newCount: tagged2.sessions.length
8444
+ });
8445
+ const filtered3 = filterSessions(tagged2.sessions, options);
8446
+ timing.total = performance.now() - agentStart;
8447
+ return {
8448
+ agent,
8449
+ heads: filtered3,
8450
+ fromCache: true,
8451
+ refreshed: true,
8452
+ timing,
8453
+ cacheTimestamp: checkResult.timestamp
8454
+ };
8667
8455
  }
8456
+ onProgress?.({ agent: agent.name, phase: "complete", newCount: cached.sessions.length });
8668
8457
  const t3 = performance.now();
8669
- const cachedWithIdentity = attachProjectIdentities(cached.sessions);
8458
+ const cachedWithIdentity = attachMissingProjectIdentities(cached.sessions);
8670
8459
  timing.identity = performance.now() - t3;
8671
8460
  const t4 = performance.now();
8672
8461
  const tagged = options.includeSmartTags === false ? { sessions: cachedWithIdentity, changed: false } : await ensureSessionTags(agent, cachedWithIdentity, options.smartTagWorkerUrl);
@@ -8718,7 +8507,7 @@ async function scanAgentFull(agent, options, onProgress, timing = { total: 0 },
8718
8507
  perf.end(scanMarker);
8719
8508
  timing.scan = performance.now() - t0;
8720
8509
  const t1 = performance.now();
8721
- const headsWithIdentity = attachProjectIdentities(heads);
8510
+ const headsWithIdentity = attachMissingProjectIdentities(heads);
8722
8511
  timing.identity = performance.now() - t1;
8723
8512
  const t2 = performance.now();
8724
8513
  const tagged = options.includeSmartTags === false ? { sessions: headsWithIdentity, changed: false } : await ensureSessionTags(agent, headsWithIdentity, options.smartTagWorkerUrl);
@@ -8795,16 +8584,16 @@ function getStateDir() {
8795
8584
  }
8796
8585
  const p = platform2();
8797
8586
  if (p === "darwin") {
8798
- return join11(homedir5(), "Library", "Application Support", "codesesh");
8587
+ return join12(homedir5(), "Library", "Application Support", "codesesh");
8799
8588
  }
8800
8589
  if (p === "win32") {
8801
8590
  const appData = process.env.APPDATA ?? process.env.LOCALAPPDATA;
8802
- return join11(appData ?? join11(homedir5(), "AppData", "Roaming"), "codesesh");
8591
+ return join12(appData ?? join12(homedir5(), "AppData", "Roaming"), "codesesh");
8803
8592
  }
8804
- return join11(process.env.XDG_DATA_HOME ?? join11(homedir5(), ".local", "share"), "codesesh");
8593
+ return join12(process.env.XDG_DATA_HOME ?? join12(homedir5(), ".local", "share"), "codesesh");
8805
8594
  }
8806
8595
  function getStateDbPath() {
8807
- return join11(getStateDir(), BOOKMARK_DB_FILENAME);
8596
+ return join12(getStateDir(), BOOKMARK_DB_FILENAME);
8808
8597
  }
8809
8598
  function useMemoryStateStore() {
8810
8599
  return process.env.CODESESH_STATE_STORE === MEMORY_STATE_STORE;
@@ -9090,6 +8879,161 @@ function deleteBookmark(agentKey, sessionId) {
9090
8879
  ).run(agentKey, sessionId);
9091
8880
  });
9092
8881
  }
8882
+ var DASHBOARD_RECENT_LIMIT = 10;
8883
+ function getTotalTokens(stats) {
8884
+ return stats.total_tokens ?? stats.total_input_tokens + stats.total_output_tokens;
8885
+ }
8886
+ function getSessionAgentName(session) {
8887
+ return session.slug.split("/")[0]?.toLowerCase() || "unknown";
8888
+ }
8889
+ function getSessionActivityTime(session) {
8890
+ return session.time_updated ?? session.time_created;
8891
+ }
8892
+ function toLocalDateKey(ts) {
8893
+ const d = new Date(ts);
8894
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(
8895
+ d.getDate()
8896
+ ).padStart(2, "0")}`;
8897
+ }
8898
+ function startOfLocalDay(ts) {
8899
+ const d = new Date(ts);
8900
+ d.setHours(0, 0, 0, 0);
8901
+ return d.getTime();
8902
+ }
8903
+ function buildDashboard(sessions, options) {
8904
+ const { byAgentNames, scope, from, to, agentInfoMap } = options;
8905
+ const agentMetrics = /* @__PURE__ */ new Map();
8906
+ const agentMetricKeyByName = /* @__PURE__ */ new Map();
8907
+ for (const name of byAgentNames) {
8908
+ if (scope.agent && name.toLowerCase() !== scope.agent) continue;
8909
+ agentMetrics.set(name, { sessions: 0, messages: 0, tokens: 0 });
8910
+ agentMetricKeyByName.set(name.toLowerCase(), name);
8911
+ }
8912
+ let totalSessions = 0;
8913
+ let totalMessages = 0;
8914
+ let totalTokens = 0;
8915
+ let totalCost = 0;
8916
+ let hasEstimatedCost = false;
8917
+ let latestActivity = 0;
8918
+ const recentCandidates = [];
8919
+ const modelAgg = /* @__PURE__ */ new Map();
8920
+ const dailyMap = /* @__PURE__ */ new Map();
8921
+ const dailyTokenMap = /* @__PURE__ */ new Map();
8922
+ if (from != null) {
8923
+ const bucketStart = startOfLocalDay(from);
8924
+ const bucketDays = Math.floor((startOfLocalDay(to) - bucketStart) / 864e5) + 1;
8925
+ for (let i = 0; i < bucketDays; i += 1) {
8926
+ const ts = bucketStart + i * 864e5;
8927
+ const key = toLocalDateKey(ts);
8928
+ dailyMap.set(key, { date: key, sessions: 0, messages: 0 });
8929
+ dailyTokenMap.set(key, { date: key, input: 0, output: 0, cache_read: 0, cache_create: 0 });
8930
+ }
8931
+ }
8932
+ for (const session of sessions) {
8933
+ const agentName = getSessionAgentName(session);
8934
+ if (scope.agent && agentName !== scope.agent) continue;
8935
+ if (scope.projectKey) {
8936
+ const identity = session.project_identity;
8937
+ if (!identity || identity.key !== scope.projectKey) continue;
8938
+ if (scope.projectKind && identity.kind !== scope.projectKind) continue;
8939
+ }
8940
+ const activity = getSessionActivityTime(session);
8941
+ if (from != null && activity < from) continue;
8942
+ if (activity > to) continue;
8943
+ const messageCount = session.stats.message_count;
8944
+ const sessionTokens = getTotalTokens(session.stats);
8945
+ totalSessions += 1;
8946
+ totalMessages += messageCount;
8947
+ totalTokens += sessionTokens;
8948
+ totalCost += session.stats.total_cost ?? 0;
8949
+ if (session.stats.cost_source === "estimated") hasEstimatedCost = true;
8950
+ if (activity > latestActivity) latestActivity = activity;
8951
+ const metricKey = agentMetricKeyByName.get(agentName);
8952
+ if (metricKey) {
8953
+ const metric = agentMetrics.get(metricKey);
8954
+ metric.sessions += 1;
8955
+ metric.messages += messageCount;
8956
+ metric.tokens += sessionTokens;
8957
+ }
8958
+ const key = toLocalDateKey(activity);
8959
+ let bucket = dailyMap.get(key);
8960
+ if (!bucket) {
8961
+ bucket = { date: key, sessions: 0, messages: 0 };
8962
+ dailyMap.set(key, bucket);
8963
+ }
8964
+ bucket.sessions += 1;
8965
+ bucket.messages += messageCount;
8966
+ let tokenBucket = dailyTokenMap.get(key);
8967
+ if (!tokenBucket) {
8968
+ tokenBucket = { date: key, input: 0, output: 0, cache_read: 0, cache_create: 0 };
8969
+ dailyTokenMap.set(key, tokenBucket);
8970
+ }
8971
+ const cacheRead = session.stats.total_cache_read_tokens ?? 0;
8972
+ const cacheCreate = session.stats.total_cache_create_tokens ?? 0;
8973
+ const pureInput = session.stats.total_input_tokens - cacheRead - cacheCreate;
8974
+ tokenBucket.input += Math.max(0, pureInput);
8975
+ tokenBucket.output += session.stats.total_output_tokens;
8976
+ tokenBucket.cache_read += cacheRead;
8977
+ tokenBucket.cache_create += cacheCreate;
8978
+ if (session.model_usage) {
8979
+ for (const [model, tokens] of Object.entries(session.model_usage)) {
8980
+ const entry = modelAgg.get(model);
8981
+ if (entry) {
8982
+ entry.tokens += tokens;
8983
+ entry.sessions += 1;
8984
+ } else {
8985
+ modelAgg.set(model, { tokens, sessions: 1 });
8986
+ }
8987
+ }
8988
+ }
8989
+ let recentIndex = recentCandidates.length;
8990
+ for (let i = 0; i < recentCandidates.length; i += 1) {
8991
+ if (activity > recentCandidates[i].activity) {
8992
+ recentIndex = i;
8993
+ break;
8994
+ }
8995
+ }
8996
+ if (recentIndex < DASHBOARD_RECENT_LIMIT) {
8997
+ recentCandidates.splice(recentIndex, 0, { session, activity });
8998
+ if (recentCandidates.length > DASHBOARD_RECENT_LIMIT) recentCandidates.pop();
8999
+ }
9000
+ }
9001
+ const perAgent = [...agentMetrics.entries()].map(([name, metrics]) => {
9002
+ const info = agentInfoMap?.get(name);
9003
+ return {
9004
+ name,
9005
+ displayName: info?.displayName ?? name,
9006
+ icon: info?.icon ?? "",
9007
+ sessions: metrics.sessions,
9008
+ messages: metrics.messages,
9009
+ tokens: metrics.tokens
9010
+ };
9011
+ }).filter((item) => item.sessions > 0).sort((a, b) => b.sessions - a.sessions);
9012
+ const dailyActivity = [...dailyMap.values()].sort((a, b) => a.date.localeCompare(b.date));
9013
+ const dailyTokenActivity = [...dailyTokenMap.values()].sort(
9014
+ (a, b) => a.date.localeCompare(b.date)
9015
+ );
9016
+ const modelDistribution = [...modelAgg.entries()].map(([model, { tokens, sessions: count }]) => ({ model, tokens, sessions: count })).sort((a, b) => b.tokens - a.tokens);
9017
+ const recentSessions = recentCandidates.map(({ session }) => {
9018
+ const agentKey = getSessionAgentName(session);
9019
+ return { ...session, agentName: agentKey };
9020
+ });
9021
+ return {
9022
+ totals: {
9023
+ sessions: totalSessions,
9024
+ messages: totalMessages,
9025
+ tokens: totalTokens,
9026
+ cost: totalCost,
9027
+ cost_source: totalCost > 0 ? hasEstimatedCost ? "estimated" : "recorded" : void 0,
9028
+ latestActivity: latestActivity || void 0
9029
+ },
9030
+ perAgent,
9031
+ dailyActivity,
9032
+ dailyTokenActivity,
9033
+ modelDistribution,
9034
+ recentSessions
9035
+ };
9036
+ }
9093
9037
 
9094
9038
  export {
9095
9039
  registerAgent,
@@ -9102,6 +9046,8 @@ export {
9102
9046
  filteredSession,
9103
9047
  getParsedSession,
9104
9048
  BaseAgent,
9049
+ FileSystemSessionSource,
9050
+ DatabaseSessionSource,
9105
9051
  firstExisting,
9106
9052
  resolveProviderRoots,
9107
9053
  getCursorDataPath,
@@ -9148,6 +9094,12 @@ export {
9148
9094
  summarizeFileActivity,
9149
9095
  extractSessionFileActivity,
9150
9096
  parseSearchQuery,
9097
+ syncSessionSearchIndex,
9098
+ syncSessionSearchIndexChanges,
9099
+ searchSessions,
9100
+ listFileActivity,
9101
+ listSessionFileActivity,
9102
+ searchFileActivitySessions,
9151
9103
  loadCachedSessions,
9152
9104
  isAgentCacheInitialized,
9153
9105
  markAgentCacheInitialized,
@@ -9156,13 +9108,12 @@ export {
9156
9108
  saveCachedSessionChanges,
9157
9109
  clearCache,
9158
9110
  getCacheInfo,
9159
- syncSessionSearchIndex,
9160
- syncSessionSearchIndexChanges,
9161
- searchSessions,
9162
- listFileActivity,
9163
- listSessionFileActivity,
9164
- searchFileActivitySessions,
9165
9111
  listCachedProjectGroups,
9112
+ attachMissingProjectIdentities,
9113
+ buildAgentCacheMeta,
9114
+ sessionSignature,
9115
+ sortSessions,
9116
+ computeSessionDiff,
9166
9117
  filterSessions,
9167
9118
  scanSessions,
9168
9119
  scanSessionsAsync,
@@ -9170,6 +9121,13 @@ export {
9170
9121
  listBookmarks,
9171
9122
  upsertBookmark,
9172
9123
  importBookmarks,
9173
- deleteBookmark
9124
+ deleteBookmark,
9125
+ DASHBOARD_RECENT_LIMIT,
9126
+ getTotalTokens,
9127
+ getSessionAgentName,
9128
+ getSessionActivityTime,
9129
+ toLocalDateKey,
9130
+ startOfLocalDay,
9131
+ buildDashboard
9174
9132
  };
9175
- //# sourceMappingURL=chunk-5UOLRSFH.js.map
9133
+ //# sourceMappingURL=chunk-BIXOP5QX.js.map