codesesh 0.9.1 → 0.10.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";
@@ -40,9 +40,10 @@ import { spawnSync } from "child_process";
40
40
  import * as os from "os";
41
41
  import * as path from "path";
42
42
  import { resolve, sep } from "path";
43
- import { existsSync as existsSync11, rmSync, unlinkSync } from "fs";
43
+ import { existsSync as existsSync11 } from "fs";
44
44
  import { join as join10 } from "path";
45
45
  import { homedir as homedir4 } from "os";
46
+ import { existsSync as existsSync12, rmSync, unlinkSync } from "fs";
46
47
  import { homedir as homedir5, platform as platform2 } from "os";
47
48
  import { join as join11 } from "path";
48
49
  var registrations = [];
@@ -88,6 +89,105 @@ var BaseAgent = class {
88
89
  return `${this.name}://${sessionId}`;
89
90
  }
90
91
  };
92
+ var FileSystemSessionSource = class extends BaseAgent {
93
+ sessionMetaMap = /* @__PURE__ */ new Map();
94
+ getSessionMetaMap() {
95
+ return this.sessionMetaMap;
96
+ }
97
+ setSessionMetaMap(meta) {
98
+ this.sessionMetaMap = meta;
99
+ }
100
+ /**
101
+ * 变更检测:枚举当前源 → 与缓存 metaMap 的指纹/路径比对。
102
+ * 新增、变更、删除三类统一产出 changedIds。
103
+ */
104
+ checkForChanges(_sinceTimestamp, cachedSessions) {
105
+ const currentRefs = this.listSessionSources();
106
+ const currentIds = new Set(currentRefs.map((ref) => ref.sessionId));
107
+ const changedIds = /* @__PURE__ */ new Set();
108
+ for (const ref of currentRefs) {
109
+ const meta = this.sessionMetaMap.get(ref.sessionId);
110
+ const samePath = meta?.sourcePath === ref.sourcePath;
111
+ const sameFingerprint = typeof meta?.sourceFingerprint === "string" && meta.sourceFingerprint === ref.fingerprint;
112
+ if (!samePath || !sameFingerprint) changedIds.add(ref.sessionId);
113
+ }
114
+ for (const session of cachedSessions) {
115
+ if (!currentIds.has(session.id)) changedIds.add(session.id);
116
+ }
117
+ const changedIdList = [...changedIds];
118
+ return {
119
+ hasChanges: changedIdList.length > 0,
120
+ changedIds: changedIdList,
121
+ timestamp: Date.now()
122
+ };
123
+ }
124
+ /**
125
+ * 增量扫描:对变更/新增源调用 scanSessionSource 重解析,
126
+ * 删除已消失的源,合并回 cachedSessions。
127
+ */
128
+ incrementalScan(cachedSessions, changedIds) {
129
+ const sessionMap = new Map(cachedSessions.map((session) => [session.id, session]));
130
+ const changedSet = new Set(changedIds);
131
+ const currentIds = /* @__PURE__ */ new Set();
132
+ for (const ref of this.listSessionSources()) {
133
+ currentIds.add(ref.sessionId);
134
+ if (!changedSet.has(ref.sessionId)) continue;
135
+ const head = this.scanSessionSource(ref.sourcePath);
136
+ if (head) {
137
+ sessionMap.set(head.id, head);
138
+ } else {
139
+ sessionMap.delete(ref.sessionId);
140
+ this.sessionMetaMap.delete(ref.sessionId);
141
+ }
142
+ }
143
+ for (const id of changedSet) {
144
+ if (!currentIds.has(id)) {
145
+ sessionMap.delete(id);
146
+ this.sessionMetaMap.delete(id);
147
+ }
148
+ }
149
+ return [...sessionMap.values()];
150
+ }
151
+ };
152
+ var DatabaseSessionSource = class extends BaseAgent {
153
+ sessionMetaMap = /* @__PURE__ */ new Map();
154
+ /** 记录单个会话的缓存 meta(sourcePath = dbPath)。 */
155
+ rememberSession(sessionId) {
156
+ const dbPath = this.getDatabasePath();
157
+ if (!dbPath) return;
158
+ this.sessionMetaMap.set(sessionId, { id: sessionId, sourcePath: dbPath });
159
+ }
160
+ getSessionMetaMap() {
161
+ return this.sessionMetaMap;
162
+ }
163
+ setSessionMetaMap(meta) {
164
+ this.sessionMetaMap = meta;
165
+ }
166
+ /**
167
+ * 变更检测:数据库内部变更难以按行定位,简单起见按库文件 mtime 判定。
168
+ * 库有变更则标记全部缓存会话刷新。
169
+ */
170
+ checkForChanges(sinceTimestamp, cachedSessions) {
171
+ const dbPath = this.getDatabasePath();
172
+ if (!dbPath || !existsSync(dbPath)) {
173
+ return { hasChanges: false, timestamp: Date.now() };
174
+ }
175
+ try {
176
+ const hasChanges = statSync(dbPath).mtimeMs > sinceTimestamp;
177
+ return {
178
+ hasChanges,
179
+ changedIds: hasChanges ? cachedSessions.map((session) => session.id) : [],
180
+ timestamp: Date.now()
181
+ };
182
+ } catch {
183
+ return { hasChanges: false, timestamp: Date.now() };
184
+ }
185
+ }
186
+ /** 增量扫描:数据库型无法增量,直接全量重扫。 */
187
+ incrementalScan(_cachedSessions, _changedIds) {
188
+ return this.scan();
189
+ }
190
+ };
91
191
  function envPath(name) {
92
192
  const value = process.env[name];
93
193
  if (!value) return null;
@@ -95,7 +195,7 @@ function envPath(name) {
95
195
  }
96
196
  function firstExisting(...paths) {
97
197
  for (const p of paths) {
98
- if (existsSync(p)) return p;
198
+ if (existsSync2(p)) return p;
99
199
  }
100
200
  return null;
101
201
  }
@@ -527,7 +627,7 @@ function parseLiteLLMData(data) {
527
627
  }
528
628
  function loadDiskCache() {
529
629
  const path2 = getCachePath();
530
- if (!existsSync2(path2)) return;
630
+ if (!existsSync3(path2)) return;
531
631
  try {
532
632
  const cached = JSON.parse(readFileSync2(path2, "utf-8"));
533
633
  if (Date.now() - cached.timestamp <= CACHE_TTL_MS) {
@@ -550,7 +650,7 @@ function hasBillablePricing(pricing) {
550
650
  }
551
651
  async function refreshPricingCache() {
552
652
  const path2 = getCachePath();
553
- if (existsSync2(path2)) {
653
+ if (existsSync3(path2)) {
554
654
  try {
555
655
  const cached = JSON.parse(readFileSync2(path2, "utf-8"));
556
656
  if (typeof cached.timestamp === "number" && Date.now() - cached.timestamp <= CACHE_TTL_MS) {
@@ -733,13 +833,13 @@ function extractClaudeUsage(data, msg) {
733
833
  cacheCreate: numericUsage(u["cache_creation_input_tokens"])
734
834
  };
735
835
  }
736
- var ClaudeCodeAgent = class extends BaseAgent {
836
+ var ClaudeCodeAgent = class extends FileSystemSessionSource {
737
837
  name = "claudecode";
738
838
  displayName = "Claude Code";
739
839
  basePath = null;
740
840
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
741
841
  sessionsIndexCache = {};
742
- sessionMetaMap = /* @__PURE__ */ new Map();
842
+ sessionsIndexMtime = {};
743
843
  findBasePath() {
744
844
  const roots = resolveProviderRoots();
745
845
  return firstExisting(join3(roots.claudeRoot, "projects"), "data/claudecode");
@@ -750,7 +850,7 @@ var ClaudeCodeAgent = class extends BaseAgent {
750
850
  try {
751
851
  for (const entry of readdirSync(this.basePath)) {
752
852
  const dir = join3(this.basePath, entry);
753
- if (existsSync3(dir) && readdirSync(dir).some((f) => f.endsWith(".jsonl"))) {
853
+ if (existsSync4(dir) && readdirSync(dir).some((f) => f.endsWith(".jsonl"))) {
754
854
  return true;
755
855
  }
756
856
  }
@@ -769,7 +869,7 @@ var ClaudeCodeAgent = class extends BaseAgent {
769
869
  const fileMarker = perf.start(`listJsonlFiles:${basename2(projectDir)}`);
770
870
  const files = this.listJsonlFiles(projectDir).filter((file) => {
771
871
  try {
772
- return matchesScanWindow(statSync(file).mtimeMs, options);
872
+ return matchesScanWindow(statSync2(file).mtimeMs, options);
773
873
  } catch {
774
874
  return false;
775
875
  }
@@ -823,18 +923,12 @@ var ClaudeCodeAgent = class extends BaseAgent {
823
923
  }
824
924
  return head;
825
925
  }
826
- getSessionMetaMap() {
827
- return this.sessionMetaMap;
828
- }
829
- setSessionMetaMap(meta) {
830
- this.sessionMetaMap = meta;
831
- }
832
926
  getSessionData(sessionId) {
833
927
  const meta = this.sessionMetaMap.get(sessionId);
834
928
  if (!meta) {
835
929
  throw new Error(`Session not found: ${sessionId}`);
836
930
  }
837
- if (!existsSync3(meta.sourcePath)) {
931
+ if (!existsSync4(meta.sourcePath)) {
838
932
  throw new Error(`Session file missing: ${meta.sourcePath}`);
839
933
  }
840
934
  const content = readFileSync3(meta.sourcePath, "utf-8");
@@ -894,83 +988,11 @@ var ClaudeCodeAgent = class extends BaseAgent {
894
988
  messages: cleanedMessages
895
989
  };
896
990
  }
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
991
  // --- Private helpers ---
970
992
  listProjectDirs() {
971
993
  if (!this.basePath) return [];
972
994
  try {
973
- return readdirSync(this.basePath).map((e) => join3(this.basePath, e)).filter((p) => existsSync3(p));
995
+ return readdirSync(this.basePath).map((e) => join3(this.basePath, e)).filter((p) => existsSync4(p));
974
996
  } catch {
975
997
  return [];
976
998
  }
@@ -989,8 +1011,8 @@ var ClaudeCodeAgent = class extends BaseAgent {
989
1011
  title: head.title,
990
1012
  sourcePath: file,
991
1013
  sourceFingerprint: this.sourceFingerprint(file, projectDir),
992
- sourceMtimeMs: statSync(file).mtimeMs,
993
- indexPath: existsSync3(indexPath) ? indexPath : null,
1014
+ sourceMtimeMs: statSync2(file).mtimeMs,
1015
+ indexPath: existsSync4(indexPath) ? indexPath : null,
994
1016
  indexMtimeMs: this.getFileMtimeMs(indexPath),
995
1017
  headIndexVersion: HEAD_INDEX_VERSION,
996
1018
  directory: head.directory,
@@ -1000,15 +1022,8 @@ var ClaudeCodeAgent = class extends BaseAgent {
1000
1022
  updatedAt: head.time_updated ?? head.time_created
1001
1023
  };
1002
1024
  }
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
1025
  sourceFingerprint(file, projectDir) {
1011
- const stat = statSync(file);
1026
+ const stat = statSync2(file);
1012
1027
  const indexPath = this.getSessionsIndexPath(projectDir);
1013
1028
  return JSON.stringify([
1014
1029
  HEAD_INDEX_VERSION,
@@ -1022,19 +1037,20 @@ var ClaudeCodeAgent = class extends BaseAgent {
1022
1037
  }
1023
1038
  getFileMtimeMs(filePath) {
1024
1039
  try {
1025
- return statSync(filePath).mtimeMs;
1040
+ return statSync2(filePath).mtimeMs;
1026
1041
  } catch {
1027
1042
  return null;
1028
1043
  }
1029
1044
  }
1030
1045
  loadSessionsIndex(projectDir) {
1031
1046
  const cacheKey = basename2(projectDir);
1032
- if (cacheKey in this.sessionsIndexCache) {
1047
+ const indexPath = this.getSessionsIndexPath(projectDir);
1048
+ const mtime = this.getFileMtimeMs(indexPath);
1049
+ if (cacheKey in this.sessionsIndexCache && this.sessionsIndexMtime[cacheKey] === mtime) {
1033
1050
  return this.sessionsIndexCache[cacheKey];
1034
1051
  }
1035
- const indexPath = this.getSessionsIndexPath(projectDir);
1036
1052
  const map = /* @__PURE__ */ new Map();
1037
- if (existsSync3(indexPath)) {
1053
+ if (existsSync4(indexPath)) {
1038
1054
  try {
1039
1055
  const data = JSON.parse(readFileSync3(indexPath, "utf-8"));
1040
1056
  const entries = data?.entries ?? [];
@@ -1048,6 +1064,7 @@ var ClaudeCodeAgent = class extends BaseAgent {
1048
1064
  }
1049
1065
  }
1050
1066
  this.sessionsIndexCache[cacheKey] = map;
1067
+ this.sessionsIndexMtime[cacheKey] = mtime;
1051
1068
  return map;
1052
1069
  }
1053
1070
  parseSessionHead(filePath, projectDir) {
@@ -1064,7 +1081,7 @@ var ClaudeCodeAgent = class extends BaseAgent {
1064
1081
  } catch {
1065
1082
  return skippedSession("malformed first record");
1066
1083
  }
1067
- const createdAt = parseTimestampMs(firstRecord) || statSync(filePath).mtimeMs;
1084
+ const createdAt = parseTimestampMs(firstRecord) || statSync2(filePath).mtimeMs;
1068
1085
  const index = this.loadSessionsIndex(projectDir);
1069
1086
  const indexEntry = index.get(sessionId);
1070
1087
  const explicitTitle = indexEntry?.summary ? String(indexEntry.summary) : null;
@@ -1636,7 +1653,7 @@ function backupDatabase(db, dbPath, label) {
1636
1653
  }
1637
1654
  const timestamp = new Date(Date.now()).toISOString().replaceAll(":", "").replaceAll(".", "-");
1638
1655
  let backupPath = join4(dirname2(dbPath), `${basename3(dbPath)}.${timestamp}.${label}.bak`);
1639
- for (let counter = 1; existsSync4(backupPath); counter += 1) {
1656
+ for (let counter = 1; existsSync5(backupPath); counter += 1) {
1640
1657
  backupPath = join4(dirname2(dbPath), `${basename3(dbPath)}.${timestamp}.${label}.${counter}.bak`);
1641
1658
  }
1642
1659
  db.exec(`VACUUM INTO ${quoteSqlString(backupPath)}`);
@@ -1709,12 +1726,16 @@ function openDb(dbPath) {
1709
1726
  function isSqliteAvailable() {
1710
1727
  return DatabaseConstructor !== null;
1711
1728
  }
1712
- var OpenCodeAgent = class extends BaseAgent {
1729
+ var OpenCodeAgent = class extends DatabaseSessionSource {
1713
1730
  name = "opencode";
1714
1731
  displayName = "OpenCode";
1715
1732
  dbPath = null;
1716
- // Session metadata for caching
1717
- sessionMetaMap = /* @__PURE__ */ new Map();
1733
+ getDatabasePath() {
1734
+ if (!this.dbPath) {
1735
+ this.dbPath = this.findDbPath();
1736
+ }
1737
+ return this.dbPath;
1738
+ }
1718
1739
  findDbPath() {
1719
1740
  if (!isSqliteAvailable()) return null;
1720
1741
  const roots = resolveProviderRoots();
@@ -1830,42 +1851,6 @@ var OpenCodeAgent = class extends BaseAgent {
1830
1851
  `
1831
1852
  ).all(cutoffTime);
1832
1853
  }
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
1854
  parsePartRow(partRow) {
1870
1855
  const partData = JSON.parse(String(partRow.data ?? "{}"));
1871
1856
  const partType = String(partData.type ?? "");
@@ -2145,11 +2130,10 @@ function extractFirstUserTitle(contextFile, wireFile) {
2145
2130
  }
2146
2131
  return null;
2147
2132
  }
2148
- var KimiAgent = class extends BaseAgent {
2133
+ var KimiAgent = class extends FileSystemSessionSource {
2149
2134
  name = "kimi";
2150
2135
  displayName = "Kimi-Cli";
2151
2136
  basePath = null;
2152
- sessionMetaMap = /* @__PURE__ */ new Map();
2153
2137
  projectMap = /* @__PURE__ */ new Map();
2154
2138
  defaultModel = null;
2155
2139
  findBasePath() {
@@ -2310,8 +2294,6 @@ var KimiAgent = class extends BaseAgent {
2310
2294
  for (const dir of this.listSessionDirs()) {
2311
2295
  const meta = getParsedSession(this.parseSessionDirResult(dir));
2312
2296
  if (!meta) continue;
2313
- meta.sourceFingerprint = this.sourceFingerprint(meta);
2314
- this.sessionMetaMap.set(meta.id, meta);
2315
2297
  refs.push({
2316
2298
  sessionId: meta.id,
2317
2299
  sourcePath: meta.sourcePath,
@@ -2336,87 +2318,6 @@ var KimiAgent = class extends BaseAgent {
2336
2318
  stats
2337
2319
  };
2338
2320
  }
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
2321
  getSessionData(sessionId) {
2421
2322
  const meta = this.sessionMetaMap.get(sessionId);
2422
2323
  if (!meta) throw new Error(`Session not found: ${sessionId}`);
@@ -3019,12 +2920,12 @@ function extractPatchContent(lines, startIndex) {
3019
2920
  }
3020
2921
  return { text: contentLines.join("\n"), nextLineIndex: i };
3021
2922
  }
3022
- var CodexAgent = class extends BaseAgent {
2923
+ var CodexAgent = class extends FileSystemSessionSource {
3023
2924
  name = "codex";
3024
2925
  displayName = "Codex";
3025
2926
  basePath = null;
3026
2927
  sessionIndexCache = /* @__PURE__ */ new Map();
3027
- sessionMetaMap = /* @__PURE__ */ new Map();
2928
+ sessionIndexMtime = null;
3028
2929
  // ---- BaseAgent implementation ----
3029
2930
  findBasePath() {
3030
2931
  const roots = resolveProviderRoots();
@@ -3087,94 +2988,6 @@ var CodexAgent = class extends BaseAgent {
3087
2988
  }
3088
2989
  return head;
3089
2990
  }
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
- }
3117
- 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
2991
  getSessionData(sessionId) {
3179
2992
  const meta = this.sessionMetaMap.get(sessionId);
3180
2993
  if (!meta) throw new Error(`Session not found: ${sessionId}`);
@@ -3352,14 +3165,6 @@ var CodexAgent = class extends BaseAgent {
3352
3165
  updatedAt: head.time_updated ?? head.time_created
3353
3166
  };
3354
3167
  }
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
3168
  sourceFingerprint(file) {
3364
3169
  const stat = statSync4(file);
3365
3170
  const sessionId = extractSessionId(file);
@@ -3384,9 +3189,12 @@ var CodexAgent = class extends BaseAgent {
3384
3189
  }
3385
3190
  // ---- Session index ----
3386
3191
  loadSessionIndex() {
3387
- if (this.sessionIndexCache.size > 0) return;
3388
3192
  const indexPath = this.getSessionIndexPath();
3389
- if (!existsSync7(indexPath)) return;
3193
+ const mtime = this.getFileMtimeMs(indexPath);
3194
+ if (this.sessionIndexCache.size > 0 && this.sessionIndexMtime === mtime) return;
3195
+ this.sessionIndexCache.clear();
3196
+ this.sessionIndexMtime = mtime;
3197
+ if (mtime === null) return;
3390
3198
  try {
3391
3199
  const content = readFileSync5(indexPath, "utf-8");
3392
3200
  for (const record of parseJsonlLines(content)) {
@@ -4122,14 +3930,18 @@ function convertActionToPart(action, timestampMs) {
4122
3930
  }
4123
3931
  return null;
4124
3932
  }
4125
- var CursorAgent = class extends BaseAgent {
3933
+ var CursorAgent = class extends DatabaseSessionSource {
4126
3934
  name = "cursor";
4127
3935
  displayName = "Cursor";
4128
3936
  dbPath = null;
4129
3937
  // Cache composer data from scan so getSessionData can reuse it
4130
3938
  composerCache = /* @__PURE__ */ new Map();
4131
- // Session metadata for caching
4132
- sessionMetaMap = /* @__PURE__ */ new Map();
3939
+ getDatabasePath() {
3940
+ if (!this.dbPath) {
3941
+ this.dbPath = this.findDbPath();
3942
+ }
3943
+ return this.dbPath;
3944
+ }
4133
3945
  findDbPath() {
4134
3946
  if (!isSqliteAvailable()) return null;
4135
3947
  const dataPath = getCursorDataPath();
@@ -4332,42 +4144,6 @@ var CursorAgent = class extends BaseAgent {
4332
4144
  db.close();
4333
4145
  }
4334
4146
  }
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
4147
  getSessionData(sessionId) {
4372
4148
  if (!this.dbPath) {
4373
4149
  this.dbPath = this.findDbPath();
@@ -4766,11 +4542,10 @@ function buildCurrentPathEntries(entries) {
4766
4542
  }
4767
4543
  return path2.reverse();
4768
4544
  }
4769
- var PiAgent = class extends BaseAgent {
4545
+ var PiAgent = class extends FileSystemSessionSource {
4770
4546
  name = "pi";
4771
4547
  displayName = "Pi";
4772
4548
  basePath = null;
4773
- sessionMetaMap = /* @__PURE__ */ new Map();
4774
4549
  findBasePath() {
4775
4550
  const roots = resolveProviderRoots();
4776
4551
  return firstExisting(join9(roots.piRoot, "agent", "sessions"), "data/pi");
@@ -4818,69 +4593,6 @@ var PiAgent = class extends BaseAgent {
4818
4593
  }
4819
4594
  return head;
4820
4595
  }
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
4596
  getSessionData(sessionId) {
4885
4597
  const meta = this.sessionMetaMap.get(sessionId);
4886
4598
  if (!meta) throw new Error(`Session not found: ${sessionId}`);
@@ -4943,11 +4655,6 @@ var PiAgent = class extends BaseAgent {
4943
4655
  updatedAt: head.time_updated ?? head.time_created
4944
4656
  };
4945
4657
  }
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
4658
  sourceFingerprint(file) {
4952
4659
  const stat = statSync6(file);
4953
4660
  return JSON.stringify([HEAD_INDEX_VERSION3, PARSER_VERSION2, stat.mtimeMs, stat.size]);
@@ -5805,12 +5512,16 @@ function extractSessionFileActivity(agentName, sessionId, projectIdentityKey, me
5805
5512
  extractFileActivityOccurrences(messages)
5806
5513
  );
5807
5514
  }
5808
- var CACHE_SCHEMA_VERSION = 13;
5809
- var CACHE_INITIALIZATION_VERSION = "session-cache-v2";
5810
5515
  var CACHE_FILENAME = "codesesh.db";
5811
5516
  var LEGACY_CACHE_FILENAME = "scan-cache.json";
5812
5517
  var SEARCH_INDEX_BULK_SYNC_THRESHOLD = 100;
5813
5518
  var ftsIntegrityCheckedPath = null;
5519
+ function getFtsIntegrityCheckedPath() {
5520
+ return ftsIntegrityCheckedPath;
5521
+ }
5522
+ function setFtsIntegrityCheckedPath(path2) {
5523
+ ftsIntegrityCheckedPath = path2;
5524
+ }
5814
5525
  function getCacheDir2() {
5815
5526
  return join10(homedir4(), ".cache", "codesesh");
5816
5527
  }
@@ -5823,408 +5534,33 @@ function getLegacyCachePath() {
5823
5534
  function hasCacheStorage() {
5824
5535
  return existsSync11(getCachePath2());
5825
5536
  }
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
- }
5537
+ function likePattern(value) {
5538
+ return `%${value.trim().toLowerCase().replace(/[\\%_]/g, "\\$&")}%`;
5838
5539
  }
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
- }
5540
+ function filePathFtsQuery(value) {
5541
+ const path2 = normalizeFilePathSearch(value);
5542
+ if (path2.length < 3) return null;
5543
+ return `"${path2.replaceAll('"', '""')}"`;
5849
5544
  }
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
- );
5876
- `);
5545
+ function escapeRegExp(value) {
5546
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5877
5547
  }
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);
5943
- `);
5944
- createMessageToolTables(db);
5548
+ function normalizeFilePathSearch(value) {
5549
+ return value.trim().replace(/^"|"$/g, "");
5945
5550
  }
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);
5961
- `);
5551
+ function stringifyOptionalJson(value) {
5552
+ return value == null ? null : JSON.stringify(value);
5962
5553
  }
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
- );
5973
- `);
5974
- createMessageSearchTriggers(db);
5554
+ function parseOptionalJson(value) {
5555
+ return value == null ? void 0 : JSON.parse(String(value));
5975
5556
  }
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;
5994
- `);
5557
+ function sourcePathFromMeta(meta) {
5558
+ return typeof meta?.sourcePath === "string" ? meta.sourcePath : null;
5995
5559
  }
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;
6001
- `);
6002
- }
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'
6046
- );
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
- }
6077
- db.exec(
6078
- "INSERT INTO session_file_activity_path_fts(session_file_activity_path_fts) VALUES ('rebuild')"
6079
- );
6080
- }
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);
6107
- }
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
- `);
6127
- }
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
- `);
6134
- }
6135
- function ensureProjectColumns(db) {
6136
- if (!tableExists(db, "session_documents")) {
6137
- return;
6138
- }
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
- );
6143
- }
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
- );
6148
- }
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
- );
6153
- }
6154
- }
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);
6173
- }
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;
6189
- }
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
- }
6203
- function recreateProjectGroupsView(db) {
6204
- db.exec("DROP VIEW IF EXISTS project_groups_v");
6205
- createProjectGroupsView(db);
6206
- }
6207
- function createLatestCacheSchema(db) {
6208
- createCacheTables(db);
6209
- createSessionTables(db);
6210
- createMessageSearchTables(db);
6211
- createFileActivityTables(db);
6212
- createSearchTables(db);
6213
- createProjectTables(db);
6214
- }
6215
- function stringifyOptionalJson(value) {
6216
- return value == null ? null : JSON.stringify(value);
6217
- }
6218
- function parseOptionalJson(value) {
6219
- return value == null ? void 0 : JSON.parse(String(value));
6220
- }
6221
- function sourcePathFromMeta(meta) {
6222
- return typeof meta?.sourcePath === "string" ? meta.sourcePath : null;
6223
- }
6224
- function sourcePathFromMetaJson(metaJson) {
6225
- if (!metaJson) return null;
6226
- const meta = JSON.parse(metaJson);
6227
- return sourcePathFromMeta(meta);
5560
+ function sourcePathFromMetaJson(metaJson) {
5561
+ if (!metaJson) return null;
5562
+ const meta = JSON.parse(metaJson);
5563
+ return sourcePathFromMeta(meta);
6228
5564
  }
6229
5565
  function prepareUpsertCachedSession(db) {
6230
5566
  return db.prepare(`
@@ -6488,1126 +5824,1165 @@ function sessionFromRow(row) {
6488
5824
  }
6489
5825
  return session;
6490
5826
  }
6491
- function recreateSearchIndexSchema(db) {
6492
- db.exec(`
6493
- DROP TRIGGER IF EXISTS session_documents_ai;
6494
- DROP TRIGGER IF EXISTS session_documents_ad;
6495
- DROP TRIGGER IF EXISTS session_documents_au;
6496
- DROP TABLE IF EXISTS session_documents_fts;
6497
- `);
6498
- createSearchTables(db);
6499
- rebuildSearchIndex(db);
5827
+ function messageFromBackfillRow(row) {
5828
+ const role = row.role === "assistant" || row.role === "tool" ? row.role : "user";
5829
+ return {
5830
+ id: String(row.message_id ?? ""),
5831
+ role,
5832
+ agent: row.agent ?? null,
5833
+ time_created: Number(row.time_created ?? 0),
5834
+ time_completed: row.time_completed == null ? null : Number(row.time_completed),
5835
+ mode: row.mode ?? null,
5836
+ model: row.model ?? null,
5837
+ provider: row.provider ?? null,
5838
+ parts: JSON.parse(String(row.parts_json ?? "[]")),
5839
+ subagent_id: row.subagent_id ?? void 0,
5840
+ nickname: row.nickname ?? void 0
5841
+ };
6500
5842
  }
6501
- function readLegacyCacheVersion(db) {
6502
- if (!tableExists(db, "cache_meta") || !columnExists(db, "cache_meta", "key") || !columnExists(db, "cache_meta", "value")) {
6503
- return 0;
5843
+ function messageFromCachedRow(row) {
5844
+ const message = messageFromBackfillRow(row);
5845
+ const tokens = parseOptionalJson(row.tokens_json);
5846
+ if (tokens) {
5847
+ message.tokens = tokens;
6504
5848
  }
6505
- const versionRow = db.prepare("SELECT value FROM cache_meta WHERE key = 'version'").get();
6506
- return Number(versionRow?.value ?? 0);
6507
- }
6508
- function inferCacheSchemaVersion(db) {
6509
- if (tableExists(db, "message_tools")) {
6510
- return 11;
6511
- }
6512
- if (tableExists(db, "session_file_activity_path_fts")) {
6513
- return 10;
6514
- }
6515
- if (tableExists(db, "messages_fts")) {
6516
- return 9;
5849
+ if (row.cost != null) {
5850
+ message.cost = Number(row.cost);
6517
5851
  }
6518
- if (tableExists(db, "session_file_activity")) {
6519
- return 8;
5852
+ if (row.cost_source) {
5853
+ message.cost_source = row.cost_source;
6520
5854
  }
6521
- if (tableExists(db, "sessions") || tableExists(db, "messages")) {
6522
- return 7;
5855
+ return message;
5856
+ }
5857
+ function appendPlainText(value, chunks) {
5858
+ if (value == null) return;
5859
+ if (typeof value === "string") {
5860
+ const normalized = value.trim();
5861
+ if (normalized) {
5862
+ chunks.push(normalized);
5863
+ }
5864
+ return;
6523
5865
  }
6524
- if (tableExists(db, "project_sessions") || columnExists(db, "session_documents", "project_identity_key")) {
6525
- return 5;
5866
+ if (typeof value === "number" || typeof value === "boolean") {
5867
+ chunks.push(String(value));
5868
+ return;
6526
5869
  }
6527
- if (tableExists(db, "session_documents")) {
6528
- return 4;
5870
+ if (Array.isArray(value)) {
5871
+ for (const item of value) {
5872
+ appendPlainText(item, chunks);
5873
+ }
5874
+ return;
6529
5875
  }
6530
- if (tableExists(db, "cached_sessions") || tableExists(db, "agent_cache")) {
6531
- return 3;
5876
+ if (typeof value === "object") {
5877
+ for (const nested of Object.values(value)) {
5878
+ appendPlainText(nested, chunks);
5879
+ }
6532
5880
  }
6533
- return 0;
6534
5881
  }
6535
- function getCurrentCacheSchemaVersion(db) {
6536
- const userVersion = getUserVersion(db);
6537
- if (userVersion > 0) {
6538
- return userVersion;
6539
- }
6540
- const legacyVersion = readLegacyCacheVersion(db);
6541
- return Math.max(legacyVersion, inferCacheSchemaVersion(db));
5882
+ function compactRecord(record) {
5883
+ return Object.fromEntries(Object.entries(record).filter(([, value]) => value != null));
6542
5884
  }
6543
- function hasAnyCacheSchema(db) {
6544
- return [
6545
- "cache_meta",
6546
- "agent_cache",
6547
- "cached_sessions",
6548
- "sessions",
6549
- "messages",
6550
- "message_tools",
6551
- "session_file_activity",
6552
- "session_file_activity_path_fts",
6553
- "session_documents",
6554
- "session_documents_fts",
6555
- "project_sessions"
6556
- ].some((table) => tableExists(db, table));
5885
+ function normalizeToolName2(value) {
5886
+ if (typeof value !== "string") return null;
5887
+ const name = value.trim().toLowerCase();
5888
+ return name || null;
6557
5889
  }
6558
- function backfillProjectSessions(db) {
6559
- if (!tableExists(db, "cached_sessions") || !tableExists(db, "project_sessions")) {
6560
- return;
6561
- }
6562
- const rows = db.prepare("SELECT agent_name, session_id, session_json FROM cached_sessions").all();
6563
- const upsert = db.prepare(`
6564
- INSERT INTO project_sessions(
6565
- agent_name,
6566
- session_id,
6567
- identity_kind,
6568
- identity_key,
6569
- display_name,
6570
- directory,
6571
- activity_time
6572
- ) VALUES (?, ?, ?, ?, ?, ?, ?)
6573
- ON CONFLICT(agent_name, session_id) DO UPDATE SET
6574
- identity_kind = excluded.identity_kind,
6575
- identity_key = excluded.identity_key,
6576
- display_name = excluded.display_name,
6577
- directory = excluded.directory,
6578
- activity_time = excluded.activity_time
6579
- `);
6580
- for (const row of rows) {
6581
- if (!row.session_json || !row.agent_name || !row.session_id) {
6582
- continue;
6583
- }
6584
- try {
6585
- const session = JSON.parse(row.session_json);
6586
- const identity = session.project_identity ?? computeIdentity(session.directory, realFs);
6587
- upsert.run(
6588
- row.agent_name,
6589
- row.session_id,
6590
- identity.kind,
6591
- identity.key,
6592
- identity.displayName,
6593
- session.directory,
6594
- session.time_updated ?? session.time_created
6595
- );
6596
- } catch {
6597
- continue;
5890
+ function toolNamesFromMetadataJson(value) {
5891
+ if (!value) return [];
5892
+ try {
5893
+ const metadata = JSON.parse(String(value));
5894
+ if (!Array.isArray(metadata)) return [];
5895
+ const tools = /* @__PURE__ */ new Set();
5896
+ for (const item of metadata) {
5897
+ if (item == null || typeof item !== "object") continue;
5898
+ const toolName = normalizeToolName2(item.tool);
5899
+ if (toolName) tools.add(toolName);
6598
5900
  }
5901
+ return [...tools];
5902
+ } catch {
5903
+ return [];
6599
5904
  }
6600
5905
  }
6601
- function backfillSessionDocumentProjects(db) {
6602
- if (!tableExists(db, "session_documents") || !columnExists(db, "session_documents", "project_identity_key")) {
6603
- return;
6604
- }
6605
- const rows = db.prepare("SELECT id, directory FROM session_documents").all();
6606
- const update = db.prepare(`
6607
- UPDATE session_documents
6608
- SET
6609
- project_identity_kind = ?,
6610
- project_identity_key = ?,
6611
- project_display_name = ?
6612
- WHERE id = ?
6613
- `);
6614
- for (const row of rows) {
6615
- const identity = computeIdentity(String(row.directory ?? ""), realFs);
6616
- update.run(identity.kind, identity.key, identity.displayName, Number(row.id));
5906
+ function toolNamesFromMessage(message) {
5907
+ const tools = /* @__PURE__ */ new Set();
5908
+ for (const part of message.parts) {
5909
+ if (part.type !== "tool") continue;
5910
+ const toolName = normalizeToolName2(part.tool);
5911
+ if (toolName) tools.add(toolName);
6617
5912
  }
5913
+ return [...tools];
6618
5914
  }
6619
- function migrateProjectIdentity(db) {
6620
- createProjectTables(db);
6621
- backfillProjectSessions(db);
6622
- backfillSessionDocumentProjects(db);
5915
+ function summarizeToolPart(part) {
5916
+ const state = part.state == null ? void 0 : compactRecord({
5917
+ status: part.state.status,
5918
+ error: part.state.error,
5919
+ metadata: part.state.metadata
5920
+ });
5921
+ return compactRecord({
5922
+ type: part.type,
5923
+ tool: part.tool,
5924
+ title: part.title,
5925
+ nickname: part.nickname,
5926
+ callID: part.callID,
5927
+ approval_status: part.approval_status,
5928
+ state
5929
+ });
6623
5930
  }
6624
- function refreshProjectIdentities(db) {
6625
- if (tableExists(db, "sessions") && columnExists(db, "sessions", "project_identity_key") && columnExists(db, "sessions", "directory")) {
6626
- const rows = db.prepare("SELECT agent_name, session_id, directory FROM sessions").all();
6627
- const update = db.prepare(`
6628
- UPDATE sessions
6629
- SET
6630
- project_identity_kind = ?,
6631
- project_identity_key = ?,
6632
- project_display_name = ?
6633
- WHERE agent_name = ? AND session_id = ?
6634
- `);
6635
- const updateFileActivity = tableExists(db, "session_file_activity") && columnExists(db, "session_file_activity", "project_identity_key") ? db.prepare(`
6636
- UPDATE session_file_activity
6637
- SET project_identity_key = ?
6638
- WHERE agent_name = ? AND session_id = ?
6639
- `) : null;
6640
- for (const row of rows) {
6641
- const identity = computeIdentity(String(row.directory ?? ""), realFs);
6642
- update.run(identity.kind, identity.key, identity.displayName, row.agent_name, row.session_id);
6643
- updateFileActivity?.run(identity.key, row.agent_name, row.session_id);
6644
- }
5931
+ function buildMessageText(message) {
5932
+ const chunks = [];
5933
+ chunks.push(message.role);
5934
+ appendPlainText(message.agent, chunks);
5935
+ appendPlainText(message.model, chunks);
5936
+ for (const part of message.parts) {
5937
+ appendPlainText(part.type, chunks);
5938
+ appendPlainText(part.title, chunks);
5939
+ appendPlainText(part.nickname, chunks);
5940
+ appendPlainText(part.tool, chunks);
5941
+ appendPlainText(part.text, chunks);
5942
+ appendPlainText(part.input, chunks);
5943
+ appendPlainText(part.output, chunks);
5944
+ appendPlainText(part.state, chunks);
6645
5945
  }
6646
- if (tableExists(db, "project_sessions") && columnExists(db, "project_sessions", "identity_key") && columnExists(db, "project_sessions", "directory")) {
6647
- const rows = db.prepare("SELECT agent_name, session_id, directory FROM project_sessions").all();
6648
- const update = db.prepare(`
6649
- UPDATE project_sessions
6650
- SET
6651
- identity_kind = ?,
6652
- identity_key = ?,
6653
- display_name = ?
6654
- WHERE agent_name = ? AND session_id = ?
6655
- `);
6656
- for (const row of rows) {
6657
- const identity = computeIdentity(String(row.directory ?? ""), realFs);
6658
- update.run(identity.kind, identity.key, identity.displayName, row.agent_name, row.session_id);
6659
- }
5946
+ return chunks.join("\n");
5947
+ }
5948
+ function normalizeMessages(session) {
5949
+ return session.messages.map((message, index) => {
5950
+ const toolMetadata = message.parts.filter((part) => part.type === "tool").map((part) => summarizeToolPart(part));
5951
+ return {
5952
+ index,
5953
+ id: message.id || `${session.id}:${index}`,
5954
+ role: message.role,
5955
+ timeCreated: message.time_created,
5956
+ timeCompleted: message.time_completed ?? null,
5957
+ agent: message.agent ?? null,
5958
+ mode: message.mode ?? null,
5959
+ model: message.model ?? null,
5960
+ provider: message.provider ?? null,
5961
+ tokensJson: stringifyOptionalJson(message.tokens),
5962
+ cost: message.cost ?? null,
5963
+ costSource: message.cost_source ?? null,
5964
+ partsJson: JSON.stringify(message.parts),
5965
+ subagentId: message.subagent_id ?? null,
5966
+ nickname: message.nickname ?? null,
5967
+ contentText: buildMessageText(message),
5968
+ toolMetadataJson: toolMetadata.length > 0 ? JSON.stringify(toolMetadata) : null,
5969
+ toolNames: toolNamesFromMessage(message)
5970
+ };
5971
+ });
5972
+ }
5973
+ function buildSessionContentFromMessages(title, messages) {
5974
+ const chunks = [];
5975
+ appendPlainText(title, chunks);
5976
+ for (const message of messages) {
5977
+ appendPlainText(message.contentText, chunks);
6660
5978
  }
6661
- backfillSessionDocumentProjects(db);
6662
- recreateProjectGroupsView(db);
5979
+ return chunks.join("\n");
6663
5980
  }
6664
- function backfillStructuredSessions(db) {
6665
- createSessionTables(db);
6666
- recreateProjectGroupsView(db);
6667
- const upsertSession = prepareUpsertSession(db);
6668
- if (tableExists(db, "cached_sessions")) {
6669
- const rows = db.prepare(
6670
- "SELECT agent_name, session_id, session_json, meta_json, rowid AS sort_index FROM cached_sessions ORDER BY agent_name, rowid"
6671
- ).all();
6672
- for (const row of rows) {
6673
- if (!row.agent_name || !row.session_json) {
6674
- continue;
6675
- }
6676
- try {
6677
- const session = JSON.parse(row.session_json);
6678
- upsertSessionRow(
6679
- upsertSession,
6680
- String(row.agent_name),
6681
- session,
6682
- row.meta_json ?? null,
6683
- Number(row.sort_index ?? 0),
6684
- sourcePathFromMetaJson(row.meta_json)
6685
- );
6686
- } catch {
6687
- continue;
6688
- }
6689
- }
5981
+ var CACHE_SCHEMA_VERSION = 13;
5982
+ function withCacheDb(fn) {
5983
+ const cachePath = getCachePath2();
5984
+ const db = openDb(cachePath);
5985
+ if (!db) return null;
5986
+ try {
5987
+ ensureSchema(db, cachePath);
5988
+ return fn(db);
5989
+ } catch {
5990
+ return null;
5991
+ } finally {
5992
+ db.close();
6690
5993
  }
6691
- if (!tableExists(db, "session_documents")) {
6692
- return;
5994
+ }
5995
+ function withCacheDbReadOnly(fn) {
5996
+ const db = openDbReadOnly(getCachePath2());
5997
+ if (!db) return null;
5998
+ try {
5999
+ return fn(db);
6000
+ } catch {
6001
+ return null;
6002
+ } finally {
6003
+ db.close();
6693
6004
  }
6694
- const documentRows = db.prepare(
6695
- `
6696
- SELECT
6697
- d.agent_name,
6698
- d.session_id,
6699
- d.slug,
6700
- d.title,
6701
- d.directory,
6702
- d.project_identity_kind,
6703
- d.project_identity_key,
6704
- d.project_display_name,
6705
- d.time_created,
6706
- d.time_updated,
6707
- d.activity_time,
6708
- d.id
6709
- FROM session_documents d
6710
- LEFT JOIN sessions s ON s.agent_name = d.agent_name AND s.session_id = d.session_id
6711
- WHERE s.session_id IS NULL
6712
- ORDER BY d.id
6713
- `
6714
- ).all();
6715
- for (const row of documentRows) {
6716
- const directory = String(row.directory ?? "");
6717
- const identity = row.project_identity_key && row.project_identity_kind && row.project_display_name ? {
6718
- kind: row.project_identity_kind,
6719
- key: String(row.project_identity_key),
6720
- displayName: String(row.project_display_name)
6721
- } : computeIdentity(directory, realFs);
6722
- upsertSessionRow(
6723
- upsertSession,
6724
- String(row.agent_name),
6725
- {
6726
- id: String(row.session_id),
6727
- slug: String(row.slug),
6728
- title: String(row.title),
6729
- directory,
6730
- project_identity: identity,
6731
- time_created: Number(row.time_created ?? row.activity_time ?? 0),
6732
- time_updated: row.time_updated == null ? void 0 : Number(row.time_updated),
6733
- stats: {
6734
- message_count: 0,
6735
- total_input_tokens: 0,
6736
- total_output_tokens: 0,
6737
- total_cost: 0
6738
- }
6739
- },
6740
- null,
6741
- Number(row.id ?? 0),
6742
- null
6005
+ }
6006
+ function createCacheTables(db) {
6007
+ db.exec(`
6008
+ CREATE TABLE IF NOT EXISTS cache_meta (
6009
+ key TEXT PRIMARY KEY,
6010
+ value TEXT NOT NULL
6743
6011
  );
6744
- }
6012
+
6013
+ CREATE TABLE IF NOT EXISTS agent_cache (
6014
+ agent_name TEXT PRIMARY KEY,
6015
+ timestamp INTEGER NOT NULL
6016
+ );
6017
+
6018
+ CREATE TABLE IF NOT EXISTS cached_sessions (
6019
+ agent_name TEXT NOT NULL,
6020
+ session_id TEXT NOT NULL,
6021
+ session_json TEXT NOT NULL,
6022
+ meta_json TEXT,
6023
+ PRIMARY KEY (agent_name, session_id)
6024
+ );
6025
+
6026
+ CREATE TABLE IF NOT EXISTS cache_initialization (
6027
+ agent_name TEXT PRIMARY KEY,
6028
+ initialized_at INTEGER NOT NULL,
6029
+ index_version TEXT NOT NULL,
6030
+ last_sync_at INTEGER NOT NULL
6031
+ );
6032
+ `);
6745
6033
  }
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
- };
6034
+ function createSessionTables(db) {
6035
+ db.exec(`
6036
+ CREATE TABLE IF NOT EXISTS sessions (
6037
+ agent_name TEXT NOT NULL,
6038
+ session_id TEXT NOT NULL,
6039
+ sort_index INTEGER NOT NULL DEFAULT 0,
6040
+ slug TEXT NOT NULL,
6041
+ title TEXT NOT NULL,
6042
+ source_path TEXT,
6043
+ directory TEXT NOT NULL,
6044
+ project_identity_kind TEXT NOT NULL,
6045
+ project_identity_key TEXT NOT NULL,
6046
+ project_display_name TEXT NOT NULL,
6047
+ time_created INTEGER NOT NULL,
6048
+ time_updated INTEGER,
6049
+ activity_time INTEGER NOT NULL,
6050
+ message_count INTEGER NOT NULL,
6051
+ total_input_tokens INTEGER NOT NULL,
6052
+ total_output_tokens INTEGER NOT NULL,
6053
+ total_cache_read_tokens INTEGER,
6054
+ total_cache_create_tokens INTEGER,
6055
+ total_cost REAL NOT NULL,
6056
+ cost_source TEXT,
6057
+ total_tokens INTEGER,
6058
+ model_usage_json TEXT,
6059
+ smart_tags_json TEXT,
6060
+ smart_tags_source_updated_at INTEGER,
6061
+ meta_json TEXT,
6062
+ PRIMARY KEY (agent_name, session_id)
6063
+ );
6064
+
6065
+ CREATE INDEX IF NOT EXISTS idx_sessions_agent_activity
6066
+ ON sessions(agent_name, activity_time);
6067
+
6068
+ CREATE INDEX IF NOT EXISTS idx_sessions_project
6069
+ ON sessions(project_identity_kind, project_identity_key, activity_time);
6070
+
6071
+ CREATE TABLE IF NOT EXISTS messages (
6072
+ agent_name TEXT NOT NULL,
6073
+ session_id TEXT NOT NULL,
6074
+ message_index INTEGER NOT NULL,
6075
+ message_id TEXT NOT NULL,
6076
+ role TEXT NOT NULL,
6077
+ time_created INTEGER NOT NULL,
6078
+ time_completed INTEGER,
6079
+ agent TEXT,
6080
+ mode TEXT,
6081
+ model TEXT,
6082
+ provider TEXT,
6083
+ tokens_json TEXT,
6084
+ cost REAL,
6085
+ cost_source TEXT,
6086
+ parts_json TEXT NOT NULL,
6087
+ subagent_id TEXT,
6088
+ nickname TEXT,
6089
+ content_text TEXT NOT NULL,
6090
+ tool_metadata_json TEXT,
6091
+ PRIMARY KEY (agent_name, session_id, message_index),
6092
+ FOREIGN KEY (agent_name, session_id)
6093
+ REFERENCES sessions(agent_name, session_id)
6094
+ ON DELETE CASCADE
6095
+ );
6096
+
6097
+ CREATE INDEX IF NOT EXISTS idx_messages_session
6098
+ ON messages(agent_name, session_id, message_index);
6099
+ `);
6100
+ createMessageToolTables(db);
6761
6101
  }
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;
6102
+ function createMessageToolTables(db) {
6103
+ db.exec(`
6104
+ CREATE TABLE IF NOT EXISTS message_tools (
6105
+ agent_name TEXT NOT NULL,
6106
+ session_id TEXT NOT NULL,
6107
+ message_index INTEGER NOT NULL,
6108
+ tool_name TEXT NOT NULL,
6109
+ PRIMARY KEY (agent_name, session_id, message_index, tool_name),
6110
+ FOREIGN KEY (agent_name, session_id, message_index)
6111
+ REFERENCES messages(agent_name, session_id, message_index)
6112
+ ON DELETE CASCADE
6113
+ );
6114
+
6115
+ CREATE INDEX IF NOT EXISTS idx_message_tools_filter
6116
+ ON message_tools(tool_name, agent_name, session_id);
6117
+ `);
6775
6118
  }
6776
- function backfillMessageTools(db) {
6777
- createMessageToolTables(db);
6119
+ function createMessageSearchTables(db) {
6778
6120
  if (!tableExists(db, "messages")) {
6779
- return;
6780
- }
6781
- db.exec("DELETE FROM message_tools");
6782
- const rows = db.prepare(
6783
- `
6784
- SELECT agent_name, session_id, message_index, tool_metadata_json
6785
- FROM messages
6786
- WHERE tool_metadata_json IS NOT NULL
6787
- `
6788
- ).all();
6789
- const insertTool = prepareInsertMessageTool(db);
6790
- for (const row of rows) {
6791
- if (!row.agent_name || !row.session_id || row.message_index == null) {
6792
- continue;
6793
- }
6794
- for (const toolName of toolNamesFromMetadataJson(row.tool_metadata_json)) {
6795
- insertTool.run(row.agent_name, row.session_id, row.message_index, toolName);
6796
- }
6121
+ createSessionTables(db);
6797
6122
  }
6123
+ db.exec(`
6124
+ CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
6125
+ content_text,
6126
+ content='messages',
6127
+ content_rowid='rowid'
6128
+ );
6129
+ `);
6130
+ createMessageSearchTriggers(db);
6798
6131
  }
6799
- function backfillFileActivity(db) {
6800
- createFileActivityTables(db);
6801
- if (!tableExists(db, "sessions") || !tableExists(db, "messages")) {
6802
- return;
6803
- }
6804
- const sessions = db.prepare(
6805
- `
6806
- SELECT agent_name, session_id, project_identity_key
6807
- FROM sessions
6808
- ORDER BY agent_name, session_id
6809
- `
6810
- ).all();
6811
- const loadMessages = db.prepare(`
6812
- SELECT
6813
- message_id,
6814
- role,
6815
- time_created,
6816
- time_completed,
6817
- agent,
6818
- mode,
6819
- model,
6820
- provider,
6821
- parts_json,
6822
- subagent_id,
6823
- nickname
6824
- FROM messages
6825
- WHERE agent_name = ? AND session_id = ?
6826
- ORDER BY message_index
6132
+ function createMessageSearchTriggers(db) {
6133
+ db.exec(`
6134
+ CREATE TRIGGER IF NOT EXISTS messages_ai AFTER INSERT ON messages BEGIN
6135
+ INSERT INTO messages_fts(rowid, content_text)
6136
+ VALUES (new.rowid, new.content_text);
6137
+ END;
6138
+
6139
+ CREATE TRIGGER IF NOT EXISTS messages_ad AFTER DELETE ON messages BEGIN
6140
+ INSERT INTO messages_fts(messages_fts, rowid, content_text)
6141
+ VALUES ('delete', old.rowid, old.content_text);
6142
+ END;
6143
+
6144
+ CREATE TRIGGER IF NOT EXISTS messages_au AFTER UPDATE ON messages BEGIN
6145
+ INSERT INTO messages_fts(messages_fts, rowid, content_text)
6146
+ VALUES ('delete', old.rowid, old.content_text);
6147
+ INSERT INTO messages_fts(rowid, content_text)
6148
+ VALUES (new.rowid, new.content_text);
6149
+ END;
6827
6150
  `);
6828
- const deleteActivity = db.prepare(
6829
- "DELETE FROM session_file_activity WHERE agent_name = ? AND session_id = ?"
6830
- );
6831
- const insertActivity = prepareInsertFileActivity(db);
6832
- for (const session of sessions) {
6833
- if (!session.agent_name || !session.session_id || !session.project_identity_key) {
6834
- continue;
6835
- }
6836
- try {
6837
- const rows = loadMessages.all(session.agent_name, session.session_id);
6838
- const messages = rows.map((row) => messageFromBackfillRow(row));
6839
- const activities = extractSessionFileActivity(
6840
- String(session.agent_name),
6841
- String(session.session_id),
6842
- String(session.project_identity_key),
6843
- messages
6844
- );
6845
- deleteActivity.run(session.agent_name, session.session_id);
6846
- writeFileActivityRows(insertActivity, activities);
6847
- } catch {
6848
- continue;
6849
- }
6850
- }
6851
6151
  }
6852
- function invalidateSearchContentHashes(db) {
6853
- if (tableExists(db, "session_documents") && columnExists(db, "session_documents", "content_hash")) {
6854
- db.exec("UPDATE session_documents SET content_hash = ''");
6855
- }
6152
+ function dropMessageSearchTriggers(db) {
6153
+ db.exec(`
6154
+ DROP TRIGGER IF EXISTS messages_ai;
6155
+ DROP TRIGGER IF EXISTS messages_ad;
6156
+ DROP TRIGGER IF EXISTS messages_au;
6157
+ `);
6856
6158
  }
6857
- function rebuildSearchIndex(db) {
6858
- if (!tableExists(db, "session_documents_fts")) {
6859
- return;
6860
- }
6861
- db.exec("INSERT INTO session_documents_fts(session_documents_fts) VALUES ('rebuild')");
6159
+ function createFileActivityTables(db) {
6160
+ db.exec(`
6161
+ CREATE TABLE IF NOT EXISTS session_file_activity (
6162
+ agent_name TEXT NOT NULL,
6163
+ session_id TEXT NOT NULL,
6164
+ project_identity_key TEXT NOT NULL,
6165
+ path TEXT NOT NULL,
6166
+ kind TEXT NOT NULL,
6167
+ count INTEGER NOT NULL,
6168
+ latest_time INTEGER NOT NULL,
6169
+ PRIMARY KEY (agent_name, session_id, project_identity_key, path, kind),
6170
+ FOREIGN KEY (agent_name, session_id)
6171
+ REFERENCES sessions(agent_name, session_id)
6172
+ ON DELETE CASCADE
6173
+ );
6174
+
6175
+ CREATE INDEX IF NOT EXISTS idx_file_activity_project_latest
6176
+ ON session_file_activity(project_identity_key, latest_time);
6177
+
6178
+ CREATE INDEX IF NOT EXISTS idx_file_activity_latest
6179
+ ON session_file_activity(latest_time DESC, count DESC, path);
6180
+
6181
+ CREATE INDEX IF NOT EXISTS idx_file_activity_agent_latest
6182
+ ON session_file_activity(agent_name, latest_time DESC, count DESC, path);
6183
+
6184
+ CREATE INDEX IF NOT EXISTS idx_file_activity_project_latest_ordered
6185
+ ON session_file_activity(project_identity_key, latest_time DESC, count DESC, path);
6186
+
6187
+ CREATE INDEX IF NOT EXISTS idx_file_activity_path
6188
+ ON session_file_activity(path);
6189
+
6190
+ CREATE INDEX IF NOT EXISTS idx_file_activity_kind
6191
+ ON session_file_activity(kind);
6192
+ `);
6193
+ createFileActivityPathSearchTables(db);
6862
6194
  }
6863
- function rebuildMessageSearchIndex(db) {
6864
- if (!tableExists(db, "messages_fts")) {
6195
+ function createFileActivityPathSearchTables(db) {
6196
+ db.exec(`
6197
+ CREATE VIRTUAL TABLE IF NOT EXISTS session_file_activity_path_fts USING fts5(
6198
+ path,
6199
+ content='session_file_activity',
6200
+ content_rowid='rowid',
6201
+ tokenize='trigram'
6202
+ );
6203
+ `);
6204
+ createFileActivityPathSearchTriggers(db);
6205
+ }
6206
+ function createFileActivityPathSearchTriggers(db) {
6207
+ db.exec(`
6208
+ CREATE TRIGGER IF NOT EXISTS session_file_activity_path_ai
6209
+ AFTER INSERT ON session_file_activity BEGIN
6210
+ INSERT INTO session_file_activity_path_fts(rowid, path)
6211
+ VALUES (new.rowid, new.path);
6212
+ END;
6213
+
6214
+ CREATE TRIGGER IF NOT EXISTS session_file_activity_path_ad
6215
+ AFTER DELETE ON session_file_activity BEGIN
6216
+ INSERT INTO session_file_activity_path_fts(session_file_activity_path_fts, rowid, path)
6217
+ VALUES ('delete', old.rowid, old.path);
6218
+ END;
6219
+
6220
+ CREATE TRIGGER IF NOT EXISTS session_file_activity_path_au
6221
+ AFTER UPDATE ON session_file_activity BEGIN
6222
+ INSERT INTO session_file_activity_path_fts(session_file_activity_path_fts, rowid, path)
6223
+ VALUES ('delete', old.rowid, old.path);
6224
+ INSERT INTO session_file_activity_path_fts(rowid, path)
6225
+ VALUES (new.rowid, new.path);
6226
+ END;
6227
+ `);
6228
+ }
6229
+ function rebuildFileActivityPathIndex(db) {
6230
+ if (!tableExists(db, "session_file_activity_path_fts")) {
6865
6231
  return;
6866
6232
  }
6867
- db.exec("INSERT INTO messages_fts(messages_fts) VALUES ('rebuild')");
6233
+ db.exec(
6234
+ "INSERT INTO session_file_activity_path_fts(session_file_activity_path_fts) VALUES ('rebuild')"
6235
+ );
6868
6236
  }
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;
6237
+ function createSearchTables(db) {
6238
+ db.exec(`
6239
+ CREATE TABLE IF NOT EXISTS session_documents (
6240
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
6241
+ agent_name TEXT NOT NULL,
6242
+ session_id TEXT NOT NULL,
6243
+ slug TEXT NOT NULL,
6244
+ title TEXT NOT NULL,
6245
+ directory TEXT NOT NULL,
6246
+ time_created INTEGER NOT NULL,
6247
+ time_updated INTEGER,
6248
+ activity_time INTEGER NOT NULL,
6249
+ content_text TEXT NOT NULL,
6250
+ content_hash TEXT NOT NULL,
6251
+ indexed_at INTEGER NOT NULL,
6252
+ UNIQUE(agent_name, session_id)
6253
+ );
6254
+
6255
+ CREATE VIRTUAL TABLE IF NOT EXISTS session_documents_fts USING fts5(
6256
+ title,
6257
+ content_text,
6258
+ content='session_documents',
6259
+ content_rowid='id'
6260
+ );
6261
+ `);
6262
+ createSearchTriggers(db);
6263
+ }
6264
+ function createSearchTriggers(db) {
6265
+ db.exec(`
6266
+ CREATE TRIGGER IF NOT EXISTS session_documents_ai AFTER INSERT ON session_documents BEGIN
6267
+ INSERT INTO session_documents_fts(rowid, title, content_text)
6268
+ VALUES (new.id, new.title, new.content_text);
6269
+ END;
6270
+
6271
+ CREATE TRIGGER IF NOT EXISTS session_documents_ad AFTER DELETE ON session_documents BEGIN
6272
+ INSERT INTO session_documents_fts(session_documents_fts, rowid, title, content_text)
6273
+ VALUES ('delete', old.id, old.title, old.content_text);
6274
+ END;
6275
+
6276
+ CREATE TRIGGER IF NOT EXISTS session_documents_au AFTER UPDATE ON session_documents BEGIN
6277
+ INSERT INTO session_documents_fts(session_documents_fts, rowid, title, content_text)
6278
+ VALUES ('delete', old.id, old.title, old.content_text);
6279
+ INSERT INTO session_documents_fts(rowid, title, content_text)
6280
+ VALUES (new.id, new.title, new.content_text);
6281
+ END;
6282
+ `);
6283
+ }
6284
+ function dropSearchTriggers(db) {
6285
+ db.exec(`
6286
+ DROP TRIGGER IF EXISTS session_documents_ai;
6287
+ DROP TRIGGER IF EXISTS session_documents_ad;
6288
+ DROP TRIGGER IF EXISTS session_documents_au;
6289
+ `);
6875
6290
  }
6876
- function ensureFtsReady(db) {
6877
- if (!tableExists(db, "session_documents_fts")) {
6878
- createSearchTables(db);
6291
+ function ensureProjectColumns(db) {
6292
+ if (!tableExists(db, "session_documents")) {
6293
+ return;
6879
6294
  }
6880
- createSearchTriggers(db);
6881
- const needsMessageSearchRebuild = !tableExists(db, "messages_fts");
6882
- createMessageSearchTables(db);
6883
- if (needsMessageSearchRebuild) {
6884
- rebuildMessageSearchIndex(db);
6295
+ if (!columnExists(db, "session_documents", "project_identity_kind")) {
6296
+ db.exec(
6297
+ "ALTER TABLE session_documents ADD COLUMN project_identity_kind TEXT NOT NULL DEFAULT 'path'"
6298
+ );
6885
6299
  }
6886
- }
6887
- function ensureFtsConsistency(db) {
6888
- ensureFtsReady(db);
6889
- const cachePath = getCachePath2();
6890
- if (ftsIntegrityCheckedPath === cachePath) {
6891
- return;
6300
+ if (!columnExists(db, "session_documents", "project_identity_key")) {
6301
+ db.exec(
6302
+ "ALTER TABLE session_documents ADD COLUMN project_identity_key TEXT NOT NULL DEFAULT ''"
6303
+ );
6892
6304
  }
6893
- try {
6305
+ if (!columnExists(db, "session_documents", "project_display_name")) {
6894
6306
  db.exec(
6895
- "INSERT INTO session_documents_fts(session_documents_fts, rank) VALUES ('integrity-check', 1)"
6307
+ "ALTER TABLE session_documents ADD COLUMN project_display_name TEXT NOT NULL DEFAULT ''"
6896
6308
  );
6897
- db.exec("INSERT INTO messages_fts(messages_fts, rank) VALUES ('integrity-check', 1)");
6898
- ftsIntegrityCheckedPath = cachePath;
6899
- } catch {
6900
- rebuildSearchIndex(db);
6901
- rebuildMessageSearchIndex(db);
6902
- ftsIntegrityCheckedPath = cachePath;
6903
6309
  }
6904
6310
  }
6905
- function setCacheSchemaVersion(db) {
6906
- createCacheTables(db);
6907
- setUserVersion(db, CACHE_SCHEMA_VERSION);
6908
- db.prepare(
6909
- `
6910
- INSERT INTO cache_meta(key, value)
6911
- VALUES ('version', ?)
6912
- ON CONFLICT(key) DO UPDATE SET value = excluded.value
6913
- `
6914
- ).run(String(CACHE_SCHEMA_VERSION));
6311
+ function createProjectTables(db) {
6312
+ ensureProjectColumns(db);
6313
+ db.exec(`
6314
+ CREATE TABLE IF NOT EXISTS project_sessions (
6315
+ agent_name TEXT NOT NULL,
6316
+ session_id TEXT NOT NULL,
6317
+ identity_kind TEXT NOT NULL,
6318
+ identity_key TEXT NOT NULL,
6319
+ display_name TEXT NOT NULL,
6320
+ directory TEXT NOT NULL,
6321
+ activity_time INTEGER NOT NULL,
6322
+ PRIMARY KEY (agent_name, session_id)
6323
+ );
6324
+
6325
+ CREATE INDEX IF NOT EXISTS idx_project_sessions_identity
6326
+ ON project_sessions(identity_kind, identity_key);
6327
+ `);
6328
+ createProjectGroupsView(db);
6915
6329
  }
6916
- function ensureSchema(db, dbPath) {
6917
- const currentVersion = getCurrentCacheSchemaVersion(db);
6918
- if (currentVersion === 0 && !hasAnyCacheSchema(db)) {
6919
- createLatestCacheSchema(db);
6920
- setCacheSchemaVersion(db);
6330
+ function createProjectGroupsView(db) {
6331
+ if (!tableExists(db, "sessions")) {
6332
+ db.exec(`
6333
+ CREATE VIEW IF NOT EXISTS project_groups_v AS
6334
+ SELECT
6335
+ identity_kind,
6336
+ identity_key,
6337
+ MIN(display_name) AS display_name,
6338
+ GROUP_CONCAT(DISTINCT agent_name) AS sources_csv,
6339
+ COUNT(*) AS session_count,
6340
+ MAX(activity_time) AS last_activity
6341
+ FROM project_sessions
6342
+ GROUP BY identity_kind, identity_key;
6343
+ `);
6921
6344
  return;
6922
6345
  }
6923
- runSchemaMigrations(db, {
6924
- dbPath,
6925
- currentVersion,
6926
- targetVersion: CACHE_SCHEMA_VERSION,
6927
- backupLabel: "cache-migration",
6928
- backupTables: [
6929
- "agent_cache",
6930
- "cache_initialization",
6931
- "cached_sessions",
6932
- "sessions",
6933
- "messages",
6934
- "message_tools",
6935
- "session_file_activity",
6936
- "session_documents",
6937
- "project_sessions"
6938
- ],
6939
- migrations: [
6940
- { version: 3, migrate: createCacheTables },
6941
- { version: 4, migrate: createSearchTables },
6942
- { version: 5, migrate: migrateProjectIdentity },
6943
- {
6944
- version: 6,
6945
- destructive: true,
6946
- migrate(db2) {
6947
- createLatestCacheSchema(db2);
6948
- recreateSearchIndexSchema(db2);
6949
- invalidateSearchContentHashes(db2);
6950
- }
6951
- },
6952
- { version: 7, migrate: backfillStructuredSessions },
6953
- { version: 8, migrate: backfillFileActivity },
6954
- {
6955
- version: 9,
6956
- migrate(db2) {
6957
- createMessageSearchTables(db2);
6958
- rebuildMessageSearchIndex(db2);
6959
- }
6960
- },
6961
- {
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
- }
6346
+ db.exec(`
6347
+ CREATE VIEW IF NOT EXISTS project_groups_v AS
6348
+ SELECT
6349
+ project_identity_kind AS identity_kind,
6350
+ project_identity_key AS identity_key,
6351
+ MIN(project_display_name) AS display_name,
6352
+ GROUP_CONCAT(DISTINCT agent_name) AS sources_csv,
6353
+ COUNT(*) AS session_count,
6354
+ MAX(activity_time) AS last_activity
6355
+ FROM sessions
6356
+ GROUP BY project_identity_kind, project_identity_key;
6357
+ `);
6987
6358
  }
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
- ]);
6359
+ function recreateProjectGroupsView(db) {
6360
+ db.exec("DROP VIEW IF EXISTS project_groups_v");
6361
+ createProjectGroupsView(db);
7004
6362
  }
7005
- function escapeFtsTerm(value) {
7006
- return value.replaceAll('"', '""');
6363
+ function createLatestCacheSchema(db) {
6364
+ createCacheTables(db);
6365
+ createSessionTables(db);
6366
+ createMessageSearchTables(db);
6367
+ createFileActivityTables(db);
6368
+ createSearchTables(db);
6369
+ createProjectTables(db);
7007
6370
  }
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;
6371
+ function recreateSearchIndexSchema(db) {
6372
+ db.exec(`
6373
+ DROP TRIGGER IF EXISTS session_documents_ai;
6374
+ DROP TRIGGER IF EXISTS session_documents_ad;
6375
+ DROP TRIGGER IF EXISTS session_documents_au;
6376
+ DROP TABLE IF EXISTS session_documents_fts;
6377
+ `);
6378
+ createSearchTables(db);
6379
+ rebuildSearchIndex(db);
7031
6380
  }
7032
- function unwrapSearchValue(value) {
7033
- const trimmed = value.trim();
7034
- if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
7035
- return trimmed.slice(1, -1).trim();
6381
+ function readLegacyCacheVersion(db) {
6382
+ if (!tableExists(db, "cache_meta") || !columnExists(db, "cache_meta", "key") || !columnExists(db, "cache_meta", "value")) {
6383
+ return 0;
7036
6384
  }
7037
- return trimmed;
6385
+ const versionRow = db.prepare("SELECT value FROM cache_meta WHERE key = 'version'").get();
6386
+ return Number(versionRow?.value ?? 0);
7038
6387
  }
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;
6388
+ function inferCacheSchemaVersion(db) {
6389
+ if (tableExists(db, "message_tools")) {
6390
+ return 11;
7046
6391
  }
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;
6392
+ if (tableExists(db, "session_file_activity_path_fts")) {
6393
+ return 10;
6394
+ }
6395
+ if (tableExists(db, "messages_fts")) {
6396
+ return 9;
6397
+ }
6398
+ if (tableExists(db, "session_file_activity")) {
6399
+ return 8;
7058
6400
  }
7059
- const amount = Number(raw);
7060
- if (!Number.isNaN(amount)) {
7061
- filters.costMin = amount;
7062
- filters.costMax = amount;
6401
+ if (tableExists(db, "sessions") || tableExists(db, "messages")) {
6402
+ return 7;
6403
+ }
6404
+ if (tableExists(db, "project_sessions") || columnExists(db, "session_documents", "project_identity_key")) {
6405
+ return 5;
6406
+ }
6407
+ if (tableExists(db, "session_documents")) {
6408
+ return 4;
6409
+ }
6410
+ if (tableExists(db, "cached_sessions") || tableExists(db, "agent_cache")) {
6411
+ return 3;
7063
6412
  }
6413
+ return 0;
7064
6414
  }
7065
- function appendUnique(values, value) {
7066
- if (values?.includes(value)) return values;
7067
- return [...values ?? [], value];
6415
+ function getCurrentCacheSchemaVersion(db) {
6416
+ const userVersion = getUserVersion(db);
6417
+ if (userVersion > 0) {
6418
+ return userVersion;
6419
+ }
6420
+ const legacyVersion = readLegacyCacheVersion(db);
6421
+ return Math.max(legacyVersion, inferCacheSchemaVersion(db));
7068
6422
  }
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";
6423
+ function hasAnyCacheSchema(db) {
6424
+ return [
6425
+ "cache_meta",
6426
+ "agent_cache",
6427
+ "cached_sessions",
6428
+ "sessions",
6429
+ "messages",
6430
+ "message_tools",
6431
+ "session_file_activity",
6432
+ "session_file_activity_path_fts",
6433
+ "session_documents",
6434
+ "session_documents_fts",
6435
+ "project_sessions"
6436
+ ].some((table) => tableExists(db, table));
7071
6437
  }
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);
6438
+ function backfillProjectSessions(db) {
6439
+ if (!tableExists(db, "cached_sessions") || !tableExists(db, "project_sessions")) {
6440
+ return;
6441
+ }
6442
+ const rows = db.prepare("SELECT agent_name, session_id, session_json FROM cached_sessions").all();
6443
+ const upsert = db.prepare(`
6444
+ INSERT INTO project_sessions(
6445
+ agent_name,
6446
+ session_id,
6447
+ identity_kind,
6448
+ identity_key,
6449
+ display_name,
6450
+ directory,
6451
+ activity_time
6452
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
6453
+ ON CONFLICT(agent_name, session_id) DO UPDATE SET
6454
+ identity_kind = excluded.identity_kind,
6455
+ identity_key = excluded.identity_key,
6456
+ display_name = excluded.display_name,
6457
+ directory = excluded.directory,
6458
+ activity_time = excluded.activity_time
6459
+ `);
6460
+ for (const row of rows) {
6461
+ if (!row.session_json || !row.agent_name || !row.session_id) {
7080
6462
  continue;
7081
6463
  }
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);
6464
+ try {
6465
+ const session = JSON.parse(row.session_json);
6466
+ const identity = session.project_identity ?? computeIdentity(session.directory, realFs);
6467
+ upsert.run(
6468
+ row.agent_name,
6469
+ row.session_id,
6470
+ identity.kind,
6471
+ identity.key,
6472
+ identity.displayName,
6473
+ session.directory,
6474
+ session.time_updated ?? session.time_created
6475
+ );
6476
+ } catch {
6477
+ continue;
7114
6478
  }
7115
6479
  }
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
6480
  }
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
- }
6481
+ function backfillSessionDocumentProjects(db) {
6482
+ if (!tableExists(db, "session_documents") || !columnExists(db, "session_documents", "project_identity_key")) {
7144
6483
  return;
7145
6484
  }
7146
- if (typeof value === "number" || typeof value === "boolean") {
7147
- chunks.push(String(value));
7148
- return;
6485
+ const rows = db.prepare("SELECT id, directory FROM session_documents").all();
6486
+ const update = db.prepare(`
6487
+ UPDATE session_documents
6488
+ SET
6489
+ project_identity_kind = ?,
6490
+ project_identity_key = ?,
6491
+ project_display_name = ?
6492
+ WHERE id = ?
6493
+ `);
6494
+ for (const row of rows) {
6495
+ const identity = computeIdentity(String(row.directory ?? ""), realFs);
6496
+ update.run(identity.kind, identity.key, identity.displayName, Number(row.id));
7149
6497
  }
7150
- if (Array.isArray(value)) {
7151
- for (const item of value) {
7152
- appendPlainText(item, chunks);
6498
+ }
6499
+ function migrateProjectIdentity(db) {
6500
+ createProjectTables(db);
6501
+ backfillProjectSessions(db);
6502
+ backfillSessionDocumentProjects(db);
6503
+ }
6504
+ function refreshProjectIdentities(db) {
6505
+ if (tableExists(db, "sessions") && columnExists(db, "sessions", "project_identity_key") && columnExists(db, "sessions", "directory")) {
6506
+ const rows = db.prepare("SELECT agent_name, session_id, directory FROM sessions").all();
6507
+ const update = db.prepare(`
6508
+ UPDATE sessions
6509
+ SET
6510
+ project_identity_kind = ?,
6511
+ project_identity_key = ?,
6512
+ project_display_name = ?
6513
+ WHERE agent_name = ? AND session_id = ?
6514
+ `);
6515
+ const updateFileActivity = tableExists(db, "session_file_activity") && columnExists(db, "session_file_activity", "project_identity_key") ? db.prepare(`
6516
+ UPDATE session_file_activity
6517
+ SET project_identity_key = ?
6518
+ WHERE agent_name = ? AND session_id = ?
6519
+ `) : null;
6520
+ for (const row of rows) {
6521
+ const identity = computeIdentity(String(row.directory ?? ""), realFs);
6522
+ update.run(identity.kind, identity.key, identity.displayName, row.agent_name, row.session_id);
6523
+ updateFileActivity?.run(identity.key, row.agent_name, row.session_id);
7153
6524
  }
7154
- return;
7155
6525
  }
7156
- if (typeof value === "object") {
7157
- for (const nested of Object.values(value)) {
7158
- appendPlainText(nested, chunks);
6526
+ if (tableExists(db, "project_sessions") && columnExists(db, "project_sessions", "identity_key") && columnExists(db, "project_sessions", "directory")) {
6527
+ const rows = db.prepare("SELECT agent_name, session_id, directory FROM project_sessions").all();
6528
+ const update = db.prepare(`
6529
+ UPDATE project_sessions
6530
+ SET
6531
+ identity_kind = ?,
6532
+ identity_key = ?,
6533
+ display_name = ?
6534
+ WHERE agent_name = ? AND session_id = ?
6535
+ `);
6536
+ for (const row of rows) {
6537
+ const identity = computeIdentity(String(row.directory ?? ""), realFs);
6538
+ update.run(identity.kind, identity.key, identity.displayName, row.agent_name, row.session_id);
7159
6539
  }
7160
6540
  }
6541
+ backfillSessionDocumentProjects(db);
6542
+ recreateProjectGroupsView(db);
7161
6543
  }
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);
6544
+ function backfillStructuredSessions(db) {
6545
+ createSessionTables(db);
6546
+ recreateProjectGroupsView(db);
6547
+ const upsertSession = prepareUpsertSession(db);
6548
+ if (tableExists(db, "cached_sessions")) {
6549
+ const rows = db.prepare(
6550
+ "SELECT agent_name, session_id, session_json, meta_json, rowid AS sort_index FROM cached_sessions ORDER BY agent_name, rowid"
6551
+ ).all();
6552
+ for (const row of rows) {
6553
+ if (!row.agent_name || !row.session_json) {
6554
+ continue;
6555
+ }
6556
+ try {
6557
+ const session = JSON.parse(row.session_json);
6558
+ upsertSessionRow(
6559
+ upsertSession,
6560
+ String(row.agent_name),
6561
+ session,
6562
+ row.meta_json ?? null,
6563
+ Number(row.sort_index ?? 0),
6564
+ sourcePathFromMetaJson(row.meta_json)
6565
+ );
6566
+ } catch {
6567
+ continue;
6568
+ }
7180
6569
  }
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
6570
  }
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);
6571
+ if (!tableExists(db, "session_documents")) {
6572
+ return;
7225
6573
  }
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);
6574
+ const documentRows = db.prepare(
6575
+ `
6576
+ SELECT
6577
+ d.agent_name,
6578
+ d.session_id,
6579
+ d.slug,
6580
+ d.title,
6581
+ d.directory,
6582
+ d.project_identity_kind,
6583
+ d.project_identity_key,
6584
+ d.project_display_name,
6585
+ d.time_created,
6586
+ d.time_updated,
6587
+ d.activity_time,
6588
+ d.id
6589
+ FROM session_documents d
6590
+ LEFT JOIN sessions s ON s.agent_name = d.agent_name AND s.session_id = d.session_id
6591
+ WHERE s.session_id IS NULL
6592
+ ORDER BY d.id
6593
+ `
6594
+ ).all();
6595
+ for (const row of documentRows) {
6596
+ const directory = String(row.directory ?? "");
6597
+ const identity = row.project_identity_key && row.project_identity_kind && row.project_display_name ? {
6598
+ kind: row.project_identity_kind,
6599
+ key: String(row.project_identity_key),
6600
+ displayName: String(row.project_display_name)
6601
+ } : computeIdentity(directory, realFs);
6602
+ upsertSessionRow(
6603
+ upsertSession,
6604
+ String(row.agent_name),
6605
+ {
6606
+ id: String(row.session_id),
6607
+ slug: String(row.slug),
6608
+ title: String(row.title),
6609
+ directory,
6610
+ project_identity: identity,
6611
+ time_created: Number(row.time_created ?? row.activity_time ?? 0),
6612
+ time_updated: row.time_updated == null ? void 0 : Number(row.time_updated),
6613
+ stats: {
6614
+ message_count: 0,
6615
+ total_input_tokens: 0,
6616
+ total_output_tokens: 0,
6617
+ total_cost: 0
6618
+ }
6619
+ },
6620
+ null,
6621
+ Number(row.id ?? 0),
6622
+ null
6623
+ );
7258
6624
  }
7259
- return chunks.join("\n");
7260
6625
  }
7261
- function deleteLegacyCacheFile() {
7262
- const legacyPath = getLegacyCachePath();
7263
- if (!existsSync11(legacyPath)) {
6626
+ function backfillMessageTools(db) {
6627
+ createMessageToolTables(db);
6628
+ if (!tableExists(db, "messages")) {
7264
6629
  return;
7265
6630
  }
7266
- try {
7267
- unlinkSync(legacyPath);
7268
- } catch {
6631
+ db.exec("DELETE FROM message_tools");
6632
+ const rows = db.prepare(
6633
+ `
6634
+ SELECT agent_name, session_id, message_index, tool_metadata_json
6635
+ FROM messages
6636
+ WHERE tool_metadata_json IS NOT NULL
6637
+ `
6638
+ ).all();
6639
+ const insertTool = prepareInsertMessageTool(db);
6640
+ for (const row of rows) {
6641
+ if (!row.agent_name || !row.session_id || row.message_index == null) {
6642
+ continue;
6643
+ }
6644
+ for (const toolName of toolNamesFromMetadataJson(row.tool_metadata_json)) {
6645
+ insertTool.run(row.agent_name, row.session_id, row.message_index, toolName);
6646
+ }
7269
6647
  }
7270
6648
  }
7271
- function loadCachedSessions(agentName) {
7272
- if (!hasCacheStorage()) {
7273
- return null;
6649
+ function backfillFileActivity(db) {
6650
+ createFileActivityTables(db);
6651
+ if (!tableExists(db, "sessions") || !tableExists(db, "messages")) {
6652
+ return;
7274
6653
  }
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(
6654
+ const sessions = db.prepare(
6655
+ `
6656
+ SELECT agent_name, session_id, project_identity_key
6657
+ FROM sessions
6658
+ ORDER BY agent_name, session_id
7282
6659
  `
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
- }
6660
+ ).all();
6661
+ const loadMessages = db.prepare(`
6662
+ SELECT
6663
+ message_id,
6664
+ role,
6665
+ time_created,
6666
+ time_completed,
6667
+ agent,
6668
+ mode,
6669
+ model,
6670
+ provider,
6671
+ parts_json,
6672
+ subagent_id,
6673
+ nickname
6674
+ FROM messages
6675
+ WHERE agent_name = ? AND session_id = ?
6676
+ ORDER BY message_index
6677
+ `);
6678
+ const deleteActivity = db.prepare(
6679
+ "DELETE FROM session_file_activity WHERE agent_name = ? AND session_id = ?"
6680
+ );
6681
+ const insertActivity = prepareInsertFileActivity(db);
6682
+ for (const session of sessions) {
6683
+ if (!session.agent_name || !session.session_id || !session.project_identity_key) {
6684
+ continue;
7320
6685
  }
7321
- return { sessions, meta, timestamp };
7322
- });
6686
+ try {
6687
+ const rows = loadMessages.all(session.agent_name, session.session_id);
6688
+ const messages = rows.map((row) => messageFromBackfillRow(row));
6689
+ const activities = extractSessionFileActivity(
6690
+ String(session.agent_name),
6691
+ String(session.session_id),
6692
+ String(session.project_identity_key),
6693
+ messages
6694
+ );
6695
+ deleteActivity.run(session.agent_name, session.session_id);
6696
+ writeFileActivityRows(insertActivity, activities);
6697
+ } catch {
6698
+ continue;
6699
+ }
6700
+ }
6701
+ }
6702
+ function invalidateSearchContentHashes(db) {
6703
+ if (tableExists(db, "session_documents") && columnExists(db, "session_documents", "content_hash")) {
6704
+ db.exec("UPDATE session_documents SET content_hash = ''");
6705
+ }
7323
6706
  }
7324
- function isAgentCacheInitialized(agentName, indexVersion = CACHE_INITIALIZATION_VERSION) {
7325
- if (!hasCacheStorage()) {
7326
- return false;
6707
+ function rebuildSearchIndex(db) {
6708
+ if (!tableExists(db, "session_documents_fts")) {
6709
+ return;
7327
6710
  }
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;
6711
+ db.exec("INSERT INTO session_documents_fts(session_documents_fts) VALUES ('rebuild')");
7339
6712
  }
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
- });
6713
+ function rebuildMessageSearchIndex(db) {
6714
+ if (!tableExists(db, "messages_fts")) {
6715
+ return;
6716
+ }
6717
+ db.exec("INSERT INTO messages_fts(messages_fts) VALUES ('rebuild')");
7353
6718
  }
7354
- function loadCachedSessionData(agentName, sessionId) {
7355
- if (!hasCacheStorage()) {
7356
- return null;
6719
+ function ensureFtsReady(db) {
6720
+ if (!tableExists(db, "session_documents_fts")) {
6721
+ createSearchTables(db);
6722
+ }
6723
+ createSearchTriggers(db);
6724
+ const needsMessageSearchRebuild = !tableExists(db, "messages_fts");
6725
+ createMessageSearchTables(db);
6726
+ if (needsMessageSearchRebuild) {
6727
+ rebuildMessageSearchIndex(db);
7357
6728
  }
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
6729
  }
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 = ?"
6730
+ function ensureFtsConsistency(db) {
6731
+ ensureFtsReady(db);
6732
+ const cachePath = getCachePath2();
6733
+ if (getFtsIntegrityCheckedPath() === cachePath) {
6734
+ return;
6735
+ }
6736
+ try {
6737
+ db.exec(
6738
+ "INSERT INTO session_documents_fts(session_documents_fts, rank) VALUES ('integrity-check', 1)"
7452
6739
  );
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);
6740
+ db.exec("INSERT INTO messages_fts(messages_fts, rank) VALUES ('integrity-check', 1)");
6741
+ setFtsIntegrityCheckedPath(cachePath);
6742
+ } catch {
6743
+ rebuildSearchIndex(db);
6744
+ rebuildMessageSearchIndex(db);
6745
+ setFtsIntegrityCheckedPath(cachePath);
6746
+ }
6747
+ }
6748
+ function setCacheSchemaVersion(db) {
6749
+ createCacheTables(db);
6750
+ setUserVersion(db, CACHE_SCHEMA_VERSION);
6751
+ db.prepare(
6752
+ `
6753
+ INSERT INTO cache_meta(key, value)
6754
+ VALUES ('version', ?)
6755
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value
6756
+ `
6757
+ ).run(String(CACHE_SCHEMA_VERSION));
6758
+ }
6759
+ function ensureSchema(db, dbPath) {
6760
+ const currentVersion = getCurrentCacheSchemaVersion(db);
6761
+ if (currentVersion === 0 && !hasAnyCacheSchema(db)) {
6762
+ createLatestCacheSchema(db);
6763
+ setCacheSchemaVersion(db);
6764
+ return;
6765
+ }
6766
+ runSchemaMigrations(db, {
6767
+ dbPath,
6768
+ currentVersion,
6769
+ targetVersion: CACHE_SCHEMA_VERSION,
6770
+ backupLabel: "cache-migration",
6771
+ backupTables: [
6772
+ "agent_cache",
6773
+ "cache_initialization",
6774
+ "cached_sessions",
6775
+ "sessions",
6776
+ "messages",
6777
+ "message_tools",
6778
+ "session_file_activity",
6779
+ "session_documents",
6780
+ "project_sessions"
6781
+ ],
6782
+ migrations: [
6783
+ { version: 3, migrate: createCacheTables },
6784
+ { version: 4, migrate: createSearchTables },
6785
+ { version: 5, migrate: migrateProjectIdentity },
6786
+ {
6787
+ version: 6,
6788
+ destructive: true,
6789
+ migrate(db2) {
6790
+ createLatestCacheSchema(db2);
6791
+ recreateSearchIndexSchema(db2);
6792
+ invalidateSearchContentHashes(db2);
7479
6793
  }
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();
6794
+ },
6795
+ { version: 7, migrate: backfillStructuredSessions },
6796
+ { version: 8, migrate: backfillFileActivity },
6797
+ {
6798
+ version: 9,
6799
+ migrate(db2) {
6800
+ createMessageSearchTables(db2);
6801
+ rebuildMessageSearchIndex(db2);
6802
+ }
6803
+ },
6804
+ {
6805
+ version: 10,
6806
+ migrate(db2) {
6807
+ createFileActivityPathSearchTables(db2);
6808
+ rebuildFileActivityPathIndex(db2);
6809
+ }
6810
+ },
6811
+ {
6812
+ version: 11,
6813
+ migrate(db2) {
6814
+ backfillMessageTools(db2);
6815
+ }
6816
+ },
6817
+ {
6818
+ version: 12,
6819
+ migrate(db2) {
6820
+ refreshProjectIdentities(db2);
6821
+ }
6822
+ },
6823
+ { version: 13, migrate: createCacheTables }
6824
+ ]
7499
6825
  });
6826
+ createLatestCacheSchema(db);
6827
+ if (getUserVersion(db) <= CACHE_SCHEMA_VERSION) {
6828
+ setCacheSchemaVersion(db);
6829
+ }
7500
6830
  }
7501
- function saveCachedSessionChanges(agentName, changes, removedSessionIds, meta = {}) {
7502
- if (changes.length === 0 && removedSessionIds.length === 0) {
7503
- return;
6831
+ function shouldBulkSyncSearchIndex(options, changedCount) {
6832
+ if (options.isBulk != null) {
6833
+ return options.isBulk;
7504
6834
  }
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);
6835
+ const threshold = options.bulkThreshold ?? SEARCH_INDEX_BULK_SYNC_THRESHOLD;
6836
+ return threshold > 0 && changedCount >= threshold;
6837
+ }
6838
+ function sessionContentHash(session) {
6839
+ return JSON.stringify([
6840
+ session.slug,
6841
+ session.title,
6842
+ session.directory,
6843
+ session.time_created,
6844
+ session.time_updated ?? session.time_created,
6845
+ session.stats.message_count,
6846
+ session.stats.total_input_tokens,
6847
+ session.stats.total_output_tokens,
6848
+ session.stats.total_cache_read_tokens ?? 0,
6849
+ session.stats.total_cache_create_tokens ?? 0,
6850
+ session.stats.total_cost,
6851
+ session.stats.cost_source ?? "",
6852
+ session.stats.total_tokens ?? 0
6853
+ ]);
6854
+ }
6855
+ function escapeFtsTerm(value) {
6856
+ return value.replaceAll('"', '""');
6857
+ }
6858
+ function splitSearchTokens(input) {
6859
+ const tokens = [];
6860
+ let token = "";
6861
+ let inQuote = false;
6862
+ for (const char of input) {
6863
+ if (char === '"') {
6864
+ inQuote = !inQuote;
6865
+ token += char;
6866
+ continue;
6867
+ }
6868
+ if (/\s/.test(char) && !inQuote) {
6869
+ if (token) {
6870
+ tokens.push(token);
6871
+ token = "";
7560
6872
  }
7561
- });
7562
- write();
7563
- deleteLegacyCacheFile();
7564
- });
6873
+ continue;
6874
+ }
6875
+ token += char;
6876
+ }
6877
+ if (token) {
6878
+ tokens.push(token);
6879
+ }
6880
+ return tokens;
7565
6881
  }
7566
- function clearCache() {
7567
- ftsIntegrityCheckedPath = null;
7568
- if (!hasCacheStorage()) {
7569
- deleteLegacyCacheFile();
6882
+ function unwrapSearchValue(value) {
6883
+ const trimmed = value.trim();
6884
+ if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
6885
+ return trimmed.slice(1, -1).trim();
6886
+ }
6887
+ return trimmed;
6888
+ }
6889
+ function parseCostQualifier(value, filters) {
6890
+ const raw = unwrapSearchValue(value);
6891
+ const range = raw.match(/^(\d+(?:\.\d+)?)\.\.(\d+(?:\.\d+)?)$/);
6892
+ if (range) {
6893
+ filters.costMin = Number(range[1]);
6894
+ filters.costMax = Number(range[2]);
7570
6895
  return;
7571
6896
  }
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)) {
6897
+ const comparison = raw.match(/^(>=|>|<=|<)(\d+(?:\.\d+)?)$/);
6898
+ if (comparison) {
6899
+ const amount2 = Number(comparison[2]);
6900
+ if (comparison[1]?.includes(">")) {
6901
+ filters.costMin = amount2;
6902
+ filters.costMinExclusive = comparison[1] === ">";
6903
+ } else {
6904
+ filters.costMax = amount2;
6905
+ filters.costMaxExclusive = comparison[1] === "<";
6906
+ }
6907
+ return;
6908
+ }
6909
+ const amount = Number(raw);
6910
+ if (!Number.isNaN(amount)) {
6911
+ filters.costMin = amount;
6912
+ filters.costMax = amount;
6913
+ }
6914
+ }
6915
+ function appendUnique(values, value) {
6916
+ if (values?.includes(value)) return values;
6917
+ return [...values ?? [], value];
6918
+ }
6919
+ function isSmartTag(value) {
6920
+ return value === "bugfix" || value === "refactoring" || value === "feature-dev" || value === "testing" || value === "docs" || value === "git-ops" || value === "build-deploy" || value === "exploration" || value === "planning";
6921
+ }
6922
+ function parseSearchQuery(input) {
6923
+ const filters = {};
6924
+ const textTokens = [];
6925
+ let hasQualifiers = false;
6926
+ for (const token of splitSearchTokens(input)) {
6927
+ const match = token.match(/^([a-zA-Z][a-zA-Z_-]*):(.+)$/);
6928
+ if (!match) {
6929
+ textTokens.push(token);
7591
6930
  continue;
7592
6931
  }
7593
- try {
7594
- rmSync(filePath, { force: true });
7595
- } catch {
6932
+ const key = match[1].toLowerCase();
6933
+ const value = unwrapSearchValue(match[2]);
6934
+ if (!value) continue;
6935
+ let consumed = true;
6936
+ if (key === "agent") filters.agent = value.toLowerCase();
6937
+ else if (key === "project") filters.project = value;
6938
+ else if (key === "projectkey" || key === "project-key") filters.projectKey = value;
6939
+ else if (key === "cwd") filters.cwd = value;
6940
+ else if (key === "tool") filters.tools = appendUnique(filters.tools, value.toLowerCase());
6941
+ else if (key === "file" || key === "path") filters.file = value;
6942
+ else if (key === "kind" || key === "filekind" || key === "file-kind") {
6943
+ if (value === "read" || value === "edit" || value === "write" || value === "delete") {
6944
+ filters.fileKind = value;
6945
+ } else {
6946
+ consumed = false;
6947
+ }
6948
+ } else if (key === "tag" || key === "signal") {
6949
+ const tag = value.toLowerCase();
6950
+ if (isSmartTag(tag)) {
6951
+ filters.tags = appendUnique(filters.tags, tag);
6952
+ } else {
6953
+ consumed = false;
6954
+ }
6955
+ } else if (key === "cost") {
6956
+ parseCostQualifier(value, filters);
6957
+ } else {
6958
+ consumed = false;
6959
+ }
6960
+ if (consumed) {
6961
+ hasQualifiers = true;
6962
+ } else {
6963
+ textTokens.push(token);
7596
6964
  }
7597
6965
  }
6966
+ return {
6967
+ text: textTokens.join(" ").trim(),
6968
+ filters,
6969
+ hasQualifiers
6970
+ };
7598
6971
  }
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 };
6972
+ function toFtsQuery(input) {
6973
+ const tokens = splitSearchTokens(input);
6974
+ const mapped = tokens.map((token) => {
6975
+ if (/^OR$/i.test(token)) {
6976
+ return "OR";
6977
+ }
6978
+ if (token.startsWith('"') && token.endsWith('"')) {
6979
+ return `"${escapeFtsTerm(token.slice(1, -1))}"`;
6980
+ }
6981
+ return `"${escapeFtsTerm(token)}"`;
6982
+ }).filter(
6983
+ (token, index, values) => token !== "OR" || index > 0 && index < values.length - 1 && values[index - 1] !== "OR" && values[index + 1] !== "OR"
6984
+ );
6985
+ return mapped.join(" ");
7611
6986
  }
7612
6987
  function loadSearchIndexEntry(agentName, change, loadSessionData) {
7613
6988
  try {
@@ -7943,17 +7318,6 @@ function sessionMatchesSearchCost(session, options) {
7943
7318
  }
7944
7319
  return true;
7945
7320
  }
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
7321
  function buildSessionSearchFilters(options) {
7958
7322
  const clauses = [];
7959
7323
  const params = [];
@@ -8081,307 +7445,656 @@ function buildTermSnippet(text, terms) {
8081
7445
  const end = Math.min(text.length, index + term.length + 80);
8082
7446
  return `${start > 0 ? "\u2026 " : ""}${highlightTerm(text.slice(start, end), term)}${end < text.length ? " \u2026" : ""}`;
8083
7447
  }
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";
7448
+ function messageMatchType(row) {
7449
+ if (row.role === "user") return "user_message";
7450
+ if (row.role === "tool" || row.mode === "tool" || row.tool_metadata_json) return "tool_output";
7451
+ return "assistant_reply";
7452
+ }
7453
+ function searchResultRowKey(row) {
7454
+ return `${String(row.agent_name)}\0${String(row.session_id)}`;
7455
+ }
7456
+ function fetchMessageSearchMatches(db, rows, ftsQuery, terms) {
7457
+ const candidates = rows.filter((row) => !textMatchesTerms(String(row.title ?? ""), terms));
7458
+ if (candidates.length === 0) {
7459
+ return /* @__PURE__ */ new Map();
7460
+ }
7461
+ const clauses = [];
7462
+ const params = [ftsQuery];
7463
+ for (const row of candidates) {
7464
+ clauses.push("(m.agent_name = ? AND m.session_id = ?)");
7465
+ params.push(String(row.agent_name), String(row.session_id));
7466
+ }
7467
+ const messageRows = db.prepare(
7468
+ `
7469
+ SELECT
7470
+ m.agent_name,
7471
+ m.session_id,
7472
+ m.message_index,
7473
+ m.role,
7474
+ m.mode,
7475
+ m.content_text,
7476
+ m.tool_metadata_json
7477
+ FROM messages_fts
7478
+ JOIN messages m ON m.rowid = messages_fts.rowid
7479
+ WHERE messages_fts MATCH ?
7480
+ AND (${clauses.join(" OR ")})
7481
+ ORDER BY m.message_index
7482
+ `
7483
+ ).all(...params);
7484
+ const matches = /* @__PURE__ */ new Map();
7485
+ for (const message of messageRows) {
7486
+ const key = searchResultRowKey(message);
7487
+ if (matches.has(key)) continue;
7488
+ const text = String(message.content_text ?? "");
7489
+ if (!textMatchesTerms(text, terms)) continue;
7490
+ matches.set(key, {
7491
+ snippet: buildTermSnippet(text, terms),
7492
+ matchType: messageMatchType(message)
7493
+ });
7494
+ }
7495
+ return matches;
7496
+ }
7497
+ function resolveSearchMatch(row, terms, messageMatches) {
7498
+ const title = String(row.title ?? "");
7499
+ if (terms.terms.length === 0) {
7500
+ return {
7501
+ snippet: `Recent session \xB7 ${String(row.directory ?? "")}`,
7502
+ matchType: "recent"
7503
+ };
7504
+ }
7505
+ if (textMatchesTerms(title, terms)) {
7506
+ return { snippet: buildTermSnippet(title, terms), matchType: "title" };
7507
+ }
7508
+ const messageMatch = messageMatches.get(searchResultRowKey(row));
7509
+ if (messageMatch) {
7510
+ return messageMatch;
7511
+ }
7512
+ return {
7513
+ snippet: String(row.snippet ?? ""),
7514
+ matchType: "assistant_reply"
7515
+ };
7516
+ }
7517
+ function rowsToSearchResults(db, rows, textQuery, ftsQuery = toFtsQuery(textQuery)) {
7518
+ const terms = parseTextTerms(textQuery);
7519
+ const messageMatches = terms.terms.length > 0 && ftsQuery ? fetchMessageSearchMatches(db, rows, ftsQuery, terms) : /* @__PURE__ */ new Map();
7520
+ return rows.map((row) => {
7521
+ const match = resolveSearchMatch(row, terms, messageMatches);
7522
+ return {
7523
+ agentName: String(row.agent_name),
7524
+ session: sessionHeadFromSearchRow(row),
7525
+ snippet: match.snippet,
7526
+ matchType: match.matchType
7527
+ };
7528
+ });
7529
+ }
7530
+ function searchSessions(query, options = {}) {
7531
+ const search = mergeSearchQueryOptions(query, options);
7532
+ const normalizedQuery = search.text.trim();
7533
+ if (!hasCacheStorage()) {
7534
+ return [];
7535
+ }
7536
+ const results = withCacheDb((db) => {
7537
+ ensureFtsReady(db);
7538
+ const filters = buildSessionSearchFilters(search.options);
7539
+ if (!normalizedQuery) {
7540
+ const rows2 = db.prepare(
7541
+ `
7542
+ SELECT
7543
+ ${searchSessionColumns()},
7544
+ '' AS snippet
7545
+ FROM sessions s
7546
+ WHERE 1 = 1
7547
+ ${filters.where}
7548
+ ORDER BY s.activity_time DESC
7549
+ LIMIT ?
7550
+ `
7551
+ ).all(...filters.params, search.options.limit ?? 50);
7552
+ return rowsToSearchResults(db, rows2, "");
7553
+ }
7554
+ const ftsQuery = toFtsQuery(normalizedQuery);
7555
+ if (!ftsQuery) return [];
7556
+ const rows = db.prepare(
7557
+ `
7558
+ SELECT
7559
+ ${searchSessionColumns()},
7560
+ COALESCE(
7561
+ NULLIF(snippet(session_documents_fts, 1, '<mark>', '</mark>', ' \u2026 ', 18), ''),
7562
+ highlight(session_documents_fts, 0, '<mark>', '</mark>')
7563
+ ) AS snippet
7564
+ FROM session_documents_fts
7565
+ JOIN session_documents d ON d.id = session_documents_fts.rowid
7566
+ JOIN sessions s ON s.agent_name = d.agent_name AND s.session_id = d.session_id
7567
+ WHERE session_documents_fts MATCH ?
7568
+ ${filters.where}
7569
+ ORDER BY bm25(session_documents_fts, 8.0, 1.0), s.activity_time DESC
7570
+ LIMIT ?
7571
+ `
7572
+ ).all(ftsQuery, ...filters.params, search.options.limit ?? 50);
7573
+ return rowsToSearchResults(db, rows, normalizedQuery, ftsQuery);
7574
+ });
7575
+ return results ?? [];
7576
+ }
7577
+ function fileActivityFilters(options) {
7578
+ const path2 = options.path ? normalizeFilePathSearch(options.path) : "";
7579
+ return {
7580
+ projectKey: options.projectKey ?? null,
7581
+ projectLike: options.project ? likePattern(options.project) : null,
7582
+ cwdKey: options.cwd ? computeIdentity(options.cwd, realFs).key : null,
7583
+ cwdLike: options.cwd ? likePattern(options.cwd) : null,
7584
+ path: path2,
7585
+ pathLike: path2 ? likePattern(path2) : null
7586
+ };
7587
+ }
7588
+ function fileActivityFromRow(row) {
7589
+ return {
7590
+ agent_name: String(row.agent_name),
7591
+ session_id: String(row.session_id),
7592
+ project_identity_key: String(row.project_identity_key ?? ""),
7593
+ path: String(row.path ?? ""),
7594
+ kind: row.kind ?? "read",
7595
+ count: Number(row.count ?? 0),
7596
+ latest_time: Number(row.latest_time ?? 0)
7597
+ };
7598
+ }
7599
+ function buildFileActivityWhere(options) {
7600
+ const filters = fileActivityFilters(options);
7601
+ const clauses = [];
7602
+ const params = [];
7603
+ if (options.agent != null) {
7604
+ clauses.push("fa.agent_name = ?");
7605
+ params.push(options.agent);
7606
+ }
7607
+ if (options.sessionId != null) {
7608
+ clauses.push("fa.session_id = ?");
7609
+ params.push(options.sessionId);
7610
+ }
7611
+ if (filters.projectKey != null) {
7612
+ clauses.push("fa.project_identity_key = ?");
7613
+ params.push(filters.projectKey);
7614
+ }
7615
+ if (filters.projectLike != null) {
7616
+ clauses.push(
7617
+ "(LOWER(fa.project_identity_key) LIKE ? ESCAPE '\\' OR LOWER(s.project_display_name) LIKE ? ESCAPE '\\' OR LOWER(s.directory) LIKE ? ESCAPE '\\')"
7618
+ );
7619
+ params.push(filters.projectLike, filters.projectLike, filters.projectLike);
7620
+ }
7621
+ if (filters.cwdKey != null) {
7622
+ clauses.push("(s.project_identity_key = ? OR LOWER(s.directory) LIKE ? ESCAPE '\\')");
7623
+ params.push(filters.cwdKey, filters.cwdLike);
7624
+ }
7625
+ if (filters.pathLike != null) {
7626
+ const pathQuery = filePathFtsQuery(filters.path);
7627
+ if (pathQuery) {
7628
+ clauses.push(
7629
+ "fa.rowid IN (SELECT rowid FROM session_file_activity_path_fts WHERE path MATCH ?)"
7630
+ );
7631
+ params.push(pathQuery);
7632
+ } else {
7633
+ clauses.push("LOWER(fa.path) LIKE ? ESCAPE '\\'");
7634
+ params.push(filters.pathLike);
7635
+ }
7636
+ }
7637
+ if (options.kind != null) {
7638
+ clauses.push("fa.kind = ?");
7639
+ params.push(options.kind);
7640
+ }
7641
+ if (options.from != null) {
7642
+ clauses.push("fa.latest_time >= ?");
7643
+ params.push(options.from);
7644
+ }
7645
+ if (options.to != null) {
7646
+ clauses.push("fa.latest_time <= ?");
7647
+ params.push(options.to);
7648
+ }
7649
+ return {
7650
+ where: clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "",
7651
+ params
7652
+ };
7653
+ }
7654
+ function listFileActivity(options = {}) {
7655
+ if (!hasCacheStorage()) {
7656
+ return [];
7657
+ }
7658
+ const filters = buildFileActivityWhere(options);
7659
+ const queryRows = (db) => db.prepare(
7660
+ `
7661
+ SELECT
7662
+ fa.agent_name,
7663
+ fa.session_id,
7664
+ fa.project_identity_key,
7665
+ fa.path,
7666
+ fa.kind,
7667
+ fa.count,
7668
+ fa.latest_time,
7669
+ s.slug,
7670
+ s.title,
7671
+ s.directory,
7672
+ s.project_identity_kind,
7673
+ s.project_display_name,
7674
+ s.time_created,
7675
+ s.time_updated,
7676
+ s.message_count,
7677
+ s.total_input_tokens,
7678
+ s.total_output_tokens,
7679
+ s.total_cache_read_tokens,
7680
+ s.total_cache_create_tokens,
7681
+ s.total_cost,
7682
+ s.cost_source,
7683
+ s.total_tokens
7684
+ FROM session_file_activity fa
7685
+ JOIN sessions s ON s.agent_name = fa.agent_name AND s.session_id = fa.session_id
7686
+ ${filters.where}
7687
+ ORDER BY fa.latest_time DESC, fa.count DESC, fa.path
7688
+ LIMIT ?
7689
+ `
7690
+ ).all(...filters.params, options.limit ?? 50);
7691
+ let rows = withCacheDbReadOnly(queryRows);
7692
+ if (rows == null && options.path) {
7693
+ rows = withCacheDb(queryRows);
7694
+ }
7695
+ return (rows ?? []).map((row) => ({
7696
+ ...fileActivityFromRow(row),
7697
+ session: sessionHeadFromSearchRow(row)
7698
+ }));
7699
+ }
7700
+ function listSessionFileActivity(agentName, sessionId) {
7701
+ return listFileActivity({ agent: agentName, sessionId, limit: 500 }).map(
7702
+ ({ session: _session, ...activity }) => activity
7703
+ );
8088
7704
  }
8089
- function searchResultRowKey(row) {
8090
- return `${String(row.agent_name)}\0${String(row.session_id)}`;
7705
+ function highlightFilePath(path2, query) {
7706
+ const needle = normalizeFilePathSearch(query);
7707
+ if (!needle) return path2;
7708
+ const lower = path2.toLowerCase();
7709
+ const index = lower.indexOf(needle.toLowerCase());
7710
+ if (index < 0) return path2;
7711
+ return `${path2.slice(0, index)}<mark>${path2.slice(index, index + needle.length)}</mark>${path2.slice(
7712
+ index + needle.length
7713
+ )}`;
8091
7714
  }
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)
7715
+ function searchFileActivitySessions(query, options = {}) {
7716
+ const search = mergeSearchQueryOptions(query, options);
7717
+ const path2 = normalizeFilePathSearch(search.options.file ?? search.text);
7718
+ if (!path2) return [];
7719
+ const rows = listFileActivity({
7720
+ agent: search.options.agent,
7721
+ projectKey: search.options.projectKey,
7722
+ project: search.options.project,
7723
+ cwd: search.options.cwd,
7724
+ path: path2,
7725
+ kind: search.options.fileKind,
7726
+ from: search.options.from,
7727
+ to: search.options.to,
7728
+ limit: (search.options.limit ?? 50) * 3
7729
+ });
7730
+ const seen = /* @__PURE__ */ new Set();
7731
+ const results = [];
7732
+ for (const row of rows) {
7733
+ const key = `${row.agent_name}/${row.session_id}`;
7734
+ if (seen.has(key)) continue;
7735
+ if (!sessionMatchesSearchCost(row.session, search.options)) continue;
7736
+ seen.add(key);
7737
+ results.push({
7738
+ agentName: row.agent_name,
7739
+ session: row.session,
7740
+ snippet: `${row.kind} ${highlightFilePath(row.path, path2)} \xB7 ${row.count} events`,
7741
+ matchType: "file_path"
8129
7742
  });
7743
+ if (results.length >= (search.options.limit ?? 50)) break;
8130
7744
  }
8131
- return matches;
7745
+ return results;
8132
7746
  }
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
- };
7747
+ var CACHE_INITIALIZATION_VERSION = "session-cache-v2";
7748
+ function deleteLegacyCacheFile() {
7749
+ const legacyPath = getLegacyCachePath();
7750
+ if (!existsSync12(legacyPath)) {
7751
+ return;
8140
7752
  }
8141
- if (textMatchesTerms(title, terms)) {
8142
- return { snippet: buildTermSnippet(title, terms), matchType: "title" };
7753
+ try {
7754
+ unlinkSync(legacyPath);
7755
+ } catch {
8143
7756
  }
8144
- const messageMatch = messageMatches.get(searchResultRowKey(row));
8145
- if (messageMatch) {
8146
- return messageMatch;
7757
+ }
7758
+ function loadCachedSessions(agentName) {
7759
+ if (!hasCacheStorage()) {
7760
+ return null;
8147
7761
  }
8148
- return {
8149
- snippet: String(row.snippet ?? ""),
8150
- matchType: "assistant_reply"
8151
- };
7762
+ return withCacheDb((db) => {
7763
+ const timestampRow = db.prepare("SELECT timestamp AS value FROM agent_cache WHERE agent_name = ?").get(agentName);
7764
+ const timestamp = Number(timestampRow?.value ?? 0);
7765
+ if (!timestamp) {
7766
+ return null;
7767
+ }
7768
+ const rows = db.prepare(
7769
+ `
7770
+ SELECT
7771
+ session_id,
7772
+ sort_index,
7773
+ slug,
7774
+ title,
7775
+ source_path,
7776
+ directory,
7777
+ project_identity_kind,
7778
+ project_identity_key,
7779
+ project_display_name,
7780
+ time_created,
7781
+ time_updated,
7782
+ message_count,
7783
+ total_input_tokens,
7784
+ total_output_tokens,
7785
+ total_cache_read_tokens,
7786
+ total_cache_create_tokens,
7787
+ total_cost,
7788
+ cost_source,
7789
+ total_tokens,
7790
+ model_usage_json,
7791
+ smart_tags_json,
7792
+ smart_tags_source_updated_at,
7793
+ meta_json
7794
+ FROM sessions
7795
+ WHERE agent_name = ?
7796
+ ORDER BY sort_index, activity_time DESC
7797
+ `
7798
+ ).all(agentName);
7799
+ const sessions = [];
7800
+ const meta = {};
7801
+ for (const row of rows) {
7802
+ const session = sessionFromRow(row);
7803
+ sessions.push(session);
7804
+ if (row.meta_json) {
7805
+ meta[session.id] = JSON.parse(row.meta_json);
7806
+ }
7807
+ }
7808
+ return { sessions, meta, timestamp };
7809
+ });
8152
7810
  }
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
- };
7811
+ function isAgentCacheInitialized(agentName, indexVersion = CACHE_INITIALIZATION_VERSION) {
7812
+ if (!hasCacheStorage()) {
7813
+ return false;
7814
+ }
7815
+ return withCacheDbReadOnly((db) => {
7816
+ if (!tableExists(db, "cache_initialization")) return false;
7817
+ const row = db.prepare(
7818
+ `
7819
+ SELECT index_version
7820
+ FROM cache_initialization
7821
+ WHERE agent_name = ?
7822
+ `
7823
+ ).get(agentName);
7824
+ return row?.index_version === indexVersion;
7825
+ }) ?? false;
7826
+ }
7827
+ function markAgentCacheInitialized(agentName, indexVersion = CACHE_INITIALIZATION_VERSION) {
7828
+ withCacheDb((db) => {
7829
+ const now = Date.now();
7830
+ db.prepare(
7831
+ `
7832
+ INSERT INTO cache_initialization(agent_name, initialized_at, index_version, last_sync_at)
7833
+ VALUES (?, ?, ?, ?)
7834
+ ON CONFLICT(agent_name) DO UPDATE SET
7835
+ index_version = excluded.index_version,
7836
+ last_sync_at = excluded.last_sync_at
7837
+ `
7838
+ ).run(agentName, now, indexVersion, now);
8164
7839
  });
8165
7840
  }
8166
- function searchSessions(query, options = {}) {
8167
- const search = mergeSearchQueryOptions(query, options);
8168
- const normalizedQuery = search.text.trim();
7841
+ function loadCachedSessionData(agentName, sessionId) {
8169
7842
  if (!hasCacheStorage()) {
8170
- return [];
7843
+ return null;
8171
7844
  }
8172
- const results = withCacheDb((db) => {
8173
- ensureFtsReady(db);
8174
- const filters = buildSessionSearchFilters(search.options);
8175
- if (!normalizedQuery) {
8176
- const rows2 = db.prepare(
7845
+ return withCacheDbReadOnly((db) => {
7846
+ const row = db.prepare(
7847
+ `
7848
+ SELECT
7849
+ session_id,
7850
+ sort_index,
7851
+ slug,
7852
+ title,
7853
+ source_path,
7854
+ directory,
7855
+ project_identity_kind,
7856
+ project_identity_key,
7857
+ project_display_name,
7858
+ time_created,
7859
+ time_updated,
7860
+ message_count,
7861
+ total_input_tokens,
7862
+ total_output_tokens,
7863
+ total_cache_read_tokens,
7864
+ total_cache_create_tokens,
7865
+ total_cost,
7866
+ cost_source,
7867
+ total_tokens,
7868
+ model_usage_json,
7869
+ smart_tags_json,
7870
+ smart_tags_source_updated_at,
7871
+ meta_json
7872
+ FROM sessions
7873
+ WHERE agent_name = ? AND session_id = ?
8177
7874
  `
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, "");
7875
+ ).get(agentName, sessionId);
7876
+ if (!row) {
7877
+ return null;
8189
7878
  }
8190
- const ftsQuery = toFtsQuery(normalizedQuery);
8191
- if (!ftsQuery) return [];
8192
- const rows = db.prepare(
7879
+ const messageRows = db.prepare(
8193
7880
  `
8194
7881
  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 ?
7882
+ message_id,
7883
+ role,
7884
+ time_created,
7885
+ time_completed,
7886
+ agent,
7887
+ mode,
7888
+ model,
7889
+ provider,
7890
+ tokens_json,
7891
+ cost,
7892
+ cost_source,
7893
+ parts_json,
7894
+ subagent_id,
7895
+ nickname
7896
+ FROM messages
7897
+ WHERE agent_name = ? AND session_id = ?
7898
+ ORDER BY message_index
8207
7899
  `
8208
- ).all(ftsQuery, ...filters.params, search.options.limit ?? 50);
8209
- return rowsToSearchResults(db, rows, normalizedQuery, ftsQuery);
7900
+ ).all(agentName, sessionId);
7901
+ const head = sessionFromRow(row);
7902
+ const fileActivityRows = db.prepare(
7903
+ `
7904
+ SELECT agent_name, session_id, project_identity_key, path, kind, count, latest_time
7905
+ FROM session_file_activity
7906
+ WHERE agent_name = ? AND session_id = ?
7907
+ ORDER BY latest_time DESC, count DESC, path
7908
+ LIMIT 500
7909
+ `
7910
+ ).all(agentName, sessionId);
7911
+ return {
7912
+ ...head,
7913
+ messages: messageRows.map((messageRow) => messageFromCachedRow(messageRow)),
7914
+ file_activity: fileActivityRows.map((activityRow) => fileActivityFromRow(activityRow))
7915
+ };
8210
7916
  });
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
7917
  }
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
- };
7918
+ function saveCachedSessions(agentName, sessions, meta = {}) {
7919
+ withCacheDb((db) => {
7920
+ const deleteAgent = db.prepare("DELETE FROM agent_cache WHERE agent_name = ?");
7921
+ const deleteLegacySessions = db.prepare("DELETE FROM cached_sessions WHERE agent_name = ?");
7922
+ const deleteSession = db.prepare(
7923
+ "DELETE FROM sessions WHERE agent_name = ? AND session_id = ?"
7924
+ );
7925
+ const deleteSearchDocument = db.prepare(
7926
+ "DELETE FROM session_documents WHERE agent_name = ? AND session_id = ?"
7927
+ );
7928
+ const deleteMessages = db.prepare(
7929
+ "DELETE FROM messages WHERE agent_name = ? AND session_id = ?"
7930
+ );
7931
+ const deleteMessageTools = db.prepare(
7932
+ "DELETE FROM message_tools WHERE agent_name = ? AND session_id = ?"
7933
+ );
7934
+ const deleteFileActivity = db.prepare(
7935
+ "DELETE FROM session_file_activity WHERE agent_name = ? AND session_id = ?"
7936
+ );
7937
+ const deleteProjectSession = db.prepare(
7938
+ "DELETE FROM project_sessions WHERE agent_name = ? AND session_id = ?"
7939
+ );
7940
+ const deleteProjectSessions = db.prepare("DELETE FROM project_sessions WHERE agent_name = ?");
7941
+ const upsertAgent = db.prepare(`
7942
+ INSERT INTO agent_cache(agent_name, timestamp)
7943
+ VALUES (?, ?)
7944
+ ON CONFLICT(agent_name) DO UPDATE SET timestamp = excluded.timestamp
7945
+ `);
7946
+ const upsertCachedSession = prepareUpsertCachedSession(db);
7947
+ const upsertSession = prepareUpsertSession(db);
7948
+ const upsertProjectSession = prepareUpsertProjectSession(db);
7949
+ const write = db.transaction(() => {
7950
+ const timestamp = Date.now();
7951
+ const sessionIds = new Set(sessions.map((session) => session.id));
7952
+ const existingSessionIds = db.prepare("SELECT session_id FROM sessions WHERE agent_name = ?").all(agentName);
7953
+ deleteAgent.run(agentName);
7954
+ deleteLegacySessions.run(agentName);
7955
+ deleteProjectSessions.run(agentName);
7956
+ upsertAgent.run(agentName, timestamp);
7957
+ for (const row of existingSessionIds) {
7958
+ const sessionId = String(row.session_id);
7959
+ if (!sessionIds.has(sessionId)) {
7960
+ deleteSearchDocument.run(agentName, sessionId);
7961
+ deleteMessageTools.run(agentName, sessionId);
7962
+ deleteMessages.run(agentName, sessionId);
7963
+ deleteFileActivity.run(agentName, sessionId);
7964
+ deleteProjectSession.run(agentName, sessionId);
7965
+ deleteSession.run(agentName, sessionId);
7966
+ }
7967
+ }
7968
+ sessions.forEach((session, index) => {
7969
+ const identity = session.project_identity ?? computeIdentity(session.directory, realFs);
7970
+ const sessionMeta = meta[session.id];
7971
+ const metaJson = sessionMeta ? JSON.stringify(sessionMeta) : null;
7972
+ upsertCachedSession.run(agentName, session.id, JSON.stringify(session), metaJson);
7973
+ upsertSessionRow(
7974
+ upsertSession,
7975
+ agentName,
7976
+ session,
7977
+ metaJson,
7978
+ index,
7979
+ sourcePathFromMeta(sessionMeta)
7980
+ );
7981
+ writeProjectSessionRow(upsertProjectSession, agentName, session, identity);
7982
+ });
7983
+ });
7984
+ write();
7985
+ deleteLegacyCacheFile();
7986
+ });
8237
7987
  }
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);
7988
+ function saveCachedSessionChanges(agentName, changes, removedSessionIds, meta = {}) {
7989
+ if (changes.length === 0 && removedSessionIds.length === 0) {
7990
+ return;
8253
7991
  }
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 '\\')"
7992
+ withCacheDb((db) => {
7993
+ const deleteLegacySession = db.prepare(
7994
+ "DELETE FROM cached_sessions WHERE agent_name = ? AND session_id = ?"
8257
7995
  );
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
- };
7996
+ const deleteSession = db.prepare(
7997
+ "DELETE FROM sessions WHERE agent_name = ? AND session_id = ?"
7998
+ );
7999
+ const deleteSearchDocument = db.prepare(
8000
+ "DELETE FROM session_documents WHERE agent_name = ? AND session_id = ?"
8001
+ );
8002
+ const deleteMessages = db.prepare(
8003
+ "DELETE FROM messages WHERE agent_name = ? AND session_id = ?"
8004
+ );
8005
+ const deleteMessageTools = db.prepare(
8006
+ "DELETE FROM message_tools WHERE agent_name = ? AND session_id = ?"
8007
+ );
8008
+ const deleteFileActivity = db.prepare(
8009
+ "DELETE FROM session_file_activity WHERE agent_name = ? AND session_id = ?"
8010
+ );
8011
+ const deleteProjectSession = db.prepare(
8012
+ "DELETE FROM project_sessions WHERE agent_name = ? AND session_id = ?"
8013
+ );
8014
+ const upsertAgent = db.prepare(`
8015
+ INSERT INTO agent_cache(agent_name, timestamp)
8016
+ VALUES (?, ?)
8017
+ ON CONFLICT(agent_name) DO UPDATE SET timestamp = excluded.timestamp
8018
+ `);
8019
+ const upsertCachedSession = prepareUpsertCachedSession(db);
8020
+ const upsertSession = prepareUpsertSession(db);
8021
+ const upsertProjectSession = prepareUpsertProjectSession(db);
8022
+ const write = db.transaction(() => {
8023
+ upsertAgent.run(agentName, Date.now());
8024
+ for (const sessionId of new Set(removedSessionIds)) {
8025
+ deleteLegacySession.run(agentName, sessionId);
8026
+ deleteSearchDocument.run(agentName, sessionId);
8027
+ deleteMessageTools.run(agentName, sessionId);
8028
+ deleteMessages.run(agentName, sessionId);
8029
+ deleteFileActivity.run(agentName, sessionId);
8030
+ deleteProjectSession.run(agentName, sessionId);
8031
+ deleteSession.run(agentName, sessionId);
8032
+ }
8033
+ for (const { session, sortIndex } of changes) {
8034
+ const identity = session.project_identity ?? computeIdentity(session.directory, realFs);
8035
+ const sessionMeta = meta[session.id];
8036
+ const metaJson = sessionMeta ? JSON.stringify(sessionMeta) : null;
8037
+ upsertCachedSession.run(agentName, session.id, JSON.stringify(session), metaJson);
8038
+ upsertSessionRow(
8039
+ upsertSession,
8040
+ agentName,
8041
+ session,
8042
+ metaJson,
8043
+ sortIndex,
8044
+ sourcePathFromMeta(sessionMeta)
8045
+ );
8046
+ writeProjectSessionRow(upsertProjectSession, agentName, session, identity);
8047
+ }
8048
+ });
8049
+ write();
8050
+ deleteLegacyCacheFile();
8051
+ });
8292
8052
  }
8293
- function listFileActivity(options = {}) {
8053
+ function clearCache() {
8054
+ setFtsIntegrityCheckedPath(null);
8294
8055
  if (!hasCacheStorage()) {
8295
- return [];
8056
+ deleteLegacyCacheFile();
8057
+ return;
8296
8058
  }
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);
8059
+ withCacheDb((db) => {
8060
+ db.exec(`
8061
+ DELETE FROM agent_cache;
8062
+ DELETE FROM cache_initialization;
8063
+ DELETE FROM cached_sessions;
8064
+ DELETE FROM session_documents;
8065
+ DELETE FROM session_file_activity;
8066
+ DELETE FROM message_tools;
8067
+ DELETE FROM messages;
8068
+ DELETE FROM sessions;
8069
+ DELETE FROM project_sessions;
8070
+ `);
8071
+ });
8072
+ deleteLegacyCacheFile();
8073
+ const cachePath = getCachePath2();
8074
+ const walPath = `${cachePath}-wal`;
8075
+ const shmPath = `${cachePath}-shm`;
8076
+ for (const filePath of [walPath, shmPath]) {
8077
+ if (!existsSync12(filePath)) {
8078
+ continue;
8079
+ }
8080
+ try {
8081
+ rmSync(filePath, { force: true });
8082
+ } catch {
8083
+ }
8333
8084
  }
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
8085
  }
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;
8086
+ function getCacheInfo() {
8087
+ if (!hasCacheStorage()) {
8088
+ return { lastScanTime: null, size: 0 };
8383
8089
  }
8384
- return results;
8090
+ const info = withCacheDb((db) => {
8091
+ const timestampRow = db.prepare("SELECT MAX(timestamp) AS value FROM agent_cache").get();
8092
+ const sizeRow = db.prepare("SELECT COUNT(*) AS value FROM sessions").get();
8093
+ const lastScanTime = Number(timestampRow?.value ?? 0) || null;
8094
+ const size = Number(sizeRow?.value ?? 0);
8095
+ return { lastScanTime, size };
8096
+ });
8097
+ return info ?? { lastScanTime: null, size: 0 };
8385
8098
  }
8386
8099
  function listCachedProjectGroups(sessions) {
8387
8100
  if (sessions) {
@@ -8423,57 +8136,89 @@ function createIdentityResolver() {
8423
8136
  return identity;
8424
8137
  };
8425
8138
  }
8426
- function attachProjectIdentities(sessions) {
8139
+ function attachMissingProjectIdentities(sessions) {
8427
8140
  const resolveIdentity = createIdentityResolver();
8428
8141
  return sessions.map((session) => {
8429
8142
  if (session.project_identity) return session;
8430
- return {
8431
- ...session,
8432
- project_identity: resolveIdentity(session.directory)
8433
- };
8143
+ return { ...session, project_identity: resolveIdentity(session.directory) };
8434
8144
  });
8435
8145
  }
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) {
8146
+ function buildAgentCacheMeta(agent, sessionIds) {
8450
8147
  const metaMap = agent.getSessionMetaMap?.();
8451
8148
  const meta = {};
8452
8149
  if (!metaMap) return meta;
8453
8150
  for (const [id, data] of metaMap.entries()) {
8151
+ if (sessionIds && !sessionIds.has(id)) continue;
8454
8152
  meta[id] = { id, ...data };
8455
8153
  }
8456
8154
  return meta;
8457
8155
  }
8458
- function sessionCacheValue(session) {
8459
- return JSON.stringify(session);
8156
+ function sessionSignature(session) {
8157
+ return JSON.stringify([
8158
+ session.title,
8159
+ session.directory,
8160
+ session.time_created,
8161
+ session.time_updated ?? session.time_created,
8162
+ session.stats.message_count,
8163
+ session.stats.total_input_tokens,
8164
+ session.stats.total_output_tokens,
8165
+ session.stats.total_cost,
8166
+ session.stats.total_tokens ?? 0,
8167
+ session.smart_tags_source_updated_at ?? null
8168
+ ]);
8169
+ }
8170
+ function sortSessions(sessions) {
8171
+ return [...sessions].sort(
8172
+ (a, b) => (b.time_updated ?? b.time_created) - (a.time_updated ?? a.time_created)
8173
+ );
8460
8174
  }
8461
- function buildCacheChanges(cachedSessions, updatedSessions, changedIds = []) {
8175
+ function computeSessionDiff(cachedSessions, updatedSessions, changedIds = [], signature = sessionSignature) {
8462
8176
  const cachedMap = new Map(cachedSessions.map((session) => [session.id, session]));
8463
8177
  const updatedIds = new Set(updatedSessions.map((session) => session.id));
8464
8178
  const changedIdSet = new Set(changedIds);
8465
- const removedSessionIds = cachedSessions.filter((session) => !updatedIds.has(session.id)).map((session) => session.id);
8466
8179
  const changes = [];
8180
+ const removedSessionIds = [];
8181
+ let newCount = 0;
8182
+ let updatedCount = 0;
8467
8183
  updatedSessions.forEach((session, sortIndex) => {
8468
8184
  const cached = cachedMap.get(session.id);
8469
- if (!cached || changedIdSet.has(session.id) || cached !== session && sessionCacheValue(cached) !== sessionCacheValue(session)) {
8185
+ if (!cached) {
8186
+ newCount += 1;
8187
+ changes.push({ session, sortIndex });
8188
+ return;
8189
+ }
8190
+ const hasSignatureChange = signature(cached) !== signature(session);
8191
+ if (changedIdSet.has(session.id) || hasSignatureChange) {
8192
+ updatedCount += 1;
8470
8193
  changes.push({ session, sortIndex });
8471
8194
  }
8472
8195
  });
8473
- return { changes, removedSessionIds };
8196
+ for (const session of cachedSessions) {
8197
+ if (!updatedIds.has(session.id)) {
8198
+ removedSessionIds.push(session.id);
8199
+ }
8200
+ }
8201
+ return {
8202
+ changes,
8203
+ removedSessionIds,
8204
+ counts: { new: newCount, updated: updatedCount, removed: removedSessionIds.length }
8205
+ };
8206
+ }
8207
+ function filterSessions(sessions, options) {
8208
+ let result = sessions;
8209
+ if (options.cwd) {
8210
+ result = filterSessionsByProjectScope(result, options.cwd);
8211
+ }
8212
+ if (options.from != null) {
8213
+ result = result.filter((s) => (s.time_updated ?? s.time_created) >= options.from);
8214
+ }
8215
+ if (options.to != null) {
8216
+ result = result.filter((s) => (s.time_updated ?? s.time_created) <= options.to);
8217
+ }
8218
+ return result;
8474
8219
  }
8475
8220
  function saveCachedSessionDiff(agent, cachedSessions, updatedSessions, changedIds = []) {
8476
- const diff = buildCacheChanges(cachedSessions, updatedSessions, changedIds);
8221
+ const diff = computeSessionDiff(cachedSessions, updatedSessions, changedIds, sessionSignature);
8477
8222
  saveCachedSessionChanges(
8478
8223
  agent.name,
8479
8224
  diff.changes,
@@ -8573,19 +8318,16 @@ async function scanAgentSmart(agent, options, onProgress) {
8573
8318
  const agentStart = performance.now();
8574
8319
  const timing = { total: 0 };
8575
8320
  const useCache = options.useCache ?? true;
8576
- const canValidateCache = Boolean(agent.checkForChanges && agent.incrementalScan);
8577
8321
  if (useCache) {
8578
8322
  const t0 = performance.now();
8579
8323
  const cached = loadCachedSessions(agent.name);
8580
8324
  timing.cacheLoad = performance.now() - t0;
8581
8325
  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);
8326
+ const metaMap = /* @__PURE__ */ new Map();
8327
+ for (const [id, meta] of Object.entries(cached.meta)) {
8328
+ metaMap.set(id, meta);
8588
8329
  }
8330
+ agent.setSessionMetaMap(metaMap);
8589
8331
  if (options.cacheOnly) {
8590
8332
  onProgress?.({
8591
8333
  agent: agent.name,
@@ -8594,7 +8336,7 @@ async function scanAgentSmart(agent, options, onProgress) {
8594
8336
  });
8595
8337
  onProgress?.({ agent: agent.name, phase: "complete", newCount: cached.sessions.length });
8596
8338
  const t32 = performance.now();
8597
- const cachedWithIdentity2 = attachProjectIdentities(cached.sessions);
8339
+ const cachedWithIdentity2 = attachMissingProjectIdentities(cached.sessions);
8598
8340
  timing.identity = performance.now() - t32;
8599
8341
  const filtered3 = filterSessions(cachedWithIdentity2, options);
8600
8342
  timing.total = performance.now() - agentStart;
@@ -8615,58 +8357,56 @@ async function scanAgentSmart(agent, options, onProgress) {
8615
8357
  phase: "cache",
8616
8358
  cachedCount: cached.sessions.length
8617
8359
  });
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)
8360
+ onProgress?.({ agent: agent.name, phase: "checking" });
8361
+ const t1 = performance.now();
8362
+ const checkResult = await Promise.resolve(
8363
+ agent.checkForChanges(cached.timestamp, cached.sessions)
8364
+ );
8365
+ timing.checkChanges = performance.now() - t1;
8366
+ if (checkResult.hasChanges) {
8367
+ onProgress?.({
8368
+ agent: agent.name,
8369
+ phase: "incremental",
8370
+ changedCount: checkResult.changedIds?.length
8371
+ });
8372
+ const t2 = performance.now();
8373
+ const updatedSessions = await Promise.resolve(
8374
+ agent.incrementalScan(cached.sessions, checkResult.changedIds || [])
8623
8375
  );
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 {
8376
+ timing.scan = performance.now() - t2;
8377
+ const t32 = performance.now();
8378
+ const sessionsWithIdentity = attachMissingProjectIdentities(updatedSessions);
8379
+ timing.identity = performance.now() - t32;
8380
+ const t42 = performance.now();
8381
+ const tagged2 = options.includeSmartTags === false ? { sessions: sessionsWithIdentity, changed: false } : await ensureSessionTags(agent, sessionsWithIdentity, options.smartTagWorkerUrl);
8382
+ timing.tags = performance.now() - t42;
8383
+ if (options.writeCache !== false) {
8384
+ saveCachedSessionDiff(
8658
8385
  agent,
8659
- heads: filtered3,
8660
- fromCache: true,
8661
- refreshed: true,
8662
- timing,
8663
- cacheTimestamp: checkResult.timestamp
8664
- };
8386
+ cached.sessions,
8387
+ tagged2.sessions,
8388
+ checkResult.changedIds ?? []
8389
+ );
8665
8390
  }
8666
- onProgress?.({ agent: agent.name, phase: "complete", newCount: cached.sessions.length });
8391
+ onProgress?.({
8392
+ agent: agent.name,
8393
+ phase: "complete",
8394
+ newCount: tagged2.sessions.length
8395
+ });
8396
+ const filtered3 = filterSessions(tagged2.sessions, options);
8397
+ timing.total = performance.now() - agentStart;
8398
+ return {
8399
+ agent,
8400
+ heads: filtered3,
8401
+ fromCache: true,
8402
+ refreshed: true,
8403
+ timing,
8404
+ cacheTimestamp: checkResult.timestamp
8405
+ };
8667
8406
  }
8407
+ onProgress?.({ agent: agent.name, phase: "complete", newCount: cached.sessions.length });
8668
8408
  const t3 = performance.now();
8669
- const cachedWithIdentity = attachProjectIdentities(cached.sessions);
8409
+ const cachedWithIdentity = attachMissingProjectIdentities(cached.sessions);
8670
8410
  timing.identity = performance.now() - t3;
8671
8411
  const t4 = performance.now();
8672
8412
  const tagged = options.includeSmartTags === false ? { sessions: cachedWithIdentity, changed: false } : await ensureSessionTags(agent, cachedWithIdentity, options.smartTagWorkerUrl);
@@ -8718,7 +8458,7 @@ async function scanAgentFull(agent, options, onProgress, timing = { total: 0 },
8718
8458
  perf.end(scanMarker);
8719
8459
  timing.scan = performance.now() - t0;
8720
8460
  const t1 = performance.now();
8721
- const headsWithIdentity = attachProjectIdentities(heads);
8461
+ const headsWithIdentity = attachMissingProjectIdentities(heads);
8722
8462
  timing.identity = performance.now() - t1;
8723
8463
  const t2 = performance.now();
8724
8464
  const tagged = options.includeSmartTags === false ? { sessions: headsWithIdentity, changed: false } : await ensureSessionTags(agent, headsWithIdentity, options.smartTagWorkerUrl);
@@ -9090,6 +8830,161 @@ function deleteBookmark(agentKey, sessionId) {
9090
8830
  ).run(agentKey, sessionId);
9091
8831
  });
9092
8832
  }
8833
+ var DASHBOARD_RECENT_LIMIT = 10;
8834
+ function getTotalTokens(stats) {
8835
+ return stats.total_tokens ?? stats.total_input_tokens + stats.total_output_tokens;
8836
+ }
8837
+ function getSessionAgentName(session) {
8838
+ return session.slug.split("/")[0]?.toLowerCase() || "unknown";
8839
+ }
8840
+ function getSessionActivityTime(session) {
8841
+ return session.time_updated ?? session.time_created;
8842
+ }
8843
+ function toLocalDateKey(ts) {
8844
+ const d = new Date(ts);
8845
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(
8846
+ d.getDate()
8847
+ ).padStart(2, "0")}`;
8848
+ }
8849
+ function startOfLocalDay(ts) {
8850
+ const d = new Date(ts);
8851
+ d.setHours(0, 0, 0, 0);
8852
+ return d.getTime();
8853
+ }
8854
+ function buildDashboard(sessions, options) {
8855
+ const { byAgentNames, scope, from, to, agentInfoMap } = options;
8856
+ const agentMetrics = /* @__PURE__ */ new Map();
8857
+ const agentMetricKeyByName = /* @__PURE__ */ new Map();
8858
+ for (const name of byAgentNames) {
8859
+ if (scope.agent && name.toLowerCase() !== scope.agent) continue;
8860
+ agentMetrics.set(name, { sessions: 0, messages: 0, tokens: 0 });
8861
+ agentMetricKeyByName.set(name.toLowerCase(), name);
8862
+ }
8863
+ let totalSessions = 0;
8864
+ let totalMessages = 0;
8865
+ let totalTokens = 0;
8866
+ let totalCost = 0;
8867
+ let hasEstimatedCost = false;
8868
+ let latestActivity = 0;
8869
+ const recentCandidates = [];
8870
+ const modelAgg = /* @__PURE__ */ new Map();
8871
+ const dailyMap = /* @__PURE__ */ new Map();
8872
+ const dailyTokenMap = /* @__PURE__ */ new Map();
8873
+ if (from != null) {
8874
+ const bucketStart = startOfLocalDay(from);
8875
+ const bucketDays = Math.floor((startOfLocalDay(to) - bucketStart) / 864e5) + 1;
8876
+ for (let i = 0; i < bucketDays; i += 1) {
8877
+ const ts = bucketStart + i * 864e5;
8878
+ const key = toLocalDateKey(ts);
8879
+ dailyMap.set(key, { date: key, sessions: 0, messages: 0 });
8880
+ dailyTokenMap.set(key, { date: key, input: 0, output: 0, cache_read: 0, cache_create: 0 });
8881
+ }
8882
+ }
8883
+ for (const session of sessions) {
8884
+ const agentName = getSessionAgentName(session);
8885
+ if (scope.agent && agentName !== scope.agent) continue;
8886
+ if (scope.projectKey) {
8887
+ const identity = session.project_identity;
8888
+ if (!identity || identity.key !== scope.projectKey) continue;
8889
+ if (scope.projectKind && identity.kind !== scope.projectKind) continue;
8890
+ }
8891
+ const activity = getSessionActivityTime(session);
8892
+ if (from != null && activity < from) continue;
8893
+ if (activity > to) continue;
8894
+ const messageCount = session.stats.message_count;
8895
+ const sessionTokens = getTotalTokens(session.stats);
8896
+ totalSessions += 1;
8897
+ totalMessages += messageCount;
8898
+ totalTokens += sessionTokens;
8899
+ totalCost += session.stats.total_cost ?? 0;
8900
+ if (session.stats.cost_source === "estimated") hasEstimatedCost = true;
8901
+ if (activity > latestActivity) latestActivity = activity;
8902
+ const metricKey = agentMetricKeyByName.get(agentName);
8903
+ if (metricKey) {
8904
+ const metric = agentMetrics.get(metricKey);
8905
+ metric.sessions += 1;
8906
+ metric.messages += messageCount;
8907
+ metric.tokens += sessionTokens;
8908
+ }
8909
+ const key = toLocalDateKey(activity);
8910
+ let bucket = dailyMap.get(key);
8911
+ if (!bucket) {
8912
+ bucket = { date: key, sessions: 0, messages: 0 };
8913
+ dailyMap.set(key, bucket);
8914
+ }
8915
+ bucket.sessions += 1;
8916
+ bucket.messages += messageCount;
8917
+ let tokenBucket = dailyTokenMap.get(key);
8918
+ if (!tokenBucket) {
8919
+ tokenBucket = { date: key, input: 0, output: 0, cache_read: 0, cache_create: 0 };
8920
+ dailyTokenMap.set(key, tokenBucket);
8921
+ }
8922
+ const cacheRead = session.stats.total_cache_read_tokens ?? 0;
8923
+ const cacheCreate = session.stats.total_cache_create_tokens ?? 0;
8924
+ const pureInput = session.stats.total_input_tokens - cacheRead - cacheCreate;
8925
+ tokenBucket.input += Math.max(0, pureInput);
8926
+ tokenBucket.output += session.stats.total_output_tokens;
8927
+ tokenBucket.cache_read += cacheRead;
8928
+ tokenBucket.cache_create += cacheCreate;
8929
+ if (session.model_usage) {
8930
+ for (const [model, tokens] of Object.entries(session.model_usage)) {
8931
+ const entry = modelAgg.get(model);
8932
+ if (entry) {
8933
+ entry.tokens += tokens;
8934
+ entry.sessions += 1;
8935
+ } else {
8936
+ modelAgg.set(model, { tokens, sessions: 1 });
8937
+ }
8938
+ }
8939
+ }
8940
+ let recentIndex = recentCandidates.length;
8941
+ for (let i = 0; i < recentCandidates.length; i += 1) {
8942
+ if (activity > recentCandidates[i].activity) {
8943
+ recentIndex = i;
8944
+ break;
8945
+ }
8946
+ }
8947
+ if (recentIndex < DASHBOARD_RECENT_LIMIT) {
8948
+ recentCandidates.splice(recentIndex, 0, { session, activity });
8949
+ if (recentCandidates.length > DASHBOARD_RECENT_LIMIT) recentCandidates.pop();
8950
+ }
8951
+ }
8952
+ const perAgent = [...agentMetrics.entries()].map(([name, metrics]) => {
8953
+ const info = agentInfoMap?.get(name);
8954
+ return {
8955
+ name,
8956
+ displayName: info?.displayName ?? name,
8957
+ icon: info?.icon ?? "",
8958
+ sessions: metrics.sessions,
8959
+ messages: metrics.messages,
8960
+ tokens: metrics.tokens
8961
+ };
8962
+ }).filter((item) => item.sessions > 0).sort((a, b) => b.sessions - a.sessions);
8963
+ const dailyActivity = [...dailyMap.values()].sort((a, b) => a.date.localeCompare(b.date));
8964
+ const dailyTokenActivity = [...dailyTokenMap.values()].sort(
8965
+ (a, b) => a.date.localeCompare(b.date)
8966
+ );
8967
+ const modelDistribution = [...modelAgg.entries()].map(([model, { tokens, sessions: count }]) => ({ model, tokens, sessions: count })).sort((a, b) => b.tokens - a.tokens);
8968
+ const recentSessions = recentCandidates.map(({ session }) => {
8969
+ const agentKey = getSessionAgentName(session);
8970
+ return { ...session, agentName: agentKey };
8971
+ });
8972
+ return {
8973
+ totals: {
8974
+ sessions: totalSessions,
8975
+ messages: totalMessages,
8976
+ tokens: totalTokens,
8977
+ cost: totalCost,
8978
+ cost_source: totalCost > 0 ? hasEstimatedCost ? "estimated" : "recorded" : void 0,
8979
+ latestActivity: latestActivity || void 0
8980
+ },
8981
+ perAgent,
8982
+ dailyActivity,
8983
+ dailyTokenActivity,
8984
+ modelDistribution,
8985
+ recentSessions
8986
+ };
8987
+ }
9093
8988
 
9094
8989
  export {
9095
8990
  registerAgent,
@@ -9102,6 +8997,8 @@ export {
9102
8997
  filteredSession,
9103
8998
  getParsedSession,
9104
8999
  BaseAgent,
9000
+ FileSystemSessionSource,
9001
+ DatabaseSessionSource,
9105
9002
  firstExisting,
9106
9003
  resolveProviderRoots,
9107
9004
  getCursorDataPath,
@@ -9148,6 +9045,12 @@ export {
9148
9045
  summarizeFileActivity,
9149
9046
  extractSessionFileActivity,
9150
9047
  parseSearchQuery,
9048
+ syncSessionSearchIndex,
9049
+ syncSessionSearchIndexChanges,
9050
+ searchSessions,
9051
+ listFileActivity,
9052
+ listSessionFileActivity,
9053
+ searchFileActivitySessions,
9151
9054
  loadCachedSessions,
9152
9055
  isAgentCacheInitialized,
9153
9056
  markAgentCacheInitialized,
@@ -9156,13 +9059,12 @@ export {
9156
9059
  saveCachedSessionChanges,
9157
9060
  clearCache,
9158
9061
  getCacheInfo,
9159
- syncSessionSearchIndex,
9160
- syncSessionSearchIndexChanges,
9161
- searchSessions,
9162
- listFileActivity,
9163
- listSessionFileActivity,
9164
- searchFileActivitySessions,
9165
9062
  listCachedProjectGroups,
9063
+ attachMissingProjectIdentities,
9064
+ buildAgentCacheMeta,
9065
+ sessionSignature,
9066
+ sortSessions,
9067
+ computeSessionDiff,
9166
9068
  filterSessions,
9167
9069
  scanSessions,
9168
9070
  scanSessionsAsync,
@@ -9170,6 +9072,13 @@ export {
9170
9072
  listBookmarks,
9171
9073
  upsertBookmark,
9172
9074
  importBookmarks,
9173
- deleteBookmark
9075
+ deleteBookmark,
9076
+ DASHBOARD_RECENT_LIMIT,
9077
+ getTotalTokens,
9078
+ getSessionAgentName,
9079
+ getSessionActivityTime,
9080
+ toLocalDateKey,
9081
+ startOfLocalDay,
9082
+ buildDashboard
9174
9083
  };
9175
- //# sourceMappingURL=chunk-5UOLRSFH.js.map
9084
+ //# sourceMappingURL=chunk-ZQMIB7QO.js.map