codesesh 1.0.2 → 1.0.3

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.
Files changed (39) hide show
  1. package/dist/{chunk-VAUC2W7I.js → chunk-PSHZZITB.js} +1919 -731
  2. package/dist/chunk-PSHZZITB.js.map +1 -0
  3. package/dist/chunk-RF3EEWPH.js +17 -0
  4. package/dist/chunk-RF3EEWPH.js.map +1 -0
  5. package/dist/{chunk-PROMURPM.js → chunk-WAKW5OPS.js} +2 -2
  6. package/dist/{dist-6QTGTYZT.js → dist-RHDY2EZX.js} +18 -20
  7. package/dist/index.js +1291 -672
  8. package/dist/index.js.map +1 -1
  9. package/dist/project-identity-worker.js +27 -0
  10. package/dist/project-identity-worker.js.map +1 -0
  11. package/dist/scan-refresh-worker.js +15 -6
  12. package/dist/scan-refresh-worker.js.map +1 -1
  13. package/dist/search-index-worker.js +16 -6
  14. package/dist/search-index-worker.js.map +1 -1
  15. package/dist/smart-tag-worker.js +2 -2
  16. package/dist/web/assets/{ErrorBoundary-D11tWzfn.js → ErrorBoundary-DIwwrdlw.js} +1 -1
  17. package/dist/web/assets/InteractiveReceipt-DmNBDVms.js +1 -0
  18. package/dist/web/assets/{OverviewScreen-DriPmCam.js → OverviewScreen-B5JekBLC.js} +1 -1
  19. package/dist/web/assets/Projects-Bj6ma2k2.js +1 -0
  20. package/dist/web/assets/{SearchFilterBar-Bx73B5e9.js → SearchFilterBar-B9VrUbym.js} +1 -1
  21. package/dist/web/assets/{SearchResultsPanel-A6_3TPhW.js → SearchResultsPanel-C1cuuOKq.js} +1 -1
  22. package/dist/web/assets/{SessionDetail-BnpvDwtH.js → SessionDetail-Dc2CQ3hH.js} +5 -5
  23. package/dist/web/assets/contract-DXvltqJG.js +1 -0
  24. package/dist/web/assets/index-3UfOiTBO.css +1 -0
  25. package/dist/web/assets/index-DBMiEd61.js +1410 -0
  26. package/dist/web/assets/panel-CLnL4kMN.js +1 -0
  27. package/dist/web/assets/{session-indexes-0oT5HSS5.js → session-indexes-BPhf1X2P.js} +2 -2
  28. package/dist/web/assets/{utils-DgUgu15E.js → utils-BKqufGdv.js} +1 -1
  29. package/dist/web/index.html +8 -8
  30. package/package.json +1 -1
  31. package/dist/chunk-VAUC2W7I.js.map +0 -1
  32. package/dist/web/assets/InteractiveReceipt-DYGyHeZd.js +0 -1
  33. package/dist/web/assets/Projects-BPDIcUBK.js +0 -1
  34. package/dist/web/assets/contract-V6QuD0UY.js +0 -1
  35. package/dist/web/assets/index-99ZxpV2y.js +0 -1410
  36. package/dist/web/assets/index-B2RqUgZ5.css +0 -1
  37. package/dist/web/assets/panel-C-gc79Gz.js +0 -1
  38. /package/dist/{chunk-PROMURPM.js.map → chunk-WAKW5OPS.js.map} +0 -0
  39. /package/dist/{dist-6QTGTYZT.js.map → dist-RHDY2EZX.js.map} +0 -0
@@ -1,6 +1,37 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // ../core/dist/chunk-PIUQAREY.mjs
3
+ // ../core/dist/chunk-46YWSMES.mjs
4
+ var UNKNOWN_AGENT_NAME = "unknown";
5
+ function normalizeSessionReference(reference) {
6
+ return {
7
+ agentName: reference.agentName.trim().toLowerCase(),
8
+ sessionId: reference.sessionId
9
+ };
10
+ }
11
+ function parseSessionReference(value) {
12
+ const separatorIndex = value.indexOf("/");
13
+ if (separatorIndex <= 0 || separatorIndex === value.length - 1) return null;
14
+ const agentName = value.slice(0, separatorIndex).trim().toLowerCase();
15
+ if (!agentName) return null;
16
+ return {
17
+ agentName,
18
+ sessionId: value.slice(separatorIndex + 1)
19
+ };
20
+ }
21
+ function formatSessionReference(reference) {
22
+ const normalized = normalizeSessionReference(reference);
23
+ return `${normalized.agentName}/${normalized.sessionId}`;
24
+ }
25
+ function getSessionAgentKey(session) {
26
+ return parseSessionReference(session.slug ?? "")?.agentName ?? UNKNOWN_AGENT_NAME;
27
+ }
28
+ function agentRoutePath(agentName) {
29
+ return `/${encodeURIComponent(agentName.trim().toLowerCase())}`;
30
+ }
31
+ function sessionRoutePath(reference) {
32
+ const normalized = normalizeSessionReference(reference);
33
+ return `${agentRoutePath(normalized.agentName)}/${encodeURIComponent(normalized.sessionId)}`;
34
+ }
4
35
  function toRecord(value) {
5
36
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
6
37
  }
@@ -121,7 +152,7 @@ function normalizeMessageParts(value) {
121
152
  });
122
153
  }
123
154
 
124
- // ../core/dist/chunk-LE5L4UBM.mjs
155
+ // ../core/dist/chunk-TBIXUIQV.mjs
125
156
  import { chmodSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
126
157
  import { basename, dirname, join } from "path";
127
158
  import { createHash } from "crypto";
@@ -135,7 +166,7 @@ import { existsSync as existsSync4, readFileSync as readFileSync2, statSync as s
135
166
  import { join as join5, basename as basename3, dirname as dirname2 } from "path";
136
167
  import { existsSync as existsSync3 } from "fs";
137
168
  import { homedir as homedir2, platform } from "os";
138
- import { join as join4 } from "path";
169
+ import { isAbsolute, join as join4, resolve } from "path";
139
170
  import { closeSync, openSync, readSync } from "fs";
140
171
  import { StringDecoder } from "string_decoder";
141
172
  import { basename as basename2 } from "path";
@@ -147,7 +178,7 @@ import { createHash as createHash2 } from "crypto";
147
178
  import { existsSync as existsSync6, readFileSync as readFileSync3, statSync as statSync5 } from "fs";
148
179
  import { join as join8, basename as basename5, dirname as dirname4 } from "path";
149
180
  import { existsSync as existsSync7, readFileSync as readFileSync4, statSync as statSync6 } from "fs";
150
- import { basename as basename6, dirname as dirname5, join as join9, resolve } from "path";
181
+ import { basename as basename6, dirname as dirname5, join as join9, resolve as resolve2 } from "path";
151
182
  import { existsSync as existsSync8, readFileSync as readFileSync5, statSync as statSync7 } from "fs";
152
183
  import { join as join10, basename as basename7 } from "path";
153
184
  import { existsSync as existsSync9, lstatSync, readdirSync as readdirSync3, readFileSync as readFileSync6 } from "fs";
@@ -162,8 +193,7 @@ import { dirname as dirname6, join as join14 } from "path";
162
193
  import { existsSync as existsSync12, readdirSync as readdirSync4, realpathSync, statSync as statSync10 } from "fs";
163
194
  import { basename as basename9, join as join17 } from "path";
164
195
  import { closeSync as closeSync2, openSync as openSync2, readFileSync as readFileSync8, readSync as readSync2, statSync as statSync9 } from "fs";
165
- import { homedir as homedir5 } from "os";
166
- import { join as join15, resolve as resolve2 } from "path";
196
+ import { join as join15 } from "path";
167
197
  import * as zlib from "zlib";
168
198
  import { createHash as createHash3 } from "crypto";
169
199
  import { readFileSync as readFileSync9 } from "fs";
@@ -968,12 +998,32 @@ function reportAgentScanFailure(failure, baselineRetained) {
968
998
  });
969
999
  }
970
1000
  var BaseAgent = class {
1001
+ /**
1002
+ * Commit the observation made by the latest checkForChanges. Called by the
1003
+ * scan orchestrator only after the scan consuming that check succeeded, so
1004
+ * a failed scan leaves the baseline behind and the next check re-detects
1005
+ * the same change instead of silently skipping it.
1006
+ */
1007
+ commitChangeCheck() {
1008
+ }
1009
+ /** Wrap an enumeration/database read so failures follow the error taxonomy. */
1010
+ scanStep(stage, sourcePath, read) {
1011
+ try {
1012
+ return read();
1013
+ } catch (error) {
1014
+ if (error instanceof SessionScanError) throw error;
1015
+ throw new SessionScanError(this.name, stage, { cause: error, sourcePath });
1016
+ }
1017
+ }
971
1018
  filterCachedSessions(sessions) {
972
1019
  return sessions;
973
1020
  }
974
1021
  getUri(sessionId) {
975
1022
  return `${this.name}://${sessionId}`;
976
1023
  }
1024
+ sessionSlug(sessionId) {
1025
+ return formatSessionReference({ agentName: this.name, sessionId });
1026
+ }
977
1027
  };
978
1028
  var FileSystemSessionSource = class extends BaseAgent {
979
1029
  sessionMetaMap = /* @__PURE__ */ new Map();
@@ -1215,6 +1265,7 @@ function latestSqliteSourceMtime(dbPath) {
1215
1265
  var DatabaseSessionSource = class extends BaseAgent {
1216
1266
  sessionMetaMap = /* @__PURE__ */ new Map();
1217
1267
  lastSourceFingerprint = null;
1268
+ pendingSourceFingerprint = null;
1218
1269
  /** 记录单个会话的缓存 meta(sourcePath = dbPath)。 */
1219
1270
  rememberSession(sessionId, additionalMeta = {}) {
1220
1271
  const dbPath = this.getDatabasePath();
@@ -1239,6 +1290,11 @@ var DatabaseSessionSource = class extends BaseAgent {
1239
1290
  setSessionMetaMap(meta) {
1240
1291
  this.sessionMetaMap = meta;
1241
1292
  }
1293
+ commitChangeCheck() {
1294
+ if (this.pendingSourceFingerprint == null) return;
1295
+ this.lastSourceFingerprint = this.pendingSourceFingerprint;
1296
+ this.pendingSourceFingerprint = null;
1297
+ }
1242
1298
  /**
1243
1299
  * 变更检测:数据库内部变更难以按行定位,按库文件集合的指纹判定。
1244
1300
  *
@@ -1263,7 +1319,7 @@ var DatabaseSessionSource = class extends BaseAgent {
1263
1319
  if (pricingChanged) return { hasChanges: true, timestamp: Date.now() };
1264
1320
  const fingerprint = sqliteSourceFingerprint(dbPath);
1265
1321
  const previous = this.lastSourceFingerprint;
1266
- this.lastSourceFingerprint = fingerprint;
1322
+ this.pendingSourceFingerprint = fingerprint;
1267
1323
  const hasChanges = previous == null ? latestSqliteSourceMtime(dbPath) > sinceTimestamp : fingerprint !== previous;
1268
1324
  return {
1269
1325
  hasChanges,
@@ -1369,10 +1425,16 @@ var PerfTracer = class {
1369
1425
  }
1370
1426
  };
1371
1427
  var perf = new PerfTracer();
1428
+ function expandHomePath(path2) {
1429
+ if (path2 === "~") return homedir2();
1430
+ if (path2.startsWith("~/") || path2.startsWith("~\\")) return join4(homedir2(), path2.slice(2));
1431
+ return path2;
1432
+ }
1372
1433
  function readEnvPath(name) {
1373
1434
  const value = process.env[name];
1374
- if (!value) return null;
1375
- return value;
1435
+ if (!value || !value.trim()) return null;
1436
+ const expanded = expandHomePath(value.trim());
1437
+ return isAbsolute(expanded) ? expanded : resolve(expanded);
1376
1438
  }
1377
1439
  function firstExisting(...paths) {
1378
1440
  for (const p of paths) {
@@ -1605,18 +1667,35 @@ function estimateTokenCost(model, tokens) {
1605
1667
  return estimateCostForTokens(model, tokens)?.cost ?? null;
1606
1668
  }
1607
1669
  var TIMEZONE_SUFFIX_PATTERN = /(?:Z|[+-]\d{2}:?\d{2})$/i;
1608
- function parseAgentTimestampMs(value, agentName) {
1670
+ function parseAgentTimestamp(value, agentName, options = {}) {
1671
+ if (value == null) return null;
1672
+ if (typeof value === "number") {
1673
+ if (Number.isFinite(value)) return value;
1674
+ reportTimestampParseFailure(agentName, value);
1675
+ return null;
1676
+ }
1677
+ if (typeof value !== "string") {
1678
+ reportTimestampParseFailure(agentName, value);
1679
+ return null;
1680
+ }
1609
1681
  const timestamp = value.trim().replace(" ", "T");
1610
- if (!timestamp) return 0;
1682
+ if (!timestamp) return null;
1683
+ if (options.numericStrings) {
1684
+ const numeric = Number(timestamp);
1685
+ if (Number.isFinite(numeric)) return numeric;
1686
+ }
1611
1687
  const normalized = TIMEZONE_SUFFIX_PATTERN.test(timestamp) ? timestamp : `${timestamp}Z`;
1612
1688
  const timestampMs = Date.parse(normalized);
1613
1689
  if (Number.isFinite(timestampMs)) return timestampMs;
1690
+ reportTimestampParseFailure(agentName, value, normalized);
1691
+ return null;
1692
+ }
1693
+ function reportTimestampParseFailure(agentName, value, normalized) {
1614
1694
  getCoreDiagnostics()?.warn("agent.timestamp_parse_failed", {
1615
1695
  agentName,
1616
- value,
1696
+ value: typeof value === "string" || typeof value === "number" ? value : typeof value,
1617
1697
  normalized
1618
1698
  });
1619
- return 0;
1620
1699
  }
1621
1700
  function asRecord(value) {
1622
1701
  if (value && typeof value === "object" && !Array.isArray(value)) {
@@ -1672,6 +1751,9 @@ var TranscriptBuilder = class {
1672
1751
  options;
1673
1752
  messages = [];
1674
1753
  pendingToolCalls = /* @__PURE__ */ new Map();
1754
+ // Tracks per-message part kinds so grouping checks are O(1) instead of
1755
+ // rescanning the growing parts array on every append (CS-283).
1756
+ partFlags = /* @__PURE__ */ new WeakMap();
1675
1757
  currentAssistant = null;
1676
1758
  latestTextAssistant = null;
1677
1759
  beginTurn() {
@@ -1681,10 +1763,13 @@ var TranscriptBuilder = class {
1681
1763
  appendMessage(input) {
1682
1764
  const message = this.createMessage(input);
1683
1765
  this.messages.push(message);
1684
- this.registerToolCalls(message.parts);
1766
+ for (const part of message.parts) {
1767
+ this.registerToolCall(part);
1768
+ this.notePart(message, part);
1769
+ }
1685
1770
  if (message.role === "assistant") {
1686
1771
  this.currentAssistant = message;
1687
- if (message.parts.some((part) => part.type === "text")) {
1772
+ if (this.flagsFor(message).hasText) {
1688
1773
  this.latestTextAssistant = message;
1689
1774
  }
1690
1775
  } else if (message.role === "user") {
@@ -1694,11 +1779,12 @@ var TranscriptBuilder = class {
1694
1779
  }
1695
1780
  appendAssistantPart(part, input, options = {}) {
1696
1781
  const current = this.currentAssistant;
1697
- const canReuse = current !== null && (options.grouping === "current" || (part.type === "text" ? !current.parts.some((item) => item.type === "tool") : part.type === "reasoning" ? !current.parts.some((item) => item.type === "text" || item.type === "tool") : false));
1782
+ const currentFlags = current === null ? null : this.flagsFor(current);
1783
+ const canReuse = current !== null && (options.grouping === "current" || (part.type === "text" ? !currentFlags.hasTool : part.type === "reasoning" ? !currentFlags.hasText && !currentFlags.hasTool : false));
1698
1784
  const message = canReuse ? current : this.appendMessage({ ...input, role: "assistant", parts: [part] });
1699
1785
  if (canReuse) {
1700
1786
  if (options.deduplicateTail) this.appendPartIfNew(message, part);
1701
- else message.parts.push(part);
1787
+ else this.pushPart(message, part);
1702
1788
  this.applyMissingMetadata(message, input);
1703
1789
  }
1704
1790
  if (part.type === "text") {
@@ -1717,7 +1803,7 @@ var TranscriptBuilder = class {
1717
1803
  parts: [part]
1718
1804
  });
1719
1805
  if (target) {
1720
- message.parts.push(part);
1806
+ this.pushPart(message, part);
1721
1807
  this.applyMissingMetadata(message, input);
1722
1808
  if (options.markModeAsTool) message.mode = "tool";
1723
1809
  this.registerToolCall(part);
@@ -1727,7 +1813,7 @@ var TranscriptBuilder = class {
1727
1813
  }
1728
1814
  appendToCurrentAssistant(part) {
1729
1815
  if (!this.currentAssistant) return false;
1730
- this.currentAssistant.parts.push(part);
1816
+ this.pushPart(this.currentAssistant, part);
1731
1817
  return true;
1732
1818
  }
1733
1819
  updateToolCall(callId, update) {
@@ -1804,9 +1890,6 @@ var TranscriptBuilder = class {
1804
1890
  nickname: input.nickname
1805
1891
  };
1806
1892
  }
1807
- registerToolCalls(parts) {
1808
- for (const part of parts) this.registerToolCall(part);
1809
- }
1810
1893
  registerToolCall(part) {
1811
1894
  if (part.type === "tool" && part.callID) {
1812
1895
  this.pendingToolCalls.set(part.callID, part);
@@ -1817,7 +1900,24 @@ var TranscriptBuilder = class {
1817
1900
  if ("text" in part && tail?.type === part.type && "text" in tail && tail.text === part.text) {
1818
1901
  return;
1819
1902
  }
1903
+ this.pushPart(message, part);
1904
+ }
1905
+ pushPart(message, part) {
1820
1906
  message.parts.push(part);
1907
+ this.notePart(message, part);
1908
+ }
1909
+ notePart(message, part) {
1910
+ const flags = this.flagsFor(message);
1911
+ if (part.type === "text") flags.hasText = true;
1912
+ else if (part.type === "tool") flags.hasTool = true;
1913
+ }
1914
+ flagsFor(message) {
1915
+ let flags = this.partFlags.get(message);
1916
+ if (!flags) {
1917
+ flags = { hasText: false, hasTool: false };
1918
+ this.partFlags.set(message, flags);
1919
+ }
1920
+ return flags;
1821
1921
  }
1822
1922
  applyMissingMetadata(message, input) {
1823
1923
  if (!message.id && input.id) message.id = input.id;
@@ -1855,18 +1955,12 @@ var TranscriptBuilder = class {
1855
1955
  };
1856
1956
  }
1857
1957
  };
1858
- var HEAD_INDEX_VERSION = "claudecode-head-v6";
1958
+ var HEAD_INDEX_VERSION = "claudecode-head-v7";
1859
1959
  function resolveClaudeCodeDataRoot() {
1860
1960
  return resolveHomePath("CLAUDE_CONFIG_DIR", ".claude");
1861
1961
  }
1862
1962
  function parseTimestampMs(data) {
1863
- const raw = data["timestamp"];
1864
- const value = asString(raw);
1865
- if (value === void 0) {
1866
- if (raw !== void 0 && raw !== null) reportFieldMismatch("claudecode", "timestamp");
1867
- return 0;
1868
- }
1869
- return parseAgentTimestampMs(value, "claudecode");
1963
+ return parseAgentTimestamp(data["timestamp"], "claudecode") ?? 0;
1870
1964
  }
1871
1965
  function readUsageNumber(usage, field) {
1872
1966
  const raw = usage[field];
@@ -2067,7 +2161,7 @@ var ClaudeCodeAgent = class extends SingleFileSessionSource {
2067
2161
  reference: { agentName: this.name, sessionId: meta.id },
2068
2162
  id: meta.id,
2069
2163
  title: meta.title,
2070
- slug: `claudecode/${meta.id}`,
2164
+ slug: this.sessionSlug(meta.id),
2071
2165
  directory: meta.directory,
2072
2166
  parent_reference: meta.parentSessionId == null ? void 0 : { agentName: this.name, sessionId: meta.parentSessionId },
2073
2167
  version: void 0,
@@ -2347,7 +2441,7 @@ var ClaudeCodeAgent = class extends SingleFileSessionSource {
2347
2441
  if (messageCount === 0) return filteredSession("no visible messages");
2348
2442
  return parsedSession({
2349
2443
  id: sessionId,
2350
- slug: `claudecode/${sessionId}`,
2444
+ slug: this.sessionSlug(sessionId),
2351
2445
  title,
2352
2446
  directory,
2353
2447
  parent_reference: child?.parentSessionId == null ? void 0 : { agentName: this.name, sessionId: child.parentSessionId },
@@ -2997,7 +3091,7 @@ var OpenCodeSqliteAgent = class extends DatabaseSessionSource {
2997
3091
  const messageTitle = context?.messageTitle ?? null;
2998
3092
  return parsedSession({
2999
3093
  id,
3000
- slug: `${this.name}/${id}`,
3094
+ slug: this.sessionSlug(id),
3001
3095
  title: resolveSessionTitle(String(row.title ?? ""), messageTitle, null),
3002
3096
  directory: String(row.directory ?? ""),
3003
3097
  parent_reference: row.parent_id == null || String(row.parent_id) === "" ? void 0 : { agentName: this.name, sessionId: String(row.parent_id) },
@@ -3379,6 +3473,14 @@ var OpenCodeAgent = class extends OpenCodeSqliteAgent {
3379
3473
  });
3380
3474
  }
3381
3475
  };
3476
+ function normalizeToolArguments(raw) {
3477
+ if (typeof raw !== "string") return raw;
3478
+ try {
3479
+ return JSON.parse(raw);
3480
+ } catch {
3481
+ return raw;
3482
+ }
3483
+ }
3382
3484
  var KIMI_TOOL_TITLE_MAP = {
3383
3485
  ReadFile: "read",
3384
3486
  Glob: "glob",
@@ -3388,7 +3490,7 @@ var KIMI_TOOL_TITLE_MAP = {
3388
3490
  Shell: "bash"
3389
3491
  };
3390
3492
  var KIMI_IGNORED_TOOLS = /* @__PURE__ */ new Set(["SetTodoList"]);
3391
- var KIMI_PARSER_REVISION = "kimi-parser-v1";
3493
+ var KIMI_PARSER_REVISION = "kimi-parser-v2";
3392
3494
  function resolveKimiDataRoot() {
3393
3495
  return resolveHomePath("KIMI_SHARE_DIR", ".kimi");
3394
3496
  }
@@ -3402,26 +3504,53 @@ function readWireTimestamp(record) {
3402
3504
  return narrowField("kimi", "wire.timestamp", record.timestamp, asNumber) ?? 0;
3403
3505
  }
3404
3506
  function parseTimestamp(raw) {
3405
- if (typeof raw === "number") return Number.isFinite(raw) ? raw : null;
3406
- if (typeof raw !== "string" || raw.trim() === "") return null;
3407
- const numeric = Number(raw);
3408
- if (Number.isFinite(numeric)) return numeric;
3409
- const parsed = Date.parse(raw);
3410
- return Number.isFinite(parsed) ? parsed : null;
3507
+ return parseAgentTimestamp(raw, "kimi", { numericStrings: true });
3411
3508
  }
3412
3509
  function extractTokenField(usage, field) {
3413
3510
  return narrowField("kimi", `usage.${field}`, usage[field], asNumber) ?? 0;
3414
3511
  }
3415
- function normalizeToolArguments(raw) {
3416
- if (typeof raw === "string") {
3417
- try {
3418
- return JSON.parse(raw);
3419
- } catch {
3420
- return raw;
3512
+ var KimiUsageAccumulator = class {
3513
+ constructor(model, usesWireTotalFallback) {
3514
+ this.model = model;
3515
+ this.usesWireTotalFallback = usesWireTotalFallback;
3516
+ }
3517
+ model;
3518
+ usesWireTotalFallback;
3519
+ stats = {
3520
+ total_cost: 0,
3521
+ total_input_tokens: 0,
3522
+ total_output_tokens: 0,
3523
+ total_tokens: 0,
3524
+ message_count: 0
3525
+ };
3526
+ totalCost = 0;
3527
+ applyContextRecord(record) {
3528
+ if (record.role !== "_usage") return;
3529
+ const tokenCount = asNumber(record.token_count);
3530
+ if (tokenCount === void 0) {
3531
+ reportFieldMismatch("kimi", "usage.token_count");
3532
+ return;
3421
3533
  }
3534
+ this.stats.total_tokens = tokenCount;
3535
+ }
3536
+ applyWireRecord(record) {
3537
+ if (this.usesWireTotalFallback) this.applyContextRecord(record);
3538
+ const tokenUsage = asRecord(asRecord(record.message)?.usage);
3539
+ if (!tokenUsage) return null;
3540
+ const inputTokens = extractTokenField(tokenUsage, "input_tokens");
3541
+ const outputTokens = extractTokenField(tokenUsage, "output_tokens");
3542
+ const cost = estimateTokenCost(this.model, { input: inputTokens, output: outputTokens });
3543
+ this.stats.total_input_tokens += inputTokens;
3544
+ this.stats.total_output_tokens += outputTokens;
3545
+ if (cost !== null) this.totalCost += cost;
3546
+ return { inputTokens, outputTokens, cost };
3547
+ }
3548
+ finish() {
3549
+ const stats = { ...this.stats, total_cost: Number(this.totalCost.toFixed(8)) };
3550
+ if (stats.total_cost > 0) stats.cost_source = "estimated";
3551
+ return stats;
3422
3552
  }
3423
- return raw;
3424
- }
3553
+ };
3425
3554
  function normalizeToolOutputParts(content, timestampMs) {
3426
3555
  if (typeof content === "string") {
3427
3556
  const text2 = cleanInternalText(content);
@@ -3647,7 +3776,7 @@ var KimiAgent = class extends FileSystemSessionSource {
3647
3776
  const stats = this.extractStats(meta.sourcePath);
3648
3777
  return parsedSession({
3649
3778
  id: meta.id,
3650
- slug: `kimi/${meta.id}`,
3779
+ slug: this.sessionSlug(meta.id),
3651
3780
  title: meta.title,
3652
3781
  directory: meta.cwd,
3653
3782
  time_created: meta.createdAt,
@@ -3667,10 +3796,12 @@ var KimiAgent = class extends FileSystemSessionSource {
3667
3796
  if (!meta.contextFile) throw new Error("context.jsonl is missing");
3668
3797
  const builder = new TranscriptBuilder();
3669
3798
  const ignoredToolCallIds = /* @__PURE__ */ new Set();
3799
+ const accumulator = new KimiUsageAccumulator(this.defaultModel, false);
3670
3800
  let seq = 0;
3671
3801
  const fallbackTs = meta.createdAt;
3672
3802
  for (const record of readJsonlFile(meta.contextFile)) {
3673
3803
  seq++;
3804
+ accumulator.applyContextRecord(record);
3674
3805
  try {
3675
3806
  const role = String(record.role ?? "");
3676
3807
  if (role === "_checkpoint" || role === "_usage" || isInternalEventType(role)) continue;
@@ -3716,8 +3847,8 @@ var KimiAgent = class extends FileSystemSessionSource {
3716
3847
  } catch {
3717
3848
  }
3718
3849
  }
3719
- const stats = this.extractStats(meta.sourcePath);
3720
- return this.buildSessionData(meta, builder, stats);
3850
+ this.collectWireUsage(meta.sourcePath, accumulator);
3851
+ return this.buildSessionData(meta, builder, accumulator.finish());
3721
3852
  }
3722
3853
  getSessionDataFromWire(meta) {
3723
3854
  const wirePath = meta.wireFile ?? join8(meta.sourcePath, "wire.jsonl");
@@ -3725,29 +3856,25 @@ var KimiAgent = class extends FileSystemSessionSource {
3725
3856
  const builder = new TranscriptBuilder();
3726
3857
  const ignoredToolCallIds = /* @__PURE__ */ new Set();
3727
3858
  const openToolArgumentBuffer = /* @__PURE__ */ new Map();
3859
+ const accumulator = new KimiUsageAccumulator(this.defaultModel, true);
3728
3860
  let openToolCallId = null;
3729
3861
  let seq = 0;
3730
3862
  for (const record of readJsonlFile(wirePath)) {
3731
3863
  seq++;
3732
3864
  try {
3865
+ const tokenUsage = accumulator.applyWireRecord(record);
3733
3866
  const message = asRecord(record.message) ?? {};
3734
3867
  const msgType = asString(message.type) ?? "";
3735
3868
  if (isInternalEventType(msgType)) continue;
3736
3869
  const payload = asRecord(message.payload) ?? {};
3737
3870
  const timestampMs = Math.floor(readWireTimestamp(record) * 1e3);
3738
- const usage = asRecord(message["usage"]);
3739
- if (usage) {
3740
- const inputTokens = extractTokenField(usage, "input_tokens");
3741
- const outputTokens = extractTokenField(usage, "output_tokens");
3742
- if (inputTokens || outputTokens) {
3743
- const tokens = { input: inputTokens, output: outputTokens };
3744
- const cost = estimateTokenCost(this.defaultModel, tokens);
3745
- builder.attachUsageToLatestAssistant(tokens, {
3746
- model: this.defaultModel,
3747
- cost: cost ?? void 0,
3748
- costSource: cost === null ? void 0 : "estimated"
3749
- });
3750
- }
3871
+ if (tokenUsage && (tokenUsage.inputTokens || tokenUsage.outputTokens)) {
3872
+ const tokens = { input: tokenUsage.inputTokens, output: tokenUsage.outputTokens };
3873
+ builder.attachUsageToLatestAssistant(tokens, {
3874
+ model: this.defaultModel,
3875
+ cost: tokenUsage.cost ?? void 0,
3876
+ costSource: tokenUsage.cost === null ? void 0 : "estimated"
3877
+ });
3751
3878
  }
3752
3879
  if (msgType === "TurnBegin") {
3753
3880
  const userInput = payload.user_input;
@@ -3852,8 +3979,7 @@ var KimiAgent = class extends FileSystemSessionSource {
3852
3979
  } catch {
3853
3980
  }
3854
3981
  }
3855
- const stats = this.extractStats(meta.sourcePath);
3856
- return this.buildSessionData(meta, builder, stats);
3982
+ return this.buildSessionData(meta, builder, accumulator.finish());
3857
3983
  }
3858
3984
  // --- Helpers ---
3859
3985
  sourceFingerprint(meta) {
@@ -3950,59 +4076,27 @@ var KimiAgent = class extends FileSystemSessionSource {
3950
4076
  if (!outputParts.length || !callId) return false;
3951
4077
  return builder.resolveToolCall(callId, { output: [...outputParts] });
3952
4078
  }
3953
- /** Applies a `_usage` record's running total; other records are ignored. */
3954
- applyUsageTotal(record, stats) {
3955
- if (record.role !== "_usage") return;
3956
- const tokenCount = asNumber(record.token_count);
3957
- if (tokenCount === void 0) {
3958
- reportFieldMismatch("kimi", "usage.token_count");
3959
- return;
3960
- }
3961
- stats.total_tokens = tokenCount;
3962
- }
3963
4079
  extractStats(sessionDir) {
3964
- let totalCost = 0;
3965
- const stats = {
3966
- total_cost: 0,
3967
- total_input_tokens: 0,
3968
- total_output_tokens: 0,
3969
- total_tokens: 0,
3970
- message_count: 0
3971
- };
3972
4080
  const contextPath = join8(sessionDir, "context.jsonl");
3973
4081
  const hasContext = existsSync6(contextPath);
4082
+ const accumulator = new KimiUsageAccumulator(this.defaultModel, !hasContext);
3974
4083
  if (hasContext) {
3975
4084
  try {
3976
- for (const record of readJsonlFile(contextPath)) this.applyUsageTotal(record, stats);
4085
+ for (const record of readJsonlFile(contextPath)) accumulator.applyContextRecord(record);
3977
4086
  } catch {
3978
4087
  }
3979
4088
  }
4089
+ this.collectWireUsage(sessionDir, accumulator);
4090
+ return accumulator.finish();
4091
+ }
4092
+ collectWireUsage(sessionDir, accumulator) {
3980
4093
  const wirePath = join8(sessionDir, "wire.jsonl");
3981
4094
  if (existsSync6(wirePath)) {
3982
4095
  try {
3983
- for (const record of readJsonlFile(wirePath)) {
3984
- const tokenUsage = asRecord(asRecord(record.message)?.usage);
3985
- if (tokenUsage) {
3986
- const inputTokens = extractTokenField(tokenUsage, "input_tokens");
3987
- const outputTokens = extractTokenField(tokenUsage, "output_tokens");
3988
- stats.total_input_tokens += inputTokens;
3989
- stats.total_output_tokens += outputTokens;
3990
- const cost = estimateTokenCost(this.defaultModel, {
3991
- input: inputTokens,
3992
- output: outputTokens
3993
- });
3994
- if (cost !== null) totalCost += cost;
3995
- }
3996
- if (!hasContext) this.applyUsageTotal(record, stats);
3997
- }
4096
+ for (const record of readJsonlFile(wirePath)) accumulator.applyWireRecord(record);
3998
4097
  } catch {
3999
4098
  }
4000
4099
  }
4001
- stats.total_cost = Number(totalCost.toFixed(8));
4002
- if (stats.total_cost > 0) {
4003
- stats.cost_source = "estimated";
4004
- }
4005
- return stats;
4006
4100
  }
4007
4101
  buildSessionData(meta, builder, stats) {
4008
4102
  const transcript = builder.finish(stats);
@@ -4010,7 +4104,7 @@ var KimiAgent = class extends FileSystemSessionSource {
4010
4104
  reference: { agentName: this.name, sessionId: meta.id },
4011
4105
  id: meta.id,
4012
4106
  title: meta.title,
4013
- slug: `kimi/${meta.id}`,
4107
+ slug: this.sessionSlug(meta.id),
4014
4108
  directory: meta.cwd,
4015
4109
  time_created: meta.createdAt,
4016
4110
  time_updated: meta.activityAt,
@@ -4036,28 +4130,15 @@ var KIMI_CODE_TOOL_TITLE_MAP = {
4036
4130
  Shell: "bash"
4037
4131
  };
4038
4132
  var KIMI_CODE_IGNORED_TOOLS = /* @__PURE__ */ new Set(["SetTodoList"]);
4039
- var KIMI_CODE_PARSER_REVISION = "kimi-code-parser-v1";
4133
+ var KIMI_CODE_PARSER_REVISION = "kimi-code-parser-v2";
4040
4134
  function resolveKimiCodeDataRoot() {
4041
4135
  return resolveHomePath("KIMI_CODE_HOME", ".kimi-code");
4042
4136
  }
4043
4137
  function mapToolTitle2(toolName) {
4044
4138
  return KIMI_CODE_TOOL_TITLE_MAP[toolName] ?? toolName;
4045
4139
  }
4046
- function normalizeToolArguments2(raw) {
4047
- if (typeof raw !== "string") return raw;
4048
- try {
4049
- return JSON.parse(raw);
4050
- } catch {
4051
- return raw;
4052
- }
4053
- }
4054
4140
  function parseTimestamp2(raw) {
4055
- if (typeof raw === "number") return Number.isFinite(raw) ? raw : null;
4056
- if (typeof raw !== "string" || raw.trim() === "") return null;
4057
- const numeric = Number(raw);
4058
- if (Number.isFinite(numeric)) return numeric;
4059
- const parsed = Date.parse(raw);
4060
- return Number.isFinite(parsed) ? parsed : null;
4141
+ return parseAgentTimestamp(raw, "kimi-code", { numericStrings: true });
4061
4142
  }
4062
4143
  function timestampFromRecord(record) {
4063
4144
  return parseTimestamp2(record.time) ?? 0;
@@ -4173,7 +4254,7 @@ function toolCallParts(message, timestampMs, ignoredToolCallIds) {
4173
4254
  continue;
4174
4255
  }
4175
4256
  const rawArguments = functionRecord?.arguments ?? callRecord?.arguments;
4176
- parts.push(toolPart(toolName, callId, normalizeToolArguments2(rawArguments), timestampMs));
4257
+ parts.push(toolPart(toolName, callId, normalizeToolArguments(rawArguments), timestampMs));
4177
4258
  }
4178
4259
  return parts;
4179
4260
  }
@@ -4274,7 +4355,7 @@ var KimiCodeAgent = class extends FileSystemSessionSource {
4274
4355
  const sessionPath = asString(record.sessionDir);
4275
4356
  const workDir = asString(record.workDir);
4276
4357
  if (!sessionPath || !workDir) continue;
4277
- this.workDirBySessionPath.set(resolve(sessionPath), workDir);
4358
+ this.workDirBySessionPath.set(resolve2(sessionPath), workDir);
4278
4359
  }
4279
4360
  }
4280
4361
  listSessionDirs() {
@@ -4308,7 +4389,7 @@ var KimiCodeAgent = class extends FileSystemSessionSource {
4308
4389
  wireStat.mtimeMs
4309
4390
  );
4310
4391
  const custom = asRecord(state.custom);
4311
- const workDir = asString(state.workDir) ?? asString(custom?.cwd) ?? this.workDirBySessionPath.get(resolve(sessionDir)) ?? "";
4392
+ const workDir = asString(state.workDir) ?? asString(custom?.cwd) ?? this.workDirBySessionPath.get(resolve2(sessionDir)) ?? "";
4312
4393
  const explicitTitle = asString(state.title) ?? asString(state.customTitle) ?? "";
4313
4394
  return parsedSession({
4314
4395
  id: basename6(sessionDir),
@@ -4416,7 +4497,7 @@ var KimiCodeAgent = class extends FileSystemSessionSource {
4416
4497
  this.sessionMetaMap.set(meta.id, meta);
4417
4498
  return parsedSession({
4418
4499
  id: meta.id,
4419
- slug: `${this.name}/${meta.id}`,
4500
+ slug: this.sessionSlug(meta.id),
4420
4501
  title: meta.title,
4421
4502
  directory: meta.workDir,
4422
4503
  time_created: meta.createdAt,
@@ -4434,7 +4515,7 @@ var KimiCodeAgent = class extends FileSystemSessionSource {
4434
4515
  reference: { agentName: this.name, sessionId: meta.id },
4435
4516
  id: meta.id,
4436
4517
  title: meta.title,
4437
- slug: `${this.name}/${meta.id}`,
4518
+ slug: this.sessionSlug(meta.id),
4438
4519
  directory: meta.workDir,
4439
4520
  time_created: meta.createdAt,
4440
4521
  time_updated: meta.activityAt,
@@ -4865,7 +4946,7 @@ var JsValueReader = class {
4865
4946
  var PROPOSED_PLAN_PATTERN = /<proposed_plan>\s*([\s\S]*?)\s*<\/proposed_plan>/;
4866
4947
  var PLAN_APPROVAL_PREFIX = "PLEASE IMPLEMENT THIS PLAN";
4867
4948
  var SUBAGENT_NOTIFICATION_PATTERN = /<subagent_notification>\s*([\s\S]*?)\s*<\/subagent_notification>/;
4868
- var HEAD_INDEX_VERSION2 = "codex-head-v1";
4949
+ var HEAD_INDEX_VERSION2 = "codex-head-v2";
4869
4950
  var PARSER_VERSION = "codex-parser-v8";
4870
4951
  function resolveCodexDataRoot() {
4871
4952
  return resolveHomePath("CODEX_HOME", ".codex");
@@ -4897,7 +4978,7 @@ function extractSessionId(filename) {
4897
4978
  return stem;
4898
4979
  }
4899
4980
  function parseTimestampMs2(data) {
4900
- return parseAgentTimestampMs(String(data["timestamp"] ?? ""), "codex");
4981
+ return parseAgentTimestamp(data["timestamp"], "codex") ?? 0;
4901
4982
  }
4902
4983
  function extractModelName(raw) {
4903
4984
  return typeof raw === "string" && raw.trim() ? raw.trim() : null;
@@ -4920,9 +5001,10 @@ function extractThreadMeta(firstRecord) {
4920
5001
  const agentNickname = asString(payload["agent_nickname"]) ?? null;
4921
5002
  return { threadSource, parentThreadId, agentNickname };
4922
5003
  }
5004
+ var LEADING_READ_CHUNK_BYTES = 64 * 1024;
4923
5005
  function readLeadingJsonlLines(filePath, limit) {
4924
5006
  const lines = [];
4925
- for (const line of readJsonlFileLines(filePath)) {
5007
+ for (const line of readJsonlFileLines(filePath, LEADING_READ_CHUNK_BYTES)) {
4926
5008
  if (!line.trim()) continue;
4927
5009
  lines.push(line);
4928
5010
  if (lines.length === limit) break;
@@ -4954,16 +5036,6 @@ function resolveToolIdentity(name, namespace) {
4954
5036
  metadata: { name, namespace: namespaceText }
4955
5037
  };
4956
5038
  }
4957
- function normalizeToolArguments3(raw) {
4958
- if (typeof raw === "string") {
4959
- try {
4960
- return JSON.parse(raw);
4961
- } catch {
4962
- return raw;
4963
- }
4964
- }
4965
- return raw;
4966
- }
4967
5039
  function normalizeCustomToolArguments(toolName, input) {
4968
5040
  if (toolName === "apply_patch") {
4969
5041
  return parseApplyPatchInput(input);
@@ -5073,6 +5145,36 @@ function extractPatchContent(lines, startIndex) {
5073
5145
  }
5074
5146
  return { text: contentLines.join("\n"), nextLineIndex: i };
5075
5147
  }
5148
+ var ChildMessageVisibilityIndex = class {
5149
+ visibleSubagentIds = /* @__PURE__ */ new Set();
5150
+ visibleNicknameTexts = /* @__PURE__ */ new Map();
5151
+ constructor(messages) {
5152
+ for (const message of messages) this.add(message);
5153
+ }
5154
+ hasEquivalent(message) {
5155
+ if (message.subagent_id !== void 0 && this.visibleSubagentIds.has(message.subagent_id)) {
5156
+ return true;
5157
+ }
5158
+ const nickname = message.nickname;
5159
+ const text = message.parts.find((part) => part.type === "text")?.text;
5160
+ return nickname !== void 0 && text !== void 0 && this.visibleNicknameTexts.get(nickname)?.has(text) === true;
5161
+ }
5162
+ add(message) {
5163
+ if (message.subagent_id !== void 0) {
5164
+ this.visibleSubagentIds.add(message.subagent_id);
5165
+ return;
5166
+ }
5167
+ if (message.nickname === void 0) return;
5168
+ let texts = this.visibleNicknameTexts.get(message.nickname);
5169
+ if (!texts) {
5170
+ texts = /* @__PURE__ */ new Set();
5171
+ this.visibleNicknameTexts.set(message.nickname, texts);
5172
+ }
5173
+ for (const part of message.parts) {
5174
+ if (part.type === "text") texts.add(part.text);
5175
+ }
5176
+ }
5177
+ };
5076
5178
  function compareSourceActivityDesc(left, right) {
5077
5179
  const leftTimestamp = sourceTimestamp(left.file, left.stat.mtimeMs);
5078
5180
  const rightTimestamp = sourceTimestamp(right.file, right.stat.mtimeMs);
@@ -5093,7 +5195,11 @@ var CodexAgent = class extends SingleFileSessionSource {
5093
5195
  sessionIndexMtime;
5094
5196
  sessionIndexPath;
5095
5197
  subagentIndex = null;
5198
+ // Thread meta lives in a rollout's immutable first line; fingerprinting by
5199
+ // (mtime, size) lets index rebuilds stat files instead of re-reading them.
5200
+ threadMetaByPath = /* @__PURE__ */ new Map();
5096
5201
  subagentStatsByParent = /* @__PURE__ */ new Map();
5202
+ childFinalMessagesByParent = /* @__PURE__ */ new Map();
5097
5203
  // ---- BaseAgent implementation ----
5098
5204
  findBasePath() {
5099
5205
  return firstExisting(join10(resolveCodexDataRoot(), "sessions"));
@@ -5126,6 +5232,7 @@ var CodexAgent = class extends SingleFileSessionSource {
5126
5232
  super.setSessionMetaMap(meta);
5127
5233
  this.subagentIndex = null;
5128
5234
  this.subagentStatsByParent.clear();
5235
+ this.childFinalMessagesByParent.clear();
5129
5236
  }
5130
5237
  /**
5131
5238
  * A changed subagent file leaves its parent's aggregated token stats stale,
@@ -5143,14 +5250,20 @@ var CodexAgent = class extends SingleFileSessionSource {
5143
5250
  expanded.add(parentId);
5144
5251
  this.subagentIndex = null;
5145
5252
  this.subagentStatsByParent.delete(parentId);
5253
+ this.childFinalMessagesByParent.delete(parentId);
5146
5254
  }
5147
5255
  return [...expanded];
5148
5256
  }
5149
5257
  readThreadMeta(filePath) {
5150
5258
  try {
5259
+ const { mtimeMs, size } = statSync7(filePath);
5260
+ const fingerprint = `${mtimeMs}:${size}`;
5261
+ const cached = this.threadMetaByPath.get(filePath);
5262
+ if (cached && cached.fingerprint === fingerprint) return cached.meta;
5151
5263
  const firstLine = readLeadingJsonlLines(filePath, 1)[0];
5152
- if (!firstLine) return null;
5153
- return extractThreadMeta(JSON.parse(firstLine));
5264
+ const meta = firstLine ? extractThreadMeta(JSON.parse(firstLine)) : null;
5265
+ this.threadMetaByPath.set(filePath, { fingerprint, meta });
5266
+ return meta;
5154
5267
  } catch (error) {
5155
5268
  getCoreDiagnostics()?.warn("codex.thread_meta_read_failed", {
5156
5269
  filePath,
@@ -5317,19 +5430,13 @@ var CodexAgent = class extends SingleFileSessionSource {
5317
5430
  });
5318
5431
  this.applyChildStats(result.stats, meta.id);
5319
5432
  const childMessages = this.collectChildMessages(meta.id);
5320
- for (const message of childMessages) {
5321
- const messageText = message.parts.find((part) => part.type === "text")?.text;
5322
- const alreadyVisible = result.messages.some(
5323
- (existing) => message.subagent_id !== void 0 && existing.subagent_id === message.subagent_id || existing.subagent_id === void 0 && message.nickname !== void 0 && messageText !== void 0 && existing.nickname === message.nickname && existing.parts.some((part) => part.type === "text" && part.text === messageText)
5324
- );
5325
- if (!alreadyVisible) result.messages.push(message);
5326
- }
5433
+ this.mergeChildMessages(result.messages, childMessages);
5327
5434
  result.stats.message_count = result.messages.length;
5328
5435
  return {
5329
5436
  reference: { agentName: this.name, sessionId: meta.id },
5330
5437
  id: meta.id,
5331
5438
  title: meta.title,
5332
- slug: `codex/${meta.id}`,
5439
+ slug: this.sessionSlug(meta.id),
5333
5440
  directory: meta.directory,
5334
5441
  parent_reference: meta.parentThreadId == null ? void 0 : { agentName: this.name, sessionId: meta.parentThreadId },
5335
5442
  time_created: meta.createdAt,
@@ -5347,7 +5454,8 @@ var CodexAgent = class extends SingleFileSessionSource {
5347
5454
  if (this.subagentIndex) return this.subagentIndex;
5348
5455
  this.basePath ??= this.findBasePath();
5349
5456
  const index = { childFilesByParent: /* @__PURE__ */ new Map(), subagentFiles: /* @__PURE__ */ new Set() };
5350
- for (const file of this.listRolloutFilePaths()) {
5457
+ const paths = this.listRolloutFilePaths();
5458
+ for (const file of paths) {
5351
5459
  const threadMeta = this.readThreadMeta(file);
5352
5460
  if (threadMeta?.threadSource !== "subagent") continue;
5353
5461
  index.subagentFiles.add(file);
@@ -5356,6 +5464,12 @@ var CodexAgent = class extends SingleFileSessionSource {
5356
5464
  if (files) files.push(file);
5357
5465
  else index.childFilesByParent.set(threadMeta.parentThreadId, [file]);
5358
5466
  }
5467
+ if (this.threadMetaByPath.size > paths.length) {
5468
+ const active = new Set(paths);
5469
+ for (const path2 of this.threadMetaByPath.keys()) {
5470
+ if (!active.has(path2)) this.threadMetaByPath.delete(path2);
5471
+ }
5472
+ }
5359
5473
  this.subagentIndex = index;
5360
5474
  return index;
5361
5475
  }
@@ -5381,17 +5495,56 @@ var CodexAgent = class extends SingleFileSessionSource {
5381
5495
  collectChildFiles(parentSessionId) {
5382
5496
  return this.ensureSubagentIndex().childFilesByParent.get(parentSessionId) ?? [];
5383
5497
  }
5498
+ mergeChildMessages(visibleMessages, childMessages) {
5499
+ const visible = new ChildMessageVisibilityIndex(visibleMessages);
5500
+ for (const message of childMessages) {
5501
+ if (visible.hasEquivalent(message)) continue;
5502
+ visibleMessages.push(message);
5503
+ visible.add(message);
5504
+ }
5505
+ }
5384
5506
  collectChildMessages(parentSessionId) {
5385
- return this.collectChildFiles(parentSessionId).flatMap((file) => {
5386
- const message = this.parseChildFinalMessage(file);
5507
+ const childFiles = this.collectChildFiles(parentSessionId);
5508
+ this.reconcileChildFinalMessageCache(parentSessionId, childFiles);
5509
+ return childFiles.flatMap((file) => {
5510
+ const message = this.getChildFinalMessage(parentSessionId, file);
5387
5511
  return message ? [message] : [];
5388
5512
  }).sort((left, right) => left.time_created - right.time_created);
5389
5513
  }
5390
- parseChildFinalMessage(filePath) {
5514
+ getChildFinalMessage(parentSessionId, filePath) {
5515
+ const sourceFingerprint = this.childFinalMessageFingerprint(filePath);
5516
+ let cache = this.childFinalMessagesByParent.get(parentSessionId);
5517
+ if (!cache) {
5518
+ cache = /* @__PURE__ */ new Map();
5519
+ this.childFinalMessagesByParent.set(parentSessionId, cache);
5520
+ }
5521
+ const cached = cache.get(filePath);
5522
+ if (cached?.sourceFingerprint === sourceFingerprint && cached.parserVersion === PARSER_VERSION) {
5523
+ return cached.message;
5524
+ }
5525
+ const message = this.readChildFinalMessage(filePath);
5526
+ cache.set(filePath, { sourceFingerprint, parserVersion: PARSER_VERSION, message });
5527
+ return message;
5528
+ }
5529
+ reconcileChildFinalMessageCache(parentSessionId, childFiles) {
5530
+ const cache = this.childFinalMessagesByParent.get(parentSessionId);
5531
+ if (!cache) return;
5532
+ const activeFiles = new Set(childFiles);
5533
+ for (const filePath of cache.keys()) {
5534
+ if (!activeFiles.has(filePath)) cache.delete(filePath);
5535
+ }
5536
+ if (cache.size === 0) this.childFinalMessagesByParent.delete(parentSessionId);
5537
+ }
5538
+ childFinalMessageFingerprint(filePath) {
5539
+ const { mtimeMs, size } = statSync7(filePath);
5540
+ return JSON.stringify([mtimeMs, size]);
5541
+ }
5542
+ readChildFinalMessage(filePath) {
5391
5543
  const sessionId = extractSessionId(filePath);
5392
5544
  const threadMeta = this.readThreadMeta(filePath);
5393
5545
  let latestOutput = null;
5394
5546
  let finalOutput = null;
5547
+ let fallbackMtimeMs = null;
5395
5548
  for (const record of readJsonlFile(filePath)) {
5396
5549
  try {
5397
5550
  const recordType = String(record["type"] ?? "");
@@ -5404,7 +5557,7 @@ var CodexAgent = class extends SingleFileSessionSource {
5404
5557
  const candidate = {
5405
5558
  id: asString(payload["id"]) ?? `codex-subagent-${sessionId}`,
5406
5559
  text,
5407
- timestampMs: parseTimestampMs2(record) || parseTimestampMs2(payload) || statSync7(filePath).mtimeMs,
5560
+ timestampMs: parseTimestampMs2(record) || parseTimestampMs2(payload) || (fallbackMtimeMs ??= statSync7(filePath).mtimeMs),
5408
5561
  isFinal: String(record["phase"] ?? "") === "final_answer" || String(payload["phase"] ?? "") === "final_answer"
5409
5562
  };
5410
5563
  latestOutput = candidate;
@@ -5533,7 +5686,11 @@ var CodexAgent = class extends SingleFileSessionSource {
5533
5686
  }
5534
5687
  this.sessionIndexCache = cache;
5535
5688
  this.sessionIndexMtime = mtime;
5536
- } catch {
5689
+ } catch (error) {
5690
+ getCoreDiagnostics()?.warn("codex.session_index_read_failed", {
5691
+ path: indexPath,
5692
+ message: error instanceof Error ? error.message : String(error)
5693
+ });
5537
5694
  this.sessionIndexCache.clear();
5538
5695
  this.sessionIndexMtime = void 0;
5539
5696
  }
@@ -5675,7 +5832,7 @@ var CodexAgent = class extends SingleFileSessionSource {
5675
5832
  const title = resolveSessionTitle(indexTitle, messageTitle, basenameTitle(directory || null));
5676
5833
  return parsedSession({
5677
5834
  id: sessionId,
5678
- slug: `codex/${sessionId}`,
5835
+ slug: this.sessionSlug(sessionId),
5679
5836
  title,
5680
5837
  directory,
5681
5838
  parent_reference: parentThreadId == null ? void 0 : { agentName: this.name, sessionId: parentThreadId },
@@ -5719,7 +5876,7 @@ var CodexAgent = class extends SingleFileSessionSource {
5719
5876
  const title = resolveSessionTitle(indexTitle, messageTitle, directoryTitle);
5720
5877
  return parsedSession({
5721
5878
  id: sessionId,
5722
- slug: `codex/${sessionId}`,
5879
+ slug: this.sessionSlug(sessionId),
5723
5880
  title,
5724
5881
  directory,
5725
5882
  parent_reference: parentThreadId == null ? void 0 : { agentName: this.name, sessionId: parentThreadId },
@@ -5934,7 +6091,7 @@ var CodexAgent = class extends SingleFileSessionSource {
5934
6091
  const name = String(payload["name"] ?? "").trim();
5935
6092
  if (!name) return;
5936
6093
  const toolIdentity = resolveToolIdentity(name, payload["namespace"]);
5937
- const arguments_ = normalizeToolArguments3(payload["arguments"]);
6094
+ const arguments_ = normalizeToolArguments(payload["arguments"]);
5938
6095
  const toolPart2 = {
5939
6096
  type: "tool",
5940
6097
  tool: toolIdentity.tool,
@@ -6407,7 +6564,7 @@ var CursorAgent = class extends DatabaseSessionSource {
6407
6564
  return getParsedSession(
6408
6565
  hasFastMessages && fastMessageCount === 0 && !hasSubagents ? filteredSession("no visible messages") : parsedSession({
6409
6566
  id: composerId,
6410
- slug: `cursor/${composerId}`,
6567
+ slug: this.sessionSlug(composerId),
6411
6568
  title: fastTitle,
6412
6569
  directory,
6413
6570
  time_created: createdAt,
@@ -6526,7 +6683,7 @@ var CursorAgent = class extends DatabaseSessionSource {
6526
6683
  reference: { agentName: this.name, sessionId: composerId },
6527
6684
  id: composerId,
6528
6685
  title,
6529
- slug: `cursor/${composerId}`,
6686
+ slug: this.sessionSlug(composerId),
6530
6687
  directory,
6531
6688
  time_created: createdAt,
6532
6689
  time_updated: updatedAt || void 0,
@@ -6604,7 +6761,7 @@ var CursorAgent = class extends DatabaseSessionSource {
6604
6761
  if (directory) this.directoryCache.set(composerId, directory);
6605
6762
  return {
6606
6763
  id: composerId,
6607
- slug: `cursor/${composerId}`,
6764
+ slug: this.sessionSlug(composerId),
6608
6765
  title,
6609
6766
  directory,
6610
6767
  time_created: createdAt,
@@ -6665,33 +6822,18 @@ var CursorAgent = class extends DatabaseSessionSource {
6665
6822
  const messageTitle = firstUserMessageTitle(messages) ?? composer.text;
6666
6823
  return resolveSessionTitle(explicit, messageTitle, null);
6667
6824
  }
6668
- /** Count messages from bubbles */
6669
- countMessagesFromBubbles(db, composerId) {
6670
- try {
6671
- const rows = db.prepare("SELECT value FROM cursorDiskKV WHERE key LIKE ?").all(`bubbleId:${composerId}:%`);
6672
- let count = 0;
6673
- for (const row of rows) {
6674
- try {
6675
- const bubble = parseBubbleRow(row.value);
6676
- if (bubble?.type === 1 || bubble?.type === 2) {
6677
- count++;
6678
- }
6679
- } catch {
6680
- }
6681
- }
6682
- return count;
6683
- } catch {
6684
- return 0;
6685
- }
6686
- }
6687
- /** Load one composer's bubbles, for the detail path that only needs a single session. */
6825
+ /**
6826
+ * Load one composer's bubbles, for the detail path that only needs a
6827
+ * single session. A failing bubble query throws instead of returning [] —
6828
+ * a broken database must not read as an empty session.
6829
+ */
6688
6830
  loadMessagesFromBubbles(db, composerId, initialModelName) {
6689
- try {
6690
- const rows = db.prepare("SELECT rowid AS row_id, key, value FROM cursorDiskKV WHERE key LIKE ?").all(`bubbleId:${composerId}:%`);
6691
- return this.messagesFromBubbles(groupBubbleRows(rows), initialModelName);
6692
- } catch {
6693
- return [];
6694
- }
6831
+ const rows = this.scanStep(
6832
+ "reading composer bubbles",
6833
+ this.getDatabasePath() ?? "",
6834
+ () => db.prepare("SELECT rowid AS row_id, key, value FROM cursorDiskKV WHERE key LIKE ?").all(`bubbleId:${composerId}:%`)
6835
+ );
6836
+ return this.messagesFromBubbles(groupBubbleRows(rows), initialModelName);
6695
6837
  }
6696
6838
  /** Build messages from bubbles already parsed once, in insertion order. */
6697
6839
  messagesFromBubbles(bubbles, initialModelName) {
@@ -6859,18 +7001,11 @@ var CursorAgent = class extends DatabaseSessionSource {
6859
7001
  }
6860
7002
  }
6861
7003
  };
6862
- var HEAD_INDEX_VERSION3 = "pi-head-v1";
7004
+ var HEAD_INDEX_VERSION3 = "pi-head-v2";
6863
7005
  var PARSER_VERSION2 = "pi-parser-v3";
6864
7006
  function resolvePiDataRoot() {
6865
7007
  return resolveHomePath("PI_HOME", ".pi");
6866
7008
  }
6867
- function parseTimestampMs3(value) {
6868
- if (typeof value === "number") return Number.isFinite(value) ? value : 0;
6869
- const text = String(value ?? "").trim();
6870
- if (!text) return 0;
6871
- const ts = Date.parse(text);
6872
- return Number.isNaN(ts) ? 0 : ts;
6873
- }
6874
7009
  function narrowPiField(field, value, narrow) {
6875
7010
  return narrowField("pi", field, value, narrow);
6876
7011
  }
@@ -6880,7 +7015,7 @@ function narrowTimestampMs(field, value) {
6880
7015
  value,
6881
7016
  (v) => typeof v === "number" || typeof v === "string" ? v : void 0
6882
7017
  );
6883
- return shaped === void 0 ? 0 : parseTimestampMs3(shaped);
7018
+ return shaped === void 0 ? 0 : parseAgentTimestamp(shaped, "pi", { numericStrings: true }) ?? 0;
6884
7019
  }
6885
7020
  function extractSessionIdFromFilename(filePath) {
6886
7021
  const stem = basename8(filePath, ".jsonl");
@@ -6974,7 +7109,7 @@ var PiAgent = class extends SingleFileSessionSource {
6974
7109
  reference: { agentName: this.name, sessionId: meta.id },
6975
7110
  id: meta.id,
6976
7111
  title: meta.title,
6977
- slug: `pi/${meta.id}`,
7112
+ slug: this.sessionSlug(meta.id),
6978
7113
  directory: meta.directory,
6979
7114
  time_created: meta.createdAt,
6980
7115
  time_updated: meta.updatedAt,
@@ -7024,7 +7159,7 @@ var PiAgent = class extends SingleFileSessionSource {
7024
7159
  const modelUsage = Object.keys(state.modelUsage).length > 0 ? state.modelUsage : void 0;
7025
7160
  return parsedSession({
7026
7161
  id: parsed.sessionId,
7027
- slug: `pi/${parsed.sessionId}`,
7162
+ slug: this.sessionSlug(parsed.sessionId),
7028
7163
  title: parsed.title,
7029
7164
  directory: parsed.directory,
7030
7165
  time_created: parsed.createdAt,
@@ -7144,7 +7279,7 @@ var PiAgent = class extends SingleFileSessionSource {
7144
7279
  const model = typeof message["model"] === "string" ? message["model"].trim() : null;
7145
7280
  const estimatedCost = usage.cost === null ? estimateTokenCost(model, usage.tokens) : null;
7146
7281
  const cost = usage.cost ?? estimatedCost ?? 0;
7147
- const costSource = cost > 0 ? usage.cost === null ? "estimated" : "recorded" : void 0;
7282
+ const costSource2 = cost > 0 ? usage.cost === null ? "estimated" : "recorded" : void 0;
7148
7283
  return {
7149
7284
  message: {
7150
7285
  id,
@@ -7156,7 +7291,7 @@ var PiAgent = class extends SingleFileSessionSource {
7156
7291
  model,
7157
7292
  tokens: usage.tokens,
7158
7293
  cost: cost || void 0,
7159
- costSource
7294
+ costSource: costSource2
7160
7295
  },
7161
7296
  totalTokens: usage.totalTokens,
7162
7297
  model
@@ -8008,7 +8143,7 @@ var GrokAgent = class extends FileSystemSessionSource {
8008
8143
  const fingerprint = this.sourceFingerprint(source);
8009
8144
  const head = {
8010
8145
  id: summary.id,
8011
- slug: `grok/${summary.id}`,
8146
+ slug: this.sessionSlug(summary.id),
8012
8147
  title,
8013
8148
  directory: summary.cwd,
8014
8149
  parent_reference: summary.parentSessionId ? { agentName: this.name, sessionId: summary.parentSessionId } : void 0,
@@ -8042,7 +8177,7 @@ var GrokAgent = class extends FileSystemSessionSource {
8042
8177
  reference: { agentName: this.name, sessionId: meta.id },
8043
8178
  id: meta.id,
8044
8179
  title: meta.title,
8045
- slug: `grok/${meta.id}`,
8180
+ slug: this.sessionSlug(meta.id),
8046
8181
  directory: meta.directory,
8047
8182
  parent_reference: meta.parentSessionId ? { agentName: this.name, sessionId: meta.parentSessionId } : void 0,
8048
8183
  version: void 0,
@@ -8122,15 +8257,8 @@ var DshSessionLogError = class extends Error {
8122
8257
  this.name = "DshSessionLogError";
8123
8258
  }
8124
8259
  };
8125
- function expandHomePath(path2) {
8126
- if (path2 === "~") return homedir5();
8127
- if (path2.startsWith("~/") || path2.startsWith("~\\")) return join15(homedir5(), path2.slice(2));
8128
- return path2;
8129
- }
8130
8260
  function resolveDshDataRoot() {
8131
- const fromEnv = process.env["DSH_HOME"];
8132
- const selected = fromEnv !== void 0 && fromEnv.trim().length > 0 ? fromEnv : join15(homedir5(), ".dsh");
8133
- return resolve2(expandHomePath(selected));
8261
+ return resolveHomePath("DSH_HOME", ".dsh");
8134
8262
  }
8135
8263
  function dshSessionsRoot(dataRoot) {
8136
8264
  return join15(dataRoot, "sessions");
@@ -9115,7 +9243,7 @@ var DshAgent = class extends FileSystemSessionSource {
9115
9243
  const stats = this.statArtifact(artifact.sourcePath);
9116
9244
  if (!stats) continue;
9117
9245
  if (!matchesScanWindow(Number(stats.mtimeMs), options)) continue;
9118
- const header = this.enumerationStep(
9246
+ const header = this.scanStep(
9119
9247
  "reading session headers",
9120
9248
  artifact.sourcePath,
9121
9249
  () => readDshSessionHeader(artifact.sourcePath, artifact.encoding)
@@ -9157,7 +9285,7 @@ var DshAgent = class extends FileSystemSessionSource {
9157
9285
  }
9158
9286
  const head = {
9159
9287
  id: header.id,
9160
- slug: `${this.name}/${header.id}`,
9288
+ slug: this.sessionSlug(header.id),
9161
9289
  title: this.resolveTitle(header, projection),
9162
9290
  directory: header.cwd ?? "",
9163
9291
  ...header.parentSession ? { parent_reference: { agentName: this.name, sessionId: header.parentSession } } : {},
@@ -9193,7 +9321,7 @@ var DshAgent = class extends FileSystemSessionSource {
9193
9321
  reference: { agentName: this.name, sessionId: header.id },
9194
9322
  id: header.id,
9195
9323
  title: this.resolveTitle(header, projection),
9196
- slug: `${this.name}/${header.id}`,
9324
+ slug: this.sessionSlug(header.id),
9197
9325
  directory: header.cwd ?? "",
9198
9326
  ...header.parentSession ? { parent_reference: { agentName: this.name, sessionId: header.parentSession } } : {},
9199
9327
  version: "0",
@@ -9301,13 +9429,6 @@ var DshAgent = class extends FileSystemSessionSource {
9301
9429
  );
9302
9430
  }
9303
9431
  }
9304
- enumerationStep(stage, sourcePath, read) {
9305
- try {
9306
- return read();
9307
- } catch (error) {
9308
- throw new SessionScanError(this.name, stage, { cause: error, sourcePath });
9309
- }
9310
- }
9311
9432
  statArtifact(sourcePath) {
9312
9433
  try {
9313
9434
  return statSync10(sourcePath, { bigint: true });
@@ -9405,39 +9526,39 @@ registerAgent({
9405
9526
  toolStrategy: "custom",
9406
9527
  create: () => new CursorAgent()
9407
9528
  });
9408
- var CACHE_SCHEMA_VERSION = 24;
9529
+ var CACHE_SCHEMA_VERSION = 29;
9409
9530
 
9410
- // ../core/dist/chunk-4L5UAZIZ.mjs
9531
+ // ../core/dist/chunk-BORKFBHP.mjs
9411
9532
  function mergeSessionsUpdatedEvents(previous, next) {
9412
9533
  const changedSessionHeads = /* @__PURE__ */ new Map();
9413
9534
  const projectionRelatedSessionHeads = /* @__PURE__ */ new Map();
9414
9535
  const projectionSessionOrder = /* @__PURE__ */ new Map();
9415
9536
  const newSessionRefs = /* @__PURE__ */ new Map();
9416
9537
  const removedSessionRefs = /* @__PURE__ */ new Map();
9417
- const sessionKey2 = (agentName, sessionId) => `${agentName}\0${sessionId}`;
9538
+ const sessionKey22 = (agentName, sessionId) => `${agentName}\0${sessionId}`;
9418
9539
  const addChanged = (item) => {
9419
- const key = sessionKey2(item.reference.agentName, item.reference.sessionId);
9540
+ const key = sessionKey22(item.reference.agentName, item.reference.sessionId);
9420
9541
  removedSessionRefs.delete(key);
9421
9542
  projectionRelatedSessionHeads.delete(key);
9422
9543
  changedSessionHeads.set(key, item);
9423
9544
  };
9424
9545
  const addProjectionRelated = (item) => {
9425
- const key = sessionKey2(item.reference.agentName, item.reference.sessionId);
9546
+ const key = sessionKey22(item.reference.agentName, item.reference.sessionId);
9426
9547
  if (changedSessionHeads.has(key) || removedSessionRefs.has(key)) return;
9427
9548
  projectionRelatedSessionHeads.set(key, item);
9428
9549
  };
9429
9550
  const addNew = (item) => {
9430
- const key = sessionKey2(item.agentName, item.sessionId);
9551
+ const key = sessionKey22(item.agentName, item.sessionId);
9431
9552
  removedSessionRefs.delete(key);
9432
9553
  newSessionRefs.set(key, item);
9433
9554
  };
9434
9555
  const addProjectionOrder = (item) => {
9435
- const key = sessionKey2(item.agentName, item.sessionId);
9556
+ const key = sessionKey22(item.agentName, item.sessionId);
9436
9557
  projectionSessionOrder.delete(key);
9437
9558
  projectionSessionOrder.set(key, item);
9438
9559
  };
9439
9560
  const addRemoved = (item) => {
9440
- const key = sessionKey2(item.agentName, item.sessionId);
9561
+ const key = sessionKey22(item.agentName, item.sessionId);
9441
9562
  changedSessionHeads.delete(key);
9442
9563
  projectionRelatedSessionHeads.delete(key);
9443
9564
  projectionSessionOrder.delete(key);
@@ -9512,37 +9633,6 @@ function matchesProjectIdentity(identity, expected) {
9512
9633
  function getProjectAgentKey(projectIdentityKey, agentName) {
9513
9634
  return `${projectIdentityKey}\0${agentName.toLowerCase()}`;
9514
9635
  }
9515
- var UNKNOWN_AGENT_NAME = "unknown";
9516
- function normalizeSessionReference(reference) {
9517
- return {
9518
- agentName: reference.agentName.trim().toLowerCase(),
9519
- sessionId: reference.sessionId
9520
- };
9521
- }
9522
- function parseSessionReference(value) {
9523
- const separatorIndex = value.indexOf("/");
9524
- if (separatorIndex <= 0 || separatorIndex === value.length - 1) return null;
9525
- const agentName = value.slice(0, separatorIndex).trim().toLowerCase();
9526
- if (!agentName) return null;
9527
- return {
9528
- agentName,
9529
- sessionId: value.slice(separatorIndex + 1)
9530
- };
9531
- }
9532
- function formatSessionReference(reference) {
9533
- const normalized = normalizeSessionReference(reference);
9534
- return `${normalized.agentName}/${normalized.sessionId}`;
9535
- }
9536
- function getSessionAgentKey(session) {
9537
- return parseSessionReference(session.slug)?.agentName ?? UNKNOWN_AGENT_NAME;
9538
- }
9539
- function agentRoutePath(agentName) {
9540
- return `/${encodeURIComponent(agentName.trim().toLowerCase())}`;
9541
- }
9542
- function sessionRoutePath(reference) {
9543
- const normalized = normalizeSessionReference(reference);
9544
- return `${agentRoutePath(normalized.agentName)}/${encodeURIComponent(normalized.sessionId)}`;
9545
- }
9546
9636
  function compareSessionActivityDesc(a, b) {
9547
9637
  return (b.time_updated ?? b.time_created) - (a.time_updated ?? a.time_created);
9548
9638
  }
@@ -9628,12 +9718,6 @@ function hasActivityInWindow(session, from, to) {
9628
9718
  const activity = activityTime(session);
9629
9719
  return (from == null || activity >= from) && (to == null || activity <= to);
9630
9720
  }
9631
- function isChildSession(session) {
9632
- return session.parent_reference != null;
9633
- }
9634
- function getRootSessions(sessions) {
9635
- return sessions.filter((session) => !session.parent_reference);
9636
- }
9637
9721
  function ownStats(session) {
9638
9722
  const stats = session.stats;
9639
9723
  const cost = stats.total_cost ?? 0;
@@ -9833,115 +9917,6 @@ function createSessionProjectionContext(previousSessions, nextSessions, changedS
9833
9917
  );
9834
9918
  return { relatedSessionHeads, sessionOrder };
9835
9919
  }
9836
- var SAMPLE_SESSION_HEAD = {
9837
- id: "session-1",
9838
- slug: "claudecode/session-1",
9839
- title: "Fix flaky search index test",
9840
- directory: "/Users/dev/project",
9841
- project_identity: {
9842
- kind: "git_remote",
9843
- key: "github.com/example/project",
9844
- displayName: "example/project"
9845
- },
9846
- time_created: 17e11,
9847
- time_updated: 17000036e5,
9848
- stats: {
9849
- message_count: 12,
9850
- total_input_tokens: 4200,
9851
- total_output_tokens: 1800,
9852
- total_cost: 0.042,
9853
- cost_source: "recorded",
9854
- total_tokens: 6e3,
9855
- total_cache_read_tokens: 3e3,
9856
- total_cache_create_tokens: 500
9857
- },
9858
- model_usage: { "claude-5-sonnet": 6e3 },
9859
- smart_tags: ["bugfix"],
9860
- smart_tags_source_updated_at: 17000036e5
9861
- };
9862
- var SAMPLE_SESSIONS_UPDATED_EVENT = {
9863
- type: "sessions-updated",
9864
- changedAgents: ["claudecode"],
9865
- newSessions: 1,
9866
- newSessionRefs: [{ agentName: "claudecode", sessionId: SAMPLE_SESSION_HEAD.id }],
9867
- updatedSessions: 0,
9868
- removedSessions: 0,
9869
- totalSessions: 43,
9870
- timestamp: 170000002e4,
9871
- changedSessionHeads: [
9872
- {
9873
- reference: { agentName: "claudecode", sessionId: SAMPLE_SESSION_HEAD.id },
9874
- session: SAMPLE_SESSION_HEAD
9875
- }
9876
- ],
9877
- projectionRelatedSessionHeads: [],
9878
- projectionSessionOrder: [{ agentName: "claudecode", sessionId: SAMPLE_SESSION_HEAD.id }],
9879
- removedSessionRefs: []
9880
- };
9881
- var SAMPLE_DASHBOARD_DATA = {
9882
- totals: {
9883
- sessions: 1,
9884
- messages: 12,
9885
- tokens: 6e3,
9886
- cost: 0.042,
9887
- costRecorded: 0.042,
9888
- costEstimated: 0,
9889
- cacheReadTokens: 3e3,
9890
- cost_source: "recorded",
9891
- latestActivity: 17000036e5,
9892
- latestActivityProject: "example/project",
9893
- latestActivityAgent: "claudecode"
9894
- },
9895
- scopeCounts: { projects: 1, agents: 1 },
9896
- perAgent: [
9897
- {
9898
- name: "claudecode",
9899
- displayName: "Claude Code",
9900
- icon: "claude",
9901
- sessions: 1,
9902
- messages: 12,
9903
- tokens: 6e3,
9904
- cost: 0.042
9905
- }
9906
- ],
9907
- dailyActivity: [
9908
- {
9909
- date: "2023-11-14",
9910
- sessions: 1,
9911
- messages: 12,
9912
- cost: 0.042,
9913
- input: 700,
9914
- output: 1800,
9915
- cache_read: 3e3,
9916
- cache_create: 500
9917
- }
9918
- ],
9919
- modelDistribution: [{ model: "claude-5-sonnet", tokens: 6e3, sessions: 1 }],
9920
- perProject: [
9921
- {
9922
- identityKind: "git_remote",
9923
- identityKey: "github.com/example/project",
9924
- displayName: "example/project",
9925
- sessions: 1,
9926
- messages: 12,
9927
- tokens: 6e3,
9928
- cost: 0.042,
9929
- cost_source: "recorded",
9930
- agents: ["claudecode"],
9931
- sparkline: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.042]
9932
- }
9933
- ],
9934
- projectRollup: { projects: 0, sessions: 0, tokens: 0, cost: 0 },
9935
- recentSessions: [
9936
- {
9937
- reference: { agentName: "claudecode", sessionId: SAMPLE_SESSION_HEAD.id },
9938
- session: SAMPLE_SESSION_HEAD
9939
- }
9940
- ],
9941
- recentFileActivities: [],
9942
- modelCost: null,
9943
- window: { from: 16999e8, to: 17000036e5, days: 1 }
9944
- };
9945
9920
 
9946
9921
  // ../core/dist/index.mjs
9947
9922
  import { availableParallelism } from "os";
@@ -9951,7 +9926,6 @@ import { spawnSync } from "child_process";
9951
9926
  import * as os from "os";
9952
9927
  import * as path from "path";
9953
9928
  import { createHash as createHash4 } from "crypto";
9954
- import { resolve as resolve3, sep } from "path";
9955
9929
  import { existsSync as existsSync32, rmSync as rmSync2, unlinkSync } from "fs";
9956
9930
  import { existsSync as existsSync22 } from "fs";
9957
9931
  import { join as join18 } from "path";
@@ -10017,6 +9991,7 @@ function normalizeGitRemote(url) {
10017
9991
  return value.toLowerCase();
10018
9992
  }
10019
9993
  var IDENTITY_CACHE_TTL_MS = 10 * 60 * 1e3;
9994
+ var IDENTITY_CACHE_MAX_ENTRIES = 512;
10020
9995
  var identityCache = /* @__PURE__ */ new Map();
10021
9996
  function computeIdentity(cwd, fs) {
10022
9997
  return computeIdentityProjection(cwd, fs).identity;
@@ -10025,15 +10000,25 @@ function normalizeProjectDirectory(cwd) {
10025
10000
  if (!cwd) return "";
10026
10001
  return getPathOps(cwd).resolve(cwd);
10027
10002
  }
10028
- function computeIdentityProjection(cwd, fs, resolverRevision = PROJECT_IDENTITY_RESOLVER_REVISION) {
10003
+ function computeIdentityProjection(cwd, fs = realFs, resolverRevision = PROJECT_IDENTITY_RESOLVER_REVISION) {
10029
10004
  if (fs !== realFs) return resolveIdentityProjection(cwd, fs, resolverRevision);
10030
10005
  const key = normalizeProjectDirectory(cwd);
10031
10006
  const cached = identityCache.get(key);
10032
- if (cached && cached.projection.resolverRevision === resolverRevision && Date.now() - cached.resolvedAt < IDENTITY_CACHE_TTL_MS) {
10033
- return cached.projection;
10007
+ if (cached) {
10008
+ if (cached.projection.resolverRevision === resolverRevision && Date.now() - cached.resolvedAt < IDENTITY_CACHE_TTL_MS) {
10009
+ identityCache.delete(key);
10010
+ identityCache.set(key, cached);
10011
+ return cached.projection;
10012
+ }
10013
+ identityCache.delete(key);
10034
10014
  }
10035
10015
  const projection = resolveIdentityProjection(cwd, fs, resolverRevision);
10036
10016
  identityCache.set(key, { projection, resolvedAt: Date.now() });
10017
+ while (identityCache.size > IDENTITY_CACHE_MAX_ENTRIES) {
10018
+ const oldestKey = identityCache.keys().next().value;
10019
+ if (oldestKey == null) break;
10020
+ identityCache.delete(oldestKey);
10021
+ }
10037
10022
  return projection;
10038
10023
  }
10039
10024
  function resolveIdentityProjection(cwd, fs, resolverRevision) {
@@ -10224,9 +10209,12 @@ function buildProjectGroups(sessions) {
10224
10209
  }
10225
10210
  function createProjectScopeMatcher(queryPath, fs = realFs) {
10226
10211
  const identity = computeIdentity(queryPath, fs);
10212
+ return createProjectScopeMatcherFromIdentity(queryPath, identity);
10213
+ }
10214
+ function createProjectScopeMatcherFromIdentity(queryPath, identity) {
10227
10215
  return {
10228
10216
  identity: { kind: identity.kind, key: identity.key },
10229
- path: normalizeScopePath(queryPath)
10217
+ path: normalizeProjectScopePath(queryPath)
10230
10218
  };
10231
10219
  }
10232
10220
  function matchesProjectScope(session, scope) {
@@ -10238,11 +10226,11 @@ function filterSessionsByProjectScope(sessions, queryPath, fs) {
10238
10226
  return sessions.filter((session) => matchesProjectScope(session, scope));
10239
10227
  }
10240
10228
  function isPathScopeMatch(queryPath, sessionPath) {
10241
- const session = normalizeScopePath(sessionPath);
10229
+ const session = normalizeProjectScopePath(sessionPath);
10242
10230
  return session === queryPath || session.startsWith(queryPath + "/") || queryPath.startsWith(session + "/");
10243
10231
  }
10244
- function normalizeScopePath(path2) {
10245
- return resolve3(path2).replaceAll(sep, "/");
10232
+ function normalizeProjectScopePath(path2) {
10233
+ return normalizeProjectDirectory(path2).replaceAll("\\", "/");
10246
10234
  }
10247
10235
  var SMART_TAG_CLASSIFIER_REVISION = "smart-tags-v1";
10248
10236
  var TAG_ORDER = [
@@ -10596,6 +10584,44 @@ function normalizeFilePathSearch(value) {
10596
10584
  return value.trim().replace(/^"|"$/g, "");
10597
10585
  }
10598
10586
  var MESSAGE_PARTS_FORMAT_VERSION = 1;
10587
+ function messageCursorContentFromCachedRow(row) {
10588
+ return {
10589
+ messageId: String(row.message_id),
10590
+ role: String(row.role),
10591
+ timeCreated: Number(row.time_created),
10592
+ timeCompleted: row.time_completed,
10593
+ agent: row.agent,
10594
+ mode: row.mode,
10595
+ model: row.model,
10596
+ provider: row.provider,
10597
+ tokensJson: row.tokens_json,
10598
+ cost: row.cost,
10599
+ costSource: row.cost_source,
10600
+ partsJson: String(row.parts_json),
10601
+ partsFormatVersion: row.parts_format_version,
10602
+ subagentId: row.subagent_id,
10603
+ nickname: row.nickname
10604
+ };
10605
+ }
10606
+ function messageCursorContentFromStructuredRecord(record) {
10607
+ return {
10608
+ messageId: record.id,
10609
+ role: record.role,
10610
+ timeCreated: record.timeCreated,
10611
+ timeCompleted: record.timeCompleted,
10612
+ agent: record.agent,
10613
+ mode: record.mode,
10614
+ model: record.model,
10615
+ provider: record.provider,
10616
+ tokensJson: record.tokensJson,
10617
+ cost: record.cost,
10618
+ costSource: record.costSource,
10619
+ partsJson: record.partsJson,
10620
+ partsFormatVersion: MESSAGE_PARTS_FORMAT_VERSION,
10621
+ subagentId: record.subagentId,
10622
+ nickname: record.nickname
10623
+ };
10624
+ }
10599
10625
  function stringifyOptionalJson(value) {
10600
10626
  return value == null ? null : JSON.stringify(value);
10601
10627
  }
@@ -10610,6 +10636,13 @@ function sourcePathFromMetaJson(metaJson) {
10610
10636
  const meta = JSON.parse(metaJson);
10611
10637
  return sourcePathFromMeta(meta);
10612
10638
  }
10639
+ function requireSessionProjectIdentity(agentName, session) {
10640
+ if (session.project_identity) return session.project_identity;
10641
+ throw new Error(`Session ${agentName}/${session.id} is missing project_identity`);
10642
+ }
10643
+ function assertSessionProjectIdentities(agentName, sessions) {
10644
+ for (const session of sessions) requireSessionProjectIdentity(agentName, session);
10645
+ }
10613
10646
  function prepareUpsertSession(db) {
10614
10647
  return db.prepare(`
10615
10648
  INSERT INTO sessions(
@@ -10749,7 +10782,7 @@ function prepareUpsertIndexedSession(db) {
10749
10782
  return prepareIndexedSession(db);
10750
10783
  }
10751
10784
  function upsertSessionRow(statement, agentName, session, metaJson, sortIndex, sourcePath) {
10752
- const identity = session.project_identity ?? computeIdentity(session.directory, realFs);
10785
+ const identity = requireSessionProjectIdentity(agentName, session);
10753
10786
  const activityTime2 = session.time_updated ?? session.time_created;
10754
10787
  statement.run(
10755
10788
  agentName,
@@ -11174,16 +11207,62 @@ function discardPublicationStaging(db, publicationId) {
11174
11207
  db.prepare("DELETE FROM search_index_publication_entries").run();
11175
11208
  }
11176
11209
  }
11177
- function withCacheConnection(fn) {
11178
- const cachePath = getCachePath2();
11179
- const connection = getCacheConnection(cachePath);
11180
- if (!connection) return null;
11181
- try {
11182
- if (getSchemaEnsuredPath() !== cachePath) {
11183
- ensureSchema(connection.db, cachePath);
11184
- setSchemaEnsuredPath(cachePath);
11185
- }
11186
- return fn(connection);
11210
+ function prepareLegacyProjectIdentityResolver(db, currentVersion) {
11211
+ const directories = /* @__PURE__ */ new Set();
11212
+ if (currentVersion < 7 && tableExists(db, "cached_sessions")) {
11213
+ const rows = db.prepare("SELECT agent_name, session_id, session_json FROM cached_sessions").all();
11214
+ for (const row of rows) {
11215
+ if (!row.session_json) continue;
11216
+ try {
11217
+ const session = JSON.parse(row.session_json);
11218
+ if (session.directory != null) {
11219
+ directories.add(String(session.directory));
11220
+ } else {
11221
+ getCoreDiagnostics()?.warn("sqlite.migration.identity_precompute.missing_directory", {
11222
+ agent_name: row.agent_name,
11223
+ session_id: row.session_id
11224
+ });
11225
+ }
11226
+ } catch {
11227
+ continue;
11228
+ }
11229
+ }
11230
+ }
11231
+ if (currentVersion < 12) {
11232
+ for (const table of ["session_documents", "sessions", "project_sessions"]) {
11233
+ if (!tableExists(db, table) || !columnExists(db, table, "directory")) continue;
11234
+ const rows = db.prepare(`SELECT directory FROM ${table}`).all();
11235
+ for (const row of rows) directories.add(String(row.directory ?? ""));
11236
+ }
11237
+ }
11238
+ const identities = /* @__PURE__ */ new Map();
11239
+ for (const directory of directories) {
11240
+ try {
11241
+ identities.set(directory, computeIdentity(directory, realFs));
11242
+ } catch (error) {
11243
+ identities.set(directory, error instanceof Error ? error : new Error(String(error)));
11244
+ }
11245
+ }
11246
+ return (directory) => {
11247
+ const identity = identities.get(directory);
11248
+ if (identity instanceof Error) throw identity;
11249
+ if (identity) return identity;
11250
+ throw new Error(`Missing precomputed project identity for legacy directory: ${directory}`);
11251
+ };
11252
+ }
11253
+ function getLegacySessionDirectory(session) {
11254
+ return typeof session.directory === "string" ? session.directory : null;
11255
+ }
11256
+ function withCacheConnection(fn) {
11257
+ const cachePath = getCachePath2();
11258
+ const connection = getCacheConnection(cachePath);
11259
+ if (!connection) return null;
11260
+ try {
11261
+ if (getSchemaEnsuredPath() !== cachePath) {
11262
+ ensureSchema(connection.db, cachePath);
11263
+ setSchemaEnsuredPath(cachePath);
11264
+ }
11265
+ return fn(connection);
11187
11266
  } catch (error) {
11188
11267
  getCoreDiagnostics()?.warn("cache.write_failed", {
11189
11268
  message: error instanceof Error ? error.message : String(error),
@@ -11211,6 +11290,13 @@ function cleanPublicationStaging(connection) {
11211
11290
  function withCacheDb(fn) {
11212
11291
  return withCacheConnection(({ db }) => fn(db));
11213
11292
  }
11293
+ function withCacheDbOutcome(fn) {
11294
+ let result = { status: "failed" };
11295
+ withCacheConnection(({ db }) => {
11296
+ result = { status: "success", value: fn(db) };
11297
+ });
11298
+ return result;
11299
+ }
11214
11300
  function withCacheDbReadOnly(fn) {
11215
11301
  const cachePath = getCachePath2();
11216
11302
  if (!hasCacheStorage()) return { status: "failed" };
@@ -11328,6 +11414,9 @@ function createSessionTables(db) {
11328
11414
  CREATE INDEX IF NOT EXISTS idx_sessions_agent_activity_order
11329
11415
  ON sessions(agent_name, activity_time DESC, session_id);
11330
11416
 
11417
+ CREATE INDEX IF NOT EXISTS idx_sessions_activity
11418
+ ON sessions(activity_time DESC, agent_name, session_id);
11419
+
11331
11420
  CREATE INDEX IF NOT EXISTS idx_sessions_project
11332
11421
  ON sessions(project_identity_kind, project_identity_key, activity_time);
11333
11422
 
@@ -11351,6 +11440,7 @@ function createSessionTables(db) {
11351
11440
  cost_source TEXT,
11352
11441
  parts_json TEXT NOT NULL,
11353
11442
  parts_format_version INTEGER NOT NULL DEFAULT 0,
11443
+ content_chain_digest TEXT,
11354
11444
  subagent_id TEXT,
11355
11445
  nickname TEXT,
11356
11446
  content_text TEXT NOT NULL,
@@ -11363,9 +11453,62 @@ function createSessionTables(db) {
11363
11453
 
11364
11454
  CREATE INDEX IF NOT EXISTS idx_messages_session
11365
11455
  ON messages(agent_name, session_id, message_index);
11456
+
11457
+ CREATE INDEX IF NOT EXISTS idx_messages_usage_time
11458
+ ON messages(
11459
+ CASE
11460
+ WHEN time_completed > 0 THEN time_completed
11461
+ WHEN time_created > 0 THEN time_created
11462
+ END,
11463
+ agent_name,
11464
+ session_id
11465
+ );
11366
11466
  `);
11467
+ createSessionModelCostTable(db);
11468
+ createSessionCostSummaryTable(db);
11367
11469
  createMessageToolTables(db);
11368
11470
  }
11471
+ function createSessionModelCostTable(db) {
11472
+ db.exec(`
11473
+ CREATE TABLE IF NOT EXISTS session_model_cost (
11474
+ agent_name TEXT NOT NULL,
11475
+ session_id TEXT NOT NULL,
11476
+ model TEXT NOT NULL,
11477
+ cost REAL NOT NULL,
11478
+ cost_recorded REAL NOT NULL,
11479
+ PRIMARY KEY (agent_name, session_id, model),
11480
+ FOREIGN KEY (agent_name, session_id)
11481
+ REFERENCES sessions(agent_name, session_id)
11482
+ ON DELETE CASCADE
11483
+ );
11484
+ `);
11485
+ }
11486
+ function createSessionCostSummaryTable(db) {
11487
+ db.exec(`
11488
+ CREATE TABLE IF NOT EXISTS session_cost_summary (
11489
+ agent_name TEXT NOT NULL,
11490
+ session_id TEXT NOT NULL,
11491
+ message_count INTEGER NOT NULL DEFAULT 0,
11492
+ untimed_message_count INTEGER NOT NULL DEFAULT 0,
11493
+ input_tokens INTEGER NOT NULL DEFAULT 0,
11494
+ output_tokens INTEGER NOT NULL DEFAULT 0,
11495
+ reasoning_tokens INTEGER NOT NULL DEFAULT 0,
11496
+ cache_read_tokens INTEGER NOT NULL DEFAULT 0,
11497
+ cache_create_tokens INTEGER NOT NULL DEFAULT 0,
11498
+ untimed_input_tokens INTEGER NOT NULL DEFAULT 0,
11499
+ untimed_output_tokens INTEGER NOT NULL DEFAULT 0,
11500
+ untimed_reasoning_tokens INTEGER NOT NULL DEFAULT 0,
11501
+ untimed_cache_read_tokens INTEGER NOT NULL DEFAULT 0,
11502
+ untimed_cache_create_tokens INTEGER NOT NULL DEFAULT 0,
11503
+ message_cost REAL NOT NULL,
11504
+ untimed_message_cost REAL NOT NULL,
11505
+ PRIMARY KEY (agent_name, session_id),
11506
+ FOREIGN KEY (agent_name, session_id)
11507
+ REFERENCES sessions(agent_name, session_id)
11508
+ ON DELETE CASCADE
11509
+ );
11510
+ `);
11511
+ }
11369
11512
  function createMessageToolTables(db) {
11370
11513
  db.exec(`
11371
11514
  CREATE TABLE IF NOT EXISTS message_tools (
@@ -11725,7 +11868,7 @@ function hasAnyCacheSchema(db) {
11725
11868
  "project_sessions"
11726
11869
  ].some((table) => tableExists(db, table));
11727
11870
  }
11728
- function backfillProjectSessions(db) {
11871
+ function backfillProjectSessions(db, resolveIdentity) {
11729
11872
  if (!tableExists(db, "cached_sessions") || !tableExists(db, "project_sessions")) {
11730
11873
  return;
11731
11874
  }
@@ -11753,14 +11896,16 @@ function backfillProjectSessions(db) {
11753
11896
  }
11754
11897
  try {
11755
11898
  const session = JSON.parse(row.session_json);
11756
- const identity = session.project_identity ?? computeIdentity(session.directory, realFs);
11899
+ const directory = getLegacySessionDirectory(session);
11900
+ if (directory == null) continue;
11901
+ const identity = session.project_identity ?? resolveIdentity(directory);
11757
11902
  upsert.run(
11758
11903
  row.agent_name,
11759
11904
  row.session_id,
11760
11905
  identity.kind,
11761
11906
  identity.key,
11762
11907
  identity.displayName,
11763
- session.directory,
11908
+ directory,
11764
11909
  session.time_updated ?? session.time_created
11765
11910
  );
11766
11911
  } catch {
@@ -11768,7 +11913,7 @@ function backfillProjectSessions(db) {
11768
11913
  }
11769
11914
  }
11770
11915
  }
11771
- function backfillSessionDocumentProjects(db) {
11916
+ function backfillSessionDocumentProjects(db, resolveIdentity) {
11772
11917
  if (!tableExists(db, "session_documents") || !columnExists(db, "session_documents", "project_identity_key")) {
11773
11918
  return;
11774
11919
  }
@@ -11782,17 +11927,17 @@ function backfillSessionDocumentProjects(db) {
11782
11927
  WHERE id = ?
11783
11928
  `);
11784
11929
  for (const row of rows) {
11785
- const identity = computeIdentity(String(row.directory ?? ""), realFs);
11930
+ const identity = resolveIdentity(String(row.directory ?? ""));
11786
11931
  update.run(identity.kind, identity.key, identity.displayName, Number(row.id));
11787
11932
  }
11788
11933
  }
11789
- function migrateProjectIdentity(db) {
11934
+ function migrateProjectIdentity(db, resolveIdentity) {
11790
11935
  ensureLegacySessionDocumentColumns(db);
11791
11936
  createProjectTables(db);
11792
- backfillProjectSessions(db);
11793
- backfillSessionDocumentProjects(db);
11937
+ backfillProjectSessions(db, resolveIdentity);
11938
+ backfillSessionDocumentProjects(db, resolveIdentity);
11794
11939
  }
11795
- function refreshProjectIdentities(db) {
11940
+ function refreshProjectIdentities(db, resolveIdentity) {
11796
11941
  if (tableExists(db, "sessions") && columnExists(db, "sessions", "project_identity_key") && columnExists(db, "sessions", "directory")) {
11797
11942
  const rows = db.prepare("SELECT agent_name, session_id, directory FROM sessions").all();
11798
11943
  const update = db.prepare(`
@@ -11809,7 +11954,7 @@ function refreshProjectIdentities(db) {
11809
11954
  WHERE agent_name = ? AND session_id = ?
11810
11955
  `) : null;
11811
11956
  for (const row of rows) {
11812
- const identity = computeIdentity(String(row.directory ?? ""), realFs);
11957
+ const identity = resolveIdentity(String(row.directory ?? ""));
11813
11958
  update.run(identity.kind, identity.key, identity.displayName, row.agent_name, row.session_id);
11814
11959
  updateFileActivity?.run(identity.key, row.agent_name, row.session_id);
11815
11960
  }
@@ -11825,14 +11970,14 @@ function refreshProjectIdentities(db) {
11825
11970
  WHERE agent_name = ? AND session_id = ?
11826
11971
  `);
11827
11972
  for (const row of rows) {
11828
- const identity = computeIdentity(String(row.directory ?? ""), realFs);
11973
+ const identity = resolveIdentity(String(row.directory ?? ""));
11829
11974
  update.run(identity.kind, identity.key, identity.displayName, row.agent_name, row.session_id);
11830
11975
  }
11831
11976
  }
11832
- backfillSessionDocumentProjects(db);
11977
+ backfillSessionDocumentProjects(db, resolveIdentity);
11833
11978
  recreateProjectGroupsView(db);
11834
11979
  }
11835
- function backfillStructuredSessions(db) {
11980
+ function backfillStructuredSessions(db, resolveIdentity) {
11836
11981
  createSessionTables(db);
11837
11982
  recreateProjectGroupsView(db);
11838
11983
  const upsertSession = prepareUpsertSession(db);
@@ -11845,7 +11990,10 @@ function backfillStructuredSessions(db) {
11845
11990
  continue;
11846
11991
  }
11847
11992
  try {
11848
- const session = JSON.parse(row.session_json);
11993
+ const parsed = JSON.parse(row.session_json);
11994
+ const directory = getLegacySessionDirectory(parsed);
11995
+ if (directory == null) continue;
11996
+ const session = parsed.project_identity == null ? { ...parsed, project_identity: resolveIdentity(directory) } : parsed;
11849
11997
  upsertSessionRow(
11850
11998
  upsertSession,
11851
11999
  String(row.agent_name),
@@ -11889,7 +12037,7 @@ function backfillStructuredSessions(db) {
11889
12037
  kind: row.project_identity_kind,
11890
12038
  key: String(row.project_identity_key),
11891
12039
  displayName: String(row.project_display_name)
11892
- } : computeIdentity(directory, realFs);
12040
+ } : resolveIdentity(directory);
11893
12041
  upsertSessionRow(
11894
12042
  upsertSession,
11895
12043
  String(row.agent_name),
@@ -12015,6 +12163,174 @@ function replaceSessionActivityIndex(db) {
12015
12163
  ON sessions(agent_name, activity_time DESC, session_id);
12016
12164
  `);
12017
12165
  }
12166
+ function addSessionActivityIndex(db) {
12167
+ if (!tableExists(db, "sessions")) return;
12168
+ db.exec(`
12169
+ CREATE INDEX IF NOT EXISTS idx_sessions_activity
12170
+ ON sessions(activity_time DESC, agent_name, session_id)
12171
+ `);
12172
+ }
12173
+ function addSessionModelCostRollup(db) {
12174
+ createSessionModelCostTable(db);
12175
+ if (!tableExists(db, "messages")) return;
12176
+ db.exec(`
12177
+ INSERT OR REPLACE INTO session_model_cost(agent_name, session_id, model, cost, cost_recorded)
12178
+ SELECT
12179
+ agent_name,
12180
+ session_id,
12181
+ model,
12182
+ SUM(COALESCE(cost, 0)),
12183
+ SUM(CASE WHEN cost_source = 'recorded' THEN COALESCE(cost, 0) ELSE 0 END)
12184
+ FROM messages
12185
+ WHERE model IS NOT NULL AND model <> ''
12186
+ GROUP BY agent_name, session_id, model
12187
+ `);
12188
+ }
12189
+ function addSessionCostSummary(db) {
12190
+ db.exec(`
12191
+ CREATE TABLE IF NOT EXISTS session_cost_summary (
12192
+ agent_name TEXT NOT NULL,
12193
+ session_id TEXT NOT NULL,
12194
+ message_cost REAL NOT NULL,
12195
+ untimed_message_cost REAL NOT NULL,
12196
+ PRIMARY KEY (agent_name, session_id),
12197
+ FOREIGN KEY (agent_name, session_id)
12198
+ REFERENCES sessions(agent_name, session_id)
12199
+ ON DELETE CASCADE
12200
+ );
12201
+ `);
12202
+ if (!tableExists(db, "messages")) return;
12203
+ db.exec(`
12204
+ CREATE INDEX IF NOT EXISTS idx_messages_cost_time
12205
+ ON messages(
12206
+ CASE
12207
+ WHEN time_completed > 0 THEN time_completed
12208
+ WHEN time_created > 0 THEN time_created
12209
+ END,
12210
+ agent_name,
12211
+ session_id
12212
+ )
12213
+ WHERE cost > 0;
12214
+ `);
12215
+ if (columnExists(db, "session_cost_summary", "message_count")) return;
12216
+ db.exec(`
12217
+
12218
+ INSERT OR REPLACE INTO session_cost_summary(
12219
+ agent_name,
12220
+ session_id,
12221
+ message_cost,
12222
+ untimed_message_cost
12223
+ )
12224
+ SELECT
12225
+ m.agent_name,
12226
+ m.session_id,
12227
+ SUM(CASE WHEN m.cost > 0 THEN m.cost ELSE 0 END),
12228
+ SUM(
12229
+ CASE
12230
+ WHEN m.cost > 0
12231
+ AND COALESCE(m.time_completed, 0) <= 0
12232
+ AND COALESCE(m.time_created, 0) <= 0
12233
+ THEN m.cost
12234
+ ELSE 0
12235
+ END
12236
+ )
12237
+ FROM messages m
12238
+ JOIN sessions s
12239
+ ON s.agent_name = m.agent_name
12240
+ AND s.session_id = m.session_id
12241
+ GROUP BY m.agent_name, m.session_id
12242
+ `);
12243
+ }
12244
+ function addSessionUsageSummary(db) {
12245
+ if (!tableExists(db, "session_cost_summary")) addSessionCostSummary(db);
12246
+ const columns = {
12247
+ message_count: "INTEGER NOT NULL DEFAULT 0",
12248
+ untimed_message_count: "INTEGER NOT NULL DEFAULT 0",
12249
+ input_tokens: "INTEGER NOT NULL DEFAULT 0",
12250
+ output_tokens: "INTEGER NOT NULL DEFAULT 0",
12251
+ reasoning_tokens: "INTEGER NOT NULL DEFAULT 0",
12252
+ cache_read_tokens: "INTEGER NOT NULL DEFAULT 0",
12253
+ cache_create_tokens: "INTEGER NOT NULL DEFAULT 0",
12254
+ untimed_input_tokens: "INTEGER NOT NULL DEFAULT 0",
12255
+ untimed_output_tokens: "INTEGER NOT NULL DEFAULT 0",
12256
+ untimed_reasoning_tokens: "INTEGER NOT NULL DEFAULT 0",
12257
+ untimed_cache_read_tokens: "INTEGER NOT NULL DEFAULT 0",
12258
+ untimed_cache_create_tokens: "INTEGER NOT NULL DEFAULT 0"
12259
+ };
12260
+ for (const [name, definition] of Object.entries(columns)) {
12261
+ if (!columnExists(db, "session_cost_summary", name)) {
12262
+ db.exec(`ALTER TABLE session_cost_summary ADD COLUMN ${name} ${definition}`);
12263
+ }
12264
+ }
12265
+ if (!tableExists(db, "messages")) return;
12266
+ db.exec(`
12267
+ DROP INDEX IF EXISTS idx_messages_cost_time;
12268
+ CREATE INDEX IF NOT EXISTS idx_messages_usage_time
12269
+ ON messages(
12270
+ CASE
12271
+ WHEN time_completed > 0 THEN time_completed
12272
+ WHEN time_created > 0 THEN time_created
12273
+ END,
12274
+ agent_name,
12275
+ session_id
12276
+ );
12277
+
12278
+ DELETE FROM session_cost_summary;
12279
+ WITH normalized AS (
12280
+ SELECT
12281
+ m.agent_name,
12282
+ m.session_id,
12283
+ COALESCE(m.time_completed, 0) <= 0 AND COALESCE(m.time_created, 0) <= 0 AS untimed,
12284
+ MAX(CAST(COALESCE(json_extract(m.tokens_json, '$.input'), 0) AS INTEGER), 0) AS input_tokens,
12285
+ MAX(CAST(COALESCE(json_extract(m.tokens_json, '$.output'), 0) AS INTEGER), 0) AS output_tokens,
12286
+ MAX(CAST(COALESCE(json_extract(m.tokens_json, '$.reasoning'), 0) AS INTEGER), 0) AS reasoning_tokens,
12287
+ MAX(CAST(COALESCE(json_extract(m.tokens_json, '$.cache_read'), 0) AS INTEGER), 0) AS cache_read_tokens,
12288
+ MAX(CAST(COALESCE(json_extract(m.tokens_json, '$.cache_create'), 0) AS INTEGER), 0) AS cache_create_tokens,
12289
+ CASE WHEN m.cost > 0 THEN m.cost ELSE 0 END AS cost
12290
+ FROM messages m
12291
+ JOIN sessions s
12292
+ ON s.agent_name = m.agent_name
12293
+ AND s.session_id = m.session_id
12294
+ )
12295
+ INSERT INTO session_cost_summary(
12296
+ agent_name,
12297
+ session_id,
12298
+ message_count,
12299
+ untimed_message_count,
12300
+ input_tokens,
12301
+ output_tokens,
12302
+ reasoning_tokens,
12303
+ cache_read_tokens,
12304
+ cache_create_tokens,
12305
+ untimed_input_tokens,
12306
+ untimed_output_tokens,
12307
+ untimed_reasoning_tokens,
12308
+ untimed_cache_read_tokens,
12309
+ untimed_cache_create_tokens,
12310
+ message_cost,
12311
+ untimed_message_cost
12312
+ )
12313
+ SELECT
12314
+ agent_name,
12315
+ session_id,
12316
+ COUNT(*),
12317
+ SUM(CASE WHEN untimed THEN 1 ELSE 0 END),
12318
+ SUM(input_tokens),
12319
+ SUM(output_tokens),
12320
+ SUM(reasoning_tokens),
12321
+ SUM(cache_read_tokens),
12322
+ SUM(cache_create_tokens),
12323
+ SUM(CASE WHEN untimed THEN input_tokens ELSE 0 END),
12324
+ SUM(CASE WHEN untimed THEN output_tokens ELSE 0 END),
12325
+ SUM(CASE WHEN untimed THEN reasoning_tokens ELSE 0 END),
12326
+ SUM(CASE WHEN untimed THEN cache_read_tokens ELSE 0 END),
12327
+ SUM(CASE WHEN untimed THEN cache_create_tokens ELSE 0 END),
12328
+ SUM(cost),
12329
+ SUM(CASE WHEN untimed THEN cost ELSE 0 END)
12330
+ FROM normalized
12331
+ GROUP BY agent_name, session_id;
12332
+ `);
12333
+ }
12018
12334
  function compactSessionDocuments(db) {
12019
12335
  if (!tableExists(db, "session_documents")) {
12020
12336
  createSearchTables(db);
@@ -12058,6 +12374,12 @@ function addMessagePartsFormatVersion(db) {
12058
12374
  }
12059
12375
  db.exec("ALTER TABLE messages ADD COLUMN parts_format_version INTEGER NOT NULL DEFAULT 0");
12060
12376
  }
12377
+ function addMessageContentChainDigest(db) {
12378
+ if (!tableExists(db, "messages") || columnExists(db, "messages", "content_chain_digest")) {
12379
+ return;
12380
+ }
12381
+ db.exec("ALTER TABLE messages ADD COLUMN content_chain_digest TEXT");
12382
+ }
12061
12383
  function addSessionParentReference(db) {
12062
12384
  if (!tableExists(db, "sessions")) return;
12063
12385
  if (!columnExists(db, "sessions", "parent_agent_name")) {
@@ -12213,6 +12535,7 @@ function ensureSchema(db, dbPath) {
12213
12535
  migrateSubagentTree(db);
12214
12536
  return;
12215
12537
  }
12538
+ const resolveLegacyProjectIdentity = prepareLegacyProjectIdentityResolver(db, currentVersion);
12216
12539
  runSchemaMigrations(db, {
12217
12540
  dbPath,
12218
12541
  currentVersion,
@@ -12232,7 +12555,10 @@ function ensureSchema(db, dbPath) {
12232
12555
  migrations: [
12233
12556
  { version: 3, migrate: createCacheTables },
12234
12557
  { version: 4, migrate: createSearchTables },
12235
- { version: 5, migrate: migrateProjectIdentity },
12558
+ {
12559
+ version: 5,
12560
+ migrate: (migrationDb) => migrateProjectIdentity(migrationDb, resolveLegacyProjectIdentity)
12561
+ },
12236
12562
  {
12237
12563
  version: 6,
12238
12564
  destructive: true,
@@ -12246,7 +12572,7 @@ function ensureSchema(db, dbPath) {
12246
12572
  version: 7,
12247
12573
  migrate(db2) {
12248
12574
  addSessionParentReference(db2);
12249
- backfillStructuredSessions(db2);
12575
+ backfillStructuredSessions(db2, resolveLegacyProjectIdentity);
12250
12576
  }
12251
12577
  },
12252
12578
  { version: 8, migrate: backfillFileActivity },
@@ -12266,7 +12592,7 @@ function ensureSchema(db, dbPath) {
12266
12592
  {
12267
12593
  version: 12,
12268
12594
  migrate(db2) {
12269
- refreshProjectIdentities(db2);
12595
+ refreshProjectIdentities(db2, resolveLegacyProjectIdentity);
12270
12596
  }
12271
12597
  },
12272
12598
  { version: 13, migrate: createCacheTables },
@@ -12279,7 +12605,12 @@ function ensureSchema(db, dbPath) {
12279
12605
  { version: 21, migrate: addSessionPublicationId },
12280
12606
  { version: 22, migrate: addAtomicPublicationStaging },
12281
12607
  { version: 23, migrate: replaceSessionActivityIndex },
12282
- { version: 24, migrate: dropLegacyMessageSearchIndex }
12608
+ { version: 24, migrate: dropLegacyMessageSearchIndex },
12609
+ { version: 25, migrate: addMessageContentChainDigest },
12610
+ { version: 26, migrate: addSessionActivityIndex },
12611
+ { version: 27, migrate: addSessionModelCostRollup },
12612
+ { version: 28, migrate: addSessionCostSummary },
12613
+ { version: 29, migrate: addSessionUsageSummary }
12283
12614
  ]
12284
12615
  });
12285
12616
  createLatestCacheSchema(db);
@@ -12293,6 +12624,25 @@ function ensureSchema(db, dbPath) {
12293
12624
  migrateOpenCodeSubagentFold(db);
12294
12625
  migrateSubagentTree(db);
12295
12626
  }
12627
+ var ANALYTICS_REVISION_KEY = "analytics_revision";
12628
+ function readAnalyticsRevision(db) {
12629
+ const row = db.prepare("SELECT value FROM cache_meta WHERE key = ?").get(ANALYTICS_REVISION_KEY);
12630
+ return String(row?.value ?? "0");
12631
+ }
12632
+ function advanceAnalyticsRevision(db) {
12633
+ db.prepare(
12634
+ `
12635
+ INSERT INTO cache_meta(key, value)
12636
+ VALUES (?, '1')
12637
+ ON CONFLICT(key) DO UPDATE SET value = CAST(value AS INTEGER) + 1
12638
+ `
12639
+ ).run(ANALYTICS_REVISION_KEY);
12640
+ }
12641
+ function getAnalyticsRevision() {
12642
+ if (!hasCacheStorage()) return null;
12643
+ const read = withCacheDbReadOnly((db) => readAnalyticsRevision(db));
12644
+ return read.status === "success" ? read.value : null;
12645
+ }
12296
12646
  function escapeFtsTerm(value) {
12297
12647
  return value.replaceAll('"', '""');
12298
12648
  }
@@ -12417,6 +12767,54 @@ function toFtsQuery(input) {
12417
12767
  (token, index, values) => token !== "OR" || index > 0 && index < values.length - 1 && values[index - 1] !== "OR" && values[index + 1] !== "OR"
12418
12768
  ).join(" ");
12419
12769
  }
12770
+ var MESSAGE_CURSOR_VERSION = 2;
12771
+ function updateField(hash, value) {
12772
+ if (value == null) {
12773
+ hash.update("n;");
12774
+ return;
12775
+ }
12776
+ const text = String(value);
12777
+ hash.update(`v${text.length}:`).update(text).update(";");
12778
+ }
12779
+ function updateMessageContent(hash, content) {
12780
+ updateField(hash, content.messageId);
12781
+ updateField(hash, content.role);
12782
+ updateField(hash, content.timeCreated);
12783
+ updateField(hash, content.timeCompleted);
12784
+ updateField(hash, content.agent);
12785
+ updateField(hash, content.mode);
12786
+ updateField(hash, content.model);
12787
+ updateField(hash, content.provider);
12788
+ updateField(hash, content.tokensJson);
12789
+ updateField(hash, content.cost);
12790
+ updateField(hash, content.costSource);
12791
+ updateField(hash, content.partsJson);
12792
+ updateField(hash, content.partsFormatVersion);
12793
+ updateField(hash, content.subagentId);
12794
+ updateField(hash, content.nickname);
12795
+ }
12796
+ function initialMessageCursorDigest(reference) {
12797
+ const hash = createHash22("sha256");
12798
+ hash.update("codesesh-session-messages\0");
12799
+ updateField(hash, MESSAGE_CURSOR_VERSION);
12800
+ updateField(hash, reference.agentName);
12801
+ updateField(hash, reference.sessionId);
12802
+ return hash.digest("hex");
12803
+ }
12804
+ function advanceMessageCursorDigest(previousDigest, content) {
12805
+ const hash = createHash22("sha256");
12806
+ hash.update("codesesh-session-messages-chain\0");
12807
+ updateField(hash, previousDigest);
12808
+ updateMessageContent(hash, content);
12809
+ return hash.digest("hex");
12810
+ }
12811
+ function computeMessageCursorDigest(reference, messages) {
12812
+ let digest = initialMessageCursorDigest(reference);
12813
+ for (const message of messages) {
12814
+ digest = advanceMessageCursorDigest(digest, message);
12815
+ }
12816
+ return digest;
12817
+ }
12420
12818
  var DETAIL_PROJECTION_VERSION = "session-detail-v1";
12421
12819
  function sessionDetailVersion(meta) {
12422
12820
  const parserVersions = Object.entries(meta ?? {}).filter(
@@ -12621,7 +13019,7 @@ function loadSearchIndexEntry(agentName, change, loadSessionData, detailVersion,
12621
13019
  try {
12622
13020
  const data = loadSessionData(change.session.id);
12623
13021
  const messages = normalizeMessages(data);
12624
- const identity = change.session.project_identity ?? data.project_identity ?? computeIdentity(change.session.directory, realFs);
13022
+ const identity = requireSessionProjectIdentity(agentName, change.session);
12625
13023
  return {
12626
13024
  session: change.session,
12627
13025
  messages,
@@ -12673,6 +13071,77 @@ function writeSearchIndexRows(db, agentName, removedSessionIds, entries, failure
12673
13071
  const deleteFileActivity = db.prepare(
12674
13072
  "DELETE FROM session_file_activity WHERE agent_name = ? AND session_id = ?"
12675
13073
  );
13074
+ const deleteModelCost = db.prepare(
13075
+ "DELETE FROM session_model_cost WHERE agent_name = ? AND session_id = ?"
13076
+ );
13077
+ const deleteCostSummary = db.prepare(
13078
+ "DELETE FROM session_cost_summary WHERE agent_name = ? AND session_id = ?"
13079
+ );
13080
+ const rebuildModelCost = db.prepare(`
13081
+ INSERT INTO session_model_cost(agent_name, session_id, model, cost, cost_recorded)
13082
+ SELECT
13083
+ agent_name,
13084
+ session_id,
13085
+ model,
13086
+ SUM(COALESCE(cost, 0)),
13087
+ SUM(CASE WHEN cost_source = 'recorded' THEN COALESCE(cost, 0) ELSE 0 END)
13088
+ FROM messages
13089
+ WHERE agent_name = ? AND session_id = ? AND model IS NOT NULL AND model <> ''
13090
+ GROUP BY agent_name, session_id, model
13091
+ `);
13092
+ const rebuildCostSummary = db.prepare(`
13093
+ WITH normalized AS (
13094
+ SELECT
13095
+ agent_name,
13096
+ session_id,
13097
+ COALESCE(time_completed, 0) <= 0 AND COALESCE(time_created, 0) <= 0 AS untimed,
13098
+ MAX(CAST(COALESCE(json_extract(tokens_json, '$.input'), 0) AS INTEGER), 0) AS input_tokens,
13099
+ MAX(CAST(COALESCE(json_extract(tokens_json, '$.output'), 0) AS INTEGER), 0) AS output_tokens,
13100
+ MAX(CAST(COALESCE(json_extract(tokens_json, '$.reasoning'), 0) AS INTEGER), 0) AS reasoning_tokens,
13101
+ MAX(CAST(COALESCE(json_extract(tokens_json, '$.cache_read'), 0) AS INTEGER), 0) AS cache_read_tokens,
13102
+ MAX(CAST(COALESCE(json_extract(tokens_json, '$.cache_create'), 0) AS INTEGER), 0) AS cache_create_tokens,
13103
+ CASE WHEN cost > 0 THEN cost ELSE 0 END AS normalized_cost
13104
+ FROM messages
13105
+ WHERE agent_name = ? AND session_id = ?
13106
+ )
13107
+ INSERT INTO session_cost_summary(
13108
+ agent_name,
13109
+ session_id,
13110
+ message_count,
13111
+ untimed_message_count,
13112
+ input_tokens,
13113
+ output_tokens,
13114
+ reasoning_tokens,
13115
+ cache_read_tokens,
13116
+ cache_create_tokens,
13117
+ untimed_input_tokens,
13118
+ untimed_output_tokens,
13119
+ untimed_reasoning_tokens,
13120
+ untimed_cache_read_tokens,
13121
+ untimed_cache_create_tokens,
13122
+ message_cost,
13123
+ untimed_message_cost
13124
+ )
13125
+ SELECT
13126
+ agent_name,
13127
+ session_id,
13128
+ COUNT(*),
13129
+ SUM(CASE WHEN untimed THEN 1 ELSE 0 END),
13130
+ SUM(input_tokens),
13131
+ SUM(output_tokens),
13132
+ SUM(reasoning_tokens),
13133
+ SUM(cache_read_tokens),
13134
+ SUM(cache_create_tokens),
13135
+ SUM(CASE WHEN untimed THEN input_tokens ELSE 0 END),
13136
+ SUM(CASE WHEN untimed THEN output_tokens ELSE 0 END),
13137
+ SUM(CASE WHEN untimed THEN reasoning_tokens ELSE 0 END),
13138
+ SUM(CASE WHEN untimed THEN cache_read_tokens ELSE 0 END),
13139
+ SUM(CASE WHEN untimed THEN cache_create_tokens ELSE 0 END),
13140
+ SUM(normalized_cost),
13141
+ SUM(CASE WHEN untimed THEN normalized_cost ELSE 0 END)
13142
+ FROM normalized
13143
+ GROUP BY agent_name, session_id
13144
+ `);
12676
13145
  const writeIndexedSession = prepareUpsertIndexedSession(db);
12677
13146
  const insertFileActivity = prepareInsertFileActivity(db);
12678
13147
  const insertMessageTool = prepareInsertMessageTool(db);
@@ -12694,11 +13163,12 @@ function writeSearchIndexRows(db, agentName, removedSessionIds, entries, failure
12694
13163
  cost_source,
12695
13164
  parts_json,
12696
13165
  parts_format_version,
13166
+ content_chain_digest,
12697
13167
  subagent_id,
12698
13168
  nickname,
12699
13169
  content_text,
12700
13170
  tool_metadata_json
12701
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
13171
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
12702
13172
  ON CONFLICT(agent_name, session_id, message_index) DO UPDATE SET
12703
13173
  message_id = excluded.message_id,
12704
13174
  role = excluded.role,
@@ -12713,6 +13183,7 @@ function writeSearchIndexRows(db, agentName, removedSessionIds, entries, failure
12713
13183
  cost_source = excluded.cost_source,
12714
13184
  parts_json = excluded.parts_json,
12715
13185
  parts_format_version = excluded.parts_format_version,
13186
+ content_chain_digest = excluded.content_chain_digest,
12716
13187
  subagent_id = excluded.subagent_id,
12717
13188
  nickname = excluded.nickname,
12718
13189
  content_text = excluded.content_text,
@@ -12748,6 +13219,8 @@ function writeSearchIndexRows(db, agentName, removedSessionIds, entries, failure
12748
13219
  deleteFileActivity.run(agentName, sessionId);
12749
13220
  deleteMessageTools.run(agentName, sessionId, 0);
12750
13221
  deleteMessages.run(agentName, sessionId, 0);
13222
+ deleteModelCost.run(agentName, sessionId);
13223
+ deleteCostSummary.run(agentName, sessionId);
12751
13224
  clearPendingReindex.run(agentName, sessionId);
12752
13225
  }
12753
13226
  let indexed = 0;
@@ -12764,7 +13237,15 @@ function writeSearchIndexRows(db, agentName, removedSessionIds, entries, failure
12764
13237
  deleteMessageTools.run(agentName, entry.session.id, 0);
12765
13238
  clearPendingReindex.run(agentName, entry.session.id);
12766
13239
  writeFileActivityRows(insertFileActivity, entry.fileActivity);
13240
+ let contentChainDigest = initialMessageCursorDigest({
13241
+ agentName,
13242
+ sessionId: entry.session.id
13243
+ });
12767
13244
  for (const message of entry.messages) {
13245
+ contentChainDigest = advanceMessageCursorDigest(
13246
+ contentChainDigest,
13247
+ messageCursorContentFromStructuredRecord(message)
13248
+ );
12768
13249
  upsertMessage.run(
12769
13250
  agentName,
12770
13251
  entry.session.id,
@@ -12782,6 +13263,7 @@ function writeSearchIndexRows(db, agentName, removedSessionIds, entries, failure
12782
13263
  message.costSource ?? null,
12783
13264
  message.partsJson,
12784
13265
  MESSAGE_PARTS_FORMAT_VERSION,
13266
+ contentChainDigest,
12785
13267
  message.subagentId ?? null,
12786
13268
  message.nickname ?? null,
12787
13269
  message.contentText,
@@ -12792,6 +13274,10 @@ function writeSearchIndexRows(db, agentName, removedSessionIds, entries, failure
12792
13274
  }
12793
13275
  }
12794
13276
  deleteMessages.run(agentName, entry.session.id, entry.messages.length);
13277
+ deleteModelCost.run(agentName, entry.session.id);
13278
+ deleteCostSummary.run(agentName, entry.session.id);
13279
+ rebuildModelCost.run(agentName, entry.session.id);
13280
+ rebuildCostSummary.run(agentName, entry.session.id);
12795
13281
  upsertRow.run(
12796
13282
  agentName,
12797
13283
  entry.session.id,
@@ -13042,11 +13528,20 @@ function executeSearchIndexPlan(db, plan, loadSessionData, largeBacklogStrategy)
13042
13528
  if (largeBacklogStrategy === "chunked" && plan.changes.length > SEARCH_INDEX_COMMIT_CHUNK_SIZE) {
13043
13529
  runSearchIndexWrite(db, false, () => {
13044
13530
  indexed += writeSearchIndexRows(db, plan.agentName, plan.removedSessionIds, [], failures);
13531
+ if (plan.removedSessionIds.length > 0) advanceAnalyticsRevision(db);
13045
13532
  });
13046
13533
  for (let offset = 0; offset < plan.changes.length; offset += SEARCH_INDEX_COMMIT_CHUNK_SIZE) {
13047
13534
  const chunk = plan.changes.slice(offset, offset + SEARCH_INDEX_COMMIT_CHUNK_SIZE);
13048
13535
  runSearchIndexWrite(db, false, () => {
13049
- indexed += writeSearchIndexRows(db, plan.agentName, [], loadEntries(chunk), failures);
13536
+ const chunkIndexed = writeSearchIndexRows(
13537
+ db,
13538
+ plan.agentName,
13539
+ [],
13540
+ loadEntries(chunk),
13541
+ failures
13542
+ );
13543
+ indexed += chunkIndexed;
13544
+ if (chunkIndexed > 0) advanceAnalyticsRevision(db);
13050
13545
  });
13051
13546
  }
13052
13547
  return searchIndexSyncResult(plan, indexed, failures, void 0, "incremental");
@@ -13059,10 +13554,12 @@ function executeSearchIndexPlan(db, plan, loadSessionData, largeBacklogStrategy)
13059
13554
  loadEntries(plan.changes),
13060
13555
  failures
13061
13556
  );
13557
+ if (indexed > 0 || plan.removedSessionIds.length > 0) advanceAnalyticsRevision(db);
13062
13558
  });
13063
13559
  return searchIndexSyncResult(plan, indexed, failures, rebuildDurationMs);
13064
13560
  }
13065
13561
  function syncSessionSearchIndex(agentName, sessions, loadSessionData, options = {}) {
13562
+ assertSessionProjectIdentities(agentName, sessions);
13066
13563
  return withSearchIndexDb(
13067
13564
  (db) => executeSearchIndexPlan(
13068
13565
  db,
@@ -13085,6 +13582,10 @@ function syncSessionSearchIndexChanges(agentName, changes, removedSessionIds, lo
13085
13582
  durationMs: 0
13086
13583
  };
13087
13584
  }
13585
+ assertSessionProjectIdentities(
13586
+ agentName,
13587
+ changes.map(({ session }) => session)
13588
+ );
13088
13589
  return withSearchIndexDb(
13089
13590
  (db) => executeSearchIndexPlan(
13090
13591
  db,
@@ -13111,7 +13612,6 @@ function mergeSearchQueryOptions(query, options) {
13111
13612
  project: options.project ?? parsed.filters.project,
13112
13613
  projectKind: options.projectKind ?? parsed.filters.projectKind,
13113
13614
  projectKey: options.projectKey ?? parsed.filters.projectKey,
13114
- cwd: options.cwd ?? parsed.filters.cwd,
13115
13615
  tags: mergeSearchLists(options.tags, parsed.filters.tags),
13116
13616
  tools: mergeSearchLists(options.tools, parsed.filters.tools),
13117
13617
  file: options.file ?? parsed.filters.file,
@@ -13124,6 +13624,9 @@ function mergeSearchQueryOptions(query, options) {
13124
13624
  parsed
13125
13625
  };
13126
13626
  }
13627
+ function getSearchProjectDirectory(query, options) {
13628
+ return options.cwd ?? parseSearchQuery(query).filters.cwd;
13629
+ }
13127
13630
  function sessionMatchesSearchCost(session, options, cost = session.stats.total_cost) {
13128
13631
  if (options.costMin != null) {
13129
13632
  if (options.costMinExclusive ? cost <= options.costMin : cost < options.costMin) {
@@ -13152,12 +13655,19 @@ function buildSessionSearchFilters(options) {
13152
13655
  clauses.push("0");
13153
13656
  }
13154
13657
  }
13155
- if (options.cwd) {
13156
- const identity = computeIdentity(options.cwd, realFs);
13658
+ if (options.projectScope) {
13659
+ const scopePath = normalizeProjectScopePath(options.projectScope.path).toLowerCase();
13660
+ const normalizedDirectory = "REPLACE(LOWER(s.directory), char(92), '/')";
13157
13661
  clauses.push(
13158
- "((s.project_identity_kind = ? AND s.project_identity_key = ?) OR LOWER(s.directory) LIKE ? ESCAPE '\\')"
13662
+ `((s.project_identity_kind = ? AND s.project_identity_key = ?) OR ${normalizedDirectory} = ? OR instr(${normalizedDirectory}, ? || '/') = 1 OR instr(?, ${normalizedDirectory} || '/') = 1)`
13663
+ );
13664
+ params.push(
13665
+ options.projectScope.identity.kind,
13666
+ options.projectScope.identity.key,
13667
+ scopePath,
13668
+ scopePath,
13669
+ scopePath
13159
13670
  );
13160
- params.push(identity.kind, identity.key, likePattern(options.cwd));
13161
13671
  }
13162
13672
  if (options.project) {
13163
13673
  clauses.push(
@@ -13348,6 +13858,7 @@ function searchResultRowKey(row) {
13348
13858
  return `${String(row.agent_name)}\0${String(row.session_id)}`;
13349
13859
  }
13350
13860
  function fetchMessageSearchMatches(db, rows, terms) {
13861
+ const startedAt = performance.now();
13351
13862
  const candidates = rows.filter((row) => !textMatchesTerms(String(row.title ?? ""), terms));
13352
13863
  if (candidates.length === 0) {
13353
13864
  return /* @__PURE__ */ new Map();
@@ -13357,11 +13868,11 @@ function fetchMessageSearchMatches(db, rows, terms) {
13357
13868
  String(row.agent_name),
13358
13869
  String(row.session_id)
13359
13870
  ]);
13360
- db.function(
13361
- "codesesh_message_matches_terms",
13362
- { deterministic: true },
13363
- (text) => textMatchesTerms(String(text ?? ""), terms) ? 1 : 0
13364
- );
13871
+ let predicateEvaluations = 0;
13872
+ db.function("codesesh_message_matches_terms", { deterministic: true }, (text) => {
13873
+ predicateEvaluations += 1;
13874
+ return textMatchesTerms(String(text ?? ""), terms) ? 1 : 0;
13875
+ });
13365
13876
  const messageRows = db.prepare(
13366
13877
  `
13367
13878
  WITH candidate_sessions(agent_name, session_id) AS (
@@ -13405,6 +13916,12 @@ function fetchMessageSearchMatches(db, rows, terms) {
13405
13916
  matchType: messageMatchType(message)
13406
13917
  });
13407
13918
  }
13919
+ getCoreDiagnostics()?.info?.("search.message_match_projection", {
13920
+ candidate_sessions: candidates.length,
13921
+ predicate_evaluations: predicateEvaluations,
13922
+ matched_sessions: matches.size,
13923
+ duration_ms: Math.round(performance.now() - startedAt)
13924
+ });
13408
13925
  return matches;
13409
13926
  }
13410
13927
  function resolveSearchMatch(row, terms, messageMatches) {
@@ -13496,14 +14013,14 @@ function searchSessions(query, options = {}) {
13496
14013
  }
13497
14014
  function fileActivityFilters(options) {
13498
14015
  const path2 = options.path ? normalizeFilePathSearch(options.path) : "";
13499
- const cwdIdentity = options.cwd ? computeIdentity(options.cwd, realFs) : null;
14016
+ const scope = options.projectScope;
13500
14017
  return {
13501
14018
  projectKind: options.projectKind ?? null,
13502
14019
  projectKey: options.projectKey ?? null,
13503
14020
  projectLike: options.project ? likePattern(options.project) : null,
13504
- cwdKind: cwdIdentity?.kind ?? null,
13505
- cwdKey: cwdIdentity?.key ?? null,
13506
- cwdLike: options.cwd ? likePattern(options.cwd) : null,
14021
+ scopeKind: scope?.identity.kind ?? null,
14022
+ scopeKey: scope?.identity.key ?? null,
14023
+ scopePath: scope ? normalizeProjectScopePath(scope.path).toLowerCase() : null,
13507
14024
  path: path2,
13508
14025
  pathLike: path2 ? likePattern(path2) : null
13509
14026
  };
@@ -13547,11 +14064,18 @@ function buildFileActivityWhere(options) {
13547
14064
  );
13548
14065
  params.push(filters.projectLike, filters.projectLike, filters.projectLike);
13549
14066
  }
13550
- if (filters.cwdKey != null) {
14067
+ if (filters.scopeKey != null && filters.scopePath != null) {
14068
+ const normalizedDirectory = "REPLACE(LOWER(s.directory), char(92), '/')";
13551
14069
  clauses.push(
13552
- "((s.project_identity_kind = ? AND s.project_identity_key = ?) OR LOWER(s.directory) LIKE ? ESCAPE '\\')"
14070
+ `((s.project_identity_kind = ? AND s.project_identity_key = ?) OR ${normalizedDirectory} = ? OR instr(${normalizedDirectory}, ? || '/') = 1 OR instr(?, ${normalizedDirectory} || '/') = 1)`
14071
+ );
14072
+ params.push(
14073
+ filters.scopeKind,
14074
+ filters.scopeKey,
14075
+ filters.scopePath,
14076
+ filters.scopePath,
14077
+ filters.scopePath
13553
14078
  );
13554
- params.push(filters.cwdKind, filters.cwdKey, filters.cwdLike);
13555
14079
  }
13556
14080
  if (filters.pathLike != null) {
13557
14081
  const pathQuery = filePathFtsQuery(filters.path);
@@ -13771,11 +14295,11 @@ function deleteLegacyCacheFile() {
13771
14295
  } catch {
13772
14296
  }
13773
14297
  }
13774
- function loadCachedSessions(agentName) {
14298
+ function readCachedSessions(agentName) {
13775
14299
  if (!hasCacheStorage()) {
13776
- return null;
14300
+ return { status: "success", value: null };
13777
14301
  }
13778
- return withCacheDb((db) => {
14302
+ return withCacheDbOutcome((db) => {
13779
14303
  const timestampRow = db.prepare("SELECT timestamp AS value FROM agent_cache WHERE agent_name = ?").get(agentName);
13780
14304
  const timestamp = Number(timestampRow?.value ?? 0);
13781
14305
  if (!timestamp) {
@@ -13801,6 +14325,10 @@ function loadCachedSessions(agentName) {
13801
14325
  return { sessions, meta, timestamp };
13802
14326
  });
13803
14327
  }
14328
+ function loadCachedSessions(agentName) {
14329
+ const outcome = readCachedSessions(agentName);
14330
+ return outcome.status === "success" ? outcome.value : null;
14331
+ }
13804
14332
  function loadCachedSessionHeads(references) {
13805
14333
  if (references.length === 0 || !hasCacheStorage()) return [];
13806
14334
  const unique2 = /* @__PURE__ */ new Map();
@@ -13859,10 +14387,6 @@ function readAgentCacheInitialization(agentName, indexVersion = CACHE_INITIALIZA
13859
14387
  return row?.index_version === indexVersion;
13860
14388
  });
13861
14389
  }
13862
- function isAgentCacheInitialized(agentName, indexVersion = CACHE_INITIALIZATION_VERSION) {
13863
- const outcome = readAgentCacheInitialization(agentName, indexVersion);
13864
- return outcome.status === "success" && outcome.value;
13865
- }
13866
14390
  function markAgentCacheInitialized(agentName, indexVersion = CACHE_INITIALIZATION_VERSION) {
13867
14391
  withCacheDb((db) => {
13868
14392
  db.prepare(
@@ -13876,7 +14400,7 @@ function markAgentCacheInitialized(agentName, indexVersion = CACHE_INITIALIZATIO
13876
14400
  });
13877
14401
  }
13878
14402
  function markAgentFullSyncStarted(agentName) {
13879
- withCacheDb((db) => {
14403
+ const persisted = withCacheDb((db) => {
13880
14404
  db.prepare(
13881
14405
  `
13882
14406
  UPDATE cache_initialization
@@ -13884,7 +14408,9 @@ function markAgentFullSyncStarted(agentName) {
13884
14408
  WHERE agent_name = ?
13885
14409
  `
13886
14410
  ).run(agentName);
14411
+ return true;
13887
14412
  });
14413
+ return persisted ?? false;
13888
14414
  }
13889
14415
  function getAgentFullSyncCursor(agentName) {
13890
14416
  if (!hasCacheStorage()) return null;
@@ -13895,8 +14421,8 @@ function getAgentFullSyncCursor(agentName) {
13895
14421
  return outcome.status === "success" ? outcome.value : null;
13896
14422
  }
13897
14423
  function markAgentFullSyncProgress(agentName, cursor) {
13898
- if (!cursor) return;
13899
- withCacheDb((db) => {
14424
+ if (!cursor) return true;
14425
+ const persisted = withCacheDb((db) => {
13900
14426
  db.prepare(
13901
14427
  `
13902
14428
  INSERT INTO cache_meta(key, value)
@@ -13904,14 +14430,9 @@ function markAgentFullSyncProgress(agentName, cursor) {
13904
14430
  ON CONFLICT(key) DO UPDATE SET value = excluded.value
13905
14431
  `
13906
14432
  ).run(`${FULL_SYNC_CURSOR_PREFIX}${agentName}`, cursor);
14433
+ return true;
13907
14434
  });
13908
- }
13909
- function clearAgentFullSyncCursor(agentName) {
13910
- withCacheDb((db) => {
13911
- db.prepare("DELETE FROM cache_meta WHERE key = ?").run(
13912
- `${FULL_SYNC_CURSOR_PREFIX}${agentName}`
13913
- );
13914
- });
14435
+ return persisted ?? false;
13915
14436
  }
13916
14437
  function readAgentLastFullSyncAt(agentName) {
13917
14438
  if (!hasCacheStorage()) {
@@ -13929,97 +14450,149 @@ function readAgentLastFullSyncAt(agentName) {
13929
14450
  return row?.last_sync_at || null;
13930
14451
  });
13931
14452
  }
13932
- function getAgentLastFullSyncAt(agentName) {
13933
- const outcome = readAgentLastFullSyncAt(agentName);
13934
- return outcome.status === "success" ? outcome.value : null;
13935
- }
13936
14453
  function markAgentFullSyncCompleted(agentName) {
13937
- withCacheDb((db) => {
13938
- db.prepare(
14454
+ const completedAt = Date.now();
14455
+ const persisted = withCacheDb((db) => {
14456
+ db.transaction(() => {
14457
+ db.prepare(
14458
+ `
14459
+ INSERT INTO cache_initialization(agent_name, initialized_at, index_version, last_sync_at)
14460
+ VALUES (?, ?, ?, ?)
14461
+ ON CONFLICT(agent_name) DO UPDATE SET
14462
+ last_sync_at = excluded.last_sync_at
14463
+ `
14464
+ ).run(agentName, completedAt, CACHE_INITIALIZATION_VERSION, completedAt);
14465
+ db.prepare("DELETE FROM cache_meta WHERE key = ?").run(
14466
+ `${FULL_SYNC_CURSOR_PREFIX}${agentName}`
14467
+ );
14468
+ }).immediate();
14469
+ return true;
14470
+ });
14471
+ return persisted ?? false;
14472
+ }
14473
+ function loadCachedSessionEntryBase(db, agentName, sessionId) {
14474
+ const row = db.prepare(
14475
+ `
14476
+ SELECT
14477
+ sessions.*,
14478
+ documents.detail_version AS detail_version
14479
+ FROM sessions
14480
+ LEFT JOIN session_documents AS documents
14481
+ ON documents.agent_name = sessions.agent_name
14482
+ AND documents.session_id = sessions.session_id
14483
+ WHERE sessions.agent_name = ?
14484
+ AND sessions.session_id = ?
14485
+ AND sessions.publication_id IS NULL
13939
14486
  `
13940
- UPDATE cache_initialization
13941
- SET last_sync_at = ?
13942
- WHERE agent_name = ?
14487
+ ).get(agentName, sessionId);
14488
+ if (!row) return null;
14489
+ const pendingReindex = db.prepare("SELECT 1 FROM pending_reindex WHERE agent_name = ? AND session_id = ?").get(agentName, sessionId) != null;
14490
+ const fileActivityRows = db.prepare(
14491
+ `
14492
+ SELECT agent_name, session_id, project_identity_key, path, kind, count, latest_time
14493
+ FROM session_file_activity
14494
+ WHERE agent_name = ? AND session_id = ?
14495
+ ORDER BY latest_time DESC, count DESC, path
14496
+ LIMIT 500
13943
14497
  `
13944
- ).run(Date.now(), agentName);
13945
- });
13946
- clearAgentFullSyncCursor(agentName);
14498
+ ).all(agentName, sessionId);
14499
+ return {
14500
+ data: {
14501
+ ...sessionFromRow(row),
14502
+ reference: { agentName, sessionId },
14503
+ file_activity: fileActivityRows.map((activityRow) => fileActivityFromRow(activityRow))
14504
+ },
14505
+ meta: parseCachedSessionMeta(row.meta_json),
14506
+ detailVersion: typeof row.detail_version === "string" ? row.detail_version : null,
14507
+ pendingReindex
14508
+ };
14509
+ }
14510
+ function readCachedSessionMessageRows(db, agentName, sessionId, startIndex) {
14511
+ return db.prepare(
14512
+ `
14513
+ SELECT
14514
+ message_id,
14515
+ role,
14516
+ time_created,
14517
+ time_completed,
14518
+ agent,
14519
+ mode,
14520
+ model,
14521
+ provider,
14522
+ tokens_json,
14523
+ cost,
14524
+ cost_source,
14525
+ parts_json,
14526
+ parts_format_version,
14527
+ content_chain_digest,
14528
+ subagent_id,
14529
+ nickname
14530
+ FROM messages
14531
+ WHERE agent_name = ? AND session_id = ? AND message_index >= ?
14532
+ ORDER BY message_index
14533
+ `
14534
+ ).all(agentName, sessionId, startIndex);
13947
14535
  }
13948
14536
  function loadCachedSessionRawEntry(agentName, sessionId) {
13949
- if (!hasCacheStorage()) {
13950
- return null;
13951
- }
14537
+ if (!hasCacheStorage()) return null;
13952
14538
  const outcome = withCacheDbReadOnly((db) => {
13953
- const row = db.prepare(
13954
- `
13955
- SELECT
13956
- sessions.*,
13957
- documents.detail_version AS detail_version
13958
- FROM sessions
13959
- LEFT JOIN session_documents AS documents
13960
- ON documents.agent_name = sessions.agent_name
13961
- AND documents.session_id = sessions.session_id
13962
- WHERE sessions.agent_name = ?
13963
- AND sessions.session_id = ?
13964
- AND sessions.publication_id IS NULL
13965
- `
13966
- ).get(agentName, sessionId);
13967
- if (!row) {
13968
- return null;
13969
- }
13970
- const pendingReindex = db.prepare("SELECT 1 FROM pending_reindex WHERE agent_name = ? AND session_id = ?").get(agentName, sessionId) != null;
13971
- const messageRows = db.prepare(
13972
- `
13973
- SELECT
13974
- message_id,
13975
- role,
13976
- time_created,
13977
- time_completed,
13978
- agent,
13979
- mode,
13980
- model,
13981
- provider,
13982
- tokens_json,
13983
- cost,
13984
- cost_source,
13985
- parts_json,
13986
- parts_format_version,
13987
- subagent_id,
13988
- nickname
13989
- FROM messages
13990
- WHERE agent_name = ? AND session_id = ?
13991
- ORDER BY message_index
13992
- `
13993
- ).all(agentName, sessionId);
13994
- const head = sessionFromRow(row);
13995
- const fileActivityRows = db.prepare(
14539
+ const entry = loadCachedSessionEntryBase(db, agentName, sessionId);
14540
+ return entry ? { ...entry, messageRows: readCachedSessionMessageRows(db, agentName, sessionId, 0) } : null;
14541
+ });
14542
+ return outcome.status === "success" ? outcome.value : null;
14543
+ }
14544
+ function readCachedSessionMessageDigest(db, agentName, sessionId, messageCount) {
14545
+ if (!Number.isSafeInteger(messageCount) || messageCount <= 0) return null;
14546
+ const row = db.prepare(
14547
+ `
14548
+ SELECT content_chain_digest
14549
+ FROM messages
14550
+ WHERE agent_name = ? AND session_id = ? AND message_index = ?
13996
14551
  `
13997
- SELECT agent_name, session_id, project_identity_key, path, kind, count, latest_time
13998
- FROM session_file_activity
13999
- WHERE agent_name = ? AND session_id = ?
14000
- ORDER BY latest_time DESC, count DESC, path
14001
- LIMIT 500
14552
+ ).get(agentName, sessionId, messageCount - 1);
14553
+ return typeof row?.content_chain_digest === "string" ? row.content_chain_digest : null;
14554
+ }
14555
+ function readCachedSessionCursor(agentName, sessionId, read) {
14556
+ if (!hasCacheStorage()) return null;
14557
+ const outcome = withCacheDbReadOnly(
14558
+ (db) => db.transaction(() => {
14559
+ const entryBase = loadCachedSessionEntryBase(db, agentName, sessionId);
14560
+ if (!entryBase) return null;
14561
+ const messageState = db.prepare(
14002
14562
  `
14003
- ).all(agentName, sessionId);
14004
- return {
14005
- data: {
14006
- ...head,
14007
- reference: { agentName, sessionId },
14008
- file_activity: fileActivityRows.map((activityRow) => fileActivityFromRow(activityRow))
14009
- },
14010
- messageRows,
14011
- meta: parseCachedSessionMeta(row.meta_json),
14012
- detailVersion: typeof row.detail_version === "string" ? row.detail_version : null,
14013
- pendingReindex
14014
- };
14015
- });
14563
+ SELECT
14564
+ COUNT(*) AS message_count,
14565
+ (
14566
+ SELECT content_chain_digest
14567
+ FROM messages
14568
+ WHERE agent_name = ? AND session_id = ?
14569
+ ORDER BY message_index DESC
14570
+ LIMIT 1
14571
+ ) AS content_chain_digest
14572
+ FROM messages
14573
+ WHERE agent_name = ? AND session_id = ?
14574
+ `
14575
+ ).get(agentName, sessionId, agentName, sessionId);
14576
+ const entry = {
14577
+ ...entryBase,
14578
+ messageCount: Number(messageState.message_count ?? 0),
14579
+ messageDigest: typeof messageState.content_chain_digest === "string" ? messageState.content_chain_digest : null
14580
+ };
14581
+ return read(entry, {
14582
+ messageDigest: (messageCount) => readCachedSessionMessageDigest(db, agentName, sessionId, messageCount),
14583
+ messageRows: (startIndex) => Number.isSafeInteger(startIndex) && startIndex >= 0 ? readCachedSessionMessageRows(db, agentName, sessionId, startIndex) : []
14584
+ });
14585
+ })()
14586
+ );
14016
14587
  return outcome.status === "success" ? outcome.value : null;
14017
14588
  }
14018
14589
  function saveCachedSessions(agentName, sessions, meta = {}, options = {}) {
14590
+ assertSessionProjectIdentities(agentName, sessions);
14019
14591
  const persisted = withCacheDb((db) => {
14020
- db.transaction(
14021
- () => writeCachedSessionSnapshot(db, agentName, sessions, meta, options)
14022
- ).immediate();
14592
+ db.transaction(() => {
14593
+ writeCachedSessionSnapshot(db, agentName, sessions, meta, options);
14594
+ advanceAnalyticsRevision(db);
14595
+ }).immediate();
14023
14596
  deleteLegacyCacheFile();
14024
14597
  return true;
14025
14598
  });
@@ -14032,6 +14605,12 @@ function writeCachedSessionSnapshot(db, agentName, sessions, meta = {}, options
14032
14605
  "DELETE FROM session_documents WHERE agent_name = ? AND session_id = ?"
14033
14606
  );
14034
14607
  const deleteMessages = db.prepare("DELETE FROM messages WHERE agent_name = ? AND session_id = ?");
14608
+ const deleteModelCost = db.prepare(
14609
+ "DELETE FROM session_model_cost WHERE agent_name = ? AND session_id = ?"
14610
+ );
14611
+ const deleteCostSummary = db.prepare(
14612
+ "DELETE FROM session_cost_summary WHERE agent_name = ? AND session_id = ?"
14613
+ );
14035
14614
  const deleteMessageTools = db.prepare(
14036
14615
  "DELETE FROM message_tools WHERE agent_name = ? AND session_id = ?"
14037
14616
  );
@@ -14058,6 +14637,8 @@ function writeCachedSessionSnapshot(db, agentName, sessions, meta = {}, options
14058
14637
  deleteSearchDocument.run(agentName, sessionId);
14059
14638
  deleteMessageTools.run(agentName, sessionId);
14060
14639
  deleteMessages.run(agentName, sessionId);
14640
+ deleteModelCost.run(agentName, sessionId);
14641
+ deleteCostSummary.run(agentName, sessionId);
14061
14642
  deleteFileActivity.run(agentName, sessionId);
14062
14643
  deleteSession.run(agentName, sessionId);
14063
14644
  }
@@ -14075,10 +14656,17 @@ function writeCachedSessionSnapshot(db, agentName, sessions, meta = {}, options
14075
14656
  });
14076
14657
  }
14077
14658
  function saveCachedSessionChanges(agentName, changes, removedSessionIds, meta = {}) {
14659
+ assertSessionProjectIdentities(
14660
+ agentName,
14661
+ changes.map(({ session }) => session)
14662
+ );
14078
14663
  const persisted = withCacheDb((db) => {
14079
- db.transaction(
14080
- () => writeCachedSessionChanges(db, agentName, changes, removedSessionIds, meta)
14081
- ).immediate();
14664
+ db.transaction(() => {
14665
+ writeCachedSessionChanges(db, agentName, changes, removedSessionIds, meta);
14666
+ if (changes.length > 0 || removedSessionIds.length > 0) {
14667
+ advanceAnalyticsRevision(db);
14668
+ }
14669
+ }).immediate();
14082
14670
  deleteLegacyCacheFile();
14083
14671
  return true;
14084
14672
  });
@@ -14090,6 +14678,12 @@ function writeCachedSessionChanges(db, agentName, changes, removedSessionIds, me
14090
14678
  "DELETE FROM session_documents WHERE agent_name = ? AND session_id = ?"
14091
14679
  );
14092
14680
  const deleteMessages = db.prepare("DELETE FROM messages WHERE agent_name = ? AND session_id = ?");
14681
+ const deleteModelCost = db.prepare(
14682
+ "DELETE FROM session_model_cost WHERE agent_name = ? AND session_id = ?"
14683
+ );
14684
+ const deleteCostSummary = db.prepare(
14685
+ "DELETE FROM session_cost_summary WHERE agent_name = ? AND session_id = ?"
14686
+ );
14093
14687
  const deleteMessageTools = db.prepare(
14094
14688
  "DELETE FROM message_tools WHERE agent_name = ? AND session_id = ?"
14095
14689
  );
@@ -14107,6 +14701,8 @@ function writeCachedSessionChanges(db, agentName, changes, removedSessionIds, me
14107
14701
  deleteSearchDocument.run(agentName, sessionId);
14108
14702
  deleteMessageTools.run(agentName, sessionId);
14109
14703
  deleteMessages.run(agentName, sessionId);
14704
+ deleteModelCost.run(agentName, sessionId);
14705
+ deleteCostSummary.run(agentName, sessionId);
14110
14706
  deleteFileActivity.run(agentName, sessionId);
14111
14707
  deleteSession.run(agentName, sessionId);
14112
14708
  }
@@ -14136,17 +14732,26 @@ function clearCache() {
14136
14732
  return;
14137
14733
  }
14138
14734
  withCacheDb((db) => {
14139
- db.exec(`
14140
- DELETE FROM agent_cache;
14141
- DELETE FROM cache_initialization;
14142
- DELETE FROM cached_sessions;
14143
- DELETE FROM session_documents;
14144
- DELETE FROM session_file_activity;
14145
- DELETE FROM message_tools;
14146
- DELETE FROM messages;
14147
- DELETE FROM sessions;
14148
- DELETE FROM project_sessions;
14149
- `);
14735
+ db.transaction(() => {
14736
+ db.exec(`
14737
+ DELETE FROM agent_cache;
14738
+ DELETE FROM cache_initialization;
14739
+ DELETE FROM cached_sessions;
14740
+ DELETE FROM pending_reindex;
14741
+ DELETE FROM search_index_publication_entries;
14742
+ DELETE FROM session_documents;
14743
+ DELETE FROM session_file_activity;
14744
+ DELETE FROM message_tools;
14745
+ DELETE FROM messages;
14746
+ DELETE FROM session_model_cost;
14747
+ DELETE FROM session_cost_summary;
14748
+ DELETE FROM sessions;
14749
+ DELETE FROM project_sessions;
14750
+ -- analytics_revision is an invalidation counter, not cached data.
14751
+ DELETE FROM cache_meta WHERE key <> 'analytics_revision';
14752
+ `);
14753
+ advanceAnalyticsRevision(db);
14754
+ }).immediate();
14150
14755
  });
14151
14756
  closeCacheStorage();
14152
14757
  deleteLegacyCacheFile();
@@ -14163,13 +14768,13 @@ function clearCache() {
14163
14768
  }
14164
14769
  }
14165
14770
  }
14166
- function attachMissingProjectIdentities(sessions, resolve22 = (directory) => computeIdentityProjection(directory, realFs)) {
14771
+ function attachMissingProjectIdentities(sessions, resolve3 = (directory) => computeIdentityProjection(directory, realFs)) {
14167
14772
  const projections = /* @__PURE__ */ new Map();
14168
14773
  return sessions.map((session) => {
14169
14774
  const directory = normalizeProjectDirectory(session.directory);
14170
14775
  let projection = projections.get(directory);
14171
14776
  if (!projection) {
14172
- projection = resolve22(directory);
14777
+ projection = resolve3(directory);
14173
14778
  projections.set(directory, projection);
14174
14779
  }
14175
14780
  const identity = session.project_identity;
@@ -14194,28 +14799,61 @@ function buildAgentCacheMeta(agent, sessionIds) {
14194
14799
  }
14195
14800
  return meta;
14196
14801
  }
14802
+ function signatureValues(value, spec) {
14803
+ const values = [];
14804
+ for (const key of Object.keys(spec)) {
14805
+ values.push(...spec[key](value));
14806
+ }
14807
+ return values;
14808
+ }
14809
+ function objectSignatureValues(value, spec) {
14810
+ const keys = Object.keys(spec);
14811
+ if (!value) return keys.map(() => null);
14812
+ return keys.map((key) => spec[key](value));
14813
+ }
14814
+ var SESSION_REFERENCE_SIGNATURE_SPEC = {
14815
+ agentName: (reference) => reference.agentName,
14816
+ sessionId: (reference) => reference.sessionId
14817
+ };
14818
+ var PROJECT_IDENTITY_SIGNATURE_SPEC = {
14819
+ kind: (identity) => identity.kind,
14820
+ key: (identity) => identity.key,
14821
+ displayName: (identity) => identity.displayName
14822
+ };
14823
+ var SESSION_STATS_SIGNATURE_SPEC = {
14824
+ message_count: (stats) => stats.message_count,
14825
+ total_input_tokens: (stats) => stats.total_input_tokens,
14826
+ total_output_tokens: (stats) => stats.total_output_tokens,
14827
+ total_cost: (stats) => stats.total_cost,
14828
+ cost_source: (stats) => stats.cost_source ?? null,
14829
+ total_tokens: (stats) => stats.total_tokens ?? 0,
14830
+ total_cache_read_tokens: (stats) => stats.total_cache_read_tokens ?? 0,
14831
+ total_cache_create_tokens: (stats) => stats.total_cache_create_tokens ?? 0
14832
+ };
14833
+ function modelUsageSignature(modelUsage) {
14834
+ if (!modelUsage) return null;
14835
+ return Object.entries(modelUsage).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([model, tokens]) => [model, tokens]);
14836
+ }
14837
+ var SESSION_HEAD_SIGNATURE_SPEC = {
14838
+ slug: (session) => [session.slug],
14839
+ title: (session) => [session.title],
14840
+ directory: (session) => [session.directory],
14841
+ parent_reference: (session) => objectSignatureValues(session.parent_reference, SESSION_REFERENCE_SIGNATURE_SPEC),
14842
+ project_identity: (session) => objectSignatureValues(session.project_identity, PROJECT_IDENTITY_SIGNATURE_SPEC),
14843
+ project_identity_resolver_revision: (session) => [
14844
+ session.project_identity_resolver_revision ?? null
14845
+ ],
14846
+ project_identity_input_signature: (session) => [session.project_identity_input_signature ?? null],
14847
+ time_created: (session) => [session.time_created],
14848
+ time_updated: (session) => [session.time_updated ?? session.time_created],
14849
+ stats: (session) => objectSignatureValues(session.stats, SESSION_STATS_SIGNATURE_SPEC),
14850
+ model_usage: (session) => [modelUsageSignature(session.model_usage)],
14851
+ smart_tags: (session) => [session.smart_tags ? [...session.smart_tags].sort() : null],
14852
+ smart_tags_source_updated_at: (session) => [session.smart_tags_source_updated_at ?? null],
14853
+ smart_tags_classifier_revision: (session) => [session.smart_tags_classifier_revision ?? null]
14854
+ };
14197
14855
  function sessionSignature(session) {
14198
- return JSON.stringify([
14199
- session.title,
14200
- session.directory,
14201
- session.parent_reference?.agentName ?? null,
14202
- session.parent_reference?.sessionId ?? null,
14203
- session.time_created,
14204
- session.time_updated ?? session.time_created,
14205
- session.stats.message_count,
14206
- session.stats.total_input_tokens,
14207
- session.stats.total_output_tokens,
14208
- session.stats.total_cost,
14209
- session.stats.total_tokens ?? 0,
14210
- session.project_identity?.kind ?? null,
14211
- session.project_identity?.key ?? null,
14212
- session.project_identity?.displayName ?? null,
14213
- session.project_identity_resolver_revision ?? null,
14214
- session.project_identity_input_signature ?? null,
14215
- session.smart_tags ? [...session.smart_tags].sort() : null,
14216
- session.smart_tags_source_updated_at ?? null,
14217
- session.smart_tags_classifier_revision ?? null
14218
- ]);
14856
+ return JSON.stringify(signatureValues(session, SESSION_HEAD_SIGNATURE_SPEC));
14219
14857
  }
14220
14858
  function sortSessions(sessions) {
14221
14859
  return sortSessionsByActivity(sessions);
@@ -14286,7 +14924,20 @@ function saveCachedSessionDiff(agent, cachedSessions, updatedSessions, changedId
14286
14924
  const diff = computeSessionDiff(cachedSessions, updatedSessions, changedIds, sessionSignature);
14287
14925
  const explicitRemovals = new Set(explicitRemovedSessionIds);
14288
14926
  const removedSessionIds = completeness === "complete" ? diff.removedSessionIds : diff.removedSessionIds.filter((sessionId) => explicitRemovals.has(sessionId));
14289
- saveCachedSessionChanges(agent.name, diff.changes, removedSessionIds, buildAgentCacheMeta(agent));
14927
+ const persisted = saveCachedSessionChanges(
14928
+ agent.name,
14929
+ diff.changes,
14930
+ removedSessionIds,
14931
+ buildAgentCacheMeta(agent)
14932
+ );
14933
+ if (persisted === false) {
14934
+ getCoreDiagnostics()?.warn("cache.save_failed", {
14935
+ agent: agent.name,
14936
+ changed_sessions: diff.changes.length,
14937
+ removed_sessions: removedSessionIds.length
14938
+ });
14939
+ }
14940
+ return persisted;
14290
14941
  }
14291
14942
  function getSmartTagWorkerCount(sessionCount) {
14292
14943
  if (sessionCount < 50) return 1;
@@ -14357,30 +15008,46 @@ function ensureSessionTagsSync(agent, sessions, onProgress, classifierRevision =
14357
15008
  });
14358
15009
  return { sessions: tagged, changed, timing };
14359
15010
  }
15011
+ var SMART_TAG_WORKER_TIMEOUT_MS = 3e5;
14360
15012
  async function classifySessionTagsInWorker(workerUrl, agentName, sessionIds, meta) {
14361
- return new Promise((resolveWorker, rejectWorker) => {
14362
- const worker = new Worker(workerUrl, {
14363
- workerData: {
14364
- pricingGenerationId: getPricingGeneration().id,
14365
- agentName,
14366
- sessionIds,
14367
- meta
14368
- }
14369
- });
14370
- worker.on("message", (message) => {
14371
- if (isWorkerLogMessage(message)) {
14372
- relayWorkerLogMessage(message);
14373
- return;
14374
- }
14375
- resolveWorker(message);
15013
+ const worker = new Worker(workerUrl, {
15014
+ workerData: {
15015
+ pricingGenerationId: getPricingGeneration().id,
15016
+ agentName,
15017
+ sessionIds,
15018
+ meta
15019
+ }
15020
+ });
15021
+ let timer;
15022
+ try {
15023
+ return await new Promise((resolveWorker, rejectWorker) => {
15024
+ timer = setTimeout(() => {
15025
+ rejectWorker(
15026
+ new Error(`Smart tag worker timed out after ${SMART_TAG_WORKER_TIMEOUT_MS}ms`)
15027
+ );
15028
+ }, SMART_TAG_WORKER_TIMEOUT_MS);
15029
+ worker.on("message", (message) => {
15030
+ if (isWorkerLogMessage(message)) {
15031
+ relayWorkerLogMessage(message);
15032
+ return;
15033
+ }
15034
+ const results = message;
15035
+ if (results.length === 0 && sessionIds.length > 0) {
15036
+ rejectWorker(new Error("Smart tag worker returned no results"));
15037
+ return;
15038
+ }
15039
+ resolveWorker(results);
15040
+ });
15041
+ worker.once("error", rejectWorker);
15042
+ worker.once("exit", (code) => {
15043
+ rejectWorker(new Error(`Smart tag worker exited with code ${code} before responding`));
15044
+ });
14376
15045
  });
14377
- worker.once("error", rejectWorker);
14378
- worker.once("exit", (code) => {
14379
- if (code !== 0) {
14380
- rejectWorker(new Error(`Smart tag worker exited with code ${code}`));
14381
- }
15046
+ } finally {
15047
+ if (timer) clearTimeout(timer);
15048
+ worker.terminate().catch(() => {
14382
15049
  });
14383
- });
15050
+ }
14384
15051
  }
14385
15052
  function relayWorkerLogMessage(message) {
14386
15053
  const detail = {
@@ -14455,18 +15122,19 @@ async function finalizeAgentScan(agent, sessions, context) {
14455
15122
  tagged = options.includeSmartTags === false ? tagged : await ensureSessionTags(agent, sessionsWithIdentity, options.smartTagWorkerUrl);
14456
15123
  timing.tags = performance.now() - tagsStart;
14457
15124
  }
15125
+ let cachePersistence = "not-requested";
14458
15126
  if (options.writeCache !== false) {
14459
15127
  if (finalization.kind === "incremental") {
14460
- saveCachedSessionDiff(
15128
+ cachePersistence = saveCachedSessionDiff(
14461
15129
  agent,
14462
15130
  finalization.cached.sessions,
14463
15131
  tagged.sessions,
14464
15132
  finalization.changedIds,
14465
15133
  context.completeness,
14466
15134
  finalization.explicitRemovedSessionIds
14467
- );
15135
+ ) === false ? "failed" : "persisted";
14468
15136
  } else if (finalization.kind === "unchanged" && (identityChanged || tagged.changed)) {
14469
- saveCachedSessionDiff(agent, finalization.cached.sessions, tagged.sessions);
15137
+ cachePersistence = saveCachedSessionDiff(agent, finalization.cached.sessions, tagged.sessions) === false ? "failed" : "persisted";
14470
15138
  }
14471
15139
  }
14472
15140
  if (isIncremental) {
@@ -14478,10 +15146,11 @@ async function finalizeAgentScan(agent, sessions, context) {
14478
15146
  status: context.completeness,
14479
15147
  agent,
14480
15148
  heads,
15149
+ cachePersistence,
14481
15150
  fromCache: true,
14482
15151
  ...isIncremental ? { refreshed: true } : {},
14483
15152
  timing,
14484
- cacheTimestamp: isIncremental ? finalization.cacheTimestamp : finalization.cached.timestamp
15153
+ cacheTimestamp: isIncremental && cachePersistence === "persisted" ? finalization.cacheTimestamp : finalization.cached.timestamp
14485
15154
  };
14486
15155
  }
14487
15156
  async function refreshCachedFileAgent(agent, cached, options, timing, agentStart, onProgress) {
@@ -14597,6 +15266,7 @@ async function scanAgentSmart(agent, options, onProgress) {
14597
15266
  )
14598
15267
  );
14599
15268
  timing.scan = performance.now() - t2;
15269
+ agent.commitChangeCheck();
14600
15270
  return finalizeAgentScan(agent, updatedSessions, {
14601
15271
  finalization: {
14602
15272
  kind: "incremental",
@@ -14611,6 +15281,7 @@ async function scanAgentSmart(agent, options, onProgress) {
14611
15281
  onProgress
14612
15282
  });
14613
15283
  }
15284
+ agent.commitChangeCheck();
14614
15285
  return finalizeAgentScan(agent, cached.sessions, {
14615
15286
  finalization: { kind: "unchanged", cached },
14616
15287
  options,
@@ -14661,15 +15332,20 @@ async function scanAgentFull(agent, options, onProgress, timing = { total: 0 },
14661
15332
  const tagged = options.includeSmartTags === false ? { sessions: headsWithIdentity, changed: false } : await ensureSessionTags(agent, headsWithIdentity, options.smartTagWorkerUrl);
14662
15333
  timing.tags = performance.now() - t2;
14663
15334
  const meta = buildAgentCacheMeta(agent);
15335
+ let cachePersistence = "not-requested";
14664
15336
  if (options.writeCache !== false) {
14665
15337
  const isFullWindow = options.from == null && options.to == null;
14666
15338
  const persisted = saveCachedSessions(agent.name, tagged.sessions, meta, {
14667
15339
  completeness: isFullWindow && sourceFailures.length === 0 ? "complete" : "partial"
14668
15340
  });
14669
- if (persisted) {
14670
- if (isFullWindow && sourceFailures.length === 0) markAgentFullSyncCompleted(agent.name);
15341
+ if (persisted !== false) {
15342
+ cachePersistence = "persisted";
14671
15343
  markAgentCacheInitialized(agent.name);
15344
+ if (isFullWindow && sourceFailures.length === 0 && !markAgentFullSyncCompleted(agent.name)) {
15345
+ getCoreDiagnostics()?.warn("cache.full_sync_marker_failed", { agent: agent.name });
15346
+ }
14672
15347
  } else {
15348
+ cachePersistence = "failed";
14673
15349
  getCoreDiagnostics()?.warn("cache.save_failed", {
14674
15350
  agent: agent.name,
14675
15351
  sessions: tagged.sessions.length
@@ -14683,6 +15359,7 @@ async function scanAgentFull(agent, options, onProgress, timing = { total: 0 },
14683
15359
  status: options.from == null && options.to == null && sourceFailures.length === 0 ? "complete" : "partial",
14684
15360
  agent,
14685
15361
  heads: filtered,
15362
+ cachePersistence,
14686
15363
  fromCache: false,
14687
15364
  timing
14688
15365
  };
@@ -14718,6 +15395,7 @@ async function scanSessions(options = {}, onProgress) {
14718
15395
  const allSessions = [];
14719
15396
  const availableAgents = [];
14720
15397
  const cacheTimestamps = {};
15398
+ const cacheFailures = {};
14721
15399
  const scanFailures = {};
14722
15400
  const agentFilter = options.agents?.length ? new Set(options.agents.map((a) => a.toLowerCase())) : null;
14723
15401
  const agentsToScan = agents.filter((agent) => {
@@ -14741,6 +15419,9 @@ async function scanSessions(options = {}, onProgress) {
14741
15419
  } else {
14742
15420
  byAgent[result.agent.name] = result.heads;
14743
15421
  allSessions.push(...result.heads);
15422
+ if (result.cachePersistence === "failed") {
15423
+ cacheFailures[result.agent.name] = { agentName: result.agent.name };
15424
+ }
14744
15425
  }
14745
15426
  if (result.timing) {
14746
15427
  timings[result.agent.name] = result.timing;
@@ -14757,11 +15438,11 @@ async function scanSessions(options = {}, onProgress) {
14757
15438
  agents: availableAgents,
14758
15439
  timings,
14759
15440
  cacheTimestamps: Object.keys(cacheTimestamps).length > 0 ? cacheTimestamps : void 0,
15441
+ cacheFailures: Object.keys(cacheFailures).length > 0 ? cacheFailures : void 0,
14760
15442
  scanFailures: Object.keys(scanFailures).length > 0 ? scanFailures : void 0
14761
15443
  };
14762
15444
  }
14763
15445
  var sessionDetailLookups = /* @__PURE__ */ new WeakMap();
14764
- var MESSAGE_CURSOR_VERSION = 1;
14765
15446
  var MAX_MESSAGE_CURSOR_LENGTH = 512;
14766
15447
  function parseMessageCursor(value) {
14767
15448
  if (!value || value.length > MAX_MESSAGE_CURSOR_LENGTH) return null;
@@ -14782,49 +15463,36 @@ function encodeMessageCursor(count, digest) {
14782
15463
  "base64url"
14783
15464
  );
14784
15465
  }
14785
- function createMessageCursorHash(reference) {
14786
- return createHash22("sha256").update("codesesh-session-messages-v1\0").update(JSON.stringify([reference.agentName, reference.sessionId])).update("\n");
14787
- }
14788
- function updateMessageCursorField(hash, value) {
14789
- if (value == null) {
14790
- hash.update("n;");
14791
- return;
14792
- }
14793
- const text = String(value);
14794
- hash.update(`v${text.length}:`).update(text).update(";");
14795
- }
14796
- function updateMessageCursorHash(hash, row) {
14797
- updateMessageCursorField(hash, row.message_id);
14798
- updateMessageCursorField(hash, row.role);
14799
- updateMessageCursorField(hash, row.time_created);
14800
- updateMessageCursorField(hash, row.time_completed);
14801
- updateMessageCursorField(hash, row.agent);
14802
- updateMessageCursorField(hash, row.mode);
14803
- updateMessageCursorField(hash, row.model);
14804
- updateMessageCursorField(hash, row.provider);
14805
- updateMessageCursorField(hash, row.tokens_json);
14806
- updateMessageCursorField(hash, row.cost);
14807
- updateMessageCursorField(hash, row.cost_source);
14808
- updateMessageCursorField(hash, row.parts_json);
14809
- updateMessageCursorField(hash, row.parts_format_version);
14810
- updateMessageCursorField(hash, row.subagent_id);
14811
- updateMessageCursorField(hash, row.nickname);
14812
- hash.update("\n");
14813
- }
14814
- function projectMessageStream(reference, rows, requestedCursor) {
15466
+ function loadCachedMessageStream(reference, entry, cursor, requestedCursor) {
14815
15467
  const requested = parseMessageCursor(requestedCursor);
14816
- const hash = createMessageCursorHash(reference);
14817
- let requestedDigest = requested?.count === 0 ? hash.copy().digest("hex") : null;
14818
- for (let index = 0; index < rows.length; index += 1) {
14819
- updateMessageCursorHash(hash, rows[index]);
14820
- if (requested?.count === index + 1) requestedDigest = hash.copy().digest("hex");
14821
- }
14822
- const digest = hash.digest("hex");
14823
- const canAppend = requested !== null && requested.count <= rows.length && requestedDigest === requested.digest;
15468
+ const initialDigest = initialMessageCursorDigest(reference);
15469
+ const hasStoredChain = entry.messageCount === 0 || entry.messageDigest !== null;
15470
+ const canCheckAppend = hasStoredChain && requested !== null && requested.count <= entry.messageCount;
15471
+ const requestedDigest = canCheckAppend && requested ? requested.count === 0 ? initialDigest : cursor.messageDigest(requested.count) : null;
15472
+ const canAppend = canCheckAppend && requested !== null && requestedDigest === requested.digest;
15473
+ let startIndex = canAppend ? requested.count : 0;
15474
+ let update = canAppend ? "append" : "reset";
15475
+ let messageRows = entry.messageCount === 0 ? [] : cursor.messageRows(startIndex);
15476
+ let messageCount = entry.messageCount;
15477
+ let digest = entry.messageCount === 0 ? initialDigest : entry.messageDigest;
15478
+ if (messageRows.length !== entry.messageCount - startIndex) {
15479
+ startIndex = 0;
15480
+ update = "reset";
15481
+ messageRows = cursor.messageRows(startIndex);
15482
+ messageCount = messageRows.length;
15483
+ digest = null;
15484
+ }
15485
+ if (!digest) {
15486
+ digest = computeMessageCursorDigest(
15487
+ reference,
15488
+ messageRows.map((row) => messageCursorContentFromCachedRow(row))
15489
+ );
15490
+ }
14824
15491
  return {
14825
- cursor: encodeMessageCursor(rows.length, digest),
14826
- startIndex: canAppend ? requested.count : 0,
14827
- update: canAppend ? "append" : "reset"
15492
+ cursor: encodeMessageCursor(messageCount, digest),
15493
+ update,
15494
+ messageRows,
15495
+ messageCount
14828
15496
  };
14829
15497
  }
14830
15498
  function sessionReferenceKey(agentName, sessionId) {
@@ -14859,14 +15527,14 @@ function getSessionDetailContext(scanResult, reference) {
14859
15527
  )
14860
15528
  };
14861
15529
  }
14862
- function cachedDetailState(cachedEntry, currentMeta) {
14863
- if (!cachedEntry || cachedEntry.messageRows.length === 0 && cachedEntry.data.stats.message_count > 0) {
15530
+ function cachedDetailState(cachedEntry, currentMeta, messageCount) {
15531
+ if (!cachedEntry || messageCount === 0 && cachedEntry.data.stats.message_count > 0) {
14864
15532
  return "missing";
14865
15533
  }
14866
15534
  return !cachedEntry.pendingReindex && cachedEntry.detailVersion === sessionDetailVersion(currentMeta) ? "fresh" : "stale";
14867
15535
  }
14868
- function getProjectIdentity(data, head) {
14869
- return head?.project_identity ?? data.project_identity ?? computeIdentity(data.directory, realFs);
15536
+ function getProjectIdentity(data, head, fallback) {
15537
+ return head?.project_identity ?? data.project_identity ?? fallback ?? null;
14870
15538
  }
14871
15539
  function getSmartTags(data) {
14872
15540
  if (Array.isArray(data.smart_tags) && data.smart_tags_classifier_revision === SMART_TAG_CLASSIFIER_REVISION) {
@@ -14874,10 +15542,14 @@ function getSmartTags(data) {
14874
15542
  }
14875
15543
  return classifySessionTags(data);
14876
15544
  }
14877
- function materializeStructuredSessionDetail(context, reference, cachedEntry = loadCachedSessionRawEntry(reference.agentName, reference.sessionId)) {
15545
+ function materializeStructuredSessionDetail(context, reference, cachedEntry = loadCachedSessionRawEntry(reference.agentName, reference.sessionId), identityFallback) {
14878
15546
  const { agent, head } = context;
14879
15547
  const currentMeta = head ? agent.getSessionMetaMap().get(reference.sessionId) : void 0;
14880
- const cacheState = cachedDetailState(cachedEntry, currentMeta);
15548
+ const cacheState = cachedDetailState(
15549
+ cachedEntry,
15550
+ currentMeta,
15551
+ cachedEntry?.messageRows.length ?? 0
15552
+ );
14881
15553
  let useCache = cacheState === "fresh";
14882
15554
  let freshness = useCache ? "fresh" : void 0;
14883
15555
  let data = useCache ? {
@@ -14912,7 +15584,8 @@ function materializeStructuredSessionDetail(context, reference, cachedEntry = lo
14912
15584
  if (!data) {
14913
15585
  return { status: "not-ready" };
14914
15586
  }
14915
- const projectIdentity = getProjectIdentity(data, head);
15587
+ const projectIdentity = getProjectIdentity(data, head, identityFallback);
15588
+ if (!projectIdentity) return { status: "needs-identity", directory: data.directory };
14916
15589
  const fileActivity = data.file_activity ?? (useCache ? listSessionFileActivity(reference.agentName, reference.sessionId) : extractSessionFileActivity(
14917
15590
  reference.agentName,
14918
15591
  reference.sessionId,
@@ -14935,23 +15608,60 @@ function materializeStructuredSessionDetail(context, reference, cachedEntry = lo
14935
15608
  }
14936
15609
  };
14937
15610
  }
14938
- function* serializeCachedMessages(entry, startIndex = 0) {
14939
- for (let index = startIndex; index < entry.messageRows.length; index += 1) {
14940
- yield messageJsonFromCachedRow(entry.messageRows[index]);
15611
+ function* serializeCachedMessages(messageRows) {
15612
+ for (const messageRow of messageRows) {
15613
+ yield messageJsonFromCachedRow(messageRow);
14941
15614
  }
14942
15615
  }
14943
15616
  function materializeSessionDetailResponse(scanResult, reference, options = {}) {
14944
15617
  const context = getSessionDetailContext(scanResult, reference);
14945
15618
  if (!context) return { status: "unknown-agent" };
14946
- const cachedEntry = loadCachedSessionRawEntry(reference.agentName, reference.sessionId);
14947
15619
  const currentMeta = context.head ? context.agent.getSessionMetaMap().get(reference.sessionId) : void 0;
14948
- const cacheState = cachedDetailState(cachedEntry, currentMeta);
15620
+ const startedAt = performance.now();
15621
+ const cursorRead = readCachedSessionCursor(
15622
+ reference.agentName,
15623
+ reference.sessionId,
15624
+ (entry, cursor) => {
15625
+ const cacheState2 = cachedDetailState(entry, currentMeta, entry.messageCount);
15626
+ const stream2 = cacheState2 === "fresh" && entry.data.smart_tags != null && entry.data.smart_tags_classifier_revision === SMART_TAG_CLASSIFIER_REVISION ? loadCachedMessageStream(reference, entry, cursor, options.messageCursor) : null;
15627
+ return { entry, cacheState: cacheState2, stream: stream2 };
15628
+ }
15629
+ );
15630
+ const cachedEntry = cursorRead?.entry ?? null;
15631
+ const cacheState = cursorRead?.cacheState ?? "missing";
14949
15632
  if (!cachedEntry || cacheState !== "fresh" || cachedEntry.data.smart_tags == null || cachedEntry.data.smart_tags_classifier_revision !== SMART_TAG_CLASSIFIER_REVISION) {
14950
- const result = materializeStructuredSessionDetail(context, reference, cachedEntry);
15633
+ const result = materializeStructuredSessionDetail(
15634
+ context,
15635
+ reference,
15636
+ void 0,
15637
+ options.projectIdentityFallback
15638
+ );
14951
15639
  return result.status === "found" ? { ...result, data: { ...result.data, message_update: "reset" } } : result;
14952
15640
  }
14953
15641
  const data = cachedEntry.data;
14954
- const stream = projectMessageStream(reference, cachedEntry.messageRows, options.messageCursor);
15642
+ const projectIdentity = getProjectIdentity(data, context.head, options.projectIdentityFallback);
15643
+ if (!projectIdentity) return { status: "needs-identity", directory: data.directory };
15644
+ const stream = cursorRead?.stream;
15645
+ if (!stream) {
15646
+ const result = materializeStructuredSessionDetail(
15647
+ context,
15648
+ reference,
15649
+ void 0,
15650
+ options.projectIdentityFallback
15651
+ );
15652
+ return result.status === "found" ? { ...result, data: { ...result.data, message_update: "reset" } } : result;
15653
+ }
15654
+ const partsJsonBytes = stream.messageRows.reduce(
15655
+ (total, row) => total + Buffer.byteLength(String(row.parts_json)),
15656
+ 0
15657
+ );
15658
+ getCoreDiagnostics()?.info?.("session_detail.cursor_stream", {
15659
+ update: stream.update,
15660
+ message_count: stream.messageCount,
15661
+ sent_message_count: stream.messageRows.length,
15662
+ parts_json_bytes: partsJsonBytes,
15663
+ duration_ms: Math.round(performance.now() - startedAt)
15664
+ });
14955
15665
  return {
14956
15666
  status: "found-json",
14957
15667
  data: {
@@ -14960,13 +15670,13 @@ function materializeSessionDetailResponse(scanResult, reference, options = {}) {
14960
15670
  detail_freshness: "fresh",
14961
15671
  message_cursor: stream.cursor,
14962
15672
  message_update: stream.update,
14963
- project_identity: getProjectIdentity(data, context.head),
15673
+ project_identity: projectIdentity,
14964
15674
  smart_tags_source_updated_at: getSmartTagSourceTimestamp(data),
14965
15675
  file_activity: data.file_activity ?? listSessionFileActivity(reference.agentName, reference.sessionId)
14966
15676
  },
14967
- messages: serializeCachedMessages(cachedEntry, stream.startIndex),
14968
- messageCount: cachedEntry.messageRows.length,
14969
- sentMessageCount: cachedEntry.messageRows.length - stream.startIndex
15677
+ messages: serializeCachedMessages(stream.messageRows),
15678
+ messageCount: stream.messageCount,
15679
+ sentMessageCount: stream.messageRows.length
14970
15680
  };
14971
15681
  }
14972
15682
  function listCachedProjectGroups(sessions) {
@@ -14997,6 +15707,158 @@ function listCachedProjectGroups(sessions) {
14997
15707
  lastActivity: row.last_activity == null ? null : Number(row.last_activity)
14998
15708
  }));
14999
15709
  }
15710
+ var EFFECTIVE_COST_TIME = `CASE
15711
+ WHEN m.time_completed > 0 THEN m.time_completed
15712
+ WHEN m.time_created > 0 THEN m.time_created
15713
+ END`;
15714
+ function sessionKey2(agentName, sessionId) {
15715
+ return `${agentName}\0${sessionId}`;
15716
+ }
15717
+ function costSource(value) {
15718
+ return value === "recorded" || value === "estimated" ? value : void 0;
15719
+ }
15720
+ function nonNegative2(value) {
15721
+ return Math.max(0, Number(value ?? 0));
15722
+ }
15723
+ function readCostFacts(db, options) {
15724
+ const summaries = db.prepare(
15725
+ `
15726
+ SELECT
15727
+ c.agent_name,
15728
+ c.session_id,
15729
+ c.message_count,
15730
+ c.untimed_message_count,
15731
+ c.input_tokens,
15732
+ c.output_tokens,
15733
+ c.reasoning_tokens,
15734
+ c.cache_read_tokens,
15735
+ c.cache_create_tokens,
15736
+ c.untimed_input_tokens,
15737
+ c.untimed_output_tokens,
15738
+ c.untimed_reasoning_tokens,
15739
+ c.untimed_cache_read_tokens,
15740
+ c.untimed_cache_create_tokens,
15741
+ c.message_cost,
15742
+ c.untimed_message_cost
15743
+ FROM session_cost_summary c
15744
+ JOIN sessions s
15745
+ ON s.agent_name = c.agent_name
15746
+ AND s.session_id = c.session_id
15747
+ AND s.publication_id IS NULL
15748
+ ORDER BY c.agent_name, c.session_id
15749
+ `
15750
+ ).all();
15751
+ const bySession = /* @__PURE__ */ new Map();
15752
+ for (const row of summaries) {
15753
+ const agentName = String(row.agent_name ?? "");
15754
+ const sessionId = String(row.session_id ?? "");
15755
+ const summary = {
15756
+ reference: { agentName, sessionId },
15757
+ messageCount: nonNegative2(row.message_count),
15758
+ untimedMessageCount: nonNegative2(row.untimed_message_count),
15759
+ inputTokens: nonNegative2(row.input_tokens),
15760
+ outputTokens: nonNegative2(row.output_tokens),
15761
+ reasoningTokens: nonNegative2(row.reasoning_tokens),
15762
+ cacheReadTokens: nonNegative2(row.cache_read_tokens),
15763
+ cacheCreateTokens: nonNegative2(row.cache_create_tokens),
15764
+ untimedInputTokens: nonNegative2(row.untimed_input_tokens),
15765
+ untimedOutputTokens: nonNegative2(row.untimed_output_tokens),
15766
+ untimedReasoningTokens: nonNegative2(row.untimed_reasoning_tokens),
15767
+ untimedCacheReadTokens: nonNegative2(row.untimed_cache_read_tokens),
15768
+ untimedCacheCreateTokens: nonNegative2(row.untimed_cache_create_tokens),
15769
+ messageCost: Number(row.message_cost ?? 0),
15770
+ untimedMessageCost: Number(row.untimed_message_cost ?? 0),
15771
+ modelCosts: []
15772
+ };
15773
+ bySession.set(sessionKey2(agentName, sessionId), summary);
15774
+ }
15775
+ const modelRows = options.includeModelCosts === false ? [] : db.prepare(
15776
+ `
15777
+ SELECT
15778
+ m.agent_name,
15779
+ m.session_id,
15780
+ m.model,
15781
+ m.cost,
15782
+ m.cost_recorded
15783
+ FROM session_model_cost m
15784
+ JOIN sessions s
15785
+ ON s.agent_name = m.agent_name
15786
+ AND s.session_id = m.session_id
15787
+ AND s.publication_id IS NULL
15788
+ ORDER BY m.agent_name, m.session_id, m.model
15789
+ `
15790
+ ).all();
15791
+ for (const row of modelRows) {
15792
+ const summary = bySession.get(
15793
+ sessionKey2(String(row.agent_name ?? ""), String(row.session_id ?? ""))
15794
+ );
15795
+ if (!summary) continue;
15796
+ summary.modelCosts.push({
15797
+ model: String(row.model ?? ""),
15798
+ cost: Number(row.cost ?? 0),
15799
+ costRecorded: Number(row.cost_recorded ?? 0)
15800
+ });
15801
+ }
15802
+ const clauses = [`${EFFECTIVE_COST_TIME} IS NOT NULL`];
15803
+ const params = [];
15804
+ if (options.from != null) {
15805
+ clauses.push(`${EFFECTIVE_COST_TIME} >= ?`);
15806
+ params.push(options.from);
15807
+ }
15808
+ if (options.to != null) {
15809
+ clauses.push(`${EFFECTIVE_COST_TIME} <= ?`);
15810
+ params.push(options.to);
15811
+ }
15812
+ const messageRows = db.prepare(
15813
+ `
15814
+ SELECT
15815
+ m.agent_name,
15816
+ m.session_id,
15817
+ ${EFFECTIVE_COST_TIME} AS cost_time,
15818
+ m.model,
15819
+ CAST(COALESCE(json_extract(m.tokens_json, '$.input'), 0) AS INTEGER) AS input_tokens,
15820
+ CAST(COALESCE(json_extract(m.tokens_json, '$.output'), 0) AS INTEGER) AS output_tokens,
15821
+ CAST(COALESCE(json_extract(m.tokens_json, '$.reasoning'), 0) AS INTEGER) AS reasoning_tokens,
15822
+ CAST(COALESCE(json_extract(m.tokens_json, '$.cache_read'), 0) AS INTEGER) AS cache_read_tokens,
15823
+ CAST(COALESCE(json_extract(m.tokens_json, '$.cache_create'), 0) AS INTEGER) AS cache_create_tokens,
15824
+ m.cost,
15825
+ m.cost_source
15826
+ FROM messages m INDEXED BY idx_messages_usage_time
15827
+ JOIN sessions s
15828
+ ON s.agent_name = m.agent_name
15829
+ AND s.session_id = m.session_id
15830
+ AND s.publication_id IS NULL
15831
+ WHERE ${clauses.join(" AND ")}
15832
+ ORDER BY cost_time, m.agent_name, m.session_id, m.message_index
15833
+ `
15834
+ ).all(...params);
15835
+ const messages = messageRows.map((row) => {
15836
+ const model = typeof row.model === "string" && row.model.length > 0 ? row.model : void 0;
15837
+ return {
15838
+ reference: {
15839
+ agentName: String(row.agent_name ?? ""),
15840
+ sessionId: String(row.session_id ?? "")
15841
+ },
15842
+ time: Number(row.cost_time ?? 0),
15843
+ inputTokens: nonNegative2(row.input_tokens),
15844
+ outputTokens: nonNegative2(row.output_tokens),
15845
+ reasoningTokens: nonNegative2(row.reasoning_tokens),
15846
+ cacheReadTokens: nonNegative2(row.cache_read_tokens),
15847
+ cacheCreateTokens: nonNegative2(row.cache_create_tokens),
15848
+ cost: Number(row.cost ?? 0),
15849
+ costSource: costSource(row.cost_source),
15850
+ ...model ? { model } : {}
15851
+ };
15852
+ });
15853
+ return { messages, sessions: [...bySession.values()] };
15854
+ }
15855
+ function listDashboardCostFacts(options = {}) {
15856
+ if (!hasCacheStorage()) return null;
15857
+ const read = withCacheDbReadOnly(
15858
+ (db) => db.transaction(() => readCostFacts(db, options))()
15859
+ );
15860
+ return read.status === "success" ? read.value : null;
15861
+ }
15000
15862
  var DEFAULT_MODEL_COST_LIMIT = 20;
15001
15863
  function buildModelCostWhere(options) {
15002
15864
  const clauses = ["m.model IS NOT NULL", "m.model <> ''"];
@@ -15026,10 +15888,10 @@ function buildModelCostWhere(options) {
15026
15888
  var MODEL_COST_SQL = `
15027
15889
  SELECT
15028
15890
  m.model AS model,
15029
- SUM(COALESCE(m.cost, 0)) AS cost,
15030
- SUM(CASE WHEN m.cost_source = 'recorded' THEN COALESCE(m.cost, 0) ELSE 0 END) AS cost_recorded,
15031
- SUM(CASE WHEN m.cost_source = 'recorded' THEN 0 ELSE COALESCE(m.cost, 0) END) AS cost_estimated
15032
- FROM messages m
15891
+ SUM(m.cost) AS cost,
15892
+ SUM(m.cost_recorded) AS cost_recorded,
15893
+ SUM(m.cost - m.cost_recorded) AS cost_estimated
15894
+ FROM session_model_cost m
15033
15895
  JOIN sessions s
15034
15896
  ON s.agent_name = m.agent_name
15035
15897
  AND s.session_id = m.session_id
@@ -15081,6 +15943,10 @@ function publicationSearchOptions(publication, options, publicationId) {
15081
15943
  };
15082
15944
  }
15083
15945
  function commitDurableSessionPublication(publication, loadSessionData, searchOptions = {}) {
15946
+ assertSessionProjectIdentities(
15947
+ publication.agentName,
15948
+ publication.kind === "snapshot" ? publication.sessions : publication.changes.map(({ session }) => session)
15949
+ );
15084
15950
  const publicationId = publication.publicationId ?? randomUUID();
15085
15951
  let failureStage = "prepare";
15086
15952
  const diagnostics2 = getCoreDiagnostics();
@@ -15139,6 +16005,7 @@ function commitDurableSessionPublication(publication, loadSessionData, searchOpt
15139
16005
  ...detail,
15140
16006
  stage: "search_staged"
15141
16007
  });
16008
+ advanceAnalyticsRevision(db);
15142
16009
  failureStage = "commit";
15143
16010
  return result;
15144
16011
  }).immediate();
@@ -15554,9 +16421,187 @@ function materializeBookmarkViews(bookmarks, options) {
15554
16421
  });
15555
16422
  return views.sort(compareBookmarkViews);
15556
16423
  }
16424
+ var COST_ABSOLUTE_TOLERANCE = 1e-8;
16425
+ var COST_RELATIVE_TOLERANCE = 1e-6;
16426
+ function costFactKey(agentName, sessionId) {
16427
+ return getSessionRouteKey(agentName, sessionId);
16428
+ }
16429
+ function indexFacts(facts) {
16430
+ const messagesBySession = /* @__PURE__ */ new Map();
16431
+ const summariesBySession = /* @__PURE__ */ new Map();
16432
+ if (!facts) return { messagesBySession, summariesBySession };
16433
+ for (const summary of facts.sessions) {
16434
+ summariesBySession.set(
16435
+ costFactKey(summary.reference.agentName, summary.reference.sessionId),
16436
+ summary
16437
+ );
16438
+ }
16439
+ for (const message of facts.messages) {
16440
+ const key = costFactKey(message.reference.agentName, message.reference.sessionId);
16441
+ const messages = messagesBySession.get(key);
16442
+ if (messages) messages.push(message);
16443
+ else messagesBySession.set(key, [message]);
16444
+ }
16445
+ return { messagesBySession, summariesBySession };
16446
+ }
16447
+ function nonNegative22(value) {
16448
+ return Math.max(0, Number(value ?? 0));
16449
+ }
16450
+ function costsReconcile(left, right) {
16451
+ const tolerance = Math.max(
16452
+ COST_ABSOLUTE_TOLERANCE,
16453
+ Math.max(Math.abs(left), Math.abs(right)) * COST_RELATIVE_TOLERANCE
16454
+ );
16455
+ return Math.abs(left - right) <= tolerance;
16456
+ }
16457
+ function hasDetailedCost(session, summary) {
16458
+ const totalCost = Math.max(0, session.stats.total_cost);
16459
+ return totalCost > 0 && summary != null && summary.untimedMessageCost <= COST_ABSOLUTE_TOLERANCE && costsReconcile(summary.messageCost, totalCost);
16460
+ }
16461
+ function hasDetailedMessages(session, summary) {
16462
+ return summary != null && summary.untimedMessageCount === 0 && summary.messageCount === nonNegative22(session.stats.message_count);
16463
+ }
16464
+ function detailedOutputIncludesReasoning(session, summary) {
16465
+ if (!summary) return null;
16466
+ const stats = session.stats;
16467
+ const sessionInput = nonNegative22(stats.total_input_tokens);
16468
+ const sessionOutput = nonNegative22(stats.total_output_tokens);
16469
+ const sessionCacheRead = nonNegative22(stats.total_cache_read_tokens);
16470
+ const sessionCacheCreate = nonNegative22(stats.total_cache_create_tokens);
16471
+ const sessionTotal = nonNegative22(stats.total_tokens ?? sessionInput + sessionOutput);
16472
+ const outputIncludesReasoning = summary.outputTokens + summary.reasoningTokens;
16473
+ const outputMatches = summary.outputTokens === sessionOutput;
16474
+ const outputWithReasoningMatches = outputIncludesReasoning === sessionOutput;
16475
+ const effectiveOutput = outputMatches ? summary.outputTokens : outputWithReasoningMatches ? outputIncludesReasoning : null;
16476
+ if (effectiveOutput == null || summary.inputTokens !== sessionInput || summary.cacheReadTokens !== sessionCacheRead || summary.cacheCreateTokens !== sessionCacheCreate || summary.inputTokens + effectiveOutput !== sessionTotal || summary.untimedInputTokens > 0 || summary.untimedOutputTokens > 0 || summary.untimedReasoningTokens > 0 || summary.untimedCacheReadTokens > 0 || summary.untimedCacheCreateTokens > 0) {
16477
+ return null;
16478
+ }
16479
+ return !outputMatches && outputWithReasoningMatches;
16480
+ }
16481
+ function isInWindow(time, from, to) {
16482
+ return (from == null || time >= from) && time <= to;
16483
+ }
16484
+ function resolvedCostSource(session, source) {
16485
+ return source ?? session.stats.cost_source ?? "recorded";
16486
+ }
16487
+ function fallbackModelCosts(summary, totalCost) {
16488
+ if (!summary) return [];
16489
+ const modelTotal = summary.modelCosts.reduce((sum, model) => sum + model.cost, 0);
16490
+ return modelTotal > totalCost && !costsReconcile(modelTotal, totalCost) ? [] : summary.modelCosts;
16491
+ }
16492
+ function visitAttributedCosts(tree, options, visit) {
16493
+ const factIndex = indexFacts(options.facts);
16494
+ const factsAvailable = options.facts != null;
16495
+ for (const entry of tree.entries) {
16496
+ if (options.matchesEntry && !options.matchesEntry(entry.session)) continue;
16497
+ const fallbackTime = entry.session.time_updated ?? entry.session.time_created;
16498
+ const pending2 = [entry];
16499
+ while (pending2.length > 0) {
16500
+ const node = pending2.pop();
16501
+ for (const child of node.children) pending2.push(child);
16502
+ const session = node.session;
16503
+ const totalCost = Math.max(0, session.stats.total_cost);
16504
+ if (totalCost <= 0) continue;
16505
+ const agentName = getSessionAgentKey(session);
16506
+ const key = costFactKey(agentName, session.id);
16507
+ const summary = factIndex.summariesBySession.get(key);
16508
+ if (factsAvailable && hasDetailedCost(session, summary)) {
16509
+ for (const message of factIndex.messagesBySession.get(key) ?? []) {
16510
+ if (!isInWindow(message.time, options.from, options.to) || message.cost <= 0) continue;
16511
+ const source = resolvedCostSource(session, message.costSource);
16512
+ visit({
16513
+ entry,
16514
+ time: message.time,
16515
+ cost: message.cost,
16516
+ source,
16517
+ modelCosts: message.model ? [
16518
+ {
16519
+ model: message.model,
16520
+ cost: message.cost,
16521
+ costRecorded: source === "recorded" ? message.cost : 0
16522
+ }
16523
+ ] : []
16524
+ });
16525
+ }
16526
+ continue;
16527
+ }
16528
+ if (!isInWindow(fallbackTime, options.from, options.to)) continue;
16529
+ visit({
16530
+ entry,
16531
+ time: fallbackTime,
16532
+ cost: totalCost,
16533
+ source: resolvedCostSource(session),
16534
+ modelCosts: factsAvailable ? fallbackModelCosts(summary, totalCost) : []
16535
+ });
16536
+ }
16537
+ }
16538
+ }
16539
+ function visitAttributedUsage(tree, options, visit) {
16540
+ const factIndex = indexFacts(options.facts);
16541
+ const factsAvailable = options.facts != null;
16542
+ for (const entry of tree.entries) {
16543
+ if (options.matchesEntry && !options.matchesEntry(entry.session)) continue;
16544
+ const fallbackTime = entry.session.time_updated ?? entry.session.time_created;
16545
+ const pending2 = [entry];
16546
+ while (pending2.length > 0) {
16547
+ const node = pending2.pop();
16548
+ for (const child of node.children) pending2.push(child);
16549
+ const session = node.session;
16550
+ const key = costFactKey(getSessionAgentKey(session), session.id);
16551
+ const summary = factIndex.summariesBySession.get(key);
16552
+ const detailedMessages = factsAvailable && hasDetailedMessages(session, summary);
16553
+ const outputIncludesReasoning = factsAvailable ? detailedOutputIncludesReasoning(session, summary) : null;
16554
+ const detailedTokens = outputIncludesReasoning != null;
16555
+ if (detailedMessages || detailedTokens) {
16556
+ for (const message of factIndex.messagesBySession.get(key) ?? []) {
16557
+ if (!isInWindow(message.time, options.from, options.to)) continue;
16558
+ const outputTokens = detailedTokens ? message.outputTokens + (outputIncludesReasoning ? message.reasoningTokens : 0) : 0;
16559
+ visit({
16560
+ entry,
16561
+ time: message.time,
16562
+ messages: detailedMessages ? 1 : 0,
16563
+ totalTokens: detailedTokens ? message.inputTokens + outputTokens : 0,
16564
+ inputTokens: detailedTokens ? message.inputTokens : 0,
16565
+ outputTokens,
16566
+ cacheReadTokens: detailedTokens ? message.cacheReadTokens : 0,
16567
+ cacheCreateTokens: detailedTokens ? message.cacheCreateTokens : 0
16568
+ });
16569
+ }
16570
+ }
16571
+ if (!isInWindow(fallbackTime, options.from, options.to)) continue;
16572
+ if (!detailedMessages) {
16573
+ visit({
16574
+ entry,
16575
+ time: fallbackTime,
16576
+ messages: nonNegative22(session.stats.message_count),
16577
+ totalTokens: 0,
16578
+ inputTokens: 0,
16579
+ outputTokens: 0,
16580
+ cacheReadTokens: 0,
16581
+ cacheCreateTokens: 0
16582
+ });
16583
+ }
16584
+ if (!detailedTokens) {
16585
+ const inputTokens = nonNegative22(session.stats.total_input_tokens);
16586
+ const outputTokens = nonNegative22(session.stats.total_output_tokens);
16587
+ visit({
16588
+ entry,
16589
+ time: fallbackTime,
16590
+ messages: 0,
16591
+ totalTokens: nonNegative22(session.stats.total_tokens ?? inputTokens + outputTokens),
16592
+ inputTokens,
16593
+ outputTokens,
16594
+ cacheReadTokens: nonNegative22(session.stats.total_cache_read_tokens),
16595
+ cacheCreateTokens: nonNegative22(session.stats.total_cache_create_tokens)
16596
+ });
16597
+ }
16598
+ }
16599
+ }
16600
+ }
15557
16601
  var DASHBOARD_RECENT_LIMIT = 10;
15558
16602
  var DASHBOARD_PROJECT_LIMIT = 12;
15559
16603
  var PROJECT_SPARKLINE_DAYS = 14;
16604
+ var MODEL_COST_LIMIT = 20;
15560
16605
  function getSessionAgentName(session) {
15561
16606
  return getSessionAgentKey(session);
15562
16607
  }
@@ -15611,7 +16656,9 @@ function createAccumulator(byAgentNames, scope, to) {
15611
16656
  agentKeys: /* @__PURE__ */ new Set(),
15612
16657
  daily: /* @__PURE__ */ new Map(),
15613
16658
  models: /* @__PURE__ */ new Map(),
16659
+ modelCosts: /* @__PURE__ */ new Map(),
15614
16660
  projects: /* @__PURE__ */ new Map(),
16661
+ projectKeys: /* @__PURE__ */ new Set(),
15615
16662
  sparklineSlots,
15616
16663
  recent: []
15617
16664
  };
@@ -15624,9 +16671,9 @@ function foldModelUsage(node, into) {
15624
16671
  }
15625
16672
  for (const child of node.children) foldModelUsage(child, into);
15626
16673
  }
15627
- function trackProject(node, acc, dayKey, agentKey) {
15628
- const identity = node.session.project_identity;
15629
- if (!identity) return;
16674
+ function getOrCreateProject(session, acc) {
16675
+ const identity = session.project_identity;
16676
+ if (!identity) return void 0;
15630
16677
  const key = getProjectIdentityKey(identity);
15631
16678
  let project = acc.projects.get(key);
15632
16679
  if (!project) {
@@ -15644,36 +16691,25 @@ function trackProject(node, acc, dayKey, agentKey) {
15644
16691
  };
15645
16692
  acc.projects.set(key, project);
15646
16693
  }
15647
- const stats = node.inclusiveStats;
16694
+ return project;
16695
+ }
16696
+ function trackProjectActivity(node, acc, agentKey) {
16697
+ const identity = node.session.project_identity;
16698
+ const project = getOrCreateProject(node.session, acc);
16699
+ if (!identity || !project) return;
15648
16700
  project.sessions += 1;
15649
- project.messages += stats.messageCount;
15650
- project.tokens += stats.totalTokens;
15651
- project.cost += stats.cost;
15652
- if (stats.costSource === "estimated") project.hasEstimatedCost = true;
15653
16701
  project.agentSessions.set(agentKey, (project.agentSessions.get(agentKey) ?? 0) + 1);
15654
- const slot = acc.sparklineSlots.get(dayKey);
15655
- if (slot != null) project.sparkline[slot] += stats.cost;
16702
+ acc.projectKeys.add(getProjectIdentityKey(identity));
15656
16703
  }
15657
- function accumulate(node, acc) {
16704
+ function accumulateActivity(node, acc) {
15658
16705
  const session = node.session;
15659
- const stats = node.inclusiveStats;
15660
16706
  const activity = getSessionActivityTime(session);
15661
16707
  const agentKey = getSessionAgentName(session);
15662
16708
  acc.sessions += 1;
15663
- acc.messages += stats.messageCount;
15664
- acc.tokens += stats.totalTokens;
15665
- acc.cost += stats.cost;
15666
- acc.cacheReadTokens += stats.cacheReadTokens;
15667
16709
  acc.agentKeys.add(agentKey);
15668
- if (stats.costSource === "recorded") acc.costRecorded += stats.cost;
15669
- else acc.costEstimated += stats.cost;
15670
- if (stats.costSource === "estimated") acc.hasEstimatedCost = true;
15671
16710
  const metric = acc.agents.get(agentKey);
15672
16711
  if (metric) {
15673
16712
  metric.sessions += 1;
15674
- metric.messages += stats.messageCount;
15675
- metric.tokens += stats.totalTokens;
15676
- metric.cost += stats.cost;
15677
16713
  }
15678
16714
  const dayKey = toCalendarDayKey(activity);
15679
16715
  let bucket = acc.daily.get(dayKey);
@@ -15682,12 +16718,6 @@ function accumulate(node, acc) {
15682
16718
  acc.daily.set(dayKey, bucket);
15683
16719
  }
15684
16720
  bucket.sessions += 1;
15685
- bucket.messages += stats.messageCount;
15686
- bucket.cost += stats.cost;
15687
- bucket.input += Math.max(0, stats.inputTokens - stats.cacheReadTokens - stats.cacheCreateTokens);
15688
- bucket.output += stats.outputTokens;
15689
- bucket.cache_read += stats.cacheReadTokens;
15690
- bucket.cache_create += stats.cacheCreateTokens;
15691
16721
  const usage = /* @__PURE__ */ new Map();
15692
16722
  foldModelUsage(node, usage);
15693
16723
  for (const [model, tokens] of usage) {
@@ -15699,7 +16729,7 @@ function accumulate(node, acc) {
15699
16729
  acc.models.set(model, { tokens, sessions: 1 });
15700
16730
  }
15701
16731
  }
15702
- trackProject(node, acc, dayKey, agentKey);
16732
+ trackProjectActivity(node, acc, agentKey);
15703
16733
  let recentIndex = acc.recent.length;
15704
16734
  for (let i = 0; i < acc.recent.length; i += 1) {
15705
16735
  if (activity > acc.recent[i].activity) {
@@ -15712,6 +16742,103 @@ function accumulate(node, acc) {
15712
16742
  if (acc.recent.length > DASHBOARD_RECENT_LIMIT) acc.recent.pop();
15713
16743
  }
15714
16744
  }
16745
+ function addUsage(acc, entry, time, usage) {
16746
+ const { messages, totalTokens, inputTokens, outputTokens, cacheReadTokens, cacheCreateTokens } = usage;
16747
+ if (messages <= 0 && totalTokens <= 0 && inputTokens <= 0 && outputTokens <= 0 && cacheReadTokens <= 0 && cacheCreateTokens <= 0) {
16748
+ return;
16749
+ }
16750
+ acc.messages += messages;
16751
+ acc.tokens += totalTokens;
16752
+ acc.cacheReadTokens += cacheReadTokens;
16753
+ const agentKey = getSessionAgentName(entry.session);
16754
+ let metric = acc.agents.get(agentKey);
16755
+ if (!metric) {
16756
+ metric = { name: agentKey, sessions: 0, messages: 0, tokens: 0, cost: 0 };
16757
+ acc.agents.set(agentKey, metric);
16758
+ }
16759
+ metric.messages += messages;
16760
+ metric.tokens += totalTokens;
16761
+ const dayKey = toCalendarDayKey(time);
16762
+ let bucket = acc.daily.get(dayKey);
16763
+ if (!bucket) {
16764
+ bucket = emptyDailyBucket(dayKey);
16765
+ acc.daily.set(dayKey, bucket);
16766
+ }
16767
+ bucket.messages += messages;
16768
+ bucket.input += Math.max(0, inputTokens - cacheReadTokens - cacheCreateTokens);
16769
+ bucket.output += outputTokens;
16770
+ bucket.cache_read += cacheReadTokens;
16771
+ bucket.cache_create += cacheCreateTokens;
16772
+ const project = getOrCreateProject(entry.session, acc);
16773
+ if (!project) return;
16774
+ project.messages += messages;
16775
+ project.tokens += totalTokens;
16776
+ }
16777
+ function accumulateUsage(tree, scope, acc, from, to, costFacts) {
16778
+ visitAttributedUsage(
16779
+ tree,
16780
+ { from, to, facts: costFacts, matchesEntry: (session) => matchesScope(session, scope) },
16781
+ ({ entry, time, ...usage }) => addUsage(acc, entry, time, usage)
16782
+ );
16783
+ }
16784
+ function addModelCost(acc, model, cost, recordedCost) {
16785
+ if (!model || cost <= 0) return;
16786
+ const recorded = Math.max(0, Math.min(cost, recordedCost));
16787
+ const current = acc.modelCosts.get(model);
16788
+ if (current) {
16789
+ current.cost += cost;
16790
+ current.costRecorded += recorded;
16791
+ current.costEstimated += cost - recorded;
16792
+ return;
16793
+ }
16794
+ acc.modelCosts.set(model, {
16795
+ model,
16796
+ cost,
16797
+ costRecorded: recorded,
16798
+ costEstimated: cost - recorded
16799
+ });
16800
+ }
16801
+ function addCost(acc, entry, time, cost, source) {
16802
+ if (cost <= 0) return;
16803
+ acc.cost += cost;
16804
+ if (source === "recorded") acc.costRecorded += cost;
16805
+ else {
16806
+ acc.costEstimated += cost;
16807
+ acc.hasEstimatedCost = true;
16808
+ }
16809
+ const agentKey = getSessionAgentName(entry.session);
16810
+ let metric = acc.agents.get(agentKey);
16811
+ if (!metric) {
16812
+ metric = { name: agentKey, sessions: 0, messages: 0, tokens: 0, cost: 0 };
16813
+ acc.agents.set(agentKey, metric);
16814
+ }
16815
+ metric.cost += cost;
16816
+ const dayKey = toCalendarDayKey(time);
16817
+ let bucket = acc.daily.get(dayKey);
16818
+ if (!bucket) {
16819
+ bucket = emptyDailyBucket(dayKey);
16820
+ acc.daily.set(dayKey, bucket);
16821
+ }
16822
+ bucket.cost += cost;
16823
+ const project = getOrCreateProject(entry.session, acc);
16824
+ if (!project) return;
16825
+ project.cost += cost;
16826
+ if (source === "estimated") project.hasEstimatedCost = true;
16827
+ const slot = acc.sparklineSlots.get(dayKey);
16828
+ if (slot != null) project.sparkline[slot] += cost;
16829
+ }
16830
+ function accumulateCosts(tree, scope, acc, from, to, costFacts) {
16831
+ visitAttributedCosts(
16832
+ tree,
16833
+ { from, to, facts: costFacts, matchesEntry: (session) => matchesScope(session, scope) },
16834
+ ({ entry, time, cost, source, modelCosts }) => {
16835
+ addCost(acc, entry, time, cost, source);
16836
+ for (const model of modelCosts) {
16837
+ addModelCost(acc, model.model, model.cost, model.costRecorded);
16838
+ }
16839
+ }
16840
+ );
16841
+ }
15715
16842
  function toPreviousTotals(acc) {
15716
16843
  return { sessions: acc.sessions, messages: acc.messages, tokens: acc.tokens, cost: acc.cost };
15717
16844
  }
@@ -15730,8 +16857,9 @@ function toProjectStat(project) {
15730
16857
  };
15731
16858
  }
15732
16859
  function buildDashboard(sessions, options) {
15733
- const { byAgentNames, scope, from, to, agentInfoMap, compare } = options;
16860
+ const { byAgentNames, scope, from, to, agentInfoMap, compare, costFacts } = options;
15734
16861
  const tree = buildSessionTree(sessions);
16862
+ const costFactsAvailable = costFacts != null;
15735
16863
  const acc = createAccumulator(byAgentNames, scope, to);
15736
16864
  if (from != null) {
15737
16865
  const bucketDays = countCalendarDays(from, to);
@@ -15740,13 +16868,17 @@ function buildDashboard(sessions, options) {
15740
16868
  acc.daily.set(key, emptyDailyBucket(key));
15741
16869
  }
15742
16870
  }
15743
- for (const node of scopedEntries(tree, scope, from, to)) accumulate(node, acc);
16871
+ for (const node of scopedEntries(tree, scope, from, to)) accumulateActivity(node, acc);
16872
+ accumulateUsage(tree, scope, acc, from, to, costFacts);
16873
+ accumulateCosts(tree, scope, acc, from, to, costFacts);
15744
16874
  let previous;
15745
16875
  if (compare) {
15746
16876
  const compareAcc = createAccumulator(byAgentNames, scope, compare.to);
15747
16877
  for (const node of scopedEntries(tree, scope, compare.from, compare.to)) {
15748
- accumulate(node, compareAcc);
16878
+ accumulateActivity(node, compareAcc);
15749
16879
  }
16880
+ accumulateUsage(tree, scope, compareAcc, compare.from, compare.to, costFacts);
16881
+ accumulateCosts(tree, scope, compareAcc, compare.from, compare.to, costFacts);
15750
16882
  previous = toPreviousTotals(compareAcc);
15751
16883
  }
15752
16884
  const perAgent = [...acc.agents.values()].map((metrics) => {
@@ -15761,9 +16893,10 @@ function buildDashboard(sessions, options) {
15761
16893
  tokens: metrics.tokens,
15762
16894
  cost: metrics.cost
15763
16895
  };
15764
- }).filter((item) => item.sessions > 0).sort((a, b) => b.sessions - a.sessions);
16896
+ }).filter((item) => item.sessions > 0 || item.messages > 0 || item.tokens > 0 || item.cost > 0).sort((a, b) => b.sessions - a.sessions || b.cost - a.cost);
15765
16897
  const dailyActivity = [...acc.daily.values()].sort((a, b) => a.date.localeCompare(b.date));
15766
16898
  const modelDistribution = [...acc.models.entries()].map(([model, { tokens, sessions: count }]) => ({ model, tokens, sessions: count })).sort((a, b) => b.tokens - a.tokens);
16899
+ const modelCost = costFactsAvailable ? [...acc.modelCosts.values()].sort((a, b) => b.cost - a.cost).slice(0, MODEL_COST_LIMIT) : null;
15767
16900
  const rankedProjects = [...acc.projects.values()].sort((a, b) => b.cost - a.cost);
15768
16901
  const perProject = rankedProjects.slice(0, DASHBOARD_PROJECT_LIMIT).map(toProjectStat);
15769
16902
  const projectRollup = rankedProjects.slice(DASHBOARD_PROJECT_LIMIT).reduce(
@@ -15795,10 +16928,11 @@ function buildDashboard(sessions, options) {
15795
16928
  latestActivityAgent: latest ? getSessionAgentName(latest.session) : void 0,
15796
16929
  previous
15797
16930
  },
15798
- scopeCounts: { projects: acc.projects.size, agents: acc.agentKeys.size },
16931
+ scopeCounts: { projects: acc.projectKeys.size, agents: acc.agentKeys.size },
15799
16932
  perAgent,
15800
16933
  dailyActivity,
15801
16934
  modelDistribution,
16935
+ modelCost,
15802
16936
  perProject,
15803
16937
  projectRollup,
15804
16938
  recentSessions
@@ -15815,15 +16949,20 @@ function emptyMetrics() {
15815
16949
  };
15816
16950
  }
15817
16951
  function attachProjectMetrics(projects, sessions) {
15818
- return attachProjectMetricsToEntries(projects, buildSessionTree(sessions).entries);
16952
+ const tree = buildSessionTree(sessions);
16953
+ return attachProjectMetricsToEntries(projects, tree, tree.entries);
15819
16954
  }
15820
- function attachProjectMetricsFromTree(projects, tree, from, to) {
16955
+ function attachProjectMetricsFromTree(projects, tree, from, to, costFacts) {
15821
16956
  return attachProjectMetricsToEntries(
15822
16957
  projects,
15823
- filterSessionTreeEntriesByActivityWindow(tree, from, to)
16958
+ tree,
16959
+ filterSessionTreeEntriesByActivityWindow(tree, from, to),
16960
+ from,
16961
+ to,
16962
+ costFacts
15824
16963
  );
15825
16964
  }
15826
- function attachProjectMetricsToEntries(projects, entries) {
16965
+ function attachProjectMetricsToEntries(projects, tree, entries, from, to, costFacts) {
15827
16966
  const metrics = /* @__PURE__ */ new Map();
15828
16967
  for (const node of entries) {
15829
16968
  const identity = node.session.project_identity;
@@ -15834,29 +16973,79 @@ function attachProjectMetricsToEntries(projects, entries) {
15834
16973
  current = emptyMetrics();
15835
16974
  metrics.set(key, current);
15836
16975
  }
15837
- const stats = node.inclusiveStats;
15838
16976
  current.sessions += 1;
15839
- current.messages += stats.messageCount;
15840
- current.tokens += stats.totalTokens;
15841
- current.cost += stats.cost;
15842
- if (stats.costSource === "estimated") current.hasEstimatedCost = true;
15843
16977
  const agentName = getSessionAgentName(node.session);
15844
16978
  const agent = current.agentStats.get(agentName);
15845
16979
  if (agent) {
15846
16980
  agent.sessions += 1;
15847
- agent.messages += stats.messageCount;
15848
- agent.tokens += stats.totalTokens;
15849
- agent.cost += stats.cost;
15850
16981
  } else {
15851
16982
  current.agentStats.set(agentName, {
15852
16983
  name: agentName,
15853
16984
  sessions: 1,
15854
- messages: stats.messageCount,
15855
- tokens: stats.totalTokens,
15856
- cost: stats.cost
16985
+ messages: 0,
16986
+ tokens: 0,
16987
+ cost: 0
15857
16988
  });
15858
16989
  }
15859
16990
  }
16991
+ visitAttributedUsage(
16992
+ tree,
16993
+ { from, to: to ?? Number.POSITIVE_INFINITY, facts: costFacts },
16994
+ ({ entry, messages, totalTokens }) => {
16995
+ const identity = entry.session.project_identity;
16996
+ if (!identity) return;
16997
+ const key = getProjectIdentityKey(identity);
16998
+ let current = metrics.get(key);
16999
+ if (!current) {
17000
+ current = emptyMetrics();
17001
+ metrics.set(key, current);
17002
+ }
17003
+ current.messages += messages;
17004
+ current.tokens += totalTokens;
17005
+ const agentName = getSessionAgentName(entry.session);
17006
+ const agent = current.agentStats.get(agentName);
17007
+ if (agent) {
17008
+ agent.messages += messages;
17009
+ agent.tokens += totalTokens;
17010
+ } else {
17011
+ current.agentStats.set(agentName, {
17012
+ name: agentName,
17013
+ sessions: 0,
17014
+ messages,
17015
+ tokens: totalTokens,
17016
+ cost: 0
17017
+ });
17018
+ }
17019
+ }
17020
+ );
17021
+ visitAttributedCosts(
17022
+ tree,
17023
+ { from, to: to ?? Number.POSITIVE_INFINITY, facts: costFacts },
17024
+ ({ entry, cost, source }) => {
17025
+ const identity = entry.session.project_identity;
17026
+ if (!identity) return;
17027
+ const key = getProjectIdentityKey(identity);
17028
+ let current = metrics.get(key);
17029
+ if (!current) {
17030
+ current = emptyMetrics();
17031
+ metrics.set(key, current);
17032
+ }
17033
+ current.cost += cost;
17034
+ if (source === "estimated") current.hasEstimatedCost = true;
17035
+ const agentName = getSessionAgentName(entry.session);
17036
+ const agent = current.agentStats.get(agentName);
17037
+ if (agent) agent.cost += cost;
17038
+ else {
17039
+ current.agentStats.set(agentName, {
17040
+ name: agentName,
17041
+ sessions: 0,
17042
+ messages: 0,
17043
+ tokens: 0,
17044
+ cost
17045
+ });
17046
+ }
17047
+ }
17048
+ );
15860
17049
  return projects.map((project) => {
15861
17050
  const metric = metrics.get(
15862
17051
  getProjectIdentityKey({ kind: project.identityKind, key: project.identityKey })
@@ -15872,10 +17061,10 @@ function attachProjectMetricsToEntries(projects, entries) {
15872
17061
  };
15873
17062
  });
15874
17063
  }
15875
- function executeSessionSearch(query, options, snapshot) {
17064
+ function executeSessionSearch(query, options, snapshot, context = {}) {
15876
17065
  const merged = mergeSearchQueryOptions(query, options);
15877
17066
  if (!needsIndexedSearch(merged.text, merged.options)) {
15878
- return searchRecentSessions(snapshot, merged.options);
17067
+ return searchRecentSessions(snapshot, merged.options, context);
15879
17068
  }
15880
17069
  return searchIndexedSessions(query, merged.text, merged.parsed, merged.options);
15881
17070
  }
@@ -15917,9 +17106,10 @@ function matchesSessionSearchFilters(agentName, session, options, projectScope =
15917
17106
  function sessionReferenceKey2(agentName, sessionId) {
15918
17107
  return `${agentName}\0${sessionId}`;
15919
17108
  }
15920
- function filterSessionSearchCandidates(candidates, options, sessionSnapshot = candidates.map((candidate) => candidate.session)) {
15921
- const projectScope = options.cwd ? createProjectScopeMatcher(options.cwd) : null;
15922
- const inclusiveCosts = buildInclusiveCostLookup(sessionSnapshot, options);
17109
+ function filterSessionSearchCandidates(candidates, options, context = {}) {
17110
+ const projectScope = options.projectScope ?? null;
17111
+ const sessionSnapshot = context.sessionSnapshot ?? candidates.map((candidate) => candidate.session);
17112
+ const inclusiveCosts = buildInclusiveCostLookup(sessionSnapshot, options, context.sessionTree);
15923
17113
  const headMatches = candidates.filter(
15924
17114
  (candidate) => matchesSessionSearchFilters(
15925
17115
  candidate.reference.agentName,
@@ -15947,11 +17137,11 @@ function filterSessionSearchCandidates(candidates, options, sessionSnapshot = ca
15947
17137
  )
15948
17138
  );
15949
17139
  }
15950
- function searchRecentSessions(snapshot, options) {
17140
+ function searchRecentSessions(snapshot, options, context) {
15951
17141
  const limit = Math.max(0, Math.trunc(options.limit ?? 50));
15952
17142
  if (limit === 0) return [];
15953
- const projectScope = options.cwd ? createProjectScopeMatcher(options.cwd) : null;
15954
- const inclusiveCosts = buildInclusiveCostLookup(snapshot.sessions, options);
17143
+ const projectScope = options.projectScope ?? null;
17144
+ const inclusiveCosts = buildInclusiveCostLookup(snapshot.sessions, options, context.sessionTree);
15955
17145
  const sessions = options.agent ? snapshot.byAgent[options.agent] ?? [] : snapshot.sessions;
15956
17146
  const results = [];
15957
17147
  for (const session of sessions) {
@@ -15976,9 +17166,9 @@ function searchRecentSessions(snapshot, options) {
15976
17166
  }
15977
17167
  return results;
15978
17168
  }
15979
- function buildInclusiveCostLookup(sessions, options) {
17169
+ function buildInclusiveCostLookup(sessions, options, sessionTree) {
15980
17170
  if (options.costMin == null && options.costMax == null) return null;
15981
- return buildSessionTree(sessions).byRouteKey;
17171
+ return (sessionTree ?? buildSessionTree(sessions)).byRouteKey;
15982
17172
  }
15983
17173
  function inclusiveCostFor(agentName, session, lookup) {
15984
17174
  return lookup?.get(getSessionRouteKey(agentName, session.id))?.inclusiveStats.cost ?? session.stats.total_cost;
@@ -16019,6 +17209,10 @@ function searchIndexedSessions(query, textQuery, parsed, options) {
16019
17209
  }
16020
17210
 
16021
17211
  export {
17212
+ normalizeSessionReference,
17213
+ formatSessionReference,
17214
+ getSessionAgentKey,
17215
+ sessionRoutePath,
16022
17216
  registerAgent,
16023
17217
  createRegisteredAgents,
16024
17218
  getRegisteredAgents,
@@ -16033,9 +17227,7 @@ export {
16033
17227
  hasPendingPricing,
16034
17228
  refreshPricingCache,
16035
17229
  PRICING_CAPTURE_EPOCH,
16036
- createSessionSourceFailure,
16037
17230
  reportSessionSourceOutcome,
16038
- diffSessionSources,
16039
17231
  synchronizeSessionSources,
16040
17232
  BaseAgent,
16041
17233
  FileSystemSessionSource,
@@ -16046,19 +17238,15 @@ export {
16046
17238
  countCalendarDays,
16047
17239
  isProjectIdentityKind,
16048
17240
  matchesProjectIdentity,
16049
- normalizeSessionReference,
16050
- formatSessionReference,
16051
- getSessionAgentKey,
16052
- sessionRoutePath,
16053
17241
  mergeSortedSessions,
16054
- getSessionRouteKey,
16055
17242
  createSessionIndex,
16056
- isChildSession,
16057
- getRootSessions,
16058
17243
  buildSessionTree,
16059
17244
  filterSessionTreeByActivityWindow,
16060
17245
  createSessionProjectionContext,
16061
- createProjectScopeMatcher,
17246
+ PROJECT_IDENTITY_RESOLVER_REVISION,
17247
+ normalizeProjectDirectory,
17248
+ computeIdentityProjection,
17249
+ createProjectScopeMatcherFromIdentity,
16062
17250
  matchesProjectScope,
16063
17251
  SMART_TAG_CLASSIFIER_REVISION,
16064
17252
  getSmartTagSourceTimestamp,
@@ -16066,23 +17254,23 @@ export {
16066
17254
  WORKER_LOG_MESSAGE_TYPE,
16067
17255
  isWorkerLogMessage,
16068
17256
  closeCacheStorage,
16069
- getCachePath2 as getCachePath,
17257
+ getAnalyticsRevision,
16070
17258
  sessionDetailVersion,
16071
17259
  readPendingSearchIndexMaintenance,
16072
17260
  syncSessionSearchIndex,
16073
17261
  syncSessionSearchIndexChanges,
16074
17262
  mergeSearchQueryOptions,
17263
+ getSearchProjectDirectory,
16075
17264
  listFileActivity,
17265
+ readCachedSessions,
16076
17266
  loadCachedSessions,
16077
17267
  loadCachedSessionHeads,
16078
17268
  readAgentCacheInitialization,
16079
- isAgentCacheInitialized,
16080
17269
  markAgentCacheInitialized,
16081
17270
  markAgentFullSyncStarted,
16082
17271
  getAgentFullSyncCursor,
16083
17272
  markAgentFullSyncProgress,
16084
17273
  readAgentLastFullSyncAt,
16085
- getAgentLastFullSyncAt,
16086
17274
  markAgentFullSyncCompleted,
16087
17275
  saveCachedSessions,
16088
17276
  saveCachedSessionChanges,
@@ -16096,6 +17284,7 @@ export {
16096
17284
  scanSessions,
16097
17285
  materializeSessionDetailResponse,
16098
17286
  listCachedProjectGroups,
17287
+ listDashboardCostFacts,
16099
17288
  listModelCostDistribution,
16100
17289
  commitDurableSessionPublication,
16101
17290
  StateStorageUnavailableError,
@@ -16107,7 +17296,6 @@ export {
16107
17296
  listSessionAliases,
16108
17297
  upsertSessionAlias,
16109
17298
  deleteSessionAlias,
16110
- compareBookmarkViews,
16111
17299
  materializeBookmarkViews,
16112
17300
  getSessionActivityTime,
16113
17301
  buildDashboard,
@@ -16116,4 +17304,4 @@ export {
16116
17304
  executeSessionSearch,
16117
17305
  filterSessionSearchCandidates
16118
17306
  };
16119
- //# sourceMappingURL=chunk-VAUC2W7I.js.map
17307
+ //# sourceMappingURL=chunk-PSHZZITB.js.map