codesesh 0.6.1 → 0.7.1

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.
@@ -31,13 +31,13 @@ import {
31
31
  import { join as join7, basename as basename5 } from "path";
32
32
  import { existsSync as existsSync8, readdirSync as readdirSync4, readFileSync as readFileSync6, statSync as statSync5 } from "fs";
33
33
  import { join as join8, normalize } from "path";
34
- import { resolve, sep } from "path";
35
34
  import { availableParallelism } from "os";
36
35
  import { Worker } from "worker_threads";
37
36
  import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
38
37
  import { spawnSync } from "child_process";
39
- import { homedir as homedir3 } from "os";
38
+ import * as os from "os";
40
39
  import * as path from "path";
40
+ import { resolve, sep } from "path";
41
41
  import { existsSync as existsSync10, rmSync, unlinkSync } from "fs";
42
42
  import { join as join9 } from "path";
43
43
  import { homedir as homedir4 } from "os";
@@ -701,7 +701,7 @@ function withEstimatedSessionCost(stats, model) {
701
701
  function estimateTokenCost(model, tokens) {
702
702
  return estimateCostForTokens(model, tokens)?.cost ?? null;
703
703
  }
704
- var RECENT_SESSION_REVALIDATION_WINDOW_MS = 24 * 60 * 60 * 1e3;
704
+ var HEAD_INDEX_VERSION = "claudecode-head-v1";
705
705
  function parseTimestampMs(data) {
706
706
  const raw = String(data["timestamp"] ?? "").trim();
707
707
  if (!raw) return 0;
@@ -743,36 +743,64 @@ var ClaudeCodeAgent = class extends BaseAgent {
743
743
  const listMarker = perf.start("listProjectDirs");
744
744
  const projectDirs = this.listProjectDirs();
745
745
  perf.end(listMarker);
746
- for (const projectDir of projectDirs) {
746
+ const filesByProject = projectDirs.map((projectDir) => {
747
747
  const fileMarker = perf.start(`listJsonlFiles:${basename2(projectDir)}`);
748
- const files = this.listJsonlFiles(projectDir);
748
+ const files = this.listJsonlFiles(projectDir).filter((file) => {
749
+ try {
750
+ return matchesScanWindow(statSync(file).mtimeMs, options);
751
+ } catch {
752
+ return false;
753
+ }
754
+ });
749
755
  perf.end(fileMarker);
756
+ return { projectDir, files };
757
+ });
758
+ const totalFiles = filesByProject.reduce((total, item) => total + item.files.length, 0);
759
+ options?.onProgress?.({ total: totalFiles, processed: 0, sessions: 0 });
760
+ let processed = 0;
761
+ for (const { projectDir, files } of filesByProject) {
750
762
  for (const file of files) {
751
763
  try {
752
- if (!matchesScanWindow(statSync(file).mtimeMs, options)) continue;
753
764
  const parseMarker = perf.start(`parseSessionHead:${basename2(file)}`);
754
765
  const head = getParsedSession(this.parseSessionHeadResult(file, projectDir));
755
766
  perf.end(parseMarker);
756
767
  if (head) {
757
768
  heads.push(head);
758
- this.sessionMetaMap.set(head.id, {
759
- id: head.id,
760
- title: head.title,
761
- sourcePath: file,
762
- directory: head.directory,
763
- model: head.stats.total_tokens ? "unknown" : void 0,
764
- messageCount: head.stats.message_count,
765
- createdAt: head.time_created,
766
- updatedAt: head.time_updated ?? head.time_created
767
- });
769
+ this.sessionMetaMap.set(head.id, this.buildSessionMeta(head, file, projectDir));
768
770
  }
769
771
  } catch {
772
+ } finally {
773
+ processed += 1;
774
+ options?.onProgress?.({ total: totalFiles, processed, sessions: heads.length });
770
775
  }
771
776
  }
772
777
  }
773
778
  perf.end(scanMarker);
774
779
  return heads;
775
780
  }
781
+ listSessionSources() {
782
+ if (!this.basePath) return [];
783
+ const refs = [];
784
+ for (const projectDir of this.listProjectDirs()) {
785
+ for (const file of this.listJsonlFiles(projectDir)) {
786
+ const sessionId = basename2(file, ".jsonl");
787
+ refs.push({
788
+ sessionId,
789
+ sourcePath: file,
790
+ fingerprint: this.sourceFingerprint(file, projectDir)
791
+ });
792
+ }
793
+ }
794
+ return refs;
795
+ }
796
+ scanSessionSource(sourcePath) {
797
+ const projectDir = dirname(sourcePath);
798
+ const head = getParsedSession(this.parseSessionHeadResult(sourcePath, projectDir));
799
+ if (head) {
800
+ this.sessionMetaMap.set(head.id, this.buildSessionMeta(head, sourcePath, projectDir));
801
+ }
802
+ return head;
803
+ }
776
804
  getSessionMetaMap() {
777
805
  return this.sessionMetaMap;
778
806
  }
@@ -844,23 +872,14 @@ var ClaudeCodeAgent = class extends BaseAgent {
844
872
  }
845
873
  /**
846
874
  * 检测文件系统变更
847
- * 通过比较文件修改时间判断是否有新内容
875
+ * - 已有 session:statSync 检测文件修改(APFS 写文件不更新 dir mtime,必须 file-level)
876
+ * - 新 session 检测:readdirSync 文件列表比对,比 dir statSync 快 ~10x
848
877
  */
849
- checkForChanges(sinceTimestamp, cachedSessions) {
878
+ checkForChanges(_sinceTimestamp, cachedSessions) {
850
879
  if (!this.basePath) {
851
880
  return { hasChanges: false, timestamp: Date.now() };
852
881
  }
853
- const now = Date.now();
854
882
  const changedIds = /* @__PURE__ */ new Set();
855
- const recentSessions = cachedSessions.filter(
856
- (session) => now - session.time_created <= RECENT_SESSION_REVALIDATION_WINDOW_MS
857
- );
858
- for (const session of recentSessions) {
859
- changedIds.add(session.id);
860
- const meta = this.sessionMetaMap.get(session.id);
861
- if (!meta) continue;
862
- delete this.sessionsIndexCache[basename2(dirname(meta.sourcePath))];
863
- }
864
883
  for (const session of cachedSessions) {
865
884
  const meta = this.sessionMetaMap.get(session.id);
866
885
  if (!meta) {
@@ -868,32 +887,36 @@ var ClaudeCodeAgent = class extends BaseAgent {
868
887
  continue;
869
888
  }
870
889
  try {
871
- const stat = statSync(meta.sourcePath);
872
- if (stat.mtimeMs > sinceTimestamp) {
890
+ if (this.hasMetaChanged(meta)) {
873
891
  changedIds.add(session.id);
892
+ delete this.sessionsIndexCache[basename2(dirname(meta.sourcePath))];
874
893
  }
875
894
  } catch {
876
895
  changedIds.add(session.id);
877
896
  }
878
897
  }
898
+ const cachedIdSet = new Set(cachedSessions.map((s) => s.id));
899
+ let hasNewFiles = false;
879
900
  try {
880
- let totalFiles = 0;
881
- for (const dir of this.listProjectDirs()) {
882
- totalFiles += this.listJsonlFiles(dir).length;
901
+ outer: for (const dir of this.listProjectDirs()) {
902
+ try {
903
+ for (const file of this.listJsonlFiles(dir)) {
904
+ if (!cachedIdSet.has(basename2(file, ".jsonl"))) {
905
+ hasNewFiles = true;
906
+ delete this.sessionsIndexCache[basename2(dir)];
907
+ break outer;
908
+ }
909
+ }
910
+ } catch {
911
+ }
883
912
  }
884
- const hasNewFiles = totalFiles > cachedSessions.length;
885
- return {
886
- hasChanges: changedIds.size > 0 || hasNewFiles,
887
- changedIds: Array.from(changedIds),
888
- timestamp: Date.now()
889
- };
890
913
  } catch {
891
- return {
892
- hasChanges: changedIds.size > 0,
893
- changedIds: Array.from(changedIds),
894
- timestamp: Date.now()
895
- };
896
914
  }
915
+ return {
916
+ hasChanges: changedIds.size > 0 || hasNewFiles,
917
+ changedIds: Array.from(changedIds),
918
+ timestamp: Date.now()
919
+ };
897
920
  }
898
921
  /**
899
922
  * 增量扫描 - 只扫描变更的会话
@@ -901,48 +924,16 @@ var ClaudeCodeAgent = class extends BaseAgent {
901
924
  incrementalScan(cachedSessions, changedIds) {
902
925
  if (!this.basePath) return cachedSessions;
903
926
  const sessionMap = new Map(cachedSessions.map((s) => [s.id, s]));
927
+ const changedSet = new Set(changedIds);
904
928
  for (const projectDir of this.listProjectDirs()) {
905
929
  for (const file of this.listJsonlFiles(projectDir)) {
906
930
  try {
907
931
  const sessionId = basename2(file, ".jsonl");
908
- if (changedIds.includes(sessionId)) {
909
- const head = getParsedSession(this.parseSessionHeadResult(file, projectDir));
910
- if (head) {
911
- sessionMap.set(head.id, head);
912
- this.sessionMetaMap.set(head.id, {
913
- id: head.id,
914
- title: head.title,
915
- sourcePath: file,
916
- directory: head.directory,
917
- model: head.stats.total_tokens ? "unknown" : void 0,
918
- messageCount: head.stats.message_count,
919
- createdAt: head.time_created,
920
- updatedAt: head.time_updated ?? head.time_created
921
- });
922
- }
923
- }
924
- } catch {
925
- }
926
- }
927
- }
928
- for (const projectDir of this.listProjectDirs()) {
929
- for (const file of this.listJsonlFiles(projectDir)) {
930
- try {
931
- const sessionId = basename2(file, ".jsonl");
932
- if (!sessionMap.has(sessionId)) {
932
+ if (changedSet.has(sessionId) || !sessionMap.has(sessionId)) {
933
933
  const head = getParsedSession(this.parseSessionHeadResult(file, projectDir));
934
934
  if (head) {
935
935
  sessionMap.set(head.id, head);
936
- this.sessionMetaMap.set(head.id, {
937
- id: head.id,
938
- title: head.title,
939
- sourcePath: file,
940
- directory: head.directory,
941
- model: head.stats.total_tokens ? "unknown" : void 0,
942
- messageCount: head.stats.message_count,
943
- createdAt: head.time_created,
944
- updatedAt: head.time_updated ?? head.time_created
945
- });
936
+ this.sessionMetaMap.set(head.id, this.buildSessionMeta(head, file, projectDir));
946
937
  }
947
938
  }
948
939
  } catch {
@@ -967,12 +958,57 @@ var ClaudeCodeAgent = class extends BaseAgent {
967
958
  return [];
968
959
  }
969
960
  }
961
+ buildSessionMeta(head, file, projectDir) {
962
+ const indexPath = this.getSessionsIndexPath(projectDir);
963
+ return {
964
+ id: head.id,
965
+ title: head.title,
966
+ sourcePath: file,
967
+ sourceFingerprint: this.sourceFingerprint(file, projectDir),
968
+ sourceMtimeMs: statSync(file).mtimeMs,
969
+ indexPath: existsSync3(indexPath) ? indexPath : null,
970
+ indexMtimeMs: this.getFileMtimeMs(indexPath),
971
+ headIndexVersion: HEAD_INDEX_VERSION,
972
+ directory: head.directory,
973
+ model: head.stats.total_tokens ? "unknown" : void 0,
974
+ messageCount: head.stats.message_count,
975
+ createdAt: head.time_created,
976
+ updatedAt: head.time_updated ?? head.time_created
977
+ };
978
+ }
979
+ hasMetaChanged(meta) {
980
+ if (meta.headIndexVersion !== HEAD_INDEX_VERSION) return true;
981
+ if (typeof meta.sourceMtimeMs !== "number") return true;
982
+ if (statSync(meta.sourcePath).mtimeMs !== meta.sourceMtimeMs) return true;
983
+ const indexPath = meta.indexPath ?? this.getSessionsIndexPath(dirname(meta.sourcePath));
984
+ return this.getFileMtimeMs(indexPath) !== (meta.indexMtimeMs ?? null);
985
+ }
986
+ sourceFingerprint(file, projectDir) {
987
+ const stat = statSync(file);
988
+ const indexPath = this.getSessionsIndexPath(projectDir);
989
+ return JSON.stringify([
990
+ HEAD_INDEX_VERSION,
991
+ stat.mtimeMs,
992
+ stat.size,
993
+ this.getFileMtimeMs(indexPath)
994
+ ]);
995
+ }
996
+ getSessionsIndexPath(projectDir) {
997
+ return join3(projectDir, "sessions-index.json");
998
+ }
999
+ getFileMtimeMs(filePath) {
1000
+ try {
1001
+ return statSync(filePath).mtimeMs;
1002
+ } catch {
1003
+ return null;
1004
+ }
1005
+ }
970
1006
  loadSessionsIndex(projectDir) {
971
1007
  const cacheKey = basename2(projectDir);
972
1008
  if (cacheKey in this.sessionsIndexCache) {
973
1009
  return this.sessionsIndexCache[cacheKey];
974
1010
  }
975
- const indexPath = join3(projectDir, "sessions-index.json");
1011
+ const indexPath = this.getSessionsIndexPath(projectDir);
976
1012
  const map = /* @__PURE__ */ new Map();
977
1013
  if (existsSync3(indexPath)) {
978
1014
  try {
@@ -1675,11 +1711,7 @@ var OpenCodeAgent = class extends BaseAgent {
1675
1711
  rows = db.prepare(`
1676
1712
  SELECT
1677
1713
  s.id, s.title, s.time_created, s.time_updated, s.slug, s.directory,
1678
- s.version, s.summary_files,
1679
- (SELECT COUNT(*) FROM message m WHERE m.session_id = s.id) AS message_count,
1680
- (SELECT m.data FROM message m
1681
- WHERE m.session_id = s.id AND m.data LIKE '%"modelID"%'
1682
- ORDER BY m.time_created DESC LIMIT 1) AS model_message_data
1714
+ s.version, s.summary_files
1683
1715
  FROM session s
1684
1716
  WHERE COALESCE(s.time_updated, s.time_created) >= ?
1685
1717
  ORDER BY s.time_created DESC
@@ -1693,17 +1725,28 @@ var OpenCodeAgent = class extends BaseAgent {
1693
1725
  ORDER BY s.time_created DESC
1694
1726
  `).all(cutoffTime);
1695
1727
  }
1728
+ const headContexts = hasMessageTable ? this.buildHeadContexts(
1729
+ this.readHeadMessageRows(db, cutoffTime),
1730
+ this.readHeadPartRows(db, cutoffTime)
1731
+ ) : /* @__PURE__ */ new Map();
1696
1732
  const heads = [];
1733
+ options?.onProgress?.({ total: rows.length, processed: 0, sessions: 0 });
1734
+ let processed = 0;
1697
1735
  for (const row of rows) {
1698
- const head = getParsedSession(this.parseSessionHeadRow(db, row, hasMessageTable));
1699
- if (!head) continue;
1700
- heads.push(head);
1701
- if (this.dbPath) {
1702
- this.sessionMetaMap.set(head.id, {
1703
- id: head.id,
1704
- sourcePath: this.dbPath
1705
- });
1736
+ const head = getParsedSession(
1737
+ this.parseSessionHeadRow(row, hasMessageTable, headContexts.get(String(row.id ?? "")))
1738
+ );
1739
+ if (head) {
1740
+ heads.push(head);
1741
+ if (this.dbPath) {
1742
+ this.sessionMetaMap.set(head.id, {
1743
+ id: head.id,
1744
+ sourcePath: this.dbPath
1745
+ });
1746
+ }
1706
1747
  }
1748
+ processed += 1;
1749
+ options?.onProgress?.({ total: rows.length, processed, sessions: heads.length });
1707
1750
  }
1708
1751
  return heads;
1709
1752
  } catch {
@@ -1712,15 +1755,15 @@ var OpenCodeAgent = class extends BaseAgent {
1712
1755
  db.close();
1713
1756
  }
1714
1757
  }
1715
- parseSessionHeadRow(db, row, hasMessageTable) {
1758
+ parseSessionHeadRow(row, hasMessageTable, context) {
1716
1759
  const id = String(row.id ?? "");
1717
1760
  if (!id) return skippedSession("missing session id");
1718
1761
  const timeCreated = Number(row.time_created ?? 0);
1719
1762
  const timeUpdated = Number(row.time_updated ?? timeCreated);
1720
- const stats = hasMessageTable ? this.readSessionStats(db, id) : null;
1721
- const messageCount = stats?.message_count ?? Number(row.message_count ?? 0);
1763
+ const stats = context?.stats ?? null;
1764
+ const messageCount = stats?.message_count ?? 0;
1722
1765
  if (hasMessageTable && messageCount === 0) return filteredSession("no visible messages");
1723
- const messageTitle = hasMessageTable ? this.readFirstUserTitle(db, id) : null;
1766
+ const messageTitle = context?.messageTitle ?? null;
1724
1767
  return parsedSession({
1725
1768
  id,
1726
1769
  slug: `opencode/${id}`,
@@ -1737,6 +1780,29 @@ var OpenCodeAgent = class extends BaseAgent {
1737
1780
  }
1738
1781
  });
1739
1782
  }
1783
+ readHeadMessageRows(db, cutoffTime) {
1784
+ return db.prepare(
1785
+ `
1786
+ SELECT m.id, m.session_id, m.data, m.time_created
1787
+ FROM message m
1788
+ JOIN session s ON s.id = m.session_id
1789
+ WHERE COALESCE(s.time_updated, s.time_created) >= ?
1790
+ ORDER BY m.session_id, m.time_created ASC
1791
+ `
1792
+ ).all(cutoffTime);
1793
+ }
1794
+ readHeadPartRows(db, cutoffTime) {
1795
+ return db.prepare(
1796
+ `
1797
+ SELECT p.message_id, p.data, p.time_created
1798
+ FROM part p
1799
+ JOIN message m ON m.id = p.message_id
1800
+ JOIN session s ON s.id = m.session_id
1801
+ WHERE COALESCE(s.time_updated, s.time_created) >= ?
1802
+ ORDER BY p.message_id, p.time_created ASC
1803
+ `
1804
+ ).all(cutoffTime);
1805
+ }
1740
1806
  getSessionMetaMap() {
1741
1807
  return this.sessionMetaMap;
1742
1808
  }
@@ -1773,92 +1839,103 @@ var OpenCodeAgent = class extends BaseAgent {
1773
1839
  incrementalScan(_cachedSessions, _changedIds) {
1774
1840
  return this.scan();
1775
1841
  }
1842
+ parsePartRow(partRow) {
1843
+ const partData = JSON.parse(String(partRow.data ?? "{}"));
1844
+ const partType = String(partData.type ?? "");
1845
+ if (isInternalEventType(partType)) return null;
1846
+ if (partType === "text" || partType === "reasoning") {
1847
+ const text = cleanInternalText(String(partData.text ?? ""));
1848
+ if (!text) return null;
1849
+ return {
1850
+ type: partType,
1851
+ text,
1852
+ time_created: Number(partRow.time_created ?? 0)
1853
+ };
1854
+ }
1855
+ if (partType === "tool") {
1856
+ return {
1857
+ type: "tool",
1858
+ tool: String(partData.tool ?? ""),
1859
+ callID: String(partData.callID ?? ""),
1860
+ title: cleanInternalText(String(partData.title ?? "")),
1861
+ state: partData.state ?? {},
1862
+ time_created: Number(partRow.time_created ?? 0)
1863
+ };
1864
+ }
1865
+ return null;
1866
+ }
1776
1867
  readMessageParts(db, messageId) {
1777
- const partRows = db.prepare("SELECT * FROM part WHERE message_id = ? ORDER BY time_created ASC").all(messageId);
1778
- const parts = [];
1779
- for (const partRow of partRows) {
1780
- const partData = JSON.parse(String(partRow.data ?? "{}"));
1781
- const partType = String(partData.type ?? "");
1782
- if (isInternalEventType(partType)) continue;
1783
- if (partType === "text" || partType === "reasoning") {
1784
- const text = cleanInternalText(String(partData.text ?? ""));
1785
- if (text) {
1786
- parts.push({
1787
- type: partType,
1788
- text,
1789
- time_created: Number(partRow.time_created ?? 0)
1790
- });
1791
- }
1792
- } else if (partType === "tool") {
1793
- parts.push({
1794
- type: "tool",
1795
- tool: String(partData.tool ?? ""),
1796
- callID: String(partData.callID ?? ""),
1797
- title: cleanInternalText(String(partData.title ?? "")),
1798
- state: partData.state ?? {},
1799
- time_created: Number(partRow.time_created ?? 0)
1800
- });
1868
+ const partRows = db.prepare("SELECT data, time_created FROM part WHERE message_id = ? ORDER BY time_created ASC").all(messageId);
1869
+ return partRows.map((partRow) => this.parsePartRow(partRow)).filter((part) => part !== null);
1870
+ }
1871
+ buildPartsByMessage(partRows) {
1872
+ const partsByMessage = /* @__PURE__ */ new Map();
1873
+ for (const row of partRows) {
1874
+ const messageId = String(row.message_id ?? "");
1875
+ if (!messageId) continue;
1876
+ const part = this.parsePartRow(row);
1877
+ if (!part) continue;
1878
+ const parts = partsByMessage.get(messageId);
1879
+ if (parts) {
1880
+ parts.push(part);
1881
+ } else {
1882
+ partsByMessage.set(messageId, [part]);
1801
1883
  }
1802
1884
  }
1803
- return parts;
1885
+ return partsByMessage;
1804
1886
  }
1805
- readFirstUserTitle(db, sessionId) {
1806
- const rows = db.prepare(
1807
- "SELECT id, data, time_created FROM message WHERE session_id = ? ORDER BY time_created ASC"
1808
- ).all(sessionId);
1809
- for (const row of rows) {
1887
+ buildHeadContexts(messageRows, partRows) {
1888
+ const partsByMessage = this.buildPartsByMessage(partRows);
1889
+ const contexts = /* @__PURE__ */ new Map();
1890
+ for (const row of messageRows) {
1891
+ const sessionId = String(row.session_id ?? "");
1892
+ if (!sessionId) continue;
1810
1893
  const msgData = JSON.parse(String(row.data ?? "{}"));
1811
1894
  if (isInternalEventType(msgData.type)) continue;
1812
- if (String(msgData.role ?? "") !== "user") continue;
1813
- const parts = this.readMessageParts(db, row.id);
1814
- const title = firstUserMessageTitle([
1815
- {
1816
- id: String(row.id ?? ""),
1817
- role: "user",
1818
- agent: null,
1819
- time_created: Number(row.time_created ?? 0),
1820
- parts
1821
- }
1822
- ]);
1823
- if (title) return title;
1895
+ const parts = partsByMessage.get(String(row.id ?? "")) ?? [];
1896
+ if (parts.length === 0) continue;
1897
+ let context = contexts.get(sessionId);
1898
+ if (!context) {
1899
+ context = {
1900
+ stats: {
1901
+ message_count: 0,
1902
+ total_input_tokens: 0,
1903
+ total_output_tokens: 0,
1904
+ total_cost: 0
1905
+ },
1906
+ messageTitle: null
1907
+ };
1908
+ contexts.set(sessionId, context);
1909
+ }
1910
+ const cost = Number(msgData.cost ?? 0);
1911
+ const tokens = msgData.tokens;
1912
+ const inputTokens = Number(tokens?.input ?? 0);
1913
+ const outputTokens = Number(tokens?.output ?? 0);
1914
+ const model = msgData.modelID ?? null;
1915
+ const estimatedCost = cost > 0 ? null : estimateTokenCost(model, { input: inputTokens, output: outputTokens });
1916
+ if (estimatedCost !== null) context.stats.cost_source = "estimated";
1917
+ context.stats.total_cost += cost || estimatedCost || 0;
1918
+ context.stats.total_input_tokens += inputTokens;
1919
+ context.stats.total_output_tokens += outputTokens;
1920
+ context.stats.message_count += 1;
1921
+ if (!context.messageTitle && String(msgData.role ?? "") === "user") {
1922
+ context.messageTitle = firstUserMessageTitle([
1923
+ {
1924
+ id: String(row.id ?? ""),
1925
+ role: "user",
1926
+ agent: null,
1927
+ time_created: Number(row.time_created ?? 0),
1928
+ parts
1929
+ }
1930
+ ]);
1931
+ }
1824
1932
  }
1825
- return null;
1826
- }
1827
- readSessionStats(db, sessionId) {
1828
- try {
1829
- const rows = db.prepare("SELECT id, data FROM message WHERE session_id = ? ORDER BY time_created ASC").all(sessionId);
1830
- let totalCost = 0;
1831
- let totalInputTokens = 0;
1832
- let totalOutputTokens = 0;
1833
- let hasEstimatedCost = false;
1834
- let messageCount = 0;
1835
- for (const row of rows) {
1836
- const msgData = JSON.parse(String(row.data ?? "{}"));
1837
- if (isInternalEventType(msgData.type)) continue;
1838
- const parts = this.readMessageParts(db, row.id);
1839
- if (parts.length === 0) continue;
1840
- const cost = Number(msgData.cost ?? 0);
1841
- const tokens = msgData.tokens;
1842
- const inputTokens = Number(tokens?.input ?? 0);
1843
- const outputTokens = Number(tokens?.output ?? 0);
1844
- const model = msgData.modelID ?? null;
1845
- const estimatedCost = cost > 0 ? null : estimateTokenCost(model, { input: inputTokens, output: outputTokens });
1846
- if (estimatedCost !== null) hasEstimatedCost = true;
1847
- totalCost += cost || estimatedCost || 0;
1848
- totalInputTokens += inputTokens;
1849
- totalOutputTokens += outputTokens;
1850
- messageCount++;
1933
+ for (const context of contexts.values()) {
1934
+ if (context.stats.total_cost > 0 && context.stats.cost_source !== "estimated") {
1935
+ context.stats.cost_source = "recorded";
1851
1936
  }
1852
- return {
1853
- message_count: messageCount,
1854
- total_input_tokens: totalInputTokens,
1855
- total_output_tokens: totalOutputTokens,
1856
- total_cost: totalCost,
1857
- cost_source: totalCost > 0 ? hasEstimatedCost ? "estimated" : "recorded" : void 0
1858
- };
1859
- } catch {
1860
- return null;
1861
1937
  }
1938
+ return contexts;
1862
1939
  }
1863
1940
  getSessionData(sessionId) {
1864
1941
  if (!this.dbPath) {
@@ -2162,14 +2239,24 @@ var KimiAgent = class extends BaseAgent {
2162
2239
  const listMarker = perf.start("listSessionDirs");
2163
2240
  const sessionDirs = this.listSessionDirs();
2164
2241
  perf.end(listMarker);
2165
- const heads = [];
2242
+ const metas = [];
2166
2243
  for (const dir of sessionDirs) {
2167
2244
  try {
2168
2245
  const parseMarker = perf.start(`parseSessionDir:${basename4(dir)}`);
2169
2246
  const meta = getParsedSession(this.parseSessionDirResult(dir));
2170
2247
  perf.end(parseMarker);
2171
- if (!meta) continue;
2172
- if (!matchesScanWindow(meta.createdAt, options)) continue;
2248
+ if (meta && matchesScanWindow(meta.createdAt, options)) {
2249
+ metas.push(meta);
2250
+ }
2251
+ } catch {
2252
+ }
2253
+ }
2254
+ options?.onProgress?.({ total: metas.length, processed: 0, sessions: 0 });
2255
+ const heads = [];
2256
+ let processed = 0;
2257
+ for (const meta of metas) {
2258
+ try {
2259
+ meta.sourceFingerprint = this.sourceFingerprint(meta);
2173
2260
  this.sessionMetaMap.set(meta.id, meta);
2174
2261
  const stats = this.extractStats(meta.sourcePath);
2175
2262
  heads.push({
@@ -2182,11 +2269,46 @@ var KimiAgent = class extends BaseAgent {
2182
2269
  stats
2183
2270
  });
2184
2271
  } catch {
2272
+ } finally {
2273
+ processed += 1;
2274
+ options?.onProgress?.({ total: metas.length, processed, sessions: heads.length });
2185
2275
  }
2186
2276
  }
2187
2277
  perf.end(scanMarker);
2188
2278
  return heads;
2189
2279
  }
2280
+ listSessionSources() {
2281
+ if (!this.basePath) return [];
2282
+ const refs = [];
2283
+ for (const dir of this.listSessionDirs()) {
2284
+ const meta = getParsedSession(this.parseSessionDirResult(dir));
2285
+ if (!meta) continue;
2286
+ meta.sourceFingerprint = this.sourceFingerprint(meta);
2287
+ this.sessionMetaMap.set(meta.id, meta);
2288
+ refs.push({
2289
+ sessionId: meta.id,
2290
+ sourcePath: meta.sourcePath,
2291
+ fingerprint: this.sourceFingerprint(meta)
2292
+ });
2293
+ }
2294
+ return refs;
2295
+ }
2296
+ scanSessionSource(sourcePath) {
2297
+ const meta = getParsedSession(this.parseSessionDirResult(sourcePath));
2298
+ if (!meta) return null;
2299
+ meta.sourceFingerprint = this.sourceFingerprint(meta);
2300
+ this.sessionMetaMap.set(meta.id, meta);
2301
+ const stats = this.extractStats(meta.sourcePath);
2302
+ return {
2303
+ id: meta.id,
2304
+ slug: `kimi/${meta.id}`,
2305
+ title: meta.title,
2306
+ directory: meta.cwd,
2307
+ time_created: meta.createdAt,
2308
+ time_updated: meta.createdAt,
2309
+ stats
2310
+ };
2311
+ }
2190
2312
  getSessionMetaMap() {
2191
2313
  return this.sessionMetaMap;
2192
2314
  }
@@ -2498,6 +2620,21 @@ var KimiAgent = class extends BaseAgent {
2498
2620
  return this.buildSessionData(meta, filteredMessages, stats);
2499
2621
  }
2500
2622
  // --- Helpers ---
2623
+ sourceFingerprint(meta) {
2624
+ const fileMtime = (path2) => {
2625
+ if (!path2) return null;
2626
+ try {
2627
+ return statSync3(path2).mtimeMs;
2628
+ } catch {
2629
+ return null;
2630
+ }
2631
+ };
2632
+ return JSON.stringify([
2633
+ fileMtime(meta.metaFile),
2634
+ fileMtime(meta.contextFile),
2635
+ fileMtime(meta.wireFile)
2636
+ ]);
2637
+ }
2501
2638
  buildMessage(opts) {
2502
2639
  return {
2503
2640
  id: opts.messageId,
@@ -2695,6 +2832,8 @@ var KimiAgent = class extends BaseAgent {
2695
2832
  var PROPOSED_PLAN_PATTERN = /<proposed_plan>\s*([\s\S]*?)\s*<\/proposed_plan>/;
2696
2833
  var PLAN_APPROVAL_PREFIX = "PLEASE IMPLEMENT THIS PLAN";
2697
2834
  var SUBAGENT_NOTIFICATION_PATTERN = /<subagent_notification>\s*([\s\S]*?)\s*<\/subagent_notification>/;
2835
+ var HEAD_INDEX_VERSION2 = "codex-head-v1";
2836
+ var PARSER_VERSION = "codex-parser-v3";
2698
2837
  var DEVELOPER_LIKE_USER_MARKERS = [
2699
2838
  "agents.md instructions for",
2700
2839
  "<instructions>",
@@ -2713,7 +2852,6 @@ var CODEX_TOOL_TITLE_MAP = {
2713
2852
  spawn_agent: "subagent",
2714
2853
  subagent: "subagent"
2715
2854
  };
2716
- var RECENT_SESSION_REVALIDATION_WINDOW_MS2 = 24 * 60 * 60 * 1e3;
2717
2855
  function extractSessionId(filename) {
2718
2856
  const stem = basename5(filename, ".jsonl");
2719
2857
  const parts = stem.split("-");
@@ -2738,8 +2876,23 @@ function extractCachedInputTokens(usage) {
2738
2876
  if (!usage) return 0;
2739
2877
  return Number(usage["cached_input_tokens"] ?? usage["cache_read_input_tokens"] ?? 0);
2740
2878
  }
2741
- function mapToolTitle2(name) {
2742
- return CODEX_TOOL_TITLE_MAP[name] ?? name;
2879
+ function resolveToolIdentity(name, namespace) {
2880
+ const mappedName = CODEX_TOOL_TITLE_MAP[name];
2881
+ if (mappedName) return { tool: mappedName };
2882
+ const namespaceText = typeof namespace === "string" ? namespace.trim() : "";
2883
+ if (!namespaceText) return { tool: name };
2884
+ const namespaceName = namespaceText.split("__").at(-1) ?? namespaceText;
2885
+ const toolName = name.replace(/^[_.]+/, "");
2886
+ if (!namespaceName) {
2887
+ return {
2888
+ tool: toolName || name,
2889
+ metadata: { name, namespace: namespaceText }
2890
+ };
2891
+ }
2892
+ return {
2893
+ tool: `${namespaceName}.${toolName || name}`,
2894
+ metadata: { name, namespace: namespaceText }
2895
+ };
2743
2896
  }
2744
2897
  function normalizeToolArguments2(raw) {
2745
2898
  if (typeof raw === "string") {
@@ -2870,6 +3023,8 @@ var CodexAgent = class extends BaseAgent {
2870
3023
  const listMarker = perf.start("listRolloutFiles");
2871
3024
  const files = this.listRolloutFiles(options);
2872
3025
  perf.end(listMarker);
3026
+ options?.onProgress?.({ total: files.length, processed: 0, sessions: 0 });
3027
+ let processed = 0;
2873
3028
  for (const file of files) {
2874
3029
  try {
2875
3030
  const parseMarker = perf.start(`parseSessionHead:${basename5(file)}`);
@@ -2877,23 +3032,34 @@ var CodexAgent = class extends BaseAgent {
2877
3032
  perf.end(parseMarker);
2878
3033
  if (head) {
2879
3034
  heads.push(head);
2880
- this.sessionMetaMap.set(head.id, {
2881
- id: head.id,
2882
- title: head.title,
2883
- sourcePath: file,
2884
- directory: head.directory,
2885
- model: null,
2886
- messageCount: head.stats.message_count,
2887
- createdAt: head.time_created,
2888
- updatedAt: head.time_updated ?? head.time_created
2889
- });
3035
+ this.sessionMetaMap.set(head.id, this.buildSessionMeta(head, file));
2890
3036
  }
2891
3037
  } catch {
3038
+ } finally {
3039
+ processed += 1;
3040
+ options?.onProgress?.({ total: files.length, processed, sessions: heads.length });
2892
3041
  }
2893
3042
  }
2894
3043
  perf.end(scanMarker);
2895
3044
  return heads;
2896
3045
  }
3046
+ listSessionSources() {
3047
+ if (!this.basePath) return [];
3048
+ this.loadSessionIndex();
3049
+ return this.listRolloutFiles().map((file) => ({
3050
+ sessionId: extractSessionId(file),
3051
+ sourcePath: file,
3052
+ fingerprint: this.sourceFingerprint(file)
3053
+ }));
3054
+ }
3055
+ scanSessionSource(sourcePath) {
3056
+ this.loadSessionIndex();
3057
+ const head = getParsedSession(this.parseSessionHeadResult(sourcePath));
3058
+ if (head) {
3059
+ this.sessionMetaMap.set(head.id, this.buildSessionMeta(head, sourcePath));
3060
+ }
3061
+ return head;
3062
+ }
2897
3063
  getSessionMetaMap() {
2898
3064
  return this.sessionMetaMap;
2899
3065
  }
@@ -2903,32 +3069,26 @@ var CodexAgent = class extends BaseAgent {
2903
3069
  /**
2904
3070
  * 检测文件系统变更
2905
3071
  */
2906
- checkForChanges(sinceTimestamp, cachedSessions) {
3072
+ checkForChanges(_sinceTimestamp, cachedSessions) {
2907
3073
  if (!this.basePath) {
2908
3074
  return { hasChanges: false, timestamp: Date.now() };
2909
3075
  }
2910
- const now = Date.now();
2911
- const changedIds = /* @__PURE__ */ new Set();
2912
3076
  const currentFiles = this.listRolloutFiles();
2913
3077
  const currentIds = new Set(currentFiles.map((file) => extractSessionId(file)));
2914
3078
  const cachedIds = new Set(cachedSessions.map((session) => session.id));
2915
- const recentIds = cachedSessions.filter((session) => now - session.time_created <= RECENT_SESSION_REVALIDATION_WINDOW_MS2).map((session) => session.id);
2916
- for (const sessionId of recentIds) {
2917
- changedIds.add(sessionId);
2918
- }
3079
+ const changedIds = /* @__PURE__ */ new Set();
2919
3080
  for (const session of cachedSessions) {
2920
- const meta = this.sessionMetaMap.get(session.id);
2921
3081
  if (!currentIds.has(session.id)) {
2922
3082
  changedIds.add(session.id);
2923
3083
  continue;
2924
3084
  }
3085
+ const meta = this.sessionMetaMap.get(session.id);
2925
3086
  if (!meta) {
2926
3087
  changedIds.add(session.id);
2927
3088
  continue;
2928
3089
  }
2929
3090
  try {
2930
- const stat = statSync4(meta.sourcePath);
2931
- if (stat.mtimeMs > sinceTimestamp) {
3091
+ if (this.hasMetaChanged(meta)) {
2932
3092
  changedIds.add(session.id);
2933
3093
  }
2934
3094
  } catch {
@@ -2936,7 +3096,7 @@ var CodexAgent = class extends BaseAgent {
2936
3096
  }
2937
3097
  }
2938
3098
  const hasAddedSessions = currentFiles.some((file) => !cachedIds.has(extractSessionId(file)));
2939
- if (recentIds.length > 0) {
3099
+ if (hasAddedSessions || changedIds.size > 0) {
2940
3100
  this.sessionIndexCache.clear();
2941
3101
  }
2942
3102
  return {
@@ -2967,16 +3127,7 @@ var CodexAgent = class extends BaseAgent {
2967
3127
  const head = this.parseSessionHead(file);
2968
3128
  if (head) {
2969
3129
  sessionMap.set(head.id, head);
2970
- this.sessionMetaMap.set(head.id, {
2971
- id: head.id,
2972
- title: head.title,
2973
- sourcePath: file,
2974
- directory: head.directory,
2975
- model: null,
2976
- messageCount: head.stats.message_count,
2977
- createdAt: head.time_created,
2978
- updatedAt: head.time_updated ?? head.time_created
2979
- });
3130
+ this.sessionMetaMap.set(head.id, this.buildSessionMeta(head, file));
2980
3131
  }
2981
3132
  }
2982
3133
  } catch {
@@ -2989,16 +3140,7 @@ var CodexAgent = class extends BaseAgent {
2989
3140
  const head = this.parseSessionHead(file);
2990
3141
  if (head) {
2991
3142
  sessionMap.set(head.id, head);
2992
- this.sessionMetaMap.set(head.id, {
2993
- id: head.id,
2994
- title: head.title,
2995
- sourcePath: file,
2996
- directory: head.directory,
2997
- model: null,
2998
- messageCount: head.stats.message_count,
2999
- createdAt: head.time_created,
3000
- updatedAt: head.time_updated ?? head.time_created
3001
- });
3143
+ this.sessionMetaMap.set(head.id, this.buildSessionMeta(head, file));
3002
3144
  }
3003
3145
  }
3004
3146
  } catch {
@@ -3145,13 +3287,18 @@ var CodexAgent = class extends BaseAgent {
3145
3287
  walkDirForRolloutFiles(dir, options) {
3146
3288
  const files = [];
3147
3289
  try {
3148
- for (const entry of readdirSync3(dir)) {
3149
- const fullPath = join7(dir, entry);
3150
- const stat = statSync4(fullPath);
3151
- if (stat.isDirectory()) {
3290
+ for (const entry of readdirSync3(dir, { withFileTypes: true })) {
3291
+ const fullPath = join7(dir, entry.name);
3292
+ if (entry.isDirectory()) {
3152
3293
  files.push(...this.walkDirForRolloutFiles(fullPath, options));
3153
- } else if (entry.endsWith(".jsonl") && entry.startsWith("rollout-")) {
3154
- if (!matchesScanWindow(stat.mtimeMs, options)) continue;
3294
+ } else if (entry.name.endsWith(".jsonl") && entry.name.startsWith("rollout-")) {
3295
+ if (options?.from != null || options?.to != null) {
3296
+ try {
3297
+ if (!matchesScanWindow(statSync4(fullPath).mtimeMs, options)) continue;
3298
+ } catch {
3299
+ continue;
3300
+ }
3301
+ }
3155
3302
  files.push(fullPath);
3156
3303
  }
3157
3304
  }
@@ -3159,11 +3306,59 @@ var CodexAgent = class extends BaseAgent {
3159
3306
  }
3160
3307
  return files;
3161
3308
  }
3309
+ buildSessionMeta(head, file) {
3310
+ const indexPath = this.getSessionIndexPath();
3311
+ return {
3312
+ id: head.id,
3313
+ title: head.title,
3314
+ sourcePath: file,
3315
+ sourceFingerprint: this.sourceFingerprint(file),
3316
+ sourceMtimeMs: statSync4(file).mtimeMs,
3317
+ indexPath: existsSync7(indexPath) ? indexPath : null,
3318
+ indexMtimeMs: this.getFileMtimeMs(indexPath),
3319
+ headIndexVersion: HEAD_INDEX_VERSION2,
3320
+ parserVersion: PARSER_VERSION,
3321
+ directory: head.directory,
3322
+ model: null,
3323
+ messageCount: head.stats.message_count,
3324
+ createdAt: head.time_created,
3325
+ updatedAt: head.time_updated ?? head.time_created
3326
+ };
3327
+ }
3328
+ hasMetaChanged(meta) {
3329
+ if (meta.headIndexVersion !== HEAD_INDEX_VERSION2) return true;
3330
+ if (meta.parserVersion !== PARSER_VERSION) return true;
3331
+ if (typeof meta.sourceMtimeMs !== "number") return true;
3332
+ if (statSync4(meta.sourcePath).mtimeMs !== meta.sourceMtimeMs) return true;
3333
+ const indexPath = meta.indexPath ?? this.getSessionIndexPath();
3334
+ return this.getFileMtimeMs(indexPath) !== (meta.indexMtimeMs ?? null);
3335
+ }
3336
+ sourceFingerprint(file) {
3337
+ const stat = statSync4(file);
3338
+ const indexPath = this.getSessionIndexPath();
3339
+ return JSON.stringify([
3340
+ HEAD_INDEX_VERSION2,
3341
+ PARSER_VERSION,
3342
+ stat.mtimeMs,
3343
+ stat.size,
3344
+ this.getFileMtimeMs(indexPath)
3345
+ ]);
3346
+ }
3347
+ getSessionIndexPath() {
3348
+ const roots = resolveProviderRoots();
3349
+ return join7(roots.codexRoot, "session_index.jsonl");
3350
+ }
3351
+ getFileMtimeMs(filePath) {
3352
+ try {
3353
+ return statSync4(filePath).mtimeMs;
3354
+ } catch {
3355
+ return null;
3356
+ }
3357
+ }
3162
3358
  // ---- Session index ----
3163
3359
  loadSessionIndex() {
3164
3360
  if (this.sessionIndexCache.size > 0) return;
3165
- const roots = resolveProviderRoots();
3166
- const indexPath = join7(roots.codexRoot, "session_index.jsonl");
3361
+ const indexPath = this.getSessionIndexPath();
3167
3362
  if (!existsSync7(indexPath)) return;
3168
3363
  try {
3169
3364
  const content = readFileSync5(indexPath, "utf-8");
@@ -3646,16 +3841,17 @@ var CodexAgent = class extends BaseAgent {
3646
3841
  if (!name) {
3647
3842
  return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan: null };
3648
3843
  }
3649
- const mappedName = mapToolTitle2(name);
3844
+ const toolIdentity = resolveToolIdentity(name, payload["namespace"]);
3650
3845
  const arguments_ = normalizeToolArguments2(payload["arguments"]);
3651
3846
  const toolPart = {
3652
3847
  type: "tool",
3653
- tool: mappedName,
3848
+ tool: toolIdentity.tool,
3654
3849
  callID: callId,
3655
- title: `Tool: ${mappedName}`,
3850
+ title: `Tool: ${toolIdentity.tool}`,
3656
3851
  state: {
3657
3852
  arguments: arguments_,
3658
- output: null
3853
+ output: null,
3854
+ metadata: toolIdentity.metadata
3659
3855
  },
3660
3856
  time_created: timestampMs
3661
3857
  };
@@ -3712,17 +3908,18 @@ var CodexAgent = class extends BaseAgent {
3712
3908
  if (!name) {
3713
3909
  return { currentAssistantIndex, latestAssistantTextIndex, pendingPlan: null };
3714
3910
  }
3715
- const mappedName = mapToolTitle2(name);
3911
+ const toolIdentity = resolveToolIdentity(name, payload["namespace"]);
3716
3912
  const rawInput = payload["input"];
3717
3913
  const normalizedInput = normalizeCustomToolArguments(name, rawInput);
3718
3914
  const toolPart = {
3719
3915
  type: "tool",
3720
- tool: mappedName,
3916
+ tool: toolIdentity.tool,
3721
3917
  callID: callId,
3722
- title: `Tool: ${mappedName}`,
3918
+ title: `Tool: ${toolIdentity.tool}`,
3723
3919
  state: {
3724
3920
  arguments: normalizedInput,
3725
- output: null
3921
+ output: null,
3922
+ metadata: toolIdentity.metadata
3726
3923
  },
3727
3924
  time_created: timestampMs
3728
3925
  };
@@ -3797,7 +3994,7 @@ var CURSOR_TOOL_TITLE_MAP = {
3797
3994
  ripgrep_raw_search: "grep",
3798
3995
  glob_file_search: "glob"
3799
3996
  };
3800
- function mapToolTitle3(toolName) {
3997
+ function mapToolTitle2(toolName) {
3801
3998
  return CURSOR_TOOL_TITLE_MAP[toolName] ?? toolName;
3802
3999
  }
3803
4000
  function normalizeToolOutputParts2(output, timestampMs) {
@@ -3866,9 +4063,9 @@ function buildToolPart(action, timestampMs) {
3866
4063
  const toolName = action.tool ?? "unknown";
3867
4064
  return {
3868
4065
  type: "tool",
3869
- tool: mapToolTitle3(toolName),
4066
+ tool: mapToolTitle2(toolName),
3870
4067
  callID: action.type ? `${action.type}:${String(action.input?.id ?? "")}` : "",
3871
- title: `Tool: ${mapToolTitle3(toolName)}`,
4068
+ title: `Tool: ${mapToolTitle2(toolName)}`,
3872
4069
  state: buildToolState(action),
3873
4070
  time_created: timestampMs
3874
4071
  };
@@ -3990,6 +4187,8 @@ var CursorAgent = class extends BaseAgent {
3990
4187
  try {
3991
4188
  const rows = db.prepare("SELECT key, value FROM cursorDiskKV WHERE key LIKE 'composerData:%'").all();
3992
4189
  const heads = [];
4190
+ options?.onProgress?.({ total: rows.length, processed: 0, sessions: 0 });
4191
+ let processed = 0;
3993
4192
  for (const row of rows) {
3994
4193
  try {
3995
4194
  const composer = JSON.parse(row.value);
@@ -4093,6 +4292,9 @@ var CursorAgent = class extends BaseAgent {
4093
4292
  sourcePath: this.dbPath || ""
4094
4293
  });
4095
4294
  } catch {
4295
+ } finally {
4296
+ processed += 1;
4297
+ options?.onProgress?.({ total: rows.length, processed, sessions: heads.length });
4096
4298
  }
4097
4299
  }
4098
4300
  perf.end(scanMarker);
@@ -4350,7 +4552,7 @@ var CursorAgent = class extends BaseAgent {
4350
4552
  convertToolFormerData(toolData, timestampMs) {
4351
4553
  if (!toolData || !toolData.name) return null;
4352
4554
  const toolName = toolData.name;
4353
- const normalizedName = toolName === "create_plan" ? "plan" : mapToolTitle3(toolName);
4555
+ const normalizedName = toolName === "create_plan" ? "plan" : mapToolTitle2(toolName);
4354
4556
  const state = {
4355
4557
  status: toolData.status === "completed" ? "completed" : "running"
4356
4558
  };
@@ -4579,7 +4781,7 @@ function computeIdentity(cwd, fs) {
4579
4781
  if (!cwd) return loose();
4580
4782
  const pathOps = getPathOps(cwd);
4581
4783
  const absoluteCwd = pathOps.resolve(cwd);
4582
- const homeDir = homedir3();
4784
+ const homeDir = os.homedir();
4583
4785
  const homePathOps = getPathOps(homeDir);
4584
4786
  const home = homePathOps === pathOps ? pathOps.resolve(homeDir) : homeDir;
4585
4787
  if (absoluteCwd === home || LOOSE_DIRS.has(absoluteCwd)) return loose();
@@ -4620,6 +4822,10 @@ function computeIdentity(cwd, fs) {
4620
4822
  displayName: deriveDisplayName({ kind: "manifest_path", key: manifestDir, fs })
4621
4823
  };
4622
4824
  }
4825
+ if (homePathOps === pathOps) {
4826
+ const synthetic = synthesizeCodexScratchIdentity(absoluteCwd, home, pathOps);
4827
+ if (synthetic) return synthetic;
4828
+ }
4623
4829
  return {
4624
4830
  kind: "path",
4625
4831
  key: absoluteCwd,
@@ -4629,6 +4835,14 @@ function computeIdentity(cwd, fs) {
4629
4835
  function loose() {
4630
4836
  return { kind: "loose", key: "loose", displayName: "Loose" };
4631
4837
  }
4838
+ function synthesizeCodexScratchIdentity(absoluteCwd, home, pathOps) {
4839
+ const root = pathOps.resolve(pathOps.join(home, "Documents", "Codex"));
4840
+ const child = pathOps.relative(root, absoluteCwd);
4841
+ if (!child || child === ".." || child.startsWith(`..${pathOps.sep}`) || pathOps.isAbsolute(child)) {
4842
+ return null;
4843
+ }
4844
+ return { kind: "synthetic", key: "codex:scratch", displayName: "Chats" };
4845
+ }
4632
4846
  function getPathOps(input) {
4633
4847
  if (/^[a-zA-Z]:[\\/]/.test(input) || input.startsWith("\\\\") || input.includes("\\")) {
4634
4848
  return path.win32;
@@ -4684,6 +4898,28 @@ function parseManifestName(file, text) {
4684
4898
  }
4685
4899
  return null;
4686
4900
  }
4901
+ function createProjectScopeMatcher(queryPath, fs = realFs) {
4902
+ return {
4903
+ identityKey: computeIdentity(queryPath, fs).key,
4904
+ path: normalizeScopePath(queryPath)
4905
+ };
4906
+ }
4907
+ function matchesProjectScope(session, scope) {
4908
+ if (!session.directory) return false;
4909
+ if (session.project_identity?.key === scope.identityKey) return true;
4910
+ return isPathScopeMatch(scope.path, session.directory);
4911
+ }
4912
+ function filterSessionsByProjectScope(sessions, queryPath, fs) {
4913
+ const scope = createProjectScopeMatcher(queryPath, fs);
4914
+ return sessions.filter((session) => matchesProjectScope(session, scope));
4915
+ }
4916
+ function isPathScopeMatch(queryPath, sessionPath) {
4917
+ const session = normalizeScopePath(sessionPath);
4918
+ return session === queryPath || session.startsWith(queryPath + "/") || queryPath.startsWith(session + "/");
4919
+ }
4920
+ function normalizeScopePath(path2) {
4921
+ return resolve(path2).replaceAll(sep, "/");
4922
+ }
4687
4923
  var TAG_ORDER = [
4688
4924
  "bugfix",
4689
4925
  "refactoring",
@@ -4953,7 +5189,8 @@ function extractSessionFileActivity(agentName, sessionId, projectIdentityKey, me
4953
5189
  extractFileActivityOccurrences(messages)
4954
5190
  );
4955
5191
  }
4956
- var CACHE_SCHEMA_VERSION = 8;
5192
+ var CACHE_SCHEMA_VERSION = 13;
5193
+ var CACHE_INITIALIZATION_VERSION = "session-cache-v2";
4957
5194
  var CACHE_TTL = 7 * 24 * 60 * 60 * 1e3;
4958
5195
  var CACHE_FILENAME = "codesesh.db";
4959
5196
  var LEGACY_CACHE_FILENAME = "scan-cache.json";
@@ -4984,6 +5221,17 @@ function withCacheDb(fn) {
4984
5221
  db.close();
4985
5222
  }
4986
5223
  }
5224
+ function withCacheDbReadOnly(fn) {
5225
+ const db = openDbReadOnly(getCachePath2());
5226
+ if (!db) return null;
5227
+ try {
5228
+ return fn(db);
5229
+ } catch {
5230
+ return null;
5231
+ } finally {
5232
+ db.close();
5233
+ }
5234
+ }
4987
5235
  function createCacheTables(db) {
4988
5236
  db.exec(`
4989
5237
  CREATE TABLE IF NOT EXISTS cache_meta (
@@ -5003,6 +5251,13 @@ function createCacheTables(db) {
5003
5251
  meta_json TEXT,
5004
5252
  PRIMARY KEY (agent_name, session_id)
5005
5253
  );
5254
+
5255
+ CREATE TABLE IF NOT EXISTS cache_initialization (
5256
+ agent_name TEXT PRIMARY KEY,
5257
+ initialized_at INTEGER NOT NULL,
5258
+ index_version TEXT NOT NULL,
5259
+ last_sync_at INTEGER NOT NULL
5260
+ );
5006
5261
  `);
5007
5262
  }
5008
5263
  function createSessionTables(db) {
@@ -5071,38 +5326,148 @@ function createSessionTables(db) {
5071
5326
  CREATE INDEX IF NOT EXISTS idx_messages_session
5072
5327
  ON messages(agent_name, session_id, message_index);
5073
5328
  `);
5329
+ createMessageToolTables(db);
5074
5330
  }
5075
- function createFileActivityTables(db) {
5331
+ function createMessageToolTables(db) {
5076
5332
  db.exec(`
5077
- CREATE TABLE IF NOT EXISTS session_file_activity (
5333
+ CREATE TABLE IF NOT EXISTS message_tools (
5078
5334
  agent_name TEXT NOT NULL,
5079
5335
  session_id TEXT NOT NULL,
5080
- project_identity_key TEXT NOT NULL,
5081
- path TEXT NOT NULL,
5082
- kind TEXT NOT NULL,
5083
- count INTEGER NOT NULL,
5084
- latest_time INTEGER NOT NULL,
5085
- PRIMARY KEY (agent_name, session_id, project_identity_key, path, kind),
5086
- FOREIGN KEY (agent_name, session_id)
5087
- REFERENCES sessions(agent_name, session_id)
5336
+ message_index INTEGER NOT NULL,
5337
+ tool_name TEXT NOT NULL,
5338
+ PRIMARY KEY (agent_name, session_id, message_index, tool_name),
5339
+ FOREIGN KEY (agent_name, session_id, message_index)
5340
+ REFERENCES messages(agent_name, session_id, message_index)
5088
5341
  ON DELETE CASCADE
5089
5342
  );
5090
5343
 
5091
- CREATE INDEX IF NOT EXISTS idx_file_activity_project_latest
5092
- ON session_file_activity(project_identity_key, latest_time);
5093
-
5094
- CREATE INDEX IF NOT EXISTS idx_file_activity_path
5095
- ON session_file_activity(path);
5096
-
5097
- CREATE INDEX IF NOT EXISTS idx_file_activity_kind
5098
- ON session_file_activity(kind);
5344
+ CREATE INDEX IF NOT EXISTS idx_message_tools_filter
5345
+ ON message_tools(tool_name, agent_name, session_id);
5099
5346
  `);
5100
5347
  }
5101
- function createSearchTables(db) {
5348
+ function createMessageSearchTables(db) {
5349
+ if (!tableExists(db, "messages")) {
5350
+ createSessionTables(db);
5351
+ }
5102
5352
  db.exec(`
5103
- CREATE TABLE IF NOT EXISTS session_documents (
5104
- id INTEGER PRIMARY KEY AUTOINCREMENT,
5105
- agent_name TEXT NOT NULL,
5353
+ CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
5354
+ content_text,
5355
+ content='messages',
5356
+ content_rowid='rowid'
5357
+ );
5358
+ `);
5359
+ createMessageSearchTriggers(db);
5360
+ }
5361
+ function createMessageSearchTriggers(db) {
5362
+ db.exec(`
5363
+ CREATE TRIGGER IF NOT EXISTS messages_ai AFTER INSERT ON messages BEGIN
5364
+ INSERT INTO messages_fts(rowid, content_text)
5365
+ VALUES (new.rowid, new.content_text);
5366
+ END;
5367
+
5368
+ CREATE TRIGGER IF NOT EXISTS messages_ad AFTER DELETE ON messages BEGIN
5369
+ INSERT INTO messages_fts(messages_fts, rowid, content_text)
5370
+ VALUES ('delete', old.rowid, old.content_text);
5371
+ END;
5372
+
5373
+ CREATE TRIGGER IF NOT EXISTS messages_au AFTER UPDATE ON messages BEGIN
5374
+ INSERT INTO messages_fts(messages_fts, rowid, content_text)
5375
+ VALUES ('delete', old.rowid, old.content_text);
5376
+ INSERT INTO messages_fts(rowid, content_text)
5377
+ VALUES (new.rowid, new.content_text);
5378
+ END;
5379
+ `);
5380
+ }
5381
+ function dropMessageSearchTriggers(db) {
5382
+ db.exec(`
5383
+ DROP TRIGGER IF EXISTS messages_ai;
5384
+ DROP TRIGGER IF EXISTS messages_ad;
5385
+ DROP TRIGGER IF EXISTS messages_au;
5386
+ `);
5387
+ }
5388
+ function createFileActivityTables(db) {
5389
+ db.exec(`
5390
+ CREATE TABLE IF NOT EXISTS session_file_activity (
5391
+ agent_name TEXT NOT NULL,
5392
+ session_id TEXT NOT NULL,
5393
+ project_identity_key TEXT NOT NULL,
5394
+ path TEXT NOT NULL,
5395
+ kind TEXT NOT NULL,
5396
+ count INTEGER NOT NULL,
5397
+ latest_time INTEGER NOT NULL,
5398
+ PRIMARY KEY (agent_name, session_id, project_identity_key, path, kind),
5399
+ FOREIGN KEY (agent_name, session_id)
5400
+ REFERENCES sessions(agent_name, session_id)
5401
+ ON DELETE CASCADE
5402
+ );
5403
+
5404
+ CREATE INDEX IF NOT EXISTS idx_file_activity_project_latest
5405
+ ON session_file_activity(project_identity_key, latest_time);
5406
+
5407
+ CREATE INDEX IF NOT EXISTS idx_file_activity_latest
5408
+ ON session_file_activity(latest_time DESC, count DESC, path);
5409
+
5410
+ CREATE INDEX IF NOT EXISTS idx_file_activity_agent_latest
5411
+ ON session_file_activity(agent_name, latest_time DESC, count DESC, path);
5412
+
5413
+ CREATE INDEX IF NOT EXISTS idx_file_activity_project_latest_ordered
5414
+ ON session_file_activity(project_identity_key, latest_time DESC, count DESC, path);
5415
+
5416
+ CREATE INDEX IF NOT EXISTS idx_file_activity_path
5417
+ ON session_file_activity(path);
5418
+
5419
+ CREATE INDEX IF NOT EXISTS idx_file_activity_kind
5420
+ ON session_file_activity(kind);
5421
+ `);
5422
+ createFileActivityPathSearchTables(db);
5423
+ }
5424
+ function createFileActivityPathSearchTables(db) {
5425
+ db.exec(`
5426
+ CREATE VIRTUAL TABLE IF NOT EXISTS session_file_activity_path_fts USING fts5(
5427
+ path,
5428
+ content='session_file_activity',
5429
+ content_rowid='rowid',
5430
+ tokenize='trigram'
5431
+ );
5432
+ `);
5433
+ createFileActivityPathSearchTriggers(db);
5434
+ }
5435
+ function createFileActivityPathSearchTriggers(db) {
5436
+ db.exec(`
5437
+ CREATE TRIGGER IF NOT EXISTS session_file_activity_path_ai
5438
+ AFTER INSERT ON session_file_activity BEGIN
5439
+ INSERT INTO session_file_activity_path_fts(rowid, path)
5440
+ VALUES (new.rowid, new.path);
5441
+ END;
5442
+
5443
+ CREATE TRIGGER IF NOT EXISTS session_file_activity_path_ad
5444
+ AFTER DELETE ON session_file_activity BEGIN
5445
+ INSERT INTO session_file_activity_path_fts(session_file_activity_path_fts, rowid, path)
5446
+ VALUES ('delete', old.rowid, old.path);
5447
+ END;
5448
+
5449
+ CREATE TRIGGER IF NOT EXISTS session_file_activity_path_au
5450
+ AFTER UPDATE ON session_file_activity BEGIN
5451
+ INSERT INTO session_file_activity_path_fts(session_file_activity_path_fts, rowid, path)
5452
+ VALUES ('delete', old.rowid, old.path);
5453
+ INSERT INTO session_file_activity_path_fts(rowid, path)
5454
+ VALUES (new.rowid, new.path);
5455
+ END;
5456
+ `);
5457
+ }
5458
+ function rebuildFileActivityPathIndex(db) {
5459
+ if (!tableExists(db, "session_file_activity_path_fts")) {
5460
+ return;
5461
+ }
5462
+ db.exec(
5463
+ "INSERT INTO session_file_activity_path_fts(session_file_activity_path_fts) VALUES ('rebuild')"
5464
+ );
5465
+ }
5466
+ function createSearchTables(db) {
5467
+ db.exec(`
5468
+ CREATE TABLE IF NOT EXISTS session_documents (
5469
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
5470
+ agent_name TEXT NOT NULL,
5106
5471
  session_id TEXT NOT NULL,
5107
5472
  slug TEXT NOT NULL,
5108
5473
  title TEXT NOT NULL,
@@ -5227,6 +5592,7 @@ function recreateProjectGroupsView(db) {
5227
5592
  function createLatestCacheSchema(db) {
5228
5593
  createCacheTables(db);
5229
5594
  createSessionTables(db);
5595
+ createMessageSearchTables(db);
5230
5596
  createFileActivityTables(db);
5231
5597
  createSearchTables(db);
5232
5598
  createProjectTables(db);
@@ -5245,6 +5611,34 @@ function sourcePathFromMetaJson(metaJson) {
5245
5611
  const meta = JSON.parse(metaJson);
5246
5612
  return sourcePathFromMeta(meta);
5247
5613
  }
5614
+ function prepareUpsertCachedSession(db) {
5615
+ return db.prepare(`
5616
+ INSERT INTO cached_sessions(agent_name, session_id, session_json, meta_json)
5617
+ VALUES (?, ?, ?, ?)
5618
+ ON CONFLICT(agent_name, session_id) DO UPDATE SET
5619
+ session_json = excluded.session_json,
5620
+ meta_json = excluded.meta_json
5621
+ `);
5622
+ }
5623
+ function prepareUpsertProjectSession(db) {
5624
+ return db.prepare(`
5625
+ INSERT INTO project_sessions(
5626
+ agent_name,
5627
+ session_id,
5628
+ identity_kind,
5629
+ identity_key,
5630
+ display_name,
5631
+ directory,
5632
+ activity_time
5633
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
5634
+ ON CONFLICT(agent_name, session_id) DO UPDATE SET
5635
+ identity_kind = excluded.identity_kind,
5636
+ identity_key = excluded.identity_key,
5637
+ display_name = excluded.display_name,
5638
+ directory = excluded.directory,
5639
+ activity_time = excluded.activity_time
5640
+ `);
5641
+ }
5248
5642
  function prepareUpsertSession(db) {
5249
5643
  return db.prepare(`
5250
5644
  INSERT INTO sessions(
@@ -5396,6 +5790,16 @@ function prepareInsertFileActivity(db) {
5396
5790
  ) VALUES (?, ?, ?, ?, ?, ?, ?)
5397
5791
  `);
5398
5792
  }
5793
+ function prepareInsertMessageTool(db) {
5794
+ return db.prepare(`
5795
+ INSERT OR IGNORE INTO message_tools(
5796
+ agent_name,
5797
+ session_id,
5798
+ message_index,
5799
+ tool_name
5800
+ ) VALUES (?, ?, ?, ?)
5801
+ `);
5802
+ }
5399
5803
  function writeFileActivityRows(statement, activities) {
5400
5804
  for (const activity of activities) {
5401
5805
  statement.run(
@@ -5409,6 +5813,17 @@ function writeFileActivityRows(statement, activities) {
5409
5813
  );
5410
5814
  }
5411
5815
  }
5816
+ function writeProjectSessionRow(statement, agentName, session, identity) {
5817
+ statement.run(
5818
+ agentName,
5819
+ session.id,
5820
+ identity.kind,
5821
+ identity.key,
5822
+ identity.displayName,
5823
+ session.directory,
5824
+ session.time_updated ?? session.time_created
5825
+ );
5826
+ }
5412
5827
  function sessionFromRow(row) {
5413
5828
  const session = {
5414
5829
  id: String(row.session_id),
@@ -5476,6 +5891,15 @@ function readLegacyCacheVersion(db) {
5476
5891
  return Number(versionRow?.value ?? 0);
5477
5892
  }
5478
5893
  function inferCacheSchemaVersion(db) {
5894
+ if (tableExists(db, "message_tools")) {
5895
+ return 11;
5896
+ }
5897
+ if (tableExists(db, "session_file_activity_path_fts")) {
5898
+ return 10;
5899
+ }
5900
+ if (tableExists(db, "messages_fts")) {
5901
+ return 9;
5902
+ }
5479
5903
  if (tableExists(db, "session_file_activity")) {
5480
5904
  return 8;
5481
5905
  }
@@ -5508,7 +5932,9 @@ function hasAnyCacheSchema(db) {
5508
5932
  "cached_sessions",
5509
5933
  "sessions",
5510
5934
  "messages",
5935
+ "message_tools",
5511
5936
  "session_file_activity",
5937
+ "session_file_activity_path_fts",
5512
5938
  "session_documents",
5513
5939
  "session_documents_fts",
5514
5940
  "project_sessions"
@@ -5580,6 +6006,46 @@ function migrateProjectIdentity(db) {
5580
6006
  backfillProjectSessions(db);
5581
6007
  backfillSessionDocumentProjects(db);
5582
6008
  }
6009
+ function refreshProjectIdentities(db) {
6010
+ if (tableExists(db, "sessions") && columnExists(db, "sessions", "project_identity_key") && columnExists(db, "sessions", "directory")) {
6011
+ const rows = db.prepare("SELECT agent_name, session_id, directory FROM sessions").all();
6012
+ const update = db.prepare(`
6013
+ UPDATE sessions
6014
+ SET
6015
+ project_identity_kind = ?,
6016
+ project_identity_key = ?,
6017
+ project_display_name = ?
6018
+ WHERE agent_name = ? AND session_id = ?
6019
+ `);
6020
+ const updateFileActivity = tableExists(db, "session_file_activity") && columnExists(db, "session_file_activity", "project_identity_key") ? db.prepare(`
6021
+ UPDATE session_file_activity
6022
+ SET project_identity_key = ?
6023
+ WHERE agent_name = ? AND session_id = ?
6024
+ `) : null;
6025
+ for (const row of rows) {
6026
+ const identity = computeIdentity(String(row.directory ?? ""), realFs);
6027
+ update.run(identity.kind, identity.key, identity.displayName, row.agent_name, row.session_id);
6028
+ updateFileActivity?.run(identity.key, row.agent_name, row.session_id);
6029
+ }
6030
+ }
6031
+ if (tableExists(db, "project_sessions") && columnExists(db, "project_sessions", "identity_key") && columnExists(db, "project_sessions", "directory")) {
6032
+ const rows = db.prepare("SELECT agent_name, session_id, directory FROM project_sessions").all();
6033
+ const update = db.prepare(`
6034
+ UPDATE project_sessions
6035
+ SET
6036
+ identity_kind = ?,
6037
+ identity_key = ?,
6038
+ display_name = ?
6039
+ WHERE agent_name = ? AND session_id = ?
6040
+ `);
6041
+ for (const row of rows) {
6042
+ const identity = computeIdentity(String(row.directory ?? ""), realFs);
6043
+ update.run(identity.kind, identity.key, identity.displayName, row.agent_name, row.session_id);
6044
+ }
6045
+ }
6046
+ backfillSessionDocumentProjects(db);
6047
+ recreateProjectGroupsView(db);
6048
+ }
5583
6049
  function backfillStructuredSessions(db) {
5584
6050
  createSessionTables(db);
5585
6051
  recreateProjectGroupsView(db);
@@ -5678,6 +6144,43 @@ function messageFromBackfillRow(row) {
5678
6144
  nickname: row.nickname ?? void 0
5679
6145
  };
5680
6146
  }
6147
+ function messageFromCachedRow(row) {
6148
+ const message = messageFromBackfillRow(row);
6149
+ const tokens = parseOptionalJson(row.tokens_json);
6150
+ if (tokens) {
6151
+ message.tokens = tokens;
6152
+ }
6153
+ if (row.cost != null) {
6154
+ message.cost = Number(row.cost);
6155
+ }
6156
+ if (row.cost_source) {
6157
+ message.cost_source = row.cost_source;
6158
+ }
6159
+ return message;
6160
+ }
6161
+ function backfillMessageTools(db) {
6162
+ createMessageToolTables(db);
6163
+ if (!tableExists(db, "messages")) {
6164
+ return;
6165
+ }
6166
+ db.exec("DELETE FROM message_tools");
6167
+ const rows = db.prepare(
6168
+ `
6169
+ SELECT agent_name, session_id, message_index, tool_metadata_json
6170
+ FROM messages
6171
+ WHERE tool_metadata_json IS NOT NULL
6172
+ `
6173
+ ).all();
6174
+ const insertTool = prepareInsertMessageTool(db);
6175
+ for (const row of rows) {
6176
+ if (!row.agent_name || !row.session_id || row.message_index == null) {
6177
+ continue;
6178
+ }
6179
+ for (const toolName of toolNamesFromMetadataJson(row.tool_metadata_json)) {
6180
+ insertTool.run(row.agent_name, row.session_id, row.message_index, toolName);
6181
+ }
6182
+ }
6183
+ }
5681
6184
  function backfillFileActivity(db) {
5682
6185
  createFileActivityTables(db);
5683
6186
  if (!tableExists(db, "sessions") || !tableExists(db, "messages")) {
@@ -5742,6 +6245,12 @@ function rebuildSearchIndex(db) {
5742
6245
  }
5743
6246
  db.exec("INSERT INTO session_documents_fts(session_documents_fts) VALUES ('rebuild')");
5744
6247
  }
6248
+ function rebuildMessageSearchIndex(db) {
6249
+ if (!tableExists(db, "messages_fts")) {
6250
+ return;
6251
+ }
6252
+ db.exec("INSERT INTO messages_fts(messages_fts) VALUES ('rebuild')");
6253
+ }
5745
6254
  function shouldBulkSyncSearchIndex(options, changedCount) {
5746
6255
  if (options.isBulk != null) {
5747
6256
  return options.isBulk;
@@ -5754,6 +6263,11 @@ function ensureFtsReady(db) {
5754
6263
  createSearchTables(db);
5755
6264
  }
5756
6265
  createSearchTriggers(db);
6266
+ const needsMessageSearchRebuild = !tableExists(db, "messages_fts");
6267
+ createMessageSearchTables(db);
6268
+ if (needsMessageSearchRebuild) {
6269
+ rebuildMessageSearchIndex(db);
6270
+ }
5757
6271
  }
5758
6272
  function ensureFtsConsistency(db) {
5759
6273
  ensureFtsReady(db);
@@ -5765,9 +6279,11 @@ function ensureFtsConsistency(db) {
5765
6279
  db.exec(
5766
6280
  "INSERT INTO session_documents_fts(session_documents_fts, rank) VALUES ('integrity-check', 1)"
5767
6281
  );
6282
+ db.exec("INSERT INTO messages_fts(messages_fts, rank) VALUES ('integrity-check', 1)");
5768
6283
  ftsIntegrityCheckedPath = cachePath;
5769
6284
  } catch {
5770
6285
  rebuildSearchIndex(db);
6286
+ rebuildMessageSearchIndex(db);
5771
6287
  ftsIntegrityCheckedPath = cachePath;
5772
6288
  }
5773
6289
  }
@@ -5796,9 +6312,11 @@ function ensureSchema(db, dbPath) {
5796
6312
  backupLabel: "cache-migration",
5797
6313
  backupTables: [
5798
6314
  "agent_cache",
6315
+ "cache_initialization",
5799
6316
  "cached_sessions",
5800
6317
  "sessions",
5801
6318
  "messages",
6319
+ "message_tools",
5802
6320
  "session_file_activity",
5803
6321
  "session_documents",
5804
6322
  "project_sessions"
@@ -5817,7 +6335,34 @@ function ensureSchema(db, dbPath) {
5817
6335
  }
5818
6336
  },
5819
6337
  { version: 7, migrate: backfillStructuredSessions },
5820
- { version: 8, migrate: backfillFileActivity }
6338
+ { version: 8, migrate: backfillFileActivity },
6339
+ {
6340
+ version: 9,
6341
+ migrate(db2) {
6342
+ createMessageSearchTables(db2);
6343
+ rebuildMessageSearchIndex(db2);
6344
+ }
6345
+ },
6346
+ {
6347
+ version: 10,
6348
+ migrate(db2) {
6349
+ createFileActivityPathSearchTables(db2);
6350
+ rebuildFileActivityPathIndex(db2);
6351
+ }
6352
+ },
6353
+ {
6354
+ version: 11,
6355
+ migrate(db2) {
6356
+ backfillMessageTools(db2);
6357
+ }
6358
+ },
6359
+ {
6360
+ version: 12,
6361
+ migrate(db2) {
6362
+ refreshProjectIdentities(db2);
6363
+ }
6364
+ },
6365
+ { version: 13, migrate: createCacheTables }
5821
6366
  ]
5822
6367
  });
5823
6368
  createLatestCacheSchema(db);
@@ -6002,6 +6547,36 @@ function appendPlainText(value, chunks) {
6002
6547
  function compactRecord(record) {
6003
6548
  return Object.fromEntries(Object.entries(record).filter(([, value]) => value != null));
6004
6549
  }
6550
+ function normalizeToolName2(value) {
6551
+ if (typeof value !== "string") return null;
6552
+ const name = value.trim().toLowerCase();
6553
+ return name || null;
6554
+ }
6555
+ function toolNamesFromMetadataJson(value) {
6556
+ if (!value) return [];
6557
+ try {
6558
+ const metadata = JSON.parse(String(value));
6559
+ if (!Array.isArray(metadata)) return [];
6560
+ const tools = /* @__PURE__ */ new Set();
6561
+ for (const item of metadata) {
6562
+ if (item == null || typeof item !== "object") continue;
6563
+ const toolName = normalizeToolName2(item.tool);
6564
+ if (toolName) tools.add(toolName);
6565
+ }
6566
+ return [...tools];
6567
+ } catch {
6568
+ return [];
6569
+ }
6570
+ }
6571
+ function toolNamesFromMessage(message) {
6572
+ const tools = /* @__PURE__ */ new Set();
6573
+ for (const part of message.parts) {
6574
+ if (part.type !== "tool") continue;
6575
+ const toolName = normalizeToolName2(part.tool);
6576
+ if (toolName) tools.add(toolName);
6577
+ }
6578
+ return [...tools];
6579
+ }
6005
6580
  function summarizeToolPart(part) {
6006
6581
  const state = part.state == null ? void 0 : compactRecord({
6007
6582
  status: part.state.status,
@@ -6055,7 +6630,8 @@ function normalizeMessages(session) {
6055
6630
  subagentId: message.subagent_id ?? null,
6056
6631
  nickname: message.nickname ?? null,
6057
6632
  contentText: buildMessageText(message),
6058
- toolMetadataJson: toolMetadata.length > 0 ? JSON.stringify(toolMetadata) : null
6633
+ toolMetadataJson: toolMetadata.length > 0 ? JSON.stringify(toolMetadata) : null,
6634
+ toolNames: toolNamesFromMessage(message)
6059
6635
  };
6060
6636
  });
6061
6637
  }
@@ -6077,14 +6653,14 @@ function deleteLegacyCacheFile() {
6077
6653
  } catch {
6078
6654
  }
6079
6655
  }
6080
- function loadCachedSessions(agentName) {
6656
+ function loadCachedSessions(agentName, options = {}) {
6081
6657
  if (!hasCacheStorage()) {
6082
6658
  return null;
6083
6659
  }
6084
6660
  return withCacheDb((db) => {
6085
6661
  const timestampRow = db.prepare("SELECT timestamp AS value FROM agent_cache WHERE agent_name = ?").get(agentName);
6086
6662
  const timestamp = Number(timestampRow?.value ?? 0);
6087
- if (!timestamp || Date.now() - timestamp > CACHE_TTL) {
6663
+ if (!timestamp || !options.ignoreTtl && Date.now() - timestamp > CACHE_TTL) {
6088
6664
  return null;
6089
6665
  }
6090
6666
  const rows = db.prepare(
@@ -6130,6 +6706,113 @@ function loadCachedSessions(agentName) {
6130
6706
  return { sessions, meta, timestamp };
6131
6707
  });
6132
6708
  }
6709
+ function isAgentCacheInitialized(agentName, indexVersion = CACHE_INITIALIZATION_VERSION) {
6710
+ if (!hasCacheStorage()) {
6711
+ return false;
6712
+ }
6713
+ return withCacheDbReadOnly((db) => {
6714
+ if (!tableExists(db, "cache_initialization")) return false;
6715
+ const row = db.prepare(
6716
+ `
6717
+ SELECT index_version
6718
+ FROM cache_initialization
6719
+ WHERE agent_name = ?
6720
+ `
6721
+ ).get(agentName);
6722
+ return row?.index_version === indexVersion;
6723
+ }) ?? false;
6724
+ }
6725
+ function markAgentCacheInitialized(agentName, indexVersion = CACHE_INITIALIZATION_VERSION) {
6726
+ withCacheDb((db) => {
6727
+ const now = Date.now();
6728
+ db.prepare(
6729
+ `
6730
+ INSERT INTO cache_initialization(agent_name, initialized_at, index_version, last_sync_at)
6731
+ VALUES (?, ?, ?, ?)
6732
+ ON CONFLICT(agent_name) DO UPDATE SET
6733
+ index_version = excluded.index_version,
6734
+ last_sync_at = excluded.last_sync_at
6735
+ `
6736
+ ).run(agentName, now, indexVersion, now);
6737
+ });
6738
+ }
6739
+ function loadCachedSessionData(agentName, sessionId) {
6740
+ if (!hasCacheStorage()) {
6741
+ return null;
6742
+ }
6743
+ return withCacheDbReadOnly((db) => {
6744
+ const row = db.prepare(
6745
+ `
6746
+ SELECT
6747
+ session_id,
6748
+ sort_index,
6749
+ slug,
6750
+ title,
6751
+ source_path,
6752
+ directory,
6753
+ project_identity_kind,
6754
+ project_identity_key,
6755
+ project_display_name,
6756
+ time_created,
6757
+ time_updated,
6758
+ message_count,
6759
+ total_input_tokens,
6760
+ total_output_tokens,
6761
+ total_cache_read_tokens,
6762
+ total_cache_create_tokens,
6763
+ total_cost,
6764
+ cost_source,
6765
+ total_tokens,
6766
+ model_usage_json,
6767
+ smart_tags_json,
6768
+ smart_tags_source_updated_at,
6769
+ meta_json
6770
+ FROM sessions
6771
+ WHERE agent_name = ? AND session_id = ?
6772
+ `
6773
+ ).get(agentName, sessionId);
6774
+ if (!row) {
6775
+ return null;
6776
+ }
6777
+ const messageRows = db.prepare(
6778
+ `
6779
+ SELECT
6780
+ message_id,
6781
+ role,
6782
+ time_created,
6783
+ time_completed,
6784
+ agent,
6785
+ mode,
6786
+ model,
6787
+ provider,
6788
+ tokens_json,
6789
+ cost,
6790
+ cost_source,
6791
+ parts_json,
6792
+ subagent_id,
6793
+ nickname
6794
+ FROM messages
6795
+ WHERE agent_name = ? AND session_id = ?
6796
+ ORDER BY message_index
6797
+ `
6798
+ ).all(agentName, sessionId);
6799
+ const head = sessionFromRow(row);
6800
+ const fileActivityRows = db.prepare(
6801
+ `
6802
+ SELECT agent_name, session_id, project_identity_key, path, kind, count, latest_time
6803
+ FROM session_file_activity
6804
+ WHERE agent_name = ? AND session_id = ?
6805
+ ORDER BY latest_time DESC, count DESC, path
6806
+ LIMIT 500
6807
+ `
6808
+ ).all(agentName, sessionId);
6809
+ return {
6810
+ ...head,
6811
+ messages: messageRows.map((messageRow) => messageFromCachedRow(messageRow)),
6812
+ file_activity: fileActivityRows.map((activityRow) => fileActivityFromRow(activityRow))
6813
+ };
6814
+ });
6815
+ }
6133
6816
  function saveCachedSessions(agentName, sessions, meta = {}) {
6134
6817
  withCacheDb((db) => {
6135
6818
  const deleteAgent = db.prepare("DELETE FROM agent_cache WHERE agent_name = ?");
@@ -6140,31 +6823,27 @@ function saveCachedSessions(agentName, sessions, meta = {}) {
6140
6823
  const deleteSearchDocument = db.prepare(
6141
6824
  "DELETE FROM session_documents WHERE agent_name = ? AND session_id = ?"
6142
6825
  );
6826
+ const deleteMessages = db.prepare(
6827
+ "DELETE FROM messages WHERE agent_name = ? AND session_id = ?"
6828
+ );
6829
+ const deleteMessageTools = db.prepare(
6830
+ "DELETE FROM message_tools WHERE agent_name = ? AND session_id = ?"
6831
+ );
6143
6832
  const deleteFileActivity = db.prepare(
6144
6833
  "DELETE FROM session_file_activity WHERE agent_name = ? AND session_id = ?"
6145
6834
  );
6835
+ const deleteProjectSession = db.prepare(
6836
+ "DELETE FROM project_sessions WHERE agent_name = ? AND session_id = ?"
6837
+ );
6146
6838
  const deleteProjectSessions = db.prepare("DELETE FROM project_sessions WHERE agent_name = ?");
6147
6839
  const upsertAgent = db.prepare(`
6148
6840
  INSERT INTO agent_cache(agent_name, timestamp)
6149
6841
  VALUES (?, ?)
6150
6842
  ON CONFLICT(agent_name) DO UPDATE SET timestamp = excluded.timestamp
6151
6843
  `);
6152
- const insertCachedSession = db.prepare(`
6153
- INSERT INTO cached_sessions(agent_name, session_id, session_json, meta_json)
6154
- VALUES (?, ?, ?, ?)
6155
- `);
6844
+ const upsertCachedSession = prepareUpsertCachedSession(db);
6156
6845
  const upsertSession = prepareUpsertSession(db);
6157
- const insertProjectSession = db.prepare(`
6158
- INSERT INTO project_sessions(
6159
- agent_name,
6160
- session_id,
6161
- identity_kind,
6162
- identity_key,
6163
- display_name,
6164
- directory,
6165
- activity_time
6166
- ) VALUES (?, ?, ?, ?, ?, ?, ?)
6167
- `);
6846
+ const upsertProjectSession = prepareUpsertProjectSession(db);
6168
6847
  const write = db.transaction(() => {
6169
6848
  const timestamp = Date.now();
6170
6849
  const sessionIds = new Set(sessions.map((session) => session.id));
@@ -6177,7 +6856,10 @@ function saveCachedSessions(agentName, sessions, meta = {}) {
6177
6856
  const sessionId = String(row.session_id);
6178
6857
  if (!sessionIds.has(sessionId)) {
6179
6858
  deleteSearchDocument.run(agentName, sessionId);
6859
+ deleteMessageTools.run(agentName, sessionId);
6860
+ deleteMessages.run(agentName, sessionId);
6180
6861
  deleteFileActivity.run(agentName, sessionId);
6862
+ deleteProjectSession.run(agentName, sessionId);
6181
6863
  deleteSession.run(agentName, sessionId);
6182
6864
  }
6183
6865
  }
@@ -6185,7 +6867,7 @@ function saveCachedSessions(agentName, sessions, meta = {}) {
6185
6867
  const identity = session.project_identity ?? computeIdentity(session.directory, realFs);
6186
6868
  const sessionMeta = meta[session.id];
6187
6869
  const metaJson = sessionMeta ? JSON.stringify(sessionMeta) : null;
6188
- insertCachedSession.run(agentName, session.id, JSON.stringify(session), metaJson);
6870
+ upsertCachedSession.run(agentName, session.id, JSON.stringify(session), metaJson);
6189
6871
  upsertSessionRow(
6190
6872
  upsertSession,
6191
6873
  agentName,
@@ -6194,16 +6876,73 @@ function saveCachedSessions(agentName, sessions, meta = {}) {
6194
6876
  index,
6195
6877
  sourcePathFromMeta(sessionMeta)
6196
6878
  );
6197
- insertProjectSession.run(
6879
+ writeProjectSessionRow(upsertProjectSession, agentName, session, identity);
6880
+ });
6881
+ });
6882
+ write();
6883
+ deleteLegacyCacheFile();
6884
+ });
6885
+ }
6886
+ function saveCachedSessionChanges(agentName, changes, removedSessionIds, meta = {}) {
6887
+ if (changes.length === 0 && removedSessionIds.length === 0) {
6888
+ return;
6889
+ }
6890
+ withCacheDb((db) => {
6891
+ const deleteLegacySession = db.prepare(
6892
+ "DELETE FROM cached_sessions WHERE agent_name = ? AND session_id = ?"
6893
+ );
6894
+ const deleteSession = db.prepare(
6895
+ "DELETE FROM sessions WHERE agent_name = ? AND session_id = ?"
6896
+ );
6897
+ const deleteSearchDocument = db.prepare(
6898
+ "DELETE FROM session_documents WHERE agent_name = ? AND session_id = ?"
6899
+ );
6900
+ const deleteMessages = db.prepare(
6901
+ "DELETE FROM messages WHERE agent_name = ? AND session_id = ?"
6902
+ );
6903
+ const deleteMessageTools = db.prepare(
6904
+ "DELETE FROM message_tools WHERE agent_name = ? AND session_id = ?"
6905
+ );
6906
+ const deleteFileActivity = db.prepare(
6907
+ "DELETE FROM session_file_activity WHERE agent_name = ? AND session_id = ?"
6908
+ );
6909
+ const deleteProjectSession = db.prepare(
6910
+ "DELETE FROM project_sessions WHERE agent_name = ? AND session_id = ?"
6911
+ );
6912
+ const upsertAgent = db.prepare(`
6913
+ INSERT INTO agent_cache(agent_name, timestamp)
6914
+ VALUES (?, ?)
6915
+ ON CONFLICT(agent_name) DO UPDATE SET timestamp = excluded.timestamp
6916
+ `);
6917
+ const upsertCachedSession = prepareUpsertCachedSession(db);
6918
+ const upsertSession = prepareUpsertSession(db);
6919
+ const upsertProjectSession = prepareUpsertProjectSession(db);
6920
+ const write = db.transaction(() => {
6921
+ upsertAgent.run(agentName, Date.now());
6922
+ for (const sessionId of new Set(removedSessionIds)) {
6923
+ deleteLegacySession.run(agentName, sessionId);
6924
+ deleteSearchDocument.run(agentName, sessionId);
6925
+ deleteMessageTools.run(agentName, sessionId);
6926
+ deleteMessages.run(agentName, sessionId);
6927
+ deleteFileActivity.run(agentName, sessionId);
6928
+ deleteProjectSession.run(agentName, sessionId);
6929
+ deleteSession.run(agentName, sessionId);
6930
+ }
6931
+ for (const { session, sortIndex } of changes) {
6932
+ const identity = session.project_identity ?? computeIdentity(session.directory, realFs);
6933
+ const sessionMeta = meta[session.id];
6934
+ const metaJson = sessionMeta ? JSON.stringify(sessionMeta) : null;
6935
+ upsertCachedSession.run(agentName, session.id, JSON.stringify(session), metaJson);
6936
+ upsertSessionRow(
6937
+ upsertSession,
6198
6938
  agentName,
6199
- session.id,
6200
- identity.kind,
6201
- identity.key,
6202
- identity.displayName,
6203
- session.directory,
6204
- session.time_updated ?? session.time_created
6939
+ session,
6940
+ metaJson,
6941
+ sortIndex,
6942
+ sourcePathFromMeta(sessionMeta)
6205
6943
  );
6206
- });
6944
+ writeProjectSessionRow(upsertProjectSession, agentName, session, identity);
6945
+ }
6207
6946
  });
6208
6947
  write();
6209
6948
  deleteLegacyCacheFile();
@@ -6218,9 +6957,11 @@ function clearCache() {
6218
6957
  withCacheDb((db) => {
6219
6958
  db.exec(`
6220
6959
  DELETE FROM agent_cache;
6960
+ DELETE FROM cache_initialization;
6221
6961
  DELETE FROM cached_sessions;
6222
6962
  DELETE FROM session_documents;
6223
6963
  DELETE FROM session_file_activity;
6964
+ DELETE FROM message_tools;
6224
6965
  DELETE FROM messages;
6225
6966
  DELETE FROM sessions;
6226
6967
  DELETE FROM project_sessions;
@@ -6253,6 +6994,173 @@ function getCacheInfo() {
6253
6994
  });
6254
6995
  return info ?? { lastScanTime: null, size: 0 };
6255
6996
  }
6997
+ function loadSearchIndexEntry(agentName, change, loadSessionData) {
6998
+ try {
6999
+ const data = loadSessionData(change.session.id);
7000
+ const messages = normalizeMessages(data);
7001
+ const identity = change.session.project_identity ?? data.project_identity ?? computeIdentity(change.session.directory, realFs);
7002
+ return {
7003
+ session: change.session,
7004
+ identity,
7005
+ messages,
7006
+ contentText: buildSessionContentFromMessages(data.title ?? change.session.title, messages),
7007
+ contentHash: sessionContentHash(change.session),
7008
+ fileActivity: extractSessionFileActivity(
7009
+ agentName,
7010
+ change.session.id,
7011
+ identity.key,
7012
+ data.messages
7013
+ ),
7014
+ sortIndex: change.sortIndex
7015
+ };
7016
+ } catch {
7017
+ return null;
7018
+ }
7019
+ }
7020
+ function writeSearchIndexRows(db, agentName, removedSessionIds, entries) {
7021
+ const deleteRow = db.prepare(
7022
+ "DELETE FROM session_documents WHERE agent_name = ? AND session_id = ?"
7023
+ );
7024
+ const deleteMessages = db.prepare(
7025
+ "DELETE FROM messages WHERE agent_name = ? AND session_id = ? AND message_index >= ?"
7026
+ );
7027
+ const deleteMessageTools = db.prepare(
7028
+ "DELETE FROM message_tools WHERE agent_name = ? AND session_id = ? AND message_index >= ?"
7029
+ );
7030
+ const deleteFileActivity = db.prepare(
7031
+ "DELETE FROM session_file_activity WHERE agent_name = ? AND session_id = ?"
7032
+ );
7033
+ const upsertIndexedSession = prepareUpsertIndexedSession(db);
7034
+ const insertFileActivity = prepareInsertFileActivity(db);
7035
+ const insertMessageTool = prepareInsertMessageTool(db);
7036
+ const upsertMessage = db.prepare(`
7037
+ INSERT INTO messages(
7038
+ agent_name,
7039
+ session_id,
7040
+ message_index,
7041
+ message_id,
7042
+ role,
7043
+ time_created,
7044
+ time_completed,
7045
+ agent,
7046
+ mode,
7047
+ model,
7048
+ provider,
7049
+ tokens_json,
7050
+ cost,
7051
+ cost_source,
7052
+ parts_json,
7053
+ subagent_id,
7054
+ nickname,
7055
+ content_text,
7056
+ tool_metadata_json
7057
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
7058
+ ON CONFLICT(agent_name, session_id, message_index) DO UPDATE SET
7059
+ message_id = excluded.message_id,
7060
+ role = excluded.role,
7061
+ time_created = excluded.time_created,
7062
+ time_completed = excluded.time_completed,
7063
+ agent = excluded.agent,
7064
+ mode = excluded.mode,
7065
+ model = excluded.model,
7066
+ provider = excluded.provider,
7067
+ tokens_json = excluded.tokens_json,
7068
+ cost = excluded.cost,
7069
+ cost_source = excluded.cost_source,
7070
+ parts_json = excluded.parts_json,
7071
+ subagent_id = excluded.subagent_id,
7072
+ nickname = excluded.nickname,
7073
+ content_text = excluded.content_text,
7074
+ tool_metadata_json = excluded.tool_metadata_json
7075
+ `);
7076
+ const upsertRow = db.prepare(`
7077
+ INSERT INTO session_documents(
7078
+ agent_name,
7079
+ session_id,
7080
+ slug,
7081
+ title,
7082
+ directory,
7083
+ project_identity_kind,
7084
+ project_identity_key,
7085
+ project_display_name,
7086
+ time_created,
7087
+ time_updated,
7088
+ activity_time,
7089
+ content_text,
7090
+ content_hash,
7091
+ indexed_at
7092
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
7093
+ ON CONFLICT(agent_name, session_id) DO UPDATE SET
7094
+ slug = excluded.slug,
7095
+ title = excluded.title,
7096
+ directory = excluded.directory,
7097
+ project_identity_kind = excluded.project_identity_kind,
7098
+ project_identity_key = excluded.project_identity_key,
7099
+ project_display_name = excluded.project_display_name,
7100
+ time_created = excluded.time_created,
7101
+ time_updated = excluded.time_updated,
7102
+ activity_time = excluded.activity_time,
7103
+ content_text = excluded.content_text,
7104
+ content_hash = excluded.content_hash,
7105
+ indexed_at = excluded.indexed_at
7106
+ `);
7107
+ for (const sessionId of new Set(removedSessionIds)) {
7108
+ deleteRow.run(agentName, sessionId);
7109
+ deleteFileActivity.run(agentName, sessionId);
7110
+ deleteMessageTools.run(agentName, sessionId, 0);
7111
+ deleteMessages.run(agentName, sessionId, 0);
7112
+ }
7113
+ for (const entry of entries) {
7114
+ const activityTime = entry.session.time_updated ?? entry.session.time_created;
7115
+ upsertSessionRow(upsertIndexedSession, agentName, entry.session, null, entry.sortIndex, null);
7116
+ deleteFileActivity.run(agentName, entry.session.id);
7117
+ deleteMessageTools.run(agentName, entry.session.id, 0);
7118
+ writeFileActivityRows(insertFileActivity, entry.fileActivity);
7119
+ for (const message of entry.messages) {
7120
+ upsertMessage.run(
7121
+ agentName,
7122
+ entry.session.id,
7123
+ message.index,
7124
+ message.id,
7125
+ message.role,
7126
+ message.timeCreated,
7127
+ message.timeCompleted ?? null,
7128
+ message.agent ?? null,
7129
+ message.mode ?? null,
7130
+ message.model ?? null,
7131
+ message.provider ?? null,
7132
+ message.tokensJson ?? null,
7133
+ message.cost ?? null,
7134
+ message.costSource ?? null,
7135
+ message.partsJson,
7136
+ message.subagentId ?? null,
7137
+ message.nickname ?? null,
7138
+ message.contentText,
7139
+ message.toolMetadataJson ?? null
7140
+ );
7141
+ for (const toolName of message.toolNames) {
7142
+ insertMessageTool.run(agentName, entry.session.id, message.index, toolName);
7143
+ }
7144
+ }
7145
+ deleteMessages.run(agentName, entry.session.id, entry.messages.length);
7146
+ upsertRow.run(
7147
+ agentName,
7148
+ entry.session.id,
7149
+ entry.session.slug,
7150
+ entry.session.title,
7151
+ entry.session.directory,
7152
+ entry.identity.kind,
7153
+ entry.identity.key,
7154
+ entry.identity.displayName,
7155
+ entry.session.time_created,
7156
+ entry.session.time_updated ?? null,
7157
+ activityTime,
7158
+ entry.contentText,
7159
+ entry.contentHash,
7160
+ Date.now()
7161
+ );
7162
+ }
7163
+ }
6256
7164
  function syncSessionSearchIndex(agentName, sessions, loadSessionData, options = {}) {
6257
7165
  return withCacheDb((db) => {
6258
7166
  ensureFtsConsistency(db);
@@ -6277,180 +7185,27 @@ function syncSessionSearchIndex(agentName, sessions, loadSessionData, options =
6277
7185
  );
6278
7186
  const changedCount = toDelete.length + toUpsert.length;
6279
7187
  const isBulk = shouldBulkSyncSearchIndex(options, changedCount);
6280
- const loaded = toUpsert.map((session) => {
6281
- try {
6282
- const data = loadSessionData(session.id);
6283
- const messages = normalizeMessages(data);
6284
- const identity = session.project_identity ?? data.project_identity ?? computeIdentity(session.directory, realFs);
6285
- return {
6286
- session,
6287
- identity,
6288
- messages,
6289
- contentText: buildSessionContentFromMessages(data.title ?? session.title, messages),
6290
- contentHash: sessionContentHash(session),
6291
- fileActivity: extractSessionFileActivity(
6292
- agentName,
6293
- session.id,
6294
- identity.key,
6295
- data.messages
6296
- )
6297
- };
6298
- } catch {
6299
- return null;
6300
- }
6301
- }).filter((entry) => entry !== null);
6302
- const deleteRow = db.prepare(
6303
- "DELETE FROM session_documents WHERE agent_name = ? AND session_id = ?"
6304
- );
6305
- const deleteMessages = db.prepare(
6306
- "DELETE FROM messages WHERE agent_name = ? AND session_id = ? AND message_index >= ?"
6307
- );
6308
- const deleteFileActivity = db.prepare(
6309
- "DELETE FROM session_file_activity WHERE agent_name = ? AND session_id = ?"
6310
- );
6311
- const upsertIndexedSession = prepareUpsertIndexedSession(db);
6312
- const insertFileActivity = prepareInsertFileActivity(db);
6313
- const upsertMessage = db.prepare(`
6314
- INSERT INTO messages(
6315
- agent_name,
6316
- session_id,
6317
- message_index,
6318
- message_id,
6319
- role,
6320
- time_created,
6321
- time_completed,
6322
- agent,
6323
- mode,
6324
- model,
6325
- provider,
6326
- tokens_json,
6327
- cost,
6328
- cost_source,
6329
- parts_json,
6330
- subagent_id,
6331
- nickname,
6332
- content_text,
6333
- tool_metadata_json
6334
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
6335
- ON CONFLICT(agent_name, session_id, message_index) DO UPDATE SET
6336
- message_id = excluded.message_id,
6337
- role = excluded.role,
6338
- time_created = excluded.time_created,
6339
- time_completed = excluded.time_completed,
6340
- agent = excluded.agent,
6341
- mode = excluded.mode,
6342
- model = excluded.model,
6343
- provider = excluded.provider,
6344
- tokens_json = excluded.tokens_json,
6345
- cost = excluded.cost,
6346
- cost_source = excluded.cost_source,
6347
- parts_json = excluded.parts_json,
6348
- subagent_id = excluded.subagent_id,
6349
- nickname = excluded.nickname,
6350
- content_text = excluded.content_text,
6351
- tool_metadata_json = excluded.tool_metadata_json
6352
- `);
6353
- const upsertRow = db.prepare(`
6354
- INSERT INTO session_documents(
6355
- agent_name,
6356
- session_id,
6357
- slug,
6358
- title,
6359
- directory,
6360
- project_identity_kind,
6361
- project_identity_key,
6362
- project_display_name,
6363
- time_created,
6364
- time_updated,
6365
- activity_time,
6366
- content_text,
6367
- content_hash,
6368
- indexed_at
6369
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
6370
- ON CONFLICT(agent_name, session_id) DO UPDATE SET
6371
- slug = excluded.slug,
6372
- title = excluded.title,
6373
- directory = excluded.directory,
6374
- project_identity_kind = excluded.project_identity_kind,
6375
- project_identity_key = excluded.project_identity_key,
6376
- project_display_name = excluded.project_display_name,
6377
- time_created = excluded.time_created,
6378
- time_updated = excluded.time_updated,
6379
- activity_time = excluded.activity_time,
6380
- content_text = excluded.content_text,
6381
- content_hash = excluded.content_hash,
6382
- indexed_at = excluded.indexed_at
6383
- `);
6384
- const writeRows = () => {
6385
- for (const sessionId of toDelete) {
6386
- deleteRow.run(agentName, sessionId);
6387
- deleteFileActivity.run(agentName, sessionId);
6388
- deleteMessages.run(agentName, sessionId, 0);
6389
- }
6390
- for (const entry of loaded) {
6391
- const activityTime = entry.session.time_updated ?? entry.session.time_created;
6392
- upsertSessionRow(
6393
- upsertIndexedSession,
6394
- agentName,
6395
- entry.session,
6396
- null,
6397
- sessionSortIndexMap.get(entry.session.id) ?? 0,
6398
- null
6399
- );
6400
- deleteFileActivity.run(agentName, entry.session.id);
6401
- writeFileActivityRows(insertFileActivity, entry.fileActivity);
6402
- for (const message of entry.messages) {
6403
- upsertMessage.run(
6404
- agentName,
6405
- entry.session.id,
6406
- message.index,
6407
- message.id,
6408
- message.role,
6409
- message.timeCreated,
6410
- message.timeCompleted ?? null,
6411
- message.agent ?? null,
6412
- message.mode ?? null,
6413
- message.model ?? null,
6414
- message.provider ?? null,
6415
- message.tokensJson ?? null,
6416
- message.cost ?? null,
6417
- message.costSource ?? null,
6418
- message.partsJson,
6419
- message.subagentId ?? null,
6420
- message.nickname ?? null,
6421
- message.contentText,
6422
- message.toolMetadataJson ?? null
6423
- );
6424
- }
6425
- deleteMessages.run(agentName, entry.session.id, entry.messages.length);
6426
- upsertRow.run(
6427
- agentName,
6428
- entry.session.id,
6429
- entry.session.slug,
6430
- entry.session.title,
6431
- entry.session.directory,
6432
- entry.identity.kind,
6433
- entry.identity.key,
6434
- entry.identity.displayName,
6435
- entry.session.time_created,
6436
- entry.session.time_updated ?? null,
6437
- activityTime,
6438
- entry.contentText,
6439
- entry.contentHash,
6440
- Date.now()
6441
- );
6442
- }
6443
- };
7188
+ const loaded = toUpsert.map(
7189
+ (session) => loadSearchIndexEntry(
7190
+ agentName,
7191
+ { session, sortIndex: sessionSortIndexMap.get(session.id) ?? 0 },
7192
+ loadSessionData
7193
+ )
7194
+ ).filter((entry) => entry !== null);
7195
+ const writeRows = () => writeSearchIndexRows(db, agentName, toDelete, loaded);
6444
7196
  let rebuildDurationMs;
6445
7197
  const needsRebuild = isBulk && (toDelete.length > 0 || loaded.length > 0);
6446
7198
  if (needsRebuild) {
6447
7199
  db.transaction(() => {
6448
7200
  dropSearchTriggers(db);
7201
+ dropMessageSearchTriggers(db);
6449
7202
  writeRows();
6450
7203
  const rebuildStartedAt = performance.now();
6451
7204
  rebuildSearchIndex(db);
7205
+ rebuildMessageSearchIndex(db);
6452
7206
  rebuildDurationMs = performance.now() - rebuildStartedAt;
6453
7207
  createSearchTriggers(db);
7208
+ createMessageSearchTriggers(db);
6454
7209
  })();
6455
7210
  } else {
6456
7211
  db.transaction(writeRows)();
@@ -6468,6 +7223,68 @@ function syncSessionSearchIndex(agentName, sessions, loadSessionData, options =
6468
7223
  };
6469
7224
  });
6470
7225
  }
7226
+ function syncSessionSearchIndexChanges(agentName, changes, removedSessionIds, loadSessionData, options = {}) {
7227
+ if (changes.length === 0 && removedSessionIds.length === 0) {
7228
+ return {
7229
+ agentName,
7230
+ mode: "incremental",
7231
+ sessions: 0,
7232
+ changed: 0,
7233
+ deleted: 0,
7234
+ indexed: 0,
7235
+ skipped: 0,
7236
+ durationMs: 0
7237
+ };
7238
+ }
7239
+ return withCacheDb((db) => {
7240
+ ensureFtsConsistency(db);
7241
+ const startedAt = performance.now();
7242
+ const getIndexedRow = db.prepare(
7243
+ "SELECT content_hash FROM session_documents WHERE agent_name = ? AND session_id = ?"
7244
+ );
7245
+ const getMessageCount = db.prepare(
7246
+ "SELECT COUNT(*) AS value FROM messages WHERE agent_name = ? AND session_id = ?"
7247
+ );
7248
+ const toUpsert = changes.filter(({ session }) => {
7249
+ const indexed = getIndexedRow.get(agentName, session.id);
7250
+ const messageCount = getMessageCount.get(agentName, session.id);
7251
+ return String(indexed?.content_hash ?? "") !== sessionContentHash(session) || Number(messageCount?.value ?? 0) !== session.stats.message_count;
7252
+ });
7253
+ const uniqueRemovedSessionIds = Array.from(new Set(removedSessionIds));
7254
+ const changedCount = uniqueRemovedSessionIds.length + toUpsert.length;
7255
+ const isBulk = shouldBulkSyncSearchIndex(options, changedCount);
7256
+ const loaded = toUpsert.map((change) => loadSearchIndexEntry(agentName, change, loadSessionData)).filter((entry) => entry !== null);
7257
+ const writeRows = () => writeSearchIndexRows(db, agentName, uniqueRemovedSessionIds, loaded);
7258
+ let rebuildDurationMs;
7259
+ const needsRebuild = isBulk && (uniqueRemovedSessionIds.length > 0 || loaded.length > 0);
7260
+ if (needsRebuild) {
7261
+ db.transaction(() => {
7262
+ dropSearchTriggers(db);
7263
+ dropMessageSearchTriggers(db);
7264
+ writeRows();
7265
+ const rebuildStartedAt = performance.now();
7266
+ rebuildSearchIndex(db);
7267
+ rebuildMessageSearchIndex(db);
7268
+ rebuildDurationMs = performance.now() - rebuildStartedAt;
7269
+ createSearchTriggers(db);
7270
+ createMessageSearchTriggers(db);
7271
+ })();
7272
+ } else {
7273
+ db.transaction(writeRows)();
7274
+ }
7275
+ return {
7276
+ agentName,
7277
+ mode: isBulk ? "bulk" : "incremental",
7278
+ sessions: changes.length,
7279
+ changed: toUpsert.length,
7280
+ deleted: uniqueRemovedSessionIds.length,
7281
+ indexed: loaded.length,
7282
+ skipped: toUpsert.length - loaded.length,
7283
+ durationMs: performance.now() - startedAt,
7284
+ rebuildDurationMs
7285
+ };
7286
+ });
7287
+ }
6471
7288
  function sessionHeadFromSearchRow(row) {
6472
7289
  return sessionFromRow(row);
6473
7290
  }
@@ -6514,6 +7331,11 @@ function sessionMatchesSearchCost(session, options) {
6514
7331
  function likePattern(value) {
6515
7332
  return `%${value.trim().toLowerCase().replace(/[\\%_]/g, "\\$&")}%`;
6516
7333
  }
7334
+ function filePathFtsQuery(value) {
7335
+ const path2 = normalizeFilePathSearch(value);
7336
+ if (path2.length < 3) return null;
7337
+ return `"${path2.replaceAll('"', '""')}"`;
7338
+ }
6517
7339
  function escapeRegExp(value) {
6518
7340
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6519
7341
  }
@@ -6544,16 +7366,26 @@ function buildSessionSearchFilters(options) {
6544
7366
  params.push(`%"${tag}"%`);
6545
7367
  }
6546
7368
  for (const tool of options.tools ?? []) {
7369
+ const toolName = normalizeToolName2(tool);
7370
+ if (!toolName) continue;
6547
7371
  clauses.push(
6548
- "EXISTS (SELECT 1 FROM messages m WHERE m.agent_name = s.agent_name AND m.session_id = s.session_id AND LOWER(m.tool_metadata_json) LIKE ? ESCAPE '\\')"
7372
+ "EXISTS (SELECT 1 FROM message_tools mt WHERE mt.tool_name = ? AND mt.agent_name = s.agent_name AND mt.session_id = s.session_id)"
6549
7373
  );
6550
- params.push(likePattern(tool));
7374
+ params.push(toolName);
6551
7375
  }
6552
7376
  if (options.file || options.fileKind) {
6553
7377
  const fileClauses = ["fa.agent_name = s.agent_name", "fa.session_id = s.session_id"];
6554
7378
  if (options.file) {
6555
- fileClauses.push("LOWER(fa.path) LIKE ? ESCAPE '\\'");
6556
- params.push(likePattern(options.file));
7379
+ const pathQuery = filePathFtsQuery(options.file);
7380
+ if (pathQuery) {
7381
+ fileClauses.push(
7382
+ "fa.rowid IN (SELECT rowid FROM session_file_activity_path_fts WHERE path MATCH ?)"
7383
+ );
7384
+ params.push(pathQuery);
7385
+ } else {
7386
+ fileClauses.push("LOWER(fa.path) LIKE ? ESCAPE '\\'");
7387
+ params.push(likePattern(options.file));
7388
+ }
6557
7389
  }
6558
7390
  if (options.fileKind) {
6559
7391
  fileClauses.push("fa.kind = ?");
@@ -6639,8 +7471,51 @@ function messageMatchType(row) {
6639
7471
  if (row.role === "tool" || row.mode === "tool" || row.tool_metadata_json) return "tool_output";
6640
7472
  return "assistant_reply";
6641
7473
  }
6642
- function resolveSearchMatch(db, row, textQuery) {
6643
- const terms = parseTextTerms(textQuery);
7474
+ function searchResultRowKey(row) {
7475
+ return `${String(row.agent_name)}\0${String(row.session_id)}`;
7476
+ }
7477
+ function fetchMessageSearchMatches(db, rows, ftsQuery, terms) {
7478
+ const candidates = rows.filter((row) => !textMatchesTerms(String(row.title ?? ""), terms));
7479
+ if (candidates.length === 0) {
7480
+ return /* @__PURE__ */ new Map();
7481
+ }
7482
+ const clauses = [];
7483
+ const params = [ftsQuery];
7484
+ for (const row of candidates) {
7485
+ clauses.push("(m.agent_name = ? AND m.session_id = ?)");
7486
+ params.push(String(row.agent_name), String(row.session_id));
7487
+ }
7488
+ const messageRows = db.prepare(
7489
+ `
7490
+ SELECT
7491
+ m.agent_name,
7492
+ m.session_id,
7493
+ m.message_index,
7494
+ m.role,
7495
+ m.mode,
7496
+ m.content_text,
7497
+ m.tool_metadata_json
7498
+ FROM messages_fts
7499
+ JOIN messages m ON m.rowid = messages_fts.rowid
7500
+ WHERE messages_fts MATCH ?
7501
+ AND (${clauses.join(" OR ")})
7502
+ ORDER BY m.message_index
7503
+ `
7504
+ ).all(...params);
7505
+ const matches = /* @__PURE__ */ new Map();
7506
+ for (const message of messageRows) {
7507
+ const key = searchResultRowKey(message);
7508
+ if (matches.has(key)) continue;
7509
+ const text = String(message.content_text ?? "");
7510
+ if (!textMatchesTerms(text, terms)) continue;
7511
+ matches.set(key, {
7512
+ snippet: buildTermSnippet(text, terms),
7513
+ matchType: messageMatchType(message)
7514
+ });
7515
+ }
7516
+ return matches;
7517
+ }
7518
+ function resolveSearchMatch(row, terms, messageMatches) {
6644
7519
  const title = String(row.title ?? "");
6645
7520
  if (terms.terms.length === 0) {
6646
7521
  return {
@@ -6651,30 +7526,20 @@ function resolveSearchMatch(db, row, textQuery) {
6651
7526
  if (textMatchesTerms(title, terms)) {
6652
7527
  return { snippet: buildTermSnippet(title, terms), matchType: "title" };
6653
7528
  }
6654
- const messages = db.prepare(
6655
- `
6656
- SELECT role, mode, content_text, tool_metadata_json
6657
- FROM messages
6658
- WHERE agent_name = ? AND session_id = ?
6659
- ORDER BY message_index
6660
- `
6661
- ).all(row.agent_name, row.session_id);
6662
- for (const message of messages) {
6663
- const text = String(message.content_text ?? "");
6664
- if (!textMatchesTerms(text, terms)) continue;
6665
- return {
6666
- snippet: buildTermSnippet(text, terms),
6667
- matchType: messageMatchType(message)
6668
- };
7529
+ const messageMatch = messageMatches.get(searchResultRowKey(row));
7530
+ if (messageMatch) {
7531
+ return messageMatch;
6669
7532
  }
6670
7533
  return {
6671
7534
  snippet: String(row.snippet ?? ""),
6672
7535
  matchType: "assistant_reply"
6673
7536
  };
6674
7537
  }
6675
- function rowsToSearchResults(db, rows, textQuery) {
7538
+ function rowsToSearchResults(db, rows, textQuery, ftsQuery = toFtsQuery(textQuery)) {
7539
+ const terms = parseTextTerms(textQuery);
7540
+ const messageMatches = terms.terms.length > 0 && ftsQuery ? fetchMessageSearchMatches(db, rows, ftsQuery, terms) : /* @__PURE__ */ new Map();
6676
7541
  return rows.map((row) => {
6677
- const match = resolveSearchMatch(db, row, textQuery);
7542
+ const match = resolveSearchMatch(row, terms, messageMatches);
6678
7543
  return {
6679
7544
  agentName: String(row.agent_name),
6680
7545
  session: sessionHeadFromSearchRow(row),
@@ -6726,7 +7591,7 @@ function searchSessions(query, options = {}) {
6726
7591
  LIMIT ?
6727
7592
  `
6728
7593
  ).all(ftsQuery, ...filters.params, search.options.limit ?? 50);
6729
- return rowsToSearchResults(db, rows, normalizedQuery);
7594
+ return rowsToSearchResults(db, rows, normalizedQuery, ftsQuery);
6730
7595
  });
6731
7596
  return results ?? [];
6732
7597
  }
@@ -6740,6 +7605,7 @@ function fileActivityFilters(options) {
6740
7605
  projectLike: options.project ? likePattern(options.project) : null,
6741
7606
  cwdKey: options.cwd ? computeIdentity(options.cwd, realFs).key : null,
6742
7607
  cwdLike: options.cwd ? likePattern(options.cwd) : null,
7608
+ path: path2,
6743
7609
  pathLike: path2 ? likePattern(path2) : null
6744
7610
  };
6745
7611
  }
@@ -6754,14 +7620,68 @@ function fileActivityFromRow(row) {
6754
7620
  latest_time: Number(row.latest_time ?? 0)
6755
7621
  };
6756
7622
  }
7623
+ function buildFileActivityWhere(options) {
7624
+ const filters = fileActivityFilters(options);
7625
+ const clauses = [];
7626
+ const params = [];
7627
+ if (options.agent != null) {
7628
+ clauses.push("fa.agent_name = ?");
7629
+ params.push(options.agent);
7630
+ }
7631
+ if (options.sessionId != null) {
7632
+ clauses.push("fa.session_id = ?");
7633
+ params.push(options.sessionId);
7634
+ }
7635
+ if (filters.projectKey != null) {
7636
+ clauses.push("fa.project_identity_key = ?");
7637
+ params.push(filters.projectKey);
7638
+ }
7639
+ if (filters.projectLike != null) {
7640
+ clauses.push(
7641
+ "(LOWER(fa.project_identity_key) LIKE ? ESCAPE '\\' OR LOWER(s.project_display_name) LIKE ? ESCAPE '\\' OR LOWER(s.directory) LIKE ? ESCAPE '\\')"
7642
+ );
7643
+ params.push(filters.projectLike, filters.projectLike, filters.projectLike);
7644
+ }
7645
+ if (filters.cwdKey != null) {
7646
+ clauses.push("(s.project_identity_key = ? OR LOWER(s.directory) LIKE ? ESCAPE '\\')");
7647
+ params.push(filters.cwdKey, filters.cwdLike);
7648
+ }
7649
+ if (filters.pathLike != null) {
7650
+ const pathQuery = filePathFtsQuery(filters.path);
7651
+ if (pathQuery) {
7652
+ clauses.push(
7653
+ "fa.rowid IN (SELECT rowid FROM session_file_activity_path_fts WHERE path MATCH ?)"
7654
+ );
7655
+ params.push(pathQuery);
7656
+ } else {
7657
+ clauses.push("LOWER(fa.path) LIKE ? ESCAPE '\\'");
7658
+ params.push(filters.pathLike);
7659
+ }
7660
+ }
7661
+ if (options.kind != null) {
7662
+ clauses.push("fa.kind = ?");
7663
+ params.push(options.kind);
7664
+ }
7665
+ if (options.from != null) {
7666
+ clauses.push("fa.latest_time >= ?");
7667
+ params.push(options.from);
7668
+ }
7669
+ if (options.to != null) {
7670
+ clauses.push("fa.latest_time <= ?");
7671
+ params.push(options.to);
7672
+ }
7673
+ return {
7674
+ where: clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "",
7675
+ params
7676
+ };
7677
+ }
6757
7678
  function listFileActivity(options = {}) {
6758
7679
  if (!hasCacheStorage()) {
6759
7680
  return [];
6760
7681
  }
6761
- const filters = fileActivityFilters(options);
6762
- const rows = withCacheDb(
6763
- (db) => db.prepare(
6764
- `
7682
+ const filters = buildFileActivityWhere(options);
7683
+ const queryRows = (db) => db.prepare(
7684
+ `
6765
7685
  SELECT
6766
7686
  fa.agent_name,
6767
7687
  fa.session_id,
@@ -6787,43 +7707,15 @@ function listFileActivity(options = {}) {
6787
7707
  s.total_tokens
6788
7708
  FROM session_file_activity fa
6789
7709
  JOIN sessions s ON s.agent_name = fa.agent_name AND s.session_id = fa.session_id
6790
- WHERE (? IS NULL OR fa.agent_name = ?)
6791
- AND (? IS NULL OR fa.session_id = ?)
6792
- AND (? IS NULL OR fa.project_identity_key = ?)
6793
- AND (? IS NULL OR LOWER(fa.project_identity_key) LIKE ? ESCAPE '\\' OR LOWER(s.project_display_name) LIKE ? ESCAPE '\\' OR LOWER(s.directory) LIKE ? ESCAPE '\\')
6794
- AND (? IS NULL OR s.project_identity_key = ? OR LOWER(s.directory) LIKE ? ESCAPE '\\')
6795
- AND (? IS NULL OR LOWER(fa.path) LIKE ? ESCAPE '\\')
6796
- AND (? IS NULL OR fa.kind = ?)
6797
- AND (? IS NULL OR fa.latest_time >= ?)
6798
- AND (? IS NULL OR fa.latest_time <= ?)
7710
+ ${filters.where}
6799
7711
  ORDER BY fa.latest_time DESC, fa.count DESC, fa.path
6800
7712
  LIMIT ?
6801
7713
  `
6802
- ).all(
6803
- options.agent ?? null,
6804
- options.agent ?? null,
6805
- options.sessionId ?? null,
6806
- options.sessionId ?? null,
6807
- filters.projectKey,
6808
- filters.projectKey,
6809
- filters.projectLike,
6810
- filters.projectLike,
6811
- filters.projectLike,
6812
- filters.projectLike,
6813
- filters.cwdKey,
6814
- filters.cwdKey,
6815
- filters.cwdLike,
6816
- filters.pathLike,
6817
- filters.pathLike,
6818
- options.kind ?? null,
6819
- options.kind ?? null,
6820
- options.from ?? null,
6821
- options.from ?? null,
6822
- options.to ?? null,
6823
- options.to ?? null,
6824
- options.limit ?? 50
6825
- )
6826
- );
7714
+ ).all(...filters.params, options.limit ?? 50);
7715
+ let rows = withCacheDbReadOnly(queryRows);
7716
+ if (rows == null && options.path) {
7717
+ rows = withCacheDb(queryRows);
7718
+ }
6827
7719
  return (rows ?? []).map((row) => ({
6828
7720
  ...fileActivityFromRow(row),
6829
7721
  session: sessionHeadFromSearchRow(row)
@@ -6905,15 +7797,6 @@ function listCachedProjectGroups(sessions) {
6905
7797
  });
6906
7798
  return groups ?? [];
6907
7799
  }
6908
- function isPathScopeMatch(queryPath, sessionPath) {
6909
- if (!sessionPath) return false;
6910
- const q = resolve(queryPath);
6911
- const s = resolve(sessionPath);
6912
- const sepNorm = (p) => p.replaceAll(sep, "/");
6913
- const sn = sepNorm(s);
6914
- const qn = sepNorm(q);
6915
- return sn === qn || sn.startsWith(qn + "/") || qn.startsWith(sn + "/");
6916
- }
6917
7800
  function createIdentityResolver() {
6918
7801
  const cache = /* @__PURE__ */ new Map();
6919
7802
  return (directory) => {
@@ -6935,17 +7818,10 @@ function attachProjectIdentities(sessions) {
6935
7818
  };
6936
7819
  });
6937
7820
  }
6938
- function isProjectScopeMatch(queryPath, session) {
6939
- if (!session.directory) return false;
6940
- const queryIdentity = computeIdentity(queryPath, realFs);
6941
- if (session.project_identity?.key === queryIdentity.key) return true;
6942
- return isPathScopeMatch(queryPath, session.directory);
6943
- }
6944
7821
  function filterSessions(sessions, options) {
6945
7822
  let result = sessions;
6946
7823
  if (options.cwd) {
6947
- const cwd = options.cwd;
6948
- result = result.filter((s) => isProjectScopeMatch(cwd, s));
7824
+ result = filterSessionsByProjectScope(result, options.cwd);
6949
7825
  }
6950
7826
  if (options.from != null) {
6951
7827
  result = result.filter((s) => (s.time_updated ?? s.time_created) >= options.from);
@@ -6964,8 +7840,34 @@ function buildAgentCacheMeta(agent) {
6964
7840
  }
6965
7841
  return meta;
6966
7842
  }
7843
+ function sessionCacheValue(session) {
7844
+ return JSON.stringify(session);
7845
+ }
7846
+ function buildCacheChanges(cachedSessions, updatedSessions, changedIds = []) {
7847
+ const cachedMap = new Map(cachedSessions.map((session) => [session.id, session]));
7848
+ const updatedIds = new Set(updatedSessions.map((session) => session.id));
7849
+ const changedIdSet = new Set(changedIds);
7850
+ const removedSessionIds = cachedSessions.filter((session) => !updatedIds.has(session.id)).map((session) => session.id);
7851
+ const changes = [];
7852
+ updatedSessions.forEach((session, sortIndex) => {
7853
+ const cached = cachedMap.get(session.id);
7854
+ if (!cached || changedIdSet.has(session.id) || cached !== session && sessionCacheValue(cached) !== sessionCacheValue(session)) {
7855
+ changes.push({ session, sortIndex });
7856
+ }
7857
+ });
7858
+ return { changes, removedSessionIds };
7859
+ }
7860
+ function saveCachedSessionDiff(agent, cachedSessions, updatedSessions, changedIds = []) {
7861
+ const diff = buildCacheChanges(cachedSessions, updatedSessions, changedIds);
7862
+ saveCachedSessionChanges(
7863
+ agent.name,
7864
+ diff.changes,
7865
+ diff.removedSessionIds,
7866
+ buildAgentCacheMeta(agent)
7867
+ );
7868
+ }
6967
7869
  function getSmartTagWorkerCount(sessionCount) {
6968
- if (sessionCount < 8) return 1;
7870
+ if (sessionCount < 50) return 1;
6969
7871
  return Math.min(sessionCount, Math.max(1, Math.min(4, availableParallelism() - 1)));
6970
7872
  }
6971
7873
  function chunkSessions(items, chunkCount) {
@@ -6998,58 +7900,12 @@ function ensureSessionTagsSync(agent, sessions) {
6998
7900
  });
6999
7901
  return { sessions: tagged, changed };
7000
7902
  }
7001
- async function classifySessionTagsInWorker(agentName, sessionIds) {
7903
+ async function classifySessionTagsInWorker(workerUrl, agentName, sessionIds, meta) {
7002
7904
  return new Promise((resolveWorker, rejectWorker) => {
7003
- const worker = new Worker(
7004
- `
7005
- const { parentPort, workerData } = require("node:worker_threads");
7006
-
7007
- (async () => {
7008
- const {
7009
- createRegisteredAgents,
7010
- classifySessionTags,
7011
- getSmartTagSourceTimestamp,
7012
- } = await import("@codesesh/core");
7013
-
7014
- const agent = createRegisteredAgents().find((item) => item.name === workerData.agentName);
7015
- const results = [];
7016
-
7017
- if (agent) {
7018
- for (const sessionId of workerData.sessionIds) {
7019
- try {
7020
- const data = agent.getSessionData(sessionId);
7021
- results.push({
7022
- id: sessionId,
7023
- tags: classifySessionTags(data),
7024
- sourceUpdatedAt: getSmartTagSourceTimestamp(data),
7025
- });
7026
- } catch (error) {
7027
- results.push({
7028
- id: sessionId,
7029
- error: error instanceof Error ? error.message : String(error),
7030
- });
7031
- }
7032
- }
7033
- }
7034
-
7035
- parentPort?.postMessage(results);
7036
- })().catch((error) => {
7037
- parentPort?.postMessage([
7038
- {
7039
- id: "",
7040
- error: error instanceof Error ? error.message : String(error),
7041
- },
7042
- ]);
7043
- });
7044
- `,
7045
- {
7046
- eval: true,
7047
- workerData: { agentName, sessionIds }
7048
- }
7049
- );
7050
- worker.once("message", (results) => {
7051
- resolveWorker(results);
7905
+ const worker = new Worker(workerUrl, {
7906
+ workerData: { agentName, sessionIds, meta }
7052
7907
  });
7908
+ worker.once("message", (results) => resolveWorker(results));
7053
7909
  worker.once("error", rejectWorker);
7054
7910
  worker.once("exit", (code) => {
7055
7911
  if (code !== 0) {
@@ -7058,7 +7914,7 @@ async function classifySessionTagsInWorker(agentName, sessionIds) {
7058
7914
  });
7059
7915
  });
7060
7916
  }
7061
- async function ensureSessionTags(agent, sessions) {
7917
+ async function ensureSessionTags(agent, sessions, workerUrl) {
7062
7918
  const staleSessions = sessions.filter((session) => {
7063
7919
  const sourceUpdatedAt = session.time_updated ?? session.time_created;
7064
7920
  const currentTags = Array.isArray(session.smart_tags) ? session.smart_tags : null;
@@ -7067,16 +7923,19 @@ async function ensureSessionTags(agent, sessions) {
7067
7923
  if (staleSessions.length === 0) {
7068
7924
  return { sessions, changed: false };
7069
7925
  }
7070
- const workerCount = getSmartTagWorkerCount(staleSessions.length);
7926
+ const workerCount = workerUrl ? getSmartTagWorkerCount(staleSessions.length) : 1;
7071
7927
  if (workerCount <= 1) {
7072
7928
  return ensureSessionTagsSync(agent, sessions);
7073
7929
  }
7930
+ const meta = buildAgentCacheMeta(agent);
7074
7931
  try {
7075
7932
  const results = (await Promise.all(
7076
7933
  chunkSessions(
7077
7934
  staleSessions.map((session) => session.id),
7078
7935
  workerCount
7079
- ).map((sessionIds) => classifySessionTagsInWorker(agent.name, sessionIds))
7936
+ ).map(
7937
+ (sessionIds) => classifySessionTagsInWorker(workerUrl, agent.name, sessionIds, meta)
7938
+ )
7080
7939
  )).flat();
7081
7940
  const resultMap = new Map(results.filter((item) => item.tags).map((item) => [item.id, item]));
7082
7941
  return {
@@ -7096,10 +7955,14 @@ async function ensureSessionTags(agent, sessions) {
7096
7955
  }
7097
7956
  }
7098
7957
  async function scanAgentSmart(agent, options, onProgress) {
7958
+ const agentStart = performance.now();
7959
+ const timing = { total: 0 };
7099
7960
  const useCache = options.useCache ?? true;
7100
7961
  const canValidateCache = Boolean(agent.checkForChanges && agent.incrementalScan);
7101
7962
  if (useCache) {
7963
+ const t0 = performance.now();
7102
7964
  const cached = loadCachedSessions(agent.name);
7965
+ timing.cacheLoad = performance.now() - t0;
7103
7966
  if (cached !== null) {
7104
7967
  if (agent.setSessionMetaMap) {
7105
7968
  const metaMap = /* @__PURE__ */ new Map();
@@ -7108,6 +7971,26 @@ async function scanAgentSmart(agent, options, onProgress) {
7108
7971
  }
7109
7972
  agent.setSessionMetaMap(metaMap);
7110
7973
  }
7974
+ if (options.cacheOnly) {
7975
+ onProgress?.({
7976
+ agent: agent.name,
7977
+ phase: "cache",
7978
+ cachedCount: cached.sessions.length
7979
+ });
7980
+ onProgress?.({ agent: agent.name, phase: "complete", newCount: cached.sessions.length });
7981
+ const t32 = performance.now();
7982
+ const cachedWithIdentity2 = attachProjectIdentities(cached.sessions);
7983
+ timing.identity = performance.now() - t32;
7984
+ const filtered3 = filterSessions(cachedWithIdentity2, options);
7985
+ timing.total = performance.now() - agentStart;
7986
+ return {
7987
+ agent,
7988
+ heads: filtered3,
7989
+ fromCache: true,
7990
+ timing,
7991
+ cacheTimestamp: cached.timestamp
7992
+ };
7993
+ }
7111
7994
  const isAvail = agent.isAvailable();
7112
7995
  if (!isAvail) {
7113
7996
  return null;
@@ -7119,22 +8002,35 @@ async function scanAgentSmart(agent, options, onProgress) {
7119
8002
  });
7120
8003
  if (canValidateCache) {
7121
8004
  onProgress?.({ agent: agent.name, phase: "checking" });
8005
+ const t1 = performance.now();
7122
8006
  const checkResult = await Promise.resolve(
7123
8007
  agent.checkForChanges(cached.timestamp, cached.sessions)
7124
8008
  );
8009
+ timing.checkChanges = performance.now() - t1;
7125
8010
  if (checkResult.hasChanges) {
7126
8011
  onProgress?.({
7127
8012
  agent: agent.name,
7128
8013
  phase: "incremental",
7129
8014
  changedCount: checkResult.changedIds?.length
7130
8015
  });
8016
+ const t2 = performance.now();
7131
8017
  const updatedSessions = await Promise.resolve(
7132
8018
  agent.incrementalScan(cached.sessions, checkResult.changedIds || [])
7133
8019
  );
8020
+ timing.scan = performance.now() - t2;
8021
+ const t32 = performance.now();
7134
8022
  const sessionsWithIdentity = attachProjectIdentities(updatedSessions);
7135
- const tagged2 = options.includeSmartTags === false ? { sessions: sessionsWithIdentity, changed: false } : await ensureSessionTags(agent, sessionsWithIdentity);
7136
- if (options.writeCache !== false && options.from == null && options.to == null) {
7137
- saveCachedSessions(agent.name, tagged2.sessions, buildAgentCacheMeta(agent));
8023
+ timing.identity = performance.now() - t32;
8024
+ const t42 = performance.now();
8025
+ const tagged2 = options.includeSmartTags === false ? { sessions: sessionsWithIdentity, changed: false } : await ensureSessionTags(agent, sessionsWithIdentity, options.smartTagWorkerUrl);
8026
+ timing.tags = performance.now() - t42;
8027
+ if (options.writeCache !== false) {
8028
+ saveCachedSessionDiff(
8029
+ agent,
8030
+ cached.sessions,
8031
+ tagged2.sessions,
8032
+ checkResult.changedIds ?? []
8033
+ );
7138
8034
  }
7139
8035
  onProgress?.({
7140
8036
  agent: agent.name,
@@ -7142,22 +8038,45 @@ async function scanAgentSmart(agent, options, onProgress) {
7142
8038
  newCount: tagged2.sessions.length
7143
8039
  });
7144
8040
  const filtered3 = filterSessions(tagged2.sessions, options);
7145
- return { agent, heads: filtered3, fromCache: true, refreshed: true };
8041
+ timing.total = performance.now() - agentStart;
8042
+ return {
8043
+ agent,
8044
+ heads: filtered3,
8045
+ fromCache: true,
8046
+ refreshed: true,
8047
+ timing,
8048
+ cacheTimestamp: checkResult.timestamp
8049
+ };
7146
8050
  }
7147
8051
  onProgress?.({ agent: agent.name, phase: "complete", newCount: cached.sessions.length });
7148
8052
  }
8053
+ const t3 = performance.now();
7149
8054
  const cachedWithIdentity = attachProjectIdentities(cached.sessions);
7150
- const tagged = options.includeSmartTags === false ? { sessions: cachedWithIdentity, changed: false } : await ensureSessionTags(agent, cachedWithIdentity);
7151
- if (tagged.changed && options.writeCache !== false && options.from == null && options.to == null) {
7152
- saveCachedSessions(agent.name, tagged.sessions, buildAgentCacheMeta(agent));
8055
+ timing.identity = performance.now() - t3;
8056
+ const t4 = performance.now();
8057
+ const tagged = options.includeSmartTags === false ? { sessions: cachedWithIdentity, changed: false } : await ensureSessionTags(agent, cachedWithIdentity, options.smartTagWorkerUrl);
8058
+ timing.tags = performance.now() - t4;
8059
+ if (tagged.changed && options.writeCache !== false) {
8060
+ saveCachedSessionDiff(agent, cached.sessions, tagged.sessions);
7153
8061
  }
7154
8062
  const filtered2 = filterSessions(tagged.sessions, options);
7155
- return { agent, heads: filtered2, fromCache: true };
8063
+ timing.total = performance.now() - agentStart;
8064
+ return {
8065
+ agent,
8066
+ heads: filtered2,
8067
+ fromCache: true,
8068
+ timing,
8069
+ cacheTimestamp: cached.timestamp
8070
+ };
7156
8071
  }
7157
8072
  }
7158
- return scanAgentFull(agent, options, onProgress);
8073
+ if (options.cacheOnly) {
8074
+ timing.total = performance.now() - agentStart;
8075
+ return null;
8076
+ }
8077
+ return scanAgentFull(agent, options, onProgress, timing, agentStart);
7159
8078
  }
7160
- async function scanAgentFull(agent, options, onProgress) {
8079
+ async function scanAgentFull(agent, options, onProgress, timing = { total: 0 }, agentStart = performance.now()) {
7161
8080
  const availMarker = perf.start(`agent:${agent.name}:isAvailable`);
7162
8081
  const isAvail = agent.isAvailable();
7163
8082
  perf.end(availMarker);
@@ -7166,17 +8085,38 @@ async function scanAgentFull(agent, options, onProgress) {
7166
8085
  }
7167
8086
  try {
7168
8087
  const scanMarker = perf.start(`agent:${agent.name}:scan`);
7169
- const heads = agent.scan({ from: options.from, to: options.to, fast: options.fast });
8088
+ const t0 = performance.now();
8089
+ const heads = agent.scan({
8090
+ from: options.from,
8091
+ to: options.to,
8092
+ fast: options.fast,
8093
+ onProgress: (progress) => {
8094
+ onProgress?.({
8095
+ agent: agent.name,
8096
+ phase: "incremental",
8097
+ cachedCount: progress.total,
8098
+ newCount: progress.sessions,
8099
+ changedCount: progress.processed
8100
+ });
8101
+ }
8102
+ });
7170
8103
  perf.end(scanMarker);
8104
+ timing.scan = performance.now() - t0;
8105
+ const t1 = performance.now();
7171
8106
  const headsWithIdentity = attachProjectIdentities(heads);
7172
- const tagged = options.includeSmartTags === false ? { sessions: headsWithIdentity, changed: false } : await ensureSessionTags(agent, headsWithIdentity);
8107
+ timing.identity = performance.now() - t1;
8108
+ const t2 = performance.now();
8109
+ const tagged = options.includeSmartTags === false ? { sessions: headsWithIdentity, changed: false } : await ensureSessionTags(agent, headsWithIdentity, options.smartTagWorkerUrl);
8110
+ timing.tags = performance.now() - t2;
7173
8111
  const meta = buildAgentCacheMeta(agent);
7174
8112
  if (options.writeCache !== false && options.from == null && options.to == null) {
7175
8113
  saveCachedSessions(agent.name, tagged.sessions, meta);
8114
+ markAgentCacheInitialized(agent.name);
7176
8115
  }
7177
8116
  onProgress?.({ agent: agent.name, phase: "complete", newCount: tagged.sessions.length });
7178
8117
  const filtered2 = filterSessions(tagged.sessions, options);
7179
- return { agent, heads: filtered2, fromCache: false };
8118
+ timing.total = performance.now() - agentStart;
8119
+ return { agent, heads: filtered2, fromCache: false, timing };
7180
8120
  } catch (err) {
7181
8121
  console.error(`Error scanning ${agent.name}:`, err);
7182
8122
  return { agent, heads: [], fromCache: false };
@@ -7188,6 +8128,7 @@ async function scanSessions(options = {}, onProgress) {
7188
8128
  const byAgent = {};
7189
8129
  const allSessions = [];
7190
8130
  const availableAgents = [];
8131
+ const cacheTimestamps = {};
7191
8132
  const agentFilter = options.agents?.length ? new Set(options.agents.map((a) => a.toLowerCase())) : null;
7192
8133
  const agentsToScan = agents.filter((agent) => {
7193
8134
  if (agentFilter && !agentFilter.has(agent.name.toLowerCase())) {
@@ -7197,15 +8138,28 @@ async function scanSessions(options = {}, onProgress) {
7197
8138
  });
7198
8139
  const scanPromises = agentsToScan.map((agent) => scanAgentSmart(agent, options, onProgress));
7199
8140
  const results = await Promise.all(scanPromises);
8141
+ const timings = {};
7200
8142
  for (const result of results) {
7201
8143
  if (result) {
7202
8144
  availableAgents.push(result.agent);
7203
8145
  byAgent[result.agent.name] = result.heads;
7204
8146
  allSessions.push(...result.heads);
8147
+ if (result.timing) {
8148
+ timings[result.agent.name] = result.timing;
8149
+ }
8150
+ if (result.cacheTimestamp != null) {
8151
+ cacheTimestamps[result.agent.name] = result.cacheTimestamp;
8152
+ }
7205
8153
  }
7206
8154
  }
7207
8155
  perf.end(scanMarker);
7208
- return { sessions: allSessions, byAgent, agents: availableAgents };
8156
+ return {
8157
+ sessions: allSessions,
8158
+ byAgent,
8159
+ agents: availableAgents,
8160
+ timings,
8161
+ cacheTimestamps: Object.keys(cacheTimestamps).length > 0 ? cacheTimestamps : void 0
8162
+ };
7209
8163
  }
7210
8164
  async function scanSessionsAsync(options = {}, onProgress) {
7211
8165
  return scanSessions(options, onProgress);
@@ -7570,6 +8524,9 @@ export {
7570
8524
  buildProjectGroups,
7571
8525
  normalizeGitRemote,
7572
8526
  computeIdentity,
8527
+ createProjectScopeMatcher,
8528
+ matchesProjectScope,
8529
+ filterSessionsByProjectScope,
7573
8530
  getSmartTagSourceTimestamp,
7574
8531
  classifySessionTags,
7575
8532
  extractFileActivityOccurrences,
@@ -7577,10 +8534,15 @@ export {
7577
8534
  extractSessionFileActivity,
7578
8535
  parseSearchQuery,
7579
8536
  loadCachedSessions,
8537
+ isAgentCacheInitialized,
8538
+ markAgentCacheInitialized,
8539
+ loadCachedSessionData,
7580
8540
  saveCachedSessions,
8541
+ saveCachedSessionChanges,
7581
8542
  clearCache,
7582
8543
  getCacheInfo,
7583
8544
  syncSessionSearchIndex,
8545
+ syncSessionSearchIndexChanges,
7584
8546
  searchSessions,
7585
8547
  listFileActivity,
7586
8548
  listSessionFileActivity,
@@ -7595,4 +8557,4 @@ export {
7595
8557
  importBookmarks,
7596
8558
  deleteBookmark
7597
8559
  };
7598
- //# sourceMappingURL=chunk-SQYHWMQV.js.map
8560
+ //# sourceMappingURL=chunk-7GQEIPVK.js.map