@wrongstack/core 0.306.4 → 0.307.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/dist/chronicle/index.js +957 -953
  2. package/dist/chronicle/metrics-ingest.d.ts +55 -0
  3. package/dist/chronicle/metrics-schema.d.ts +86 -0
  4. package/dist/chronicle/metrics-store.d.ts +5 -120
  5. package/dist/chronicle/project-server.js +1083 -1084
  6. package/dist/chronicle/sqlite-journal-quota.d.ts +39 -0
  7. package/dist/chronicle/sqlite-journal-schema.d.ts +25 -0
  8. package/dist/chronicle/sqlite-journal.d.ts +12 -177
  9. package/dist/coordination/agents/index.js +1738 -1690
  10. package/dist/coordination/agents/project-agent-capture.d.ts +48 -0
  11. package/dist/coordination/agents/project-agent-identity.d.ts +3 -129
  12. package/dist/coordination/agents/project-agent-roster.d.ts +17 -0
  13. package/dist/coordination/index.js +6011 -5894
  14. package/dist/coordination/mail-tools.d.ts +1 -1
  15. package/dist/coordination/mailbox-http-actor-query.d.ts +16 -0
  16. package/dist/coordination/mailbox-http-router.d.ts +0 -48
  17. package/dist/coordination/mailbox-http-sse.d.ts +12 -0
  18. package/dist/coordination/mailbox-message-types.d.ts +71 -0
  19. package/dist/coordination/mailbox-predicates.d.ts +13 -0
  20. package/dist/coordination/mailbox-project-server.js +1 -1
  21. package/dist/coordination/mailbox-session-sync.d.ts +8 -0
  22. package/dist/coordination/mailbox-types.d.ts +6 -793
  23. package/dist/core/agent-loop-context.d.ts +28 -0
  24. package/dist/core/agent-loop-detector.d.ts +25 -0
  25. package/dist/core/fallback-doctor.d.ts +72 -0
  26. package/dist/core/index.d.ts +1 -0
  27. package/dist/core/index.js +1159 -888
  28. package/dist/defaults/index.js +3579 -3545
  29. package/dist/execution/autonomy-brain-llm.d.ts +55 -0
  30. package/dist/execution/autonomy-brain.d.ts +3 -156
  31. package/dist/execution/compaction-budget.d.ts +38 -0
  32. package/dist/execution/compaction-core.d.ts +3 -170
  33. package/dist/execution/compaction-elision.d.ts +58 -0
  34. package/dist/execution/council-orchestrator-helpers.d.ts +39 -0
  35. package/dist/execution/council-orchestrator.d.ts +3 -42
  36. package/dist/execution/council-response-parser.d.ts +56 -0
  37. package/dist/execution/index.js +2415 -2443
  38. package/dist/execution/prompt-enhancer.js +11 -3
  39. package/dist/execution/tool-executor-guard.d.ts +18 -0
  40. package/dist/execution/tool-executor-runner.d.ts +12 -0
  41. package/dist/execution/tool-executor.d.ts +0 -75
  42. package/dist/goal/index.js +19 -6
  43. package/dist/hq/index.js +8 -2
  44. package/dist/index.d.ts +1 -0
  45. package/dist/index.js +7813 -7292
  46. package/dist/infrastructure/index.js +3 -1
  47. package/dist/kernel/events/agent-events.d.ts +2 -0
  48. package/dist/kernel/events/provider-events.d.ts +13 -0
  49. package/dist/plugin/index.js +2590 -2561
  50. package/dist/plugins/auto-review-config.d.ts +53 -0
  51. package/dist/plugins/auto-review-git.d.ts +19 -0
  52. package/dist/plugins/auto-review-plugin.d.ts +2 -140
  53. package/dist/plugins/review-finding-verification.d.ts +7 -0
  54. package/dist/prompts/index.d.ts +1 -0
  55. package/dist/prompts/prompt-journal.d.ts +94 -0
  56. package/dist/security/index.js +627 -652
  57. package/dist/security/permission-explain.d.ts +19 -0
  58. package/dist/security/permission-policy.d.ts +0 -101
  59. package/dist/security/yolo-risk.d.ts +5 -4
  60. package/dist/session-catalog/index.js +220 -212
  61. package/dist/session-catalog/project-server.js +227 -219
  62. package/dist/session-catalog/store-rebuild.d.ts +9 -0
  63. package/dist/session-catalog/store-schema.d.ts +48 -0
  64. package/dist/session-catalog/store.d.ts +0 -17
  65. package/dist/storage/director-state.d.ts +6 -0
  66. package/dist/storage/file-session-writer.d.ts +2 -58
  67. package/dist/storage/index.d.ts +2 -1
  68. package/dist/storage/index.js +2057 -1925
  69. package/dist/storage/orphan-lock-cleaner.d.ts +15 -0
  70. package/dist/storage/session-recovery.d.ts +9 -0
  71. package/dist/storage/session-store/rename-session.d.ts +16 -0
  72. package/dist/storage/session-store/resume-session.d.ts +26 -0
  73. package/dist/storage/session-store/session-store-clear.d.ts +11 -0
  74. package/dist/storage/session-store/session-store-index.d.ts +13 -0
  75. package/dist/storage/session-store.d.ts +1 -129
  76. package/dist/storage/session-summary-tracker.d.ts +39 -0
  77. package/dist/storage/session-write-buffer.d.ts +43 -0
  78. package/dist/storage/session-writer/session-writer-flush.d.ts +4 -0
  79. package/dist/storage/session-writer/session-writer-summary-tracker.d.ts +28 -0
  80. package/dist/tools/fallback-agent-model-assign-tool.d.ts +18 -0
  81. package/dist/tools/fallback-leader-model-set-tool.d.ts +18 -0
  82. package/dist/tools/fallback-manage-tools.d.ts +8 -45
  83. package/dist/tools/fallback-profile-manage-tool.d.ts +16 -0
  84. package/dist/tools/fallback-provider-key-set-tool.d.ts +17 -0
  85. package/dist/tools/fallback-provider-manage-tool.d.ts +26 -0
  86. package/dist/tools/index.js +5070 -5071
  87. package/dist/types/config/root.d.ts +7 -2
  88. package/dist/types/default-config.d.ts +1 -1
  89. package/dist/types/index.d.ts +54 -55
  90. package/dist/types/index.js +953 -932
  91. package/dist/types/runtime-capability-manifest.d.ts +5 -5
  92. package/dist/types/session-markers.d.ts +12 -0
  93. package/dist/types/session-markers.js +19 -0
  94. package/dist/worktree/worktree-manager.d.ts +1 -1
  95. package/instructions/coordination/subagent-baseline.md +2 -2
  96. package/instructions/llm/prompt-enhancer.md +2 -1
  97. package/instructions/sections/tool/common-patterns.md +21 -0
  98. package/instructions/sections/tool/delegation-compact.md +3 -1
  99. package/instructions/sections/tool/delegation-full.md +5 -1
  100. package/instructions/system-lite.md +28 -3
  101. package/instructions/system-pro.md +37 -8
  102. package/instructions/system.md +4 -2
  103. package/package.json +5 -5
@@ -4,7 +4,7 @@
4
4
  import { randomBytes } from "node:crypto";
5
5
  import * as fsp4 from "node:fs/promises";
6
6
  import * as net from "node:net";
7
- import * as path15 from "node:path";
7
+ import * as path16 from "node:path";
8
8
  import { bindProjectEndpoint } from "@wrongstack/persistence";
9
9
 
10
10
  // src/security/file-permissions.ts
@@ -327,8 +327,8 @@ async function pruneOldSessions(root, currentDir) {
327
327
  const sessions = await Promise.all(
328
328
  entries.filter((entry) => entry.isDirectory() && entry.name !== path2.basename(currentDir)).map(async (entry) => {
329
329
  const fullPath = path2.join(root, entry.name);
330
- const stat5 = await fsp.stat(fullPath);
331
- return { fullPath, mtimeMs: stat5.mtimeMs };
330
+ const stat6 = await fsp.stat(fullPath);
331
+ return { fullPath, mtimeMs: stat6.mtimeMs };
332
332
  })
333
333
  );
334
334
  sessions.sort((a, b) => b.mtimeMs - a.mtimeMs);
@@ -985,9 +985,9 @@ async function startChronicleFileObserver(options) {
985
985
  const recentToolMutations = /* @__PURE__ */ new Map();
986
986
  const noteToolMutation = (hint) => {
987
987
  const absolute = path5.isAbsolute(hint.path) ? path5.normalize(hint.path) : path5.resolve(root, hint.path);
988
- const relative4 = normalizeRelative(path5.relative(root, absolute));
989
- if (relative4.startsWith("../") || isExcluded(relative4, excluded, excludedPaths)) return;
990
- recentToolMutations.set(relative4, {
988
+ const relative5 = normalizeRelative(path5.relative(root, absolute));
989
+ if (relative5.startsWith("../") || isExcluded(relative5, excluded, excludedPaths)) return;
990
+ recentToolMutations.set(relative5, {
991
991
  at: hint.at ?? Date.now(),
992
992
  toolUseId: hint.toolUseId,
993
993
  toolName: hint.toolName,
@@ -1049,9 +1049,9 @@ async function startChronicleFileObserver(options) {
1049
1049
  requestFullRescan();
1050
1050
  return;
1051
1051
  }
1052
- const relative4 = normalizeRelative(String(filename));
1053
- if (!relative4 || isExcluded(relative4, excluded, excludedPaths)) return;
1054
- pending.add(relative4);
1052
+ const relative5 = normalizeRelative(String(filename));
1053
+ if (!relative5 || isExcluded(relative5, excluded, excludedPaths)) return;
1054
+ pending.add(relative5);
1055
1055
  bumpDebounce();
1056
1056
  };
1057
1057
  const reconcile = async (changedPaths) => {
@@ -1059,15 +1059,15 @@ async function startChronicleFileObserver(options) {
1059
1059
  if (wantsFullScan) lastFullScanAt = Date.now();
1060
1060
  const fullScan = wantsFullScan ? await scanProject(root, excluded, excludedPaths, maxHashBytes, options.onError, known) : void 0;
1061
1061
  const candidates = (fullScan ? unionKeys(known, fullScan.files) : changedPaths).filter(
1062
- (relative4) => !isExcluded(relative4, excluded, excludedPaths)
1062
+ (relative5) => !isExcluded(relative5, excluded, excludedPaths)
1063
1063
  );
1064
1064
  const changes = [];
1065
- for (const relative4 of candidates) {
1066
- const before = known.get(relative4);
1067
- const cached = fullScan?.files.get(relative4);
1068
- const after = cached ?? (fullScan?.complete ? void 0 : await fingerprint(path5.join(root, relative4), maxHashBytes));
1065
+ for (const relative5 of candidates) {
1066
+ const before = known.get(relative5);
1067
+ const cached = fullScan?.files.get(relative5);
1068
+ const after = cached ?? (fullScan?.complete ? void 0 : await fingerprint(path5.join(root, relative5), maxHashBytes));
1069
1069
  if (sameFingerprint(before, after)) continue;
1070
- changes.push({ relative: relative4, before, after });
1070
+ changes.push({ relative: relative5, before, after });
1071
1071
  }
1072
1072
  const deleted = changes.filter((change) => change.before && !change.after);
1073
1073
  const created = changes.filter((change) => !change.before && change.after);
@@ -1261,25 +1261,25 @@ async function scanProject(root, excluded, excludedPaths, maxHashBytes, onError,
1261
1261
  const entries = await fsp3.readdir(path5.join(root, relativeDir), { withFileTypes: true });
1262
1262
  const filePaths = [];
1263
1263
  for (const entry of entries) {
1264
- const relative4 = normalizeRelative(path5.join(relativeDir, entry.name));
1265
- if (isExcluded(relative4, excluded, excludedPaths)) continue;
1264
+ const relative5 = normalizeRelative(path5.join(relativeDir, entry.name));
1265
+ if (isExcluded(relative5, excluded, excludedPaths)) continue;
1266
1266
  if (entry.isDirectory()) {
1267
- dirs.push(relative4);
1267
+ dirs.push(relative5);
1268
1268
  } else if (entry.isFile()) {
1269
- filePaths.push(relative4);
1269
+ filePaths.push(relative5);
1270
1270
  }
1271
1271
  }
1272
1272
  const fingerprints = await mapWithConcurrency(
1273
1273
  filePaths,
1274
1274
  SCAN_HASH_CONCURRENCY,
1275
- async (relative4) => {
1275
+ async (relative5) => {
1276
1276
  try {
1277
1277
  const value = await fingerprint(
1278
- path5.join(root, relative4),
1278
+ path5.join(root, relative5),
1279
1279
  maxHashBytes,
1280
- previous?.get(relative4)
1280
+ previous?.get(relative5)
1281
1281
  );
1282
- return value ? [relative4, value] : null;
1282
+ return value ? [relative5, value] : null;
1283
1283
  } catch (error) {
1284
1284
  complete = false;
1285
1285
  onError?.(error);
@@ -1299,14 +1299,14 @@ async function scanProject(root, excluded, excludedPaths, maxHashBytes, onError,
1299
1299
  }
1300
1300
  async function fingerprint(filePath, maxHashBytes, previous) {
1301
1301
  try {
1302
- const stat5 = await fsp3.stat(filePath);
1303
- if (!stat5.isFile()) return void 0;
1304
- const base = { size: stat5.size, mtimeMs: stat5.mtimeMs };
1305
- if (previous?.hash !== void 0 && previous.size === stat5.size && previous.mtimeMs === stat5.mtimeMs) {
1302
+ const stat6 = await fsp3.stat(filePath);
1303
+ if (!stat6.isFile()) return void 0;
1304
+ const base = { size: stat6.size, mtimeMs: stat6.mtimeMs };
1305
+ if (previous?.hash !== void 0 && previous.size === stat6.size && previous.mtimeMs === stat6.mtimeMs) {
1306
1306
  base.hash = previous.hash;
1307
1307
  return base;
1308
1308
  }
1309
- if (stat5.size <= maxHashBytes) {
1309
+ if (stat6.size <= maxHashBytes) {
1310
1310
  base.hash = createHash("sha256").update(await fsp3.readFile(filePath)).digest("hex");
1311
1311
  }
1312
1312
  return base;
@@ -1321,8 +1321,8 @@ function sameFingerprint(a, b) {
1321
1321
  if (a.hash !== void 0 && b.hash !== void 0) return a.hash === b.hash;
1322
1322
  return a.size === b.size && a.mtimeMs === b.mtimeMs;
1323
1323
  }
1324
- function isExcluded(relative4, excluded, excludedPaths) {
1325
- const normalized = normalizeRelative(relative4);
1324
+ function isExcluded(relative5, excluded, excludedPaths) {
1325
+ const normalized = normalizeRelative(relative5);
1326
1326
  if (normalized.split("/").some((segment) => excluded.has(segment))) return true;
1327
1327
  for (const prefix of excludedPaths) {
1328
1328
  if (normalized === prefix || normalized.startsWith(`${prefix}/`)) return true;
@@ -1332,8 +1332,8 @@ function isExcluded(relative4, excluded, excludedPaths) {
1332
1332
  function normalizeExcludedPaths(root, values) {
1333
1333
  const result = /* @__PURE__ */ new Set();
1334
1334
  for (const value of values) {
1335
- const relative4 = path5.isAbsolute(value) ? path5.relative(root, path5.resolve(value)) : value;
1336
- const normalized = normalizeRelative(relative4).replace(/\/+$/u, "");
1335
+ const relative5 = path5.isAbsolute(value) ? path5.relative(root, path5.resolve(value)) : value;
1336
+ const normalized = normalizeRelative(relative5).replace(/\/+$/u, "");
1337
1337
  if (!normalized || normalized === "." || normalized.startsWith("../")) continue;
1338
1338
  result.add(normalized);
1339
1339
  }
@@ -1935,8 +1935,8 @@ async function readLastEntryState(filePath) {
1935
1935
  throw error;
1936
1936
  }
1937
1937
  try {
1938
- const stat5 = await handle.stat();
1939
- const size = stat5.size;
1938
+ const stat6 = await handle.stat();
1939
+ const size = stat6.size;
1940
1940
  let position = size, suffix = "";
1941
1941
  while (position > 0) {
1942
1942
  const length = Math.min(65536, position);
@@ -1953,23 +1953,25 @@ async function readLastEntryState(filePath) {
1953
1953
  return {
1954
1954
  entry: JSON.parse(trimmed),
1955
1955
  size,
1956
- birthtimeMs: stat5.birthtimeMs
1956
+ birthtimeMs: stat6.birthtimeMs
1957
1957
  };
1958
1958
  } catch {
1959
1959
  }
1960
1960
  }
1961
1961
  suffix = lines[0] ?? "";
1962
1962
  }
1963
- return { size, birthtimeMs: stat5.birthtimeMs };
1963
+ return { size, birthtimeMs: stat6.birthtimeMs };
1964
1964
  } finally {
1965
1965
  await handle.close();
1966
1966
  }
1967
1967
  }
1968
1968
  async function* streamEntriesStrict(filePath) {
1969
1969
  let lineNumber = 0;
1970
+ let input;
1971
+ let lines;
1970
1972
  try {
1971
- const input = createReadStream(filePath, { encoding: "utf8", highWaterMark: 256 * 1024 });
1972
- const lines = createInterface({ input, crlfDelay: Infinity });
1973
+ input = createReadStream(filePath, { encoding: "utf8", highWaterMark: 256 * 1024 });
1974
+ lines = createInterface({ input, crlfDelay: Infinity });
1973
1975
  for await (const line of lines) {
1974
1976
  lineNumber++;
1975
1977
  const trimmed = line.trim();
@@ -1985,6 +1987,9 @@ async function* streamEntriesStrict(filePath) {
1985
1987
  } catch (error) {
1986
1988
  if (isNotFound(error)) return;
1987
1989
  throw error;
1990
+ } finally {
1991
+ lines?.close();
1992
+ input?.destroy();
1988
1993
  }
1989
1994
  }
1990
1995
  async function readEntriesStrict(filePath) {
@@ -2040,12 +2045,14 @@ async function importLegacyChronicleJournal(journal, directory) {
2040
2045
  }
2041
2046
  const families = await discoverFamilies(directory);
2042
2047
  const quarantined = [];
2048
+ const boundary = {};
2043
2049
  let importedEvents = 0;
2044
2050
  let importedFamilies = 0;
2045
2051
  for (const familyBase of families) {
2046
2052
  const day = dayOfFamily(familyBase);
2047
2053
  const basePath = path9.join(directory, `${familyBase}.jsonl`);
2048
2054
  let familyEvents = 0;
2055
+ const familyBoundary = {};
2049
2056
  if (journal.hasImportedDay(day)) continue;
2050
2057
  try {
2051
2058
  await journal.runFamilyImport(async (sink) => {
@@ -2059,6 +2066,7 @@ async function importLegacyChronicleJournal(journal, directory) {
2059
2066
  let expectedSequence = (checkpoint?.sequence ?? 0) + 1;
2060
2067
  let previousHash = checkpoint?.hash ?? GENESIS_HASH;
2061
2068
  for (const partition of await collectPartitions(basePath)) {
2069
+ familyBoundary[path9.relative(directory, partition).replaceAll("\\", "/")] = (await fs4.stat(partition)).size;
2062
2070
  for await (const event of streamEntriesStrict(partition)) {
2063
2071
  if (event.sequence !== expectedSequence) {
2064
2072
  throw new ChronicleImportError(
@@ -2093,10 +2101,13 @@ async function importLegacyChronicleJournal(journal, directory) {
2093
2101
  quarantined.push({ day, sequence: error.sequence, reason: error.message });
2094
2102
  continue;
2095
2103
  }
2104
+ Object.assign(boundary, familyBoundary);
2096
2105
  if (familyEvents > 0) importedFamilies += 1;
2097
2106
  importedEvents += familyEvents;
2107
+ journal.recordLegacyJsonlBoundary(boundary);
2098
2108
  }
2099
2109
  journal.recordQuarantinedFamilies(quarantined);
2110
+ journal.recordLegacyJsonlBoundary(boundary);
2100
2111
  journal.markLegacyJournalImported();
2101
2112
  return {
2102
2113
  alreadyImported: false,
@@ -2107,33 +2118,12 @@ async function importLegacyChronicleJournal(journal, directory) {
2107
2118
  }
2108
2119
 
2109
2120
  // src/chronicle/metrics-store.ts
2121
+ import * as path14 from "node:path";
2122
+
2123
+ // src/chronicle/metrics-ingest.ts
2110
2124
  import * as fs8 from "node:fs/promises";
2111
- import { createRequire as createRequire2 } from "node:module";
2112
2125
  import * as path13 from "node:path";
2113
2126
 
2114
- // src/utils/sqlite-warning.ts
2115
- var SQLITE_EXPERIMENTAL_WARNING_RE = /sqlite is an experimental feature/i;
2116
- function isSqliteExperimentalWarning(warning, rest) {
2117
- const message = typeof warning === "string" ? warning : warning instanceof Error ? warning.message : "";
2118
- const typeOrOptions = rest[0];
2119
- const warningType = typeof warning === "string" ? typeof typeOrOptions === "string" ? typeOrOptions : typeof typeOrOptions === "object" && typeOrOptions !== null && "type" in typeOrOptions && typeof typeOrOptions.type === "string" ? typeOrOptions.type : "" : warning instanceof Error ? warning.name : "";
2120
- const warningCode = typeof warning === "string" ? typeof typeOrOptions === "object" && typeOrOptions !== null && "code" in typeOrOptions && typeof typeOrOptions.code === "string" ? typeOrOptions.code : typeof rest[1] === "string" ? rest[1] : "" : warning instanceof Error && "code" in warning && typeof warning.code === "string" ? warning.code : "";
2121
- return SQLITE_EXPERIMENTAL_WARNING_RE.test(message) && (warningType === "ExperimentalWarning" || warningCode === "ExperimentalWarning");
2122
- }
2123
- function withSqliteExperimentalWarningSuppressed(run) {
2124
- const originalEmitWarning = process.emitWarning;
2125
- const forwardWarning = originalEmitWarning.bind(process);
2126
- process.emitWarning = ((warning, ...rest) => {
2127
- if (isSqliteExperimentalWarning(warning, rest)) return;
2128
- forwardWarning(warning, ...rest);
2129
- });
2130
- try {
2131
- return run();
2132
- } finally {
2133
- process.emitWarning = originalEmitWarning;
2134
- }
2135
- }
2136
-
2137
2127
  // src/chronicle/query.ts
2138
2128
  import { createHash as createHash4 } from "node:crypto";
2139
2129
  import { createReadStream as createReadStream2 } from "node:fs";
@@ -2215,14 +2205,21 @@ var ChroniclePartitionRangeCache = class {
2215
2205
 
2216
2206
  // src/chronicle/query.ts
2217
2207
  var MAX_CURSOR_SNAPSHOT_ENTRIES = 1e4;
2218
- function streamLines(filePath, maxBytes) {
2208
+ async function* streamLines(filePath, maxBytes) {
2219
2209
  const stream = createReadStream2(filePath, {
2220
2210
  encoding: "utf8",
2221
2211
  highWaterMark: 256 * 1024,
2222
2212
  ...maxBytes !== void 0 ? { end: maxBytes - 1 } : {}
2223
2213
  });
2224
2214
  const rl = createInterface2({ input: stream, crlfDelay: Infinity });
2225
- return rl[Symbol.asyncIterator]();
2215
+ try {
2216
+ for await (const line of rl) {
2217
+ yield line;
2218
+ }
2219
+ } finally {
2220
+ rl.close();
2221
+ stream.destroy();
2222
+ }
2226
2223
  }
2227
2224
  async function computeOccurredAtRange(file, size) {
2228
2225
  let min;
@@ -2924,9 +2921,255 @@ function facetValue(event, field) {
2924
2921
 
2925
2922
  // src/chronicle/sqlite-journal.ts
2926
2923
  import { randomUUID as randomUUID3 } from "node:crypto";
2924
+ import * as path12 from "node:path";
2925
+
2926
+ // src/chronicle/sqlite-journal-quota.ts
2927
2927
  import * as fs7 from "node:fs";
2928
+ var SQLITE_FIXED_OVERHEAD_BYTES = 16 * 1024 * 1024;
2929
+ var MIN_SQLITE_PAGE_BUDGET_BYTES = 64 * 1024;
2930
+ var MAX_WAL_RESERVE_BYTES = 32 * 1024 * 1024;
2931
+ var WAL_AUTOCHECKPOINT_PAGES = 2e3;
2932
+ var WAL_SIZE_LIMIT_BYTES = 16 * 1024 * 1024;
2933
+ var ChronicleStorageQuotaError = class extends Error {
2934
+ currentBytes;
2935
+ batchBytes;
2936
+ maxBytes;
2937
+ path;
2938
+ constructor(details) {
2939
+ super(
2940
+ `Chronicle SQLite quota exceeded at ${details.path}: ${details.currentBytes} live bytes + ${details.batchBytes} batch bytes exceeds ${details.maxBytes}; lower chronicle retentionDays/maxEvents to shed data (run chronicle compact to return the freed pages to the filesystem)`
2941
+ );
2942
+ this.name = "ChronicleStorageQuotaError";
2943
+ this.currentBytes = details.currentBytes;
2944
+ this.batchBytes = details.batchBytes;
2945
+ this.maxBytes = details.maxBytes;
2946
+ this.path = details.path;
2947
+ }
2948
+ };
2949
+ var ChronicleQuotaManager = class {
2950
+ constructor(db, dbPath, maxBytes) {
2951
+ this.db = db;
2952
+ this.dbPath = dbPath;
2953
+ this.maxBytes = maxBytes;
2954
+ }
2955
+ db;
2956
+ dbPath;
2957
+ maxBytes;
2958
+ cachedPageSize;
2959
+ quotaHeadroomBytes = 0;
2960
+ bytesSinceQuotaCheck = Number.POSITIVE_INFINITY;
2961
+ configure() {
2962
+ if (this.maxBytes === void 0) {
2963
+ this.db.exec("PRAGMA max_page_count = 2147483646");
2964
+ return;
2965
+ }
2966
+ const halfSplit = Math.floor((this.maxBytes - SQLITE_FIXED_OVERHEAD_BYTES) / 2);
2967
+ const sidecarReserve = Math.min(MAX_WAL_RESERVE_BYTES, halfSplit);
2968
+ const mainBudget = this.maxBytes - SQLITE_FIXED_OVERHEAD_BYTES - sidecarReserve;
2969
+ if (mainBudget < MIN_SQLITE_PAGE_BUDGET_BYTES) {
2970
+ throw new Error(
2971
+ `maxBytes must be at least ${SQLITE_FIXED_OVERHEAD_BYTES + 2 * MIN_SQLITE_PAGE_BUDGET_BYTES}`
2972
+ );
2973
+ }
2974
+ const maxPages = Math.max(1, Math.floor(mainBudget / this.pageSizeBytes()));
2975
+ this.db.exec(`PRAGMA max_page_count = ${maxPages}`);
2976
+ }
2977
+ pageSizeBytes() {
2978
+ if (this.cachedPageSize === void 0) {
2979
+ this.cachedPageSize = Number(
2980
+ this.db.prepare("PRAGMA page_size").get().page_size
2981
+ );
2982
+ }
2983
+ return this.cachedPageSize;
2984
+ }
2985
+ aggregateLiveBytes() {
2986
+ let total = 0;
2987
+ for (const file of [`${this.dbPath}-journal`, `${this.dbPath}-wal`, `${this.dbPath}-shm`]) {
2988
+ try {
2989
+ total += fs7.statSync(file).size;
2990
+ } catch (error) {
2991
+ if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) throw error;
2992
+ }
2993
+ }
2994
+ return total + this.mainDatabaseLiveBytes();
2995
+ }
2996
+ mainDatabaseLiveBytes() {
2997
+ const pageCount = Number(
2998
+ this.db.prepare("PRAGMA page_count").get().page_count
2999
+ );
3000
+ const freelist = Number(
3001
+ this.db.prepare("PRAGMA freelist_count").get().freelist_count
3002
+ );
3003
+ return Math.max(0, pageCount - freelist) * this.pageSizeBytes();
3004
+ }
3005
+ invalidateEstimate() {
3006
+ this.quotaHeadroomBytes = 0;
3007
+ this.bytesSinceQuotaCheck = Number.POSITIVE_INFINITY;
3008
+ }
3009
+ recordAppendedBytes(bytes) {
3010
+ this.bytesSinceQuotaCheck += bytes;
3011
+ }
3012
+ assertWithinByteQuota(batchBytes) {
3013
+ if (this.maxBytes === void 0) return;
3014
+ if (!this.shouldMeasureQuota(batchBytes)) return;
3015
+ const currentBytes = this.aggregateLiveBytes();
3016
+ this.recordQuotaHeadroom(currentBytes);
3017
+ if (currentBytes + batchBytes <= this.maxBytes) return;
3018
+ throw new ChronicleStorageQuotaError({
3019
+ currentBytes,
3020
+ batchBytes,
3021
+ maxBytes: this.maxBytes,
3022
+ path: this.dbPath
3023
+ });
3024
+ }
3025
+ assertActualAllocationWithinQuota() {
3026
+ if (this.maxBytes === void 0) return;
3027
+ if (!this.shouldMeasureQuota()) return;
3028
+ const currentBytes = this.aggregateLiveBytes();
3029
+ this.recordQuotaHeadroom(currentBytes);
3030
+ if (currentBytes <= this.maxBytes) return;
3031
+ throw new ChronicleStorageQuotaError({
3032
+ currentBytes,
3033
+ batchBytes: 0,
3034
+ maxBytes: this.maxBytes,
3035
+ path: this.dbPath
3036
+ });
3037
+ }
3038
+ normalizeQuotaError(error) {
3039
+ if (this.maxBytes === void 0 || !(error instanceof Error)) return error;
3040
+ const sqliteError = error;
3041
+ const quotaExhausted = sqliteError.code === "SQLITE_FULL" || sqliteError.code === 13 || sqliteError.errcode === 13 || /database or disk is full/i.test(sqliteError.message);
3042
+ if (!quotaExhausted) return error;
3043
+ return new ChronicleStorageQuotaError({
3044
+ currentBytes: this.aggregateLiveBytes(),
3045
+ batchBytes: 0,
3046
+ maxBytes: this.maxBytes,
3047
+ path: this.dbPath
3048
+ });
3049
+ }
3050
+ shouldMeasureQuota(batchBytes = 0) {
3051
+ return this.bytesSinceQuotaCheck + batchBytes >= this.quotaHeadroomBytes / 2;
3052
+ }
3053
+ recordQuotaHeadroom(currentBytes) {
3054
+ this.quotaHeadroomBytes = Math.max(0, (this.maxBytes ?? 0) - currentBytes);
3055
+ this.bytesSinceQuotaCheck = 0;
3056
+ }
3057
+ };
3058
+
3059
+ // src/chronicle/sqlite-journal-schema.ts
2928
3060
  import { createRequire } from "node:module";
2929
- import * as path12 from "node:path";
3061
+
3062
+ // src/utils/sqlite-warning.ts
3063
+ var SQLITE_EXPERIMENTAL_WARNING_RE = /sqlite is an experimental feature/i;
3064
+ function isSqliteExperimentalWarning(warning, rest) {
3065
+ const message = typeof warning === "string" ? warning : warning instanceof Error ? warning.message : "";
3066
+ const typeOrOptions = rest[0];
3067
+ const warningType = typeof warning === "string" ? typeof typeOrOptions === "string" ? typeOrOptions : typeof typeOrOptions === "object" && typeOrOptions !== null && "type" in typeOrOptions && typeof typeOrOptions.type === "string" ? typeOrOptions.type : "" : warning instanceof Error ? warning.name : "";
3068
+ const warningCode = typeof warning === "string" ? typeof typeOrOptions === "object" && typeOrOptions !== null && "code" in typeOrOptions && typeof typeOrOptions.code === "string" ? typeOrOptions.code : typeof rest[1] === "string" ? rest[1] : "" : warning instanceof Error && "code" in warning && typeof warning.code === "string" ? warning.code : "";
3069
+ return SQLITE_EXPERIMENTAL_WARNING_RE.test(message) && (warningType === "ExperimentalWarning" || warningCode === "ExperimentalWarning");
3070
+ }
3071
+ function withSqliteExperimentalWarningSuppressed(run) {
3072
+ const originalEmitWarning = process.emitWarning;
3073
+ const forwardWarning = originalEmitWarning.bind(process);
3074
+ process.emitWarning = ((warning, ...rest) => {
3075
+ if (isSqliteExperimentalWarning(warning, rest)) return;
3076
+ forwardWarning(warning, ...rest);
3077
+ });
3078
+ try {
3079
+ return run();
3080
+ } finally {
3081
+ process.emitWarning = originalEmitWarning;
3082
+ }
3083
+ }
3084
+
3085
+ // src/chronicle/sqlite-journal-schema.ts
3086
+ var CHRONICLE_SQLITE_FILE = "chronicle.sqlite";
3087
+ var LEGACY_JSONL_MIGRATION_KEY = "legacy-jsonl-v1";
3088
+ var LEGACY_JSONL_QUARANTINE_KEY = "legacy-jsonl-v1:quarantine";
3089
+ var LEGACY_JSONL_BOUNDARY_KEY = "legacy-jsonl-v1:boundary";
3090
+ var SCHEMA_VERSION = 2;
3091
+ var Ctor;
3092
+ function loadDatabaseSync() {
3093
+ if (Ctor) return Ctor;
3094
+ if (Ctor === null) throw new Error("node:sqlite is unavailable in this runtime");
3095
+ try {
3096
+ Ctor = withSqliteExperimentalWarningSuppressed(
3097
+ () => createRequire(import.meta.url)("node:sqlite").DatabaseSync
3098
+ );
3099
+ return Ctor;
3100
+ } catch (error) {
3101
+ Ctor = null;
3102
+ throw new Error(
3103
+ "The Chronicle journal needs Node's built-in SQLite (node:sqlite, Node >= 22.5): " + (error instanceof Error ? error.message : String(error))
3104
+ );
3105
+ }
3106
+ }
3107
+ function projectEvent(event) {
3108
+ const occurredAt = event.occurredAt ?? event.observedAt;
3109
+ return {
3110
+ occurredAt,
3111
+ outcome: event.outcome ?? null,
3112
+ projectId: event.scope.projectId ?? null,
3113
+ sessionId: event.scope.sessionId ?? null,
3114
+ agentId: event.scope.agentId ?? null,
3115
+ taskId: event.scope.taskId ?? null,
3116
+ traceId: event.correlation?.traceId ?? null,
3117
+ logicalRequestId: event.correlation?.logicalRequestId ?? null,
3118
+ resourceKind: event.resource?.kind ?? null,
3119
+ resourceId: event.resource?.id ?? null,
3120
+ resourcePath: event.resource?.path ?? null,
3121
+ durationNs: event.durationNs ?? null
3122
+ };
3123
+ }
3124
+ function ensureChronicleSchema(db) {
3125
+ const version = db.prepare("PRAGMA user_version").get().user_version;
3126
+ db.exec(`
3127
+ CREATE TABLE IF NOT EXISTS events (
3128
+ day TEXT NOT NULL,
3129
+ sequence INTEGER NOT NULL,
3130
+ event_id TEXT NOT NULL UNIQUE,
3131
+ hash TEXT NOT NULL,
3132
+ previous_hash TEXT NOT NULL,
3133
+ occurred_at TEXT NOT NULL,
3134
+ event_type TEXT NOT NULL,
3135
+ outcome TEXT,
3136
+ project_id TEXT,
3137
+ session_id TEXT,
3138
+ agent_id TEXT,
3139
+ task_id TEXT,
3140
+ trace_id TEXT,
3141
+ logical_request_id TEXT,
3142
+ resource_kind TEXT,
3143
+ resource_id TEXT,
3144
+ resource_path TEXT,
3145
+ duration_ns TEXT,
3146
+ payload TEXT NOT NULL,
3147
+ PRIMARY KEY (day, sequence)
3148
+ );
3149
+ CREATE INDEX IF NOT EXISTS events_occurred_at ON events(occurred_at);
3150
+ CREATE INDEX IF NOT EXISTS events_type_outcome ON events(event_type, outcome);
3151
+ CREATE INDEX IF NOT EXISTS events_session ON events(session_id, day, sequence);
3152
+ CREATE INDEX IF NOT EXISTS events_trace ON events(trace_id);
3153
+ CREATE INDEX IF NOT EXISTS events_logical_request ON events(logical_request_id);
3154
+
3155
+ CREATE TABLE IF NOT EXISTS chain_checkpoint (
3156
+ day TEXT PRIMARY KEY,
3157
+ sequence INTEGER NOT NULL,
3158
+ hash TEXT NOT NULL
3159
+ );
3160
+
3161
+ CREATE TABLE IF NOT EXISTS chronicle_meta (
3162
+ key TEXT PRIMARY KEY,
3163
+ value TEXT NOT NULL
3164
+ );
3165
+ `);
3166
+ if (version < 2) {
3167
+ db.exec("DROP INDEX IF EXISTS events_resource_path");
3168
+ }
3169
+ if (version !== SCHEMA_VERSION) {
3170
+ db.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`);
3171
+ }
3172
+ }
2930
3173
 
2931
3174
  // src/chronicle/sqlite-query.ts
2932
3175
  var MAX_LIMIT = 1e4;
@@ -3166,49 +3409,8 @@ var ChronicleSqliteQueryEngine = class {
3166
3409
  };
3167
3410
 
3168
3411
  // src/chronicle/sqlite-journal.ts
3169
- var CHRONICLE_SQLITE_FILE = "chronicle.sqlite";
3170
- var LEGACY_JSONL_MIGRATION_KEY = "legacy-jsonl-v1";
3171
- var LEGACY_JSONL_QUARANTINE_KEY = "legacy-jsonl-v1:quarantine";
3172
- var SCHEMA_VERSION = 2;
3173
- var SQLITE_FIXED_OVERHEAD_BYTES = 16 * 1024 * 1024;
3174
- var MIN_SQLITE_PAGE_BUDGET_BYTES = 64 * 1024;
3175
- var MAX_WAL_RESERVE_BYTES = 32 * 1024 * 1024;
3176
- var WAL_AUTOCHECKPOINT_PAGES = 2e3;
3177
- var WAL_SIZE_LIMIT_BYTES = 16 * 1024 * 1024;
3178
3412
  var TRIM_SLACK_RATIO = 0.02;
3179
3413
  var MAX_TRIM_SLACK_EVENTS = 2e3;
3180
- var ChronicleStorageQuotaError = class extends Error {
3181
- currentBytes;
3182
- batchBytes;
3183
- maxBytes;
3184
- path;
3185
- constructor(details) {
3186
- super(
3187
- `Chronicle SQLite quota exceeded at ${details.path}: ${details.currentBytes} live bytes + ${details.batchBytes} batch bytes exceeds ${details.maxBytes}; lower chronicle retentionDays/maxEvents to shed data (run chronicle compact to return the freed pages to the filesystem)`
3188
- );
3189
- this.name = "ChronicleStorageQuotaError";
3190
- this.currentBytes = details.currentBytes;
3191
- this.batchBytes = details.batchBytes;
3192
- this.maxBytes = details.maxBytes;
3193
- this.path = details.path;
3194
- }
3195
- };
3196
- var Ctor;
3197
- function loadDatabaseSync() {
3198
- if (Ctor) return Ctor;
3199
- if (Ctor === null) throw new Error("node:sqlite is unavailable in this runtime");
3200
- try {
3201
- Ctor = withSqliteExperimentalWarningSuppressed(
3202
- () => createRequire(import.meta.url)("node:sqlite").DatabaseSync
3203
- );
3204
- return Ctor;
3205
- } catch (error) {
3206
- Ctor = null;
3207
- throw new Error(
3208
- "The Chronicle journal needs Node's built-in SQLite (node:sqlite, Node >= 22.5): " + (error instanceof Error ? error.message : String(error))
3209
- );
3210
- }
3211
- }
3212
3414
  var ChronicleSqliteJournal = class {
3213
3415
  db;
3214
3416
  dbPath;
@@ -3217,21 +3419,12 @@ var ChronicleSqliteJournal = class {
3217
3419
  idFactory;
3218
3420
  retentionDays;
3219
3421
  maxEvents;
3220
- maxBytes;
3221
3422
  retentionCheckIntervalMs;
3222
3423
  /** Overshoot allowed above `maxEvents` before eviction runs; see TRIM_SLACK_RATIO. */
3223
3424
  trimSlack;
3425
+ quotaManager;
3224
3426
  nextRetentionCheckAt = 0;
3225
3427
  retainedEventCount = 0;
3226
- /** Resolved lazily by `pageSizeBytes()`; constant for an open database. */
3227
- cachedPageSize;
3228
- /**
3229
- * Bytes the quota is known to have had spare at the last real measurement,
3230
- * and the bytes appended since. `assertWithinByteQuota` re-measures only once
3231
- * the second could plausibly have consumed the first — see its comment.
3232
- */
3233
- quotaHeadroomBytes = 0;
3234
- bytesSinceQuotaCheck = Number.POSITIVE_INFINITY;
3235
3428
  /**
3236
3429
  * Statements reused across appends.
3237
3430
  *
@@ -3279,7 +3472,6 @@ var ChronicleSqliteJournal = class {
3279
3472
  }
3280
3473
  this.retentionDays = options.retentionDays;
3281
3474
  this.maxEvents = options.maxEvents;
3282
- this.maxBytes = options.maxBytes;
3283
3475
  this.retentionCheckIntervalMs = options.retentionCheckIntervalMs ?? 60 * 60 * 1e3;
3284
3476
  this.trimSlack = this.maxEvents === void 0 ? 0 : Math.min(MAX_TRIM_SLACK_EVENTS, Math.floor(this.maxEvents * TRIM_SLACK_RATIO));
3285
3477
  const Database = loadDatabaseSync();
@@ -3288,8 +3480,9 @@ var ChronicleSqliteJournal = class {
3288
3480
  this.db.exec("PRAGMA synchronous = FULL");
3289
3481
  this.db.exec(`PRAGMA wal_autocheckpoint = ${WAL_AUTOCHECKPOINT_PAGES}`);
3290
3482
  this.db.exec(`PRAGMA journal_size_limit = ${WAL_SIZE_LIMIT_BYTES}`);
3291
- this.ensureSchema();
3292
- this.configureByteQuota();
3483
+ ensureChronicleSchema(this.db);
3484
+ this.quotaManager = new ChronicleQuotaManager(this.db, this.dbPath, options.maxBytes);
3485
+ this.quotaManager.configure();
3293
3486
  if (this.maxEvents !== void 0) {
3294
3487
  const row = this.db.prepare("SELECT COUNT(*) AS count FROM events").get();
3295
3488
  this.retainedEventCount = row.count;
@@ -3303,16 +3496,10 @@ var ChronicleSqliteJournal = class {
3303
3496
  return {
3304
3497
  ...this.counters,
3305
3498
  pendingEvents: 0,
3306
- // Retained at zero for wire compatibility: partitions do not exist here,
3307
- // but `ChronicleJournalStats` is part of the IPC health payload.
3308
3499
  partitionRolls: 0,
3309
3500
  ...this.lastBatchDurationMs !== void 0 ? { lastBatchDurationMs: this.lastBatchDurationMs } : {}
3310
3501
  };
3311
3502
  }
3312
- /**
3313
- * Writes are synchronous and transactional, so there is nothing buffered to
3314
- * flush. Kept to satisfy `ChronicleEventSink`.
3315
- */
3316
3503
  async flush() {
3317
3504
  return Promise.resolve();
3318
3505
  }
@@ -3320,13 +3507,6 @@ var ChronicleSqliteJournal = class {
3320
3507
  const [event] = await this.appendBatch([input]);
3321
3508
  return event;
3322
3509
  }
3323
- /**
3324
- * Append a batch as one transaction.
3325
- *
3326
- * The chain is computed in memory from a single anchor, so a partially
3327
- * applied batch would leave a hole in `sequence` that verification can never
3328
- * reconcile. Rollback plus anchor invalidation is what prevents that.
3329
- */
3330
3510
  async appendBatch(inputs) {
3331
3511
  if (inputs.length === 0) return [];
3332
3512
  const started = performance.now();
@@ -3359,7 +3539,7 @@ var ChronicleSqliteJournal = class {
3359
3539
  let retainedCountAfterCommit = this.retainedEventCount;
3360
3540
  try {
3361
3541
  this.db.exec("BEGIN IMMEDIATE");
3362
- this.assertWithinByteQuota(batchBytes);
3542
+ this.quotaManager.assertWithinByteQuota(batchBytes);
3363
3543
  const { insert } = this.preparedStatements();
3364
3544
  for (const [index, event] of events.entries()) {
3365
3545
  const row = projectEvent(event);
@@ -3388,7 +3568,7 @@ var ChronicleSqliteJournal = class {
3388
3568
  retainedCountAfterCommit = this.enforceEventLimitWithinTransaction(
3389
3569
  this.retainedEventCount + events.length
3390
3570
  );
3391
- this.assertActualAllocationWithinQuota();
3571
+ this.quotaManager.assertActualAllocationWithinQuota();
3392
3572
  this.db.exec("COMMIT");
3393
3573
  } catch (error) {
3394
3574
  try {
@@ -3396,15 +3576,15 @@ var ChronicleSqliteJournal = class {
3396
3576
  } catch {
3397
3577
  }
3398
3578
  this.anchors.clear();
3399
- this.invalidateQuotaEstimate();
3579
+ this.quotaManager.invalidateEstimate();
3400
3580
  this.counters.failedEvents += inputs.length;
3401
3581
  this.lastBatchDurationMs = performance.now() - started;
3402
- throw this.normalizeQuotaError(error);
3582
+ throw this.quotaManager.normalizeQuotaError(error);
3403
3583
  }
3404
3584
  this.anchors.set(day, previous);
3405
3585
  this.counters.persistedEvents += events.length;
3406
3586
  this.retainedEventCount = retainedCountAfterCommit;
3407
- this.bytesSinceQuotaCheck += batchBytes;
3587
+ this.quotaManager.recordAppendedBytes(batchBytes);
3408
3588
  this.lastBatchDurationMs = performance.now() - started;
3409
3589
  await this.enforceRetentionIfDue();
3410
3590
  return events;
@@ -3413,19 +3593,6 @@ var ChronicleSqliteJournal = class {
3413
3593
  const rows = this.db.prepare("SELECT payload FROM events ORDER BY day, sequence").all();
3414
3594
  return rows.map((row) => JSON.parse(row.payload));
3415
3595
  }
3416
- /**
3417
- * Walk every chain and prove none has been edited.
3418
- *
3419
- * Chronicle chains are scoped to a day, not to the journal: the JSONL writer
3420
- * anchors each `<day>.events.jsonl` family at `GENESIS_HASH` independently,
3421
- * so `sequence` restarts at 1 every day. Verification mirrors that — each day
3422
- * is validated on its own, and a break in one does not implicate the others.
3423
- *
3424
- * Three independent properties per day, because each catches a different
3425
- * failure: a dense `sequence` catches deletions from the middle, the
3426
- * `previousHash` link catches reordering, and re-hashing the payload catches
3427
- * an in-place edit that left the links intact.
3428
- */
3429
3596
  async verify() {
3430
3597
  let entries = 0;
3431
3598
  let lastSequence = 0;
@@ -3477,155 +3644,6 @@ var ChronicleSqliteJournal = class {
3477
3644
  } catch {
3478
3645
  }
3479
3646
  }
3480
- configureByteQuota() {
3481
- if (this.maxBytes === void 0) {
3482
- this.db.exec("PRAGMA max_page_count = 2147483646");
3483
- return;
3484
- }
3485
- const halfSplit = Math.floor((this.maxBytes - SQLITE_FIXED_OVERHEAD_BYTES) / 2);
3486
- const sidecarReserve = Math.min(MAX_WAL_RESERVE_BYTES, halfSplit);
3487
- const mainBudget = this.maxBytes - SQLITE_FIXED_OVERHEAD_BYTES - sidecarReserve;
3488
- if (mainBudget < MIN_SQLITE_PAGE_BUDGET_BYTES) {
3489
- throw new Error(
3490
- `maxBytes must be at least ${SQLITE_FIXED_OVERHEAD_BYTES + 2 * MIN_SQLITE_PAGE_BUDGET_BYTES}`
3491
- );
3492
- }
3493
- const maxPages = Math.max(1, Math.floor(mainBudget / this.pageSizeBytes()));
3494
- this.db.exec(`PRAGMA max_page_count = ${maxPages}`);
3495
- }
3496
- /**
3497
- * Bytes the journal actually occupies: live pages in the main database plus
3498
- * whatever the rollback/WAL sidecars currently hold.
3499
- *
3500
- * The quota MUST be measured this way rather than by `statSync` on the main
3501
- * database file. SQLite never returns freed pages to the filesystem — it
3502
- * parks them on the freelist and reuses them — so a database that once grew
3503
- * large keeps that size forever, even after retention and `maxEvents` have
3504
- * evicted almost everything. Measuring the file instead wedged the journal
3505
- * permanently: a 3.9 GB file whose live data was 243 MB failed a 512 MB
3506
- * quota on EVERY append, and the only escape (`chronicle compact`) refuses
3507
- * to run while the daemon is up — and the daemon respawns on demand. Live
3508
- * pages shrink when retention deletes rows, so the quota can recover on its
3509
- * own; allocated size cannot.
3510
- *
3511
- * The sidecars keep their on-disk size: they are transient and genuinely
3512
- * bounded by `configureByteQuota`'s half-of-budget split.
3513
- */
3514
- aggregateLiveBytes() {
3515
- let total = 0;
3516
- for (const file of [`${this.dbPath}-journal`, `${this.dbPath}-wal`, `${this.dbPath}-shm`]) {
3517
- try {
3518
- total += fs7.statSync(file).size;
3519
- } catch (error) {
3520
- if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) throw error;
3521
- }
3522
- }
3523
- return total + this.mainDatabaseLiveBytes();
3524
- }
3525
- /** Live (non-freelist) pages of the main database, in bytes. */
3526
- mainDatabaseLiveBytes() {
3527
- const pageCount = Number(
3528
- this.db.prepare("PRAGMA page_count").get().page_count
3529
- );
3530
- const freelist = Number(
3531
- this.db.prepare("PRAGMA freelist_count").get().freelist_count
3532
- );
3533
- return Math.max(0, pageCount - freelist) * this.pageSizeBytes();
3534
- }
3535
- /** Page size never changes for an open database; resolve it once. */
3536
- pageSizeBytes() {
3537
- if (this.cachedPageSize === void 0) {
3538
- this.cachedPageSize = Number(
3539
- this.db.prepare("PRAGMA page_size").get().page_size
3540
- );
3541
- }
3542
- return this.cachedPageSize;
3543
- }
3544
- normalizeQuotaError(error) {
3545
- if (this.maxBytes === void 0 || !(error instanceof Error)) return error;
3546
- const sqliteError = error;
3547
- const quotaExhausted = sqliteError.code === "SQLITE_FULL" || sqliteError.code === 13 || sqliteError.errcode === 13 || /database or disk is full/i.test(sqliteError.message);
3548
- if (!quotaExhausted) return error;
3549
- return new ChronicleStorageQuotaError({
3550
- currentBytes: this.aggregateLiveBytes(),
3551
- batchBytes: 0,
3552
- maxBytes: this.maxBytes,
3553
- path: this.dbPath
3554
- });
3555
- }
3556
- /** Drop the cached headroom so the next quota check measures for real. */
3557
- invalidateQuotaEstimate() {
3558
- this.quotaHeadroomBytes = 0;
3559
- this.bytesSinceQuotaCheck = Number.POSITIVE_INFINITY;
3560
- }
3561
- assertActualAllocationWithinQuota() {
3562
- if (this.maxBytes === void 0) return;
3563
- if (!this.shouldMeasureQuota()) return;
3564
- const currentBytes = this.aggregateLiveBytes();
3565
- this.recordQuotaHeadroom(currentBytes);
3566
- if (currentBytes <= this.maxBytes) return;
3567
- throw new ChronicleStorageQuotaError({
3568
- currentBytes,
3569
- batchBytes: 0,
3570
- maxBytes: this.maxBytes,
3571
- path: this.dbPath
3572
- });
3573
- }
3574
- /**
3575
- * Refuse a batch that would take the journal past its byte quota.
3576
- *
3577
- * This is the *courteous* bound, not the enforced one: `max_page_count` makes
3578
- * SQLite itself fail the write with `SQLITE_FULL`, which `normalizeQuotaError`
3579
- * turns into the same {@link ChronicleStorageQuotaError}. Its only job is to
3580
- * produce that error before a doomed batch has been written, so it does not
3581
- * have to run on every append — and it should not, because measuring costs two
3582
- * PRAGMA round trips and three `statSync` calls, twice per transaction.
3583
- *
3584
- * So it measures adaptively instead of on a fixed cadence: after a real
3585
- * measurement it knows the spare bytes, and it may skip until the bytes
3586
- * appended since then could plausibly have consumed half of them. Far from the
3587
- * ceiling that is thousands of appends; close to it, every one. The bound
3588
- * tightens exactly where being wrong would matter.
3589
- */
3590
- assertWithinByteQuota(batchBytes) {
3591
- if (this.maxBytes === void 0) return;
3592
- if (!this.shouldMeasureQuota(batchBytes)) return;
3593
- const currentBytes = this.aggregateLiveBytes();
3594
- this.recordQuotaHeadroom(currentBytes);
3595
- if (currentBytes + batchBytes <= this.maxBytes) return;
3596
- throw new ChronicleStorageQuotaError({
3597
- currentBytes,
3598
- batchBytes,
3599
- maxBytes: this.maxBytes,
3600
- path: this.dbPath
3601
- });
3602
- }
3603
- /**
3604
- * Is the cached headroom still large enough to vouch for this batch?
3605
- *
3606
- * Half the headroom is the budget deliberately: rows carry index and page
3607
- * overhead beyond their payload bytes, so `bytesSinceQuotaCheck` understates
3608
- * real growth, and the factor absorbs that without needing to model it.
3609
- */
3610
- shouldMeasureQuota(batchBytes = 0) {
3611
- return this.bytesSinceQuotaCheck + batchBytes >= this.quotaHeadroomBytes / 2;
3612
- }
3613
- recordQuotaHeadroom(currentBytes) {
3614
- this.quotaHeadroomBytes = Math.max(0, (this.maxBytes ?? 0) - currentBytes);
3615
- this.bytesSinceQuotaCheck = 0;
3616
- }
3617
- /**
3618
- * Evict the oldest events once the row ceiling has been overshot by `trimSlack`.
3619
- *
3620
- * @param count Rows now in the table. The caller tracks this rather than the
3621
- * method querying it: `SELECT COUNT(*)` walks the whole table, and at the
3622
- * ceiling — where every long-lived journal lives — that was a full scan of
3623
- * `maxEvents` rows on every append, to learn a number the append path already
3624
- * knew. The count is re-derived from the database whenever it could have
3625
- * drifted (open, import, purge), never accumulated blindly across those.
3626
- * @param slack Overshoot tolerated before evicting; callers that run once,
3627
- * rather than per append, pass 0 to land on the exact ceiling.
3628
- */
3629
3647
  enforceEventLimitWithinTransaction(count, slack = this.trimSlack) {
3630
3648
  if (this.maxEvents === void 0 || count <= this.maxEvents + slack) return count;
3631
3649
  const excess = count - this.maxEvents;
@@ -3655,15 +3673,6 @@ var ChronicleSqliteJournal = class {
3655
3673
  throw error;
3656
3674
  }
3657
3675
  }
3658
- /**
3659
- * Drop events older than the retention window.
3660
- *
3661
- * Retention is day-granular and chains are day-scoped, so a purge removes
3662
- * whole chains rather than truncating one. That is why nothing needs to be
3663
- * checkpointed here: there is no surviving suffix left dangling without an
3664
- * anchor. Any checkpoint imported from a partially-purged legacy day family
3665
- * is dropped alongside its events.
3666
- */
3667
3676
  async purge(options) {
3668
3677
  const empty = {
3669
3678
  deletedCount: 0,
@@ -3701,25 +3710,9 @@ var ChronicleSqliteJournal = class {
3701
3710
  }
3702
3711
  this.anchors.clear();
3703
3712
  this.retainedEventCount = this.countRows();
3704
- this.invalidateQuotaEstimate();
3713
+ this.quotaManager.invalidateEstimate();
3705
3714
  return { ...empty, deletedCount: count };
3706
3715
  }
3707
- /**
3708
- * Run one day family's legacy import inside its own transaction.
3709
- *
3710
- * Deliberately separate from `appendBatch`: the append path *computes*
3711
- * `sequence`, `previousHash` and `hash`, while an import must carry them over
3712
- * untouched. Fusing the two would put a code path one refactor away from
3713
- * re-hashing historical events, which is the one change that silently
3714
- * destroys their tamper evidence.
3715
- *
3716
- * The transaction is scoped to a single family because chains are: `sequence`
3717
- * restarts at 1 each day, so one day's break says nothing about the next
3718
- * day's integrity. A whole-journal transaction made every future day hostage
3719
- * to the worst day on disk — one corrupt family and the daemon could never
3720
- * open its store again. The family is still all-or-nothing: a break rolls
3721
- * back that day entirely, so no partial chain is ever visible.
3722
- */
3723
3716
  async runFamilyImport(load) {
3724
3717
  const insert = this.db.prepare(
3725
3718
  `INSERT INTO events (
@@ -3735,8 +3728,8 @@ var ChronicleSqliteJournal = class {
3735
3728
  this.db.exec("BEGIN IMMEDIATE");
3736
3729
  let retainedCountAfterCommit = this.retainedEventCount;
3737
3730
  try {
3738
- this.invalidateQuotaEstimate();
3739
- this.assertWithinByteQuota(0);
3731
+ this.quotaManager.invalidateEstimate();
3732
+ this.quotaManager.assertWithinByteQuota(0);
3740
3733
  await load({
3741
3734
  insert: (day, event) => {
3742
3735
  const row = projectEvent(event);
@@ -3767,32 +3760,24 @@ var ChronicleSqliteJournal = class {
3767
3760
  }
3768
3761
  });
3769
3762
  retainedCountAfterCommit = this.enforceEventLimitWithinTransaction(this.countRows(), 0);
3770
- this.invalidateQuotaEstimate();
3771
- this.assertActualAllocationWithinQuota();
3763
+ this.quotaManager.invalidateEstimate();
3764
+ this.quotaManager.assertActualAllocationWithinQuota();
3772
3765
  this.db.exec("COMMIT");
3773
3766
  } catch (error) {
3774
3767
  try {
3775
3768
  this.db.exec("ROLLBACK");
3776
3769
  } catch {
3777
3770
  }
3778
- this.invalidateQuotaEstimate();
3779
- throw this.normalizeQuotaError(error);
3771
+ this.quotaManager.invalidateEstimate();
3772
+ throw this.quotaManager.normalizeQuotaError(error);
3780
3773
  } finally {
3781
3774
  this.anchors.clear();
3782
3775
  }
3783
3776
  this.retainedEventCount = retainedCountAfterCommit;
3784
3777
  }
3785
- /**
3786
- * A read engine over this journal's own connection.
3787
- *
3788
- * Sharing the connection rather than opening a second one keeps the
3789
- * single-writer guarantee intact and means a reader can never observe a
3790
- * half-applied batch: SQLite serialises statements on one handle.
3791
- */
3792
3778
  queryEngine(options) {
3793
3779
  return new ChronicleSqliteQueryEngine(this.db, options);
3794
3780
  }
3795
- /** Has the legacy JSONL import already run? */
3796
3781
  hasImportedLegacyJournal() {
3797
3782
  return this.readMeta(LEGACY_JSONL_MIGRATION_KEY) !== void 0;
3798
3783
  }
@@ -3802,15 +3787,6 @@ var ChronicleSqliteJournal = class {
3802
3787
  ON CONFLICT(key) DO UPDATE SET value = 'done'`
3803
3788
  ).run(LEGACY_JSONL_MIGRATION_KEY);
3804
3789
  }
3805
- /**
3806
- * Record the day families the import refused to move.
3807
- *
3808
- * Persisted rather than merely logged because the import runs once: after the
3809
- * marker is set nothing re-reads the JSONL, so this row is the only surviving
3810
- * evidence that a day was dropped. Health reports read it back to say
3811
- * "degraded, and here is exactly what is missing" instead of quietly serving
3812
- * a journal with a hole in it.
3813
- */
3814
3790
  recordQuarantinedFamilies(families) {
3815
3791
  if (families.length === 0) return;
3816
3792
  this.db.prepare(
@@ -3818,20 +3794,31 @@ var ChronicleSqliteJournal = class {
3818
3794
  ON CONFLICT(key) DO UPDATE SET value = excluded.value`
3819
3795
  ).run(LEGACY_JSONL_QUARANTINE_KEY, JSON.stringify(families));
3820
3796
  }
3821
- /**
3822
- * Does this day already hold rows?
3823
- *
3824
- * Each family commits on its own, so an import interrupted between families
3825
- * leaves a database that is complete for the days it reached. `(day,
3826
- * sequence)` is the primary key, so re-inserting one of those days would
3827
- * abort on a constraint violation rather than start over — this is what lets
3828
- * the next run resume at the first day it never got to.
3829
- */
3797
+ /** Record per-partition-file JSONL byte offsets at import time (merged — the
3798
+ * import persists per family, so a crash mid-run keeps earlier boundaries).
3799
+ * The metrics ingester folds only bytes beyond these boundaries after the
3800
+ * migration, so post-migration appends (the jsonl-store fallback) are
3801
+ * ingested while the migrated bytes are never re-counted. Files absent from
3802
+ * the map were quarantined (or created post-migration) their events live
3803
+ * only in JSONL. */
3804
+ recordLegacyJsonlBoundary(boundary) {
3805
+ let merged = boundary;
3806
+ try {
3807
+ const row = this.db.prepare("SELECT value FROM chronicle_meta WHERE key = ?").get(LEGACY_JSONL_BOUNDARY_KEY);
3808
+ if (row?.value) {
3809
+ merged = { ...JSON.parse(row.value), ...boundary };
3810
+ }
3811
+ } catch {
3812
+ }
3813
+ this.db.prepare(
3814
+ `INSERT INTO chronicle_meta (key, value) VALUES (?, ?)
3815
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value`
3816
+ ).run(LEGACY_JSONL_BOUNDARY_KEY, JSON.stringify(merged));
3817
+ }
3830
3818
  hasImportedDay(day) {
3831
3819
  const row = this.db.prepare("SELECT 1 AS present FROM events WHERE day = ? LIMIT 1").get(day);
3832
3820
  return row !== void 0;
3833
3821
  }
3834
- /** Day families the legacy import refused to move, oldest first. */
3835
3822
  quarantinedFamilies() {
3836
3823
  const raw = this.readMeta(LEGACY_JSONL_QUARANTINE_KEY);
3837
3824
  if (!raw) return [];
@@ -3842,7 +3829,6 @@ var ChronicleSqliteJournal = class {
3842
3829
  return [];
3843
3830
  }
3844
3831
  }
3845
- // ─── internals ────────────────────────────────────────────────────────────
3846
3832
  preparedStatements() {
3847
3833
  this.statements ??= {
3848
3834
  insert: this.db.prepare(
@@ -3866,7 +3852,6 @@ var ChronicleSqliteJournal = class {
3866
3852
  };
3867
3853
  return this.statements;
3868
3854
  }
3869
- /** Rows currently in the table, straight from the database. */
3870
3855
  countRows() {
3871
3856
  return this.db.prepare("SELECT COUNT(*) AS count FROM events").get().count;
3872
3857
  }
@@ -3877,16 +3862,10 @@ var ChronicleSqliteJournal = class {
3877
3862
  readCheckpoint(day) {
3878
3863
  return this.db.prepare("SELECT sequence, hash FROM chain_checkpoint WHERE day = ?").get(day);
3879
3864
  }
3880
- /** Days that currently hold at least one event, oldest first. */
3881
3865
  days() {
3882
3866
  const rows = this.db.prepare("SELECT DISTINCT day FROM events ORDER BY day").all();
3883
3867
  return rows.map((row) => row.day);
3884
3868
  }
3885
- /**
3886
- * Resolve the chain head, in the same precedence the JSONL journal uses: the
3887
- * newest event, else the retention checkpoint (every event before it was
3888
- * purged), else genesis.
3889
- */
3890
3869
  readAnchor(day) {
3891
3870
  const cached = this.anchors.get(day);
3892
3871
  if (cached) return cached;
@@ -3895,75 +3874,10 @@ var ChronicleSqliteJournal = class {
3895
3874
  this.anchors.set(day, anchor);
3896
3875
  return anchor;
3897
3876
  }
3898
- ensureSchema() {
3899
- const version = this.db.prepare("PRAGMA user_version").get().user_version;
3900
- this.db.exec(`
3901
- CREATE TABLE IF NOT EXISTS events (
3902
- day TEXT NOT NULL,
3903
- sequence INTEGER NOT NULL,
3904
- event_id TEXT NOT NULL UNIQUE,
3905
- hash TEXT NOT NULL,
3906
- previous_hash TEXT NOT NULL,
3907
- occurred_at TEXT NOT NULL,
3908
- event_type TEXT NOT NULL,
3909
- outcome TEXT,
3910
- project_id TEXT,
3911
- session_id TEXT,
3912
- agent_id TEXT,
3913
- task_id TEXT,
3914
- trace_id TEXT,
3915
- logical_request_id TEXT,
3916
- resource_kind TEXT,
3917
- resource_id TEXT,
3918
- resource_path TEXT,
3919
- duration_ns TEXT,
3920
- payload TEXT NOT NULL,
3921
- PRIMARY KEY (day, sequence)
3922
- );
3923
- CREATE INDEX IF NOT EXISTS events_occurred_at ON events(occurred_at);
3924
- CREATE INDEX IF NOT EXISTS events_type_outcome ON events(event_type, outcome);
3925
- CREATE INDEX IF NOT EXISTS events_session ON events(session_id, day, sequence);
3926
- CREATE INDEX IF NOT EXISTS events_trace ON events(trace_id);
3927
- CREATE INDEX IF NOT EXISTS events_logical_request ON events(logical_request_id);
3928
-
3929
- CREATE TABLE IF NOT EXISTS chain_checkpoint (
3930
- day TEXT PRIMARY KEY,
3931
- sequence INTEGER NOT NULL,
3932
- hash TEXT NOT NULL
3933
- );
3934
-
3935
- CREATE TABLE IF NOT EXISTS chronicle_meta (
3936
- key TEXT PRIMARY KEY,
3937
- value TEXT NOT NULL
3938
- );
3939
- `);
3940
- if (version < 2) {
3941
- this.db.exec("DROP INDEX IF EXISTS events_resource_path");
3942
- }
3943
- if (version !== SCHEMA_VERSION) {
3944
- this.db.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`);
3945
- }
3946
- }
3947
3877
  };
3948
- function projectEvent(event) {
3949
- const occurredAt = event.occurredAt ?? event.observedAt;
3950
- return {
3951
- occurredAt,
3952
- outcome: event.outcome ?? null,
3953
- projectId: event.scope.projectId ?? null,
3954
- sessionId: event.scope.sessionId ?? null,
3955
- agentId: event.scope.agentId ?? null,
3956
- taskId: event.scope.taskId ?? null,
3957
- traceId: event.correlation?.traceId ?? null,
3958
- logicalRequestId: event.correlation?.logicalRequestId ?? null,
3959
- resourceKind: event.resource?.kind ?? null,
3960
- resourceId: event.resource?.id ?? null,
3961
- resourcePath: event.resource?.path ?? null,
3962
- durationNs: event.durationNs ?? null
3963
- };
3964
- }
3965
3878
 
3966
- // src/chronicle/metrics-store.ts
3879
+ // src/chronicle/metrics-schema.ts
3880
+ import { createRequire as createRequire2 } from "node:module";
3967
3881
  var SCHEMA_VERSION2 = 3;
3968
3882
  var READ_CHUNK_BYTES = 1024 * 1024;
3969
3883
  var SQLITE_SOURCE_PREFIX = "sqlite:";
@@ -3995,26 +3909,155 @@ function loadDatabaseSync2() {
3995
3909
  );
3996
3910
  }
3997
3911
  }
3998
- var ChronicleMetricsStore = class _ChronicleMetricsStore {
3912
+ function ensureMetricsSchema(db) {
3913
+ const version = db.prepare("PRAGMA user_version").get().user_version;
3914
+ if (version !== 0 && version !== SCHEMA_VERSION2) {
3915
+ db.exec(
3916
+ "DROP TABLE IF EXISTS ingest_state; DROP TABLE IF EXISTS provider_daily;DROP TABLE IF EXISTS task_outcomes; DROP TABLE IF EXISTS file_lineage;DROP TABLE IF EXISTS token_cost; DROP TABLE IF EXISTS daily_counters;DROP TABLE IF EXISTS family_daily; DROP TABLE IF EXISTS agent_daily;DROP TABLE IF EXISTS logical_request_daily; DROP TABLE IF EXISTS file_seen_daily;"
3917
+ );
3918
+ }
3919
+ db.exec(`
3920
+ CREATE TABLE IF NOT EXISTS ingest_state (
3921
+ file TEXT PRIMARY KEY,
3922
+ bytes INTEGER NOT NULL
3923
+ );
3924
+ CREATE TABLE IF NOT EXISTS provider_daily (
3925
+ day TEXT NOT NULL,
3926
+ provider_id TEXT NOT NULL,
3927
+ model_id TEXT NOT NULL,
3928
+ attempts INTEGER NOT NULL DEFAULT 0,
3929
+ completed INTEGER NOT NULL DEFAULT 0,
3930
+ failed INTEGER NOT NULL DEFAULT 0,
3931
+ retries INTEGER NOT NULL DEFAULT 0,
3932
+ fallbacks INTEGER NOT NULL DEFAULT 0,
3933
+ input_tokens INTEGER NOT NULL DEFAULT 0,
3934
+ output_tokens INTEGER NOT NULL DEFAULT 0,
3935
+ cache_read_tokens INTEGER NOT NULL DEFAULT 0,
3936
+ cache_write_tokens INTEGER NOT NULL DEFAULT 0,
3937
+ duration_ms_total REAL NOT NULL DEFAULT 0,
3938
+ duration_ms_max REAL NOT NULL DEFAULT 0,
3939
+ duration_count INTEGER NOT NULL DEFAULT 0,
3940
+ PRIMARY KEY (day, provider_id, model_id)
3941
+ );
3942
+ CREATE TABLE IF NOT EXISTS task_outcomes (
3943
+ task_id TEXT PRIMARY KEY,
3944
+ run_id TEXT NOT NULL DEFAULT '',
3945
+ board_id TEXT NOT NULL DEFAULT '',
3946
+ session_id TEXT NOT NULL DEFAULT '',
3947
+ agent_id TEXT NOT NULL DEFAULT '',
3948
+ status TEXT NOT NULL DEFAULT 'started',
3949
+ started_at TEXT,
3950
+ ended_at TEXT,
3951
+ duration_ms REAL,
3952
+ retries INTEGER NOT NULL DEFAULT 0,
3953
+ verification_failures INTEGER NOT NULL DEFAULT 0
3954
+ );
3955
+ CREATE TABLE IF NOT EXISTS file_lineage (
3956
+ event_id TEXT PRIMARY KEY,
3957
+ path TEXT NOT NULL,
3958
+ path_key TEXT NOT NULL,
3959
+ operation TEXT NOT NULL,
3960
+ occurred_at TEXT NOT NULL,
3961
+ session_id TEXT NOT NULL DEFAULT '',
3962
+ agent_id TEXT NOT NULL DEFAULT '',
3963
+ task_id TEXT NOT NULL DEFAULT '',
3964
+ board_id TEXT NOT NULL DEFAULT '',
3965
+ run_id TEXT NOT NULL DEFAULT '',
3966
+ tool_name TEXT NOT NULL DEFAULT '',
3967
+ provider_id TEXT NOT NULL DEFAULT '',
3968
+ model_id TEXT NOT NULL DEFAULT '',
3969
+ source TEXT NOT NULL DEFAULT ''
3970
+ );
3971
+ CREATE INDEX IF NOT EXISTS idx_file_lineage_path ON file_lineage(path_key, occurred_at);
3972
+ CREATE INDEX IF NOT EXISTS idx_file_lineage_task ON file_lineage(task_id);
3973
+ CREATE TABLE IF NOT EXISTS token_cost (
3974
+ scope_key TEXT PRIMARY KEY,
3975
+ day TEXT NOT NULL,
3976
+ occurred_at TEXT NOT NULL,
3977
+ sequence INTEGER NOT NULL,
3978
+ cost REAL NOT NULL
3979
+ );
3980
+ CREATE TABLE IF NOT EXISTS daily_counters (
3981
+ day TEXT PRIMARY KEY,
3982
+ tool_calls INTEGER NOT NULL DEFAULT 0,
3983
+ completed_tools INTEGER NOT NULL DEFAULT 0,
3984
+ failed_tools INTEGER NOT NULL DEFAULT 0,
3985
+ tool_duration_ms_total REAL NOT NULL DEFAULT 0,
3986
+ tool_duration_ms_max REAL NOT NULL DEFAULT 0,
3987
+ tool_duration_count INTEGER NOT NULL DEFAULT 0,
3988
+ processes INTEGER NOT NULL DEFAULT 0,
3989
+ failed_processes INTEGER NOT NULL DEFAULT 0,
3990
+ file_events_all INTEGER NOT NULL DEFAULT 0,
3991
+ decisions INTEGER NOT NULL DEFAULT 0,
3992
+ escalations INTEGER NOT NULL DEFAULT 0,
3993
+ agent_events INTEGER NOT NULL DEFAULT 0,
3994
+ failures INTEGER NOT NULL DEFAULT 0,
3995
+ cancellations INTEGER NOT NULL DEFAULT 0
3996
+ );
3997
+ CREATE TABLE IF NOT EXISTS family_daily (
3998
+ day TEXT NOT NULL,
3999
+ family TEXT NOT NULL,
4000
+ count INTEGER NOT NULL DEFAULT 0,
4001
+ failure_count INTEGER NOT NULL DEFAULT 0,
4002
+ PRIMARY KEY (day, family)
4003
+ );
4004
+ CREATE TABLE IF NOT EXISTS agent_daily (day TEXT NOT NULL, agent_id TEXT NOT NULL, PRIMARY KEY (day, agent_id));
4005
+ CREATE TABLE IF NOT EXISTS logical_request_daily (day TEXT NOT NULL, logical_request_id TEXT NOT NULL, PRIMARY KEY (day, logical_request_id));
4006
+ CREATE TABLE IF NOT EXISTS file_seen_daily (day TEXT NOT NULL, path_key TEXT NOT NULL, PRIMARY KEY (day, path_key));
4007
+ PRAGMA user_version = ${SCHEMA_VERSION2};
4008
+ `);
4009
+ }
4010
+ function eventDay(event) {
4011
+ return (event.occurredAt ?? event.observedAt).slice(0, 10);
4012
+ }
4013
+ function durationMs2(event) {
4014
+ const value = Number(event.durationNs ?? 0) / 1e6;
4015
+ return Number.isFinite(value) && value > 0 ? value : 0;
4016
+ }
4017
+ function numberOrDuration(event, attributes) {
4018
+ const explicit = attributes.durationMs;
4019
+ if (typeof explicit === "number" && Number.isFinite(explicit)) return explicit;
4020
+ return durationMs2(event);
4021
+ }
4022
+ function numberAt2(event, dotPath) {
4023
+ const value = readPath2(event.attributes ?? {}, dotPath);
4024
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
4025
+ }
4026
+ function readPath2(value, key) {
4027
+ return key.split(".").reduce(
4028
+ (current, part) => current && typeof current === "object" ? current[part] : void 0,
4029
+ value
4030
+ );
4031
+ }
4032
+ function stringAt(record, key) {
4033
+ const value = record[key];
4034
+ return typeof value === "string" && value.length > 0 ? value : void 0;
4035
+ }
4036
+ function asString(value) {
4037
+ return typeof value === "string" && value.length > 0 ? value : void 0;
4038
+ }
4039
+ function clampLimit(limit, fallback) {
4040
+ if (typeof limit !== "number" || !Number.isFinite(limit) || limit <= 0) return fallback;
4041
+ return Math.min(Math.floor(limit), 1e4);
4042
+ }
4043
+ function normalizeKey(value) {
4044
+ return value.replaceAll("\\", "/");
4045
+ }
4046
+ function normalizePathKey(value) {
4047
+ return value.replaceAll("\\", "/").replace(/^\.\//, "").toLowerCase();
4048
+ }
4049
+
4050
+ // src/chronicle/metrics-ingest.ts
4051
+ var REBUILD_MARKER_KEY = `${SQLITE_SOURCE_PREFIX}rebuild_done`;
4052
+ var ChronicleMetricsIngester = class {
4053
+ constructor(db, directory, dbPath) {
4054
+ this.db = db;
4055
+ this.directory = directory;
4056
+ this.dbPath = dbPath;
4057
+ }
3999
4058
  db;
4000
4059
  directory;
4001
4060
  dbPath;
4002
- constructor(directory) {
4003
- this.directory = path13.resolve(directory);
4004
- this.dbPath = path13.join(this.directory, "metrics.db");
4005
- const Database = loadDatabaseSync2();
4006
- this.db = new Database(this.dbPath);
4007
- this.db.exec("PRAGMA journal_mode = WAL");
4008
- this.ensureSchema();
4009
- }
4010
- static open(chronicleDirectory2) {
4011
- return new _ChronicleMetricsStore(chronicleDirectory2);
4012
- }
4013
- close() {
4014
- this.db.close();
4015
- }
4016
- /** Incrementally ingest journal bytes appended since the last refresh.
4017
- * Safe across processes: guarded by a file lock on the database path. */
4018
4061
  async refresh() {
4019
4062
  const result = {
4020
4063
  ingestedEvents: 0,
@@ -4025,546 +4068,267 @@ var ChronicleMetricsStore = class _ChronicleMetricsStore {
4025
4068
  await withFileLock(this.dbPath, async () => {
4026
4069
  const files = await findChroniclePartitions(this.directory);
4027
4070
  const offsets = this.loadOffsets();
4028
- for (const file of files) {
4029
- const key = normalizeKey(path13.relative(this.directory, file));
4030
- const consumed = offsets.get(key) ?? 0;
4031
- const ingested = await this.ingestFile(file, key, consumed, result);
4032
- if (ingested) result.sourceFiles++;
4071
+ const journalPath = path13.join(this.directory, CHRONICLE_SQLITE_FILE);
4072
+ let source = null;
4073
+ try {
4074
+ source = new (loadDatabaseSync2())(journalPath, { readOnly: true });
4075
+ } catch {
4033
4076
  }
4034
- this.pruneOffsets(files);
4035
- this.ingestSqliteJournal(offsets, result);
4036
- });
4037
- return result;
4077
+ try {
4078
+ const migrated = source !== null && this.legacyJsonlConsumed(source);
4079
+ const boundary = migrated && source !== null ? this.loadJsonlBoundary(source) : null;
4080
+ const rebuilt = migrated && this.needsSqliteRebuild(offsets);
4081
+ if (rebuilt && source !== null) {
4082
+ this.rebuildFromSqliteJournal(source, result);
4083
+ }
4084
+ if (!migrated || boundary !== null) {
4085
+ for (const file of files) {
4086
+ const key = normalizeKey(path13.relative(this.directory, file));
4087
+ let base;
4088
+ if (!migrated) {
4089
+ base = offsets.get(key) ?? 0;
4090
+ } else if (boundary !== null && boundary.has(key)) {
4091
+ base = Math.max(offsets.get(key) ?? 0, boundary.get(key));
4092
+ } else {
4093
+ base = rebuilt ? 0 : offsets.get(key) ?? 0;
4094
+ }
4095
+ const ingested = await this.ingestFile(file, key, base, result);
4096
+ if (ingested) result.sourceFiles++;
4097
+ }
4098
+ }
4099
+ this.pruneOffsets(files);
4100
+ if (source !== null && !rebuilt) {
4101
+ this.ingestSqliteJournal(source, offsets, result, false);
4102
+ if (migrated) this.ensureRebuildMarker();
4103
+ }
4104
+ } finally {
4105
+ if (source !== null) source.close();
4106
+ }
4107
+ });
4108
+ return result;
4038
4109
  }
4039
- providerDaily(options = {}) {
4040
- const clauses = [];
4041
- const params = [];
4042
- if (options.from) {
4043
- clauses.push("day >= ?");
4044
- params.push(options.from.slice(0, 10));
4110
+ loadOffsets() {
4111
+ const rows = this.db.prepare("SELECT file, bytes FROM ingest_state").all();
4112
+ return new Map(rows.map((row) => [row.file, Number(row.bytes)]));
4113
+ }
4114
+ /** True once the legacy JSONL journal was imported into the SQLite journal
4115
+ * (`importLegacyChronicleJournal` records the marker in `chronicle_meta`).
4116
+ * After that point the SQLite `events` table already holds every JSONL
4117
+ * event, so folding the leftover partition files again would double-count
4118
+ * every migrated event in the aggregates. `source` is the already-opened
4119
+ * read-only journal handle (null when no SQLite journal exists yet). */
4120
+ legacyJsonlConsumed(source) {
4121
+ if (source === null) return false;
4122
+ try {
4123
+ const row = source.prepare("SELECT value FROM chronicle_meta WHERE key = ?").get(LEGACY_JSONL_MIGRATION_KEY);
4124
+ return row?.value !== void 0;
4125
+ } catch {
4126
+ return false;
4045
4127
  }
4046
- if (options.to) {
4047
- clauses.push("day <= ?");
4048
- params.push(options.to.slice(0, 10));
4128
+ }
4129
+ /** Per-partition-file JSONL byte offsets recorded by
4130
+ * `importLegacyChronicleJournal` at import time (keyed by the same relative
4131
+ * path the fold uses). Files absent from the map were quarantined (or
4132
+ * created after the migration) — their events live only in JSONL. */
4133
+ loadJsonlBoundary(source) {
4134
+ try {
4135
+ const row = source.prepare("SELECT value FROM chronicle_meta WHERE key = ?").get(LEGACY_JSONL_BOUNDARY_KEY);
4136
+ if (!row?.value) return null;
4137
+ const parsed2 = JSON.parse(row.value);
4138
+ return new Map(Object.entries(parsed2));
4139
+ } catch {
4140
+ return null;
4049
4141
  }
4050
- const where = clauses.length > 0 ? ` WHERE ${clauses.join(" AND ")}` : "";
4051
- const rows = this.db.prepare(
4052
- `SELECT day, provider_id, model_id, attempts, completed, failed, retries, fallbacks,
4053
- input_tokens, output_tokens, cache_read_tokens, cache_write_tokens,
4054
- duration_ms_total, duration_ms_max, duration_count
4055
- FROM provider_daily${where} ORDER BY day DESC, provider_id, model_id`
4056
- ).all(...params);
4057
- return rows.map((row) => ({
4058
- day: String(row.day),
4059
- providerId: String(row.provider_id),
4060
- modelId: String(row.model_id),
4061
- attempts: Number(row.attempts),
4062
- completed: Number(row.completed),
4063
- failed: Number(row.failed),
4064
- retries: Number(row.retries),
4065
- fallbacks: Number(row.fallbacks),
4066
- inputTokens: Number(row.input_tokens),
4067
- outputTokens: Number(row.output_tokens),
4068
- cacheReadTokens: Number(row.cache_read_tokens),
4069
- cacheWriteTokens: Number(row.cache_write_tokens),
4070
- avgDurationMs: Number(row.duration_count) > 0 ? Number(row.duration_ms_total) / Number(row.duration_count) : 0,
4071
- maxDurationMs: Number(row.duration_ms_max)
4072
- }));
4073
4142
  }
4074
- taskOutcomes(options = {}) {
4075
- const clauses = [];
4076
- const params = [];
4077
- if (options.runId) {
4078
- clauses.push("t.run_id = ?");
4079
- params.push(options.runId);
4143
+ /** Persist the rebuild sentinel idempotently. Its presence makes
4144
+ * `needsSqliteRebuild` return false, so post-migration jsonl-store offsets
4145
+ * are never mistaken for pre-migration progress. */
4146
+ ensureRebuildMarker() {
4147
+ this.db.prepare(
4148
+ "INSERT INTO ingest_state (file, bytes) VALUES (?, ?) ON CONFLICT(file) DO UPDATE SET bytes = excluded.bytes"
4149
+ ).run(REBUILD_MARKER_KEY, 1);
4150
+ }
4151
+ /** Upgrade path: `metrics.db` already aggregated the legacy JSONL, then the
4152
+ * one-shot import copied those same events into the SQLite journal. There
4153
+ * is no `sqlite:` cursor yet, so a plain incremental fold would replay the
4154
+ * entire migrated history on top of the existing aggregates. The metrics
4155
+ * projection is disposable (fully derived from the journal), so rebuild it
4156
+ * from the SQLite source exactly once — the rebuild writes the cursors and
4157
+ * subsequent refreshes go back to incremental folding. */
4158
+ needsSqliteRebuild(offsets) {
4159
+ if (offsets.has(REBUILD_MARKER_KEY)) return false;
4160
+ for (const [key, bytes] of offsets) {
4161
+ if (!key.startsWith(SQLITE_SOURCE_PREFIX) && bytes > 0) return true;
4162
+ }
4163
+ return false;
4164
+ }
4165
+ rebuildFromSqliteJournal(source, result) {
4166
+ this.db.exec(
4167
+ "DELETE FROM ingest_state; DELETE FROM provider_daily; DELETE FROM task_outcomes; DELETE FROM file_lineage; DELETE FROM token_cost; DELETE FROM daily_counters; DELETE FROM family_daily; DELETE FROM agent_daily; DELETE FROM logical_request_daily; DELETE FROM file_seen_daily;"
4168
+ );
4169
+ this.ingestSqliteJournal(source, /* @__PURE__ */ new Map(), result, true);
4170
+ this.ensureRebuildMarker();
4171
+ }
4172
+ pruneOffsets(existingFiles) {
4173
+ const keep = new Set(
4174
+ existingFiles.map((file) => normalizeKey(path13.relative(this.directory, file)))
4175
+ );
4176
+ for (const row of this.db.prepare("SELECT file FROM ingest_state").all()) {
4177
+ if (row.file.startsWith(SQLITE_SOURCE_PREFIX)) continue;
4178
+ if (!keep.has(row.file))
4179
+ this.db.prepare("DELETE FROM ingest_state WHERE file = ?").run(row.file);
4080
4180
  }
4081
- if (options.boardId) {
4082
- clauses.push("t.board_id = ?");
4083
- params.push(options.boardId);
4181
+ }
4182
+ ingestSqliteJournal(source, offsets, result, propagateErrors) {
4183
+ try {
4184
+ const days = source.prepare("SELECT DISTINCT day FROM events ORDER BY day").all();
4185
+ const read = source.prepare(
4186
+ "SELECT sequence, payload FROM events WHERE day = ? AND sequence > ? ORDER BY sequence LIMIT ?"
4187
+ );
4188
+ const writeCursor = this.db.prepare(
4189
+ "INSERT INTO ingest_state (file, bytes) VALUES (?, ?) ON CONFLICT(file) DO UPDATE SET bytes = excluded.bytes"
4190
+ );
4191
+ for (const { day } of days) {
4192
+ const key = `${SQLITE_SOURCE_PREFIX}${day}`;
4193
+ const from = offsets.get(key) ?? 0;
4194
+ let cursor = from;
4195
+ this.db.exec("BEGIN");
4196
+ try {
4197
+ for (; ; ) {
4198
+ const rows = read.all(day, cursor, SQLITE_INGEST_BATCH);
4199
+ if (rows.length === 0) break;
4200
+ for (const row of rows) {
4201
+ try {
4202
+ this.ingestEventAtomically(JSON.parse(row.payload), result);
4203
+ } catch {
4204
+ result.invalidLines++;
4205
+ }
4206
+ result.ingestedBytes += row.payload.length;
4207
+ cursor = Number(row.sequence);
4208
+ }
4209
+ if (rows.length < SQLITE_INGEST_BATCH) break;
4210
+ }
4211
+ if (cursor > from) writeCursor.run(key, cursor);
4212
+ this.db.exec("COMMIT");
4213
+ } catch (error) {
4214
+ this.db.exec("ROLLBACK");
4215
+ throw error;
4216
+ }
4217
+ if (cursor > from) result.sourceFiles++;
4218
+ }
4219
+ } catch (error) {
4220
+ if (propagateErrors) throw error;
4084
4221
  }
4085
- if (options.sessionId) {
4086
- clauses.push("t.session_id = ?");
4087
- params.push(options.sessionId);
4222
+ }
4223
+ async ingestFile(file, key, consumed, result) {
4224
+ let handle;
4225
+ try {
4226
+ handle = await fs8.open(file, "r");
4227
+ } catch {
4228
+ return false;
4088
4229
  }
4089
- if (options.status) {
4090
- clauses.push("t.status = ?");
4091
- params.push(options.status);
4230
+ try {
4231
+ const size = (await handle.stat()).size;
4232
+ if (size <= consumed) return false;
4233
+ let position = consumed;
4234
+ let remainder = Buffer.alloc(0);
4235
+ let advanced = consumed;
4236
+ this.db.exec("BEGIN");
4237
+ try {
4238
+ while (position < size) {
4239
+ const length = Math.min(READ_CHUNK_BYTES, size - position);
4240
+ const buffer = Buffer.allocUnsafe(length);
4241
+ const { bytesRead } = await handle.read(buffer, 0, length, position);
4242
+ if (bytesRead <= 0) break;
4243
+ position += bytesRead;
4244
+ const data = remainder.length > 0 ? Buffer.concat([remainder, buffer.subarray(0, bytesRead)]) : buffer.subarray(0, bytesRead);
4245
+ const lastNewline = data.lastIndexOf(10);
4246
+ if (lastNewline < 0) {
4247
+ remainder = Buffer.from(data);
4248
+ continue;
4249
+ }
4250
+ for (const line of data.subarray(0, lastNewline).toString("utf8").split("\n")) {
4251
+ const trimmed = line.trim();
4252
+ if (!trimmed) continue;
4253
+ try {
4254
+ this.ingestEventAtomically(JSON.parse(trimmed), result);
4255
+ } catch {
4256
+ result.invalidLines++;
4257
+ }
4258
+ }
4259
+ advanced += lastNewline + 1;
4260
+ remainder = Buffer.from(data.subarray(lastNewline + 1));
4261
+ }
4262
+ this.db.prepare(
4263
+ "INSERT INTO ingest_state (file, bytes) VALUES (?, ?) ON CONFLICT(file) DO UPDATE SET bytes = excluded.bytes"
4264
+ ).run(key, advanced);
4265
+ this.db.exec("COMMIT");
4266
+ } catch (error) {
4267
+ this.db.exec("ROLLBACK");
4268
+ throw error;
4269
+ }
4270
+ result.ingestedBytes += advanced - consumed;
4271
+ return advanced > consumed;
4272
+ } finally {
4273
+ await handle.close();
4092
4274
  }
4093
- const where = clauses.length > 0 ? ` WHERE ${clauses.join(" AND ")}` : "";
4094
- params.push(clampLimit(options.limit, 100));
4095
- const rows = this.db.prepare(
4096
- `SELECT t.*, (SELECT COUNT(*) FROM file_lineage f WHERE f.task_id = t.task_id) AS files_touched
4097
- FROM task_outcomes t${where}
4098
- ORDER BY COALESCE(t.started_at, '') DESC LIMIT ?`
4099
- ).all(...params);
4100
- return rows.map((row) => ({
4101
- taskId: String(row.task_id),
4102
- runId: String(row.run_id),
4103
- boardId: String(row.board_id),
4104
- sessionId: String(row.session_id),
4105
- agentId: String(row.agent_id),
4106
- status: String(row.status),
4107
- startedAt: row.started_at === null ? null : String(row.started_at),
4108
- endedAt: row.ended_at === null ? null : String(row.ended_at),
4109
- durationMs: row.duration_ms === null ? null : Number(row.duration_ms),
4110
- retries: Number(row.retries),
4111
- verificationFailures: Number(row.verification_failures),
4112
- filesTouched: Number(row.files_touched)
4113
- }));
4114
4275
  }
4115
- fileLineage(options = {}) {
4116
- const clauses = [];
4117
- const params = [];
4118
- if (options.path) {
4119
- clauses.push("path_key = ?");
4120
- params.push(normalizePathKey(options.path));
4121
- }
4122
- if (options.paths) {
4123
- const pathKeys = [...new Set(options.paths.map(normalizePathKey))];
4124
- if (pathKeys.length === 0) return [];
4125
- clauses.push(`path_key IN (${pathKeys.map(() => "?").join(",")})`);
4126
- params.push(...pathKeys);
4276
+ /** Fold one event into the aggregates atomically. `ingestEvent` runs several
4277
+ * statements; when one throws mid-way (e.g. a malformed resource path), the
4278
+ * statements before it already mutated the aggregates. The savepoint rolls
4279
+ * those partial updates back: `ROLLBACK TO` rewinds, `RELEASE` (reached
4280
+ * before the re-throw) pops the savepoint so the next event in the batch
4281
+ * starts from a clean stack, and the batch commits only fully-succeeded
4282
+ * events. Dropping the `RELEASE` would leak the savepoint into the next
4283
+ * event's stack — the outer transaction ROLLBACK is never reached because
4284
+ * the per-event catch swallows the error and continues the batch. */
4285
+ ingestEventAtomically(event, result) {
4286
+ this.db.exec("SAVEPOINT ingest_event");
4287
+ try {
4288
+ this.ingestEvent(event);
4289
+ this.db.exec("RELEASE ingest_event");
4290
+ result.ingestedEvents++;
4291
+ } catch (error) {
4292
+ this.db.exec("ROLLBACK TO ingest_event");
4293
+ this.db.exec("RELEASE ingest_event");
4294
+ throw error;
4127
4295
  }
4128
- if (options.taskId) {
4129
- clauses.push("task_id = ?");
4130
- params.push(options.taskId);
4296
+ }
4297
+ ingestEvent(event) {
4298
+ if (typeof event?.eventType !== "string" || !event.scope) return;
4299
+ this.ingestDailyCounters(event);
4300
+ const type = event.eventType;
4301
+ if (type.startsWith("provider.attempt.") || type === "provider.fallback") {
4302
+ this.ingestProvider(event);
4303
+ } else if (type === "token.accounted") {
4304
+ this.ingestTokenCost(event);
4305
+ } else if (/^(?:sdd|subagent|kanban)\.task[._]/.test(type)) {
4306
+ this.ingestTask(event);
4307
+ } else if (type === "file.event" || /^file\.(?:tool|external)\./.test(type)) {
4308
+ this.ingestFileEvent(event);
4131
4309
  }
4132
- if (options.boardId) {
4133
- clauses.push("board_id = ?");
4134
- params.push(options.boardId);
4310
+ }
4311
+ ingestDailyCounters(event) {
4312
+ const day = eventDay(event);
4313
+ this.db.prepare("INSERT OR IGNORE INTO daily_counters (day) VALUES (?)").run(day);
4314
+ const bump = (sql, ...params) => this.db.prepare(`UPDATE daily_counters SET ${sql} WHERE day = ?`).run(...params, day);
4315
+ const family = signalFamily(event);
4316
+ const failed = isTerminalFailure(event) ? 1 : 0;
4317
+ this.db.prepare(
4318
+ `INSERT INTO family_daily (day, family, count, failure_count) VALUES (?, ?, 1, ?)
4319
+ ON CONFLICT(day, family) DO UPDATE SET count = count + 1, failure_count = failure_count + excluded.failure_count`
4320
+ ).run(day, family, failed);
4321
+ if (failed) bump("failures = failures + 1");
4322
+ if (event.outcome === "cancelled" || event.outcome === "abandoned")
4323
+ bump("cancellations = cancellations + 1");
4324
+ if (family === "agent") bump("agent_events = agent_events + 1");
4325
+ if (event.correlation.logicalRequestId) {
4326
+ this.db.prepare(
4327
+ "INSERT OR IGNORE INTO logical_request_daily (day, logical_request_id) VALUES (?, ?)"
4328
+ ).run(day, event.correlation.logicalRequestId);
4135
4329
  }
4136
- if (options.sessionId) {
4137
- clauses.push("session_id = ?");
4138
- params.push(options.sessionId);
4139
- }
4140
- const where = clauses.length > 0 ? ` WHERE ${clauses.join(" AND ")}` : "";
4141
- params.push(clampLimit(options.limit, 200));
4142
- const projection = `path, operation, occurred_at, session_id, agent_id, task_id, board_id, run_id,
4143
- tool_name, provider_id, model_id, source`;
4144
- const sql = options.latestPerPath ? `SELECT ${projection} FROM (
4145
- SELECT ${projection}, ROW_NUMBER() OVER (
4146
- PARTITION BY path_key ORDER BY occurred_at DESC, event_id DESC
4147
- ) AS path_rank
4148
- FROM file_lineage${where}
4149
- ) WHERE path_rank = 1 ORDER BY occurred_at DESC LIMIT ?` : `SELECT ${projection}
4150
- FROM file_lineage${where} ORDER BY occurred_at DESC LIMIT ?`;
4151
- const rows = this.db.prepare(sql).all(...params);
4152
- return rows.map((row) => ({
4153
- path: row.path,
4154
- operation: row.operation,
4155
- occurredAt: row.occurred_at,
4156
- sessionId: row.session_id,
4157
- agentId: row.agent_id,
4158
- taskId: row.task_id,
4159
- boardId: row.board_id,
4160
- runId: row.run_id,
4161
- toolName: row.tool_name,
4162
- providerId: row.provider_id,
4163
- modelId: row.model_id,
4164
- source: row.source
4165
- }));
4166
- }
4167
- summary() {
4168
- const provider = this.db.prepare(
4169
- "SELECT COALESCE(SUM(attempts),0) a, COALESCE(SUM(completed),0) c, COALESCE(SUM(failed),0) f FROM provider_daily"
4170
- ).get();
4171
- const tasks = {};
4172
- for (const row of this.db.prepare("SELECT status, COUNT(*) n FROM task_outcomes GROUP BY status").all()) {
4173
- tasks[row.status] = Number(row.n);
4174
- }
4175
- const files = this.db.prepare("SELECT COUNT(*) n, COUNT(DISTINCT path) p FROM file_lineage").get();
4176
- const cost = this.db.prepare("SELECT COALESCE(SUM(cost),0) c FROM token_cost").get();
4177
- const terminal = Number(provider.c) + Number(provider.f);
4178
- return {
4179
- providers: {
4180
- attempts: Number(provider.a),
4181
- completed: Number(provider.c),
4182
- failed: Number(provider.f),
4183
- successRate: terminal > 0 ? Number(provider.c) / terminal : 0
4184
- },
4185
- tasks,
4186
- files: { mutations: Number(files.n), uniquePaths: Number(files.p) },
4187
- estimatedCostUsd: Number(cost.c)
4188
- };
4189
- }
4190
- /**
4191
- * A `ChronicleSummary` for the default/unfiltered dashboard view — only
4192
- * `from`/`to` (day-precision) narrow it. Any other ad hoc filter (text,
4193
- * path, provider, model, session) can't be answered from these
4194
- * fixed-dimension aggregates; callers must fall back to query.ts's
4195
- * raw-scan summary for those.
4196
- */
4197
- defaultSummary(options = {}) {
4198
- const fromDay = options.from?.slice(0, 10);
4199
- const toDay = options.to?.slice(0, 10);
4200
- const dayFilter = (column) => {
4201
- const clauses = [];
4202
- const params = [];
4203
- if (fromDay) {
4204
- clauses.push(`${column} >= ?`);
4205
- params.push(fromDay);
4206
- }
4207
- if (toDay) {
4208
- clauses.push(`${column} <= ?`);
4209
- params.push(toDay);
4210
- }
4211
- return { where: clauses.length > 0 ? ` WHERE ${clauses.join(" AND ")}` : "", params };
4212
- };
4213
- const providerRange = dayFilter("day");
4214
- const provider = this.db.prepare(
4215
- `SELECT COALESCE(SUM(attempts),0) attempts, COALESCE(SUM(completed),0) completed, COALESCE(SUM(failed),0) failed,
4216
- COALESCE(SUM(retries),0) retries, COALESCE(SUM(fallbacks),0) fallbacks,
4217
- COUNT(DISTINCT provider_id) providers, COUNT(DISTINCT model_id) models,
4218
- COALESCE(SUM(input_tokens),0) inputTokens, COALESCE(SUM(output_tokens),0) outputTokens,
4219
- COALESCE(SUM(cache_read_tokens),0) cacheReadTokens, COALESCE(SUM(cache_write_tokens),0) cacheWriteTokens,
4220
- COALESCE(SUM(duration_ms_total),0) durationTotal, COALESCE(MAX(duration_ms_max),0) durationMax,
4221
- COALESCE(SUM(duration_count),0) durationCount
4222
- FROM provider_daily${providerRange.where}`
4223
- ).get(...providerRange.params);
4224
- const counterRange = dayFilter("day");
4225
- const counters = this.db.prepare(
4226
- `SELECT COALESCE(SUM(tool_calls),0) toolCalls, COALESCE(SUM(completed_tools),0) completedTools,
4227
- COALESCE(SUM(failed_tools),0) failedTools, COALESCE(SUM(tool_duration_ms_total),0) toolDurationTotal,
4228
- COALESCE(SUM(tool_duration_count),0) toolDurationCount, COALESCE(SUM(processes),0) processes,
4229
- COALESCE(SUM(failed_processes),0) failedProcesses, COALESCE(SUM(file_events_all),0) fileEvents,
4230
- COALESCE(SUM(decisions),0) decisions, COALESCE(SUM(escalations),0) escalations,
4231
- COALESCE(SUM(agent_events),0) agentEvents, COALESCE(SUM(failures),0) failures,
4232
- COALESCE(SUM(cancellations),0) cancellations
4233
- FROM daily_counters${counterRange.where}`
4234
- ).get(...counterRange.params);
4235
- const familyRange = dayFilter("day");
4236
- const familyRows = this.db.prepare(`SELECT family, count, failure_count FROM family_daily${familyRange.where}`).all(...familyRange.params);
4237
- const families = { ...EMPTY_FAMILIES };
4238
- const failuresByFamily = { ...EMPTY_FAMILIES };
4239
- for (const row of familyRows) {
4240
- const family = row.family;
4241
- families[family] = Number(row.count);
4242
- failuresByFamily[family] = Number(row.failure_count);
4243
- }
4244
- const agentRange = dayFilter("day");
4245
- const uniqueAgents = this.db.prepare(`SELECT COUNT(DISTINCT agent_id) n FROM agent_daily${agentRange.where}`).get(...agentRange.params).n;
4246
- const requestRange = dayFilter("day");
4247
- const logicalRequests = this.db.prepare(
4248
- `SELECT COUNT(DISTINCT logical_request_id) n FROM logical_request_daily${requestRange.where}`
4249
- ).get(...requestRange.params).n;
4250
- const fileRange = dayFilter("day");
4251
- const uniqueFiles = this.db.prepare(`SELECT COUNT(DISTINCT path_key) n FROM file_seen_daily${fileRange.where}`).get(...fileRange.params).n;
4252
- const costRange = dayFilter("day");
4253
- const cost = this.db.prepare(`SELECT COALESCE(SUM(cost),0) c FROM token_cost${costRange.where}`).get(...costRange.params).c;
4254
- return {
4255
- logicalRequests: Number(logicalRequests),
4256
- modelAttempts: Number(provider.attempts),
4257
- completedAttempts: Number(provider.completed),
4258
- failedAttempts: Number(provider.failed),
4259
- scheduledRetries: Number(provider.retries),
4260
- fallbacks: Number(provider.fallbacks),
4261
- providers: Number(provider.providers),
4262
- models: Number(provider.models),
4263
- inputTokens: Number(provider.inputTokens),
4264
- outputTokens: Number(provider.outputTokens),
4265
- cacheReadTokens: Number(provider.cacheReadTokens),
4266
- cacheWriteTokens: Number(provider.cacheWriteTokens),
4267
- estimatedCostUsd: Number(cost),
4268
- providerAvgDurationMs: Number(provider.durationCount) > 0 ? Number(provider.durationTotal) / Number(provider.durationCount) : 0,
4269
- // True p95 needs a retained distribution; this per-day aggregate only
4270
- // keeps sum/max/count, so approximate with the observed max rather
4271
- // than adding a per-attempt histogram write (would add the same kind
4272
- // of per-event overhead this whole effort is trying to remove).
4273
- providerP95DurationMs: Number(provider.durationMax),
4274
- toolCalls: Number(counters.toolCalls),
4275
- completedTools: Number(counters.completedTools),
4276
- failedTools: Number(counters.failedTools),
4277
- toolAvgDurationMs: Number(counters.toolDurationCount) > 0 ? Number(counters.toolDurationTotal) / Number(counters.toolDurationCount) : 0,
4278
- processes: Number(counters.processes),
4279
- failedProcesses: Number(counters.failedProcesses),
4280
- fileEvents: Number(counters.fileEvents),
4281
- uniqueFiles: Number(uniqueFiles),
4282
- agentEvents: Number(counters.agentEvents),
4283
- uniqueAgents: Number(uniqueAgents),
4284
- decisions: Number(counters.decisions),
4285
- escalations: Number(counters.escalations),
4286
- failures: Number(counters.failures),
4287
- cancellations: Number(counters.cancellations),
4288
- families,
4289
- failuresByFamily
4290
- };
4291
- }
4292
- // ─── Ingest internals ─────────────────────────────────────────────────────
4293
- ensureSchema() {
4294
- const version = this.db.prepare("PRAGMA user_version").get().user_version;
4295
- if (version !== 0 && version !== SCHEMA_VERSION2) {
4296
- this.db.exec(
4297
- "DROP TABLE IF EXISTS ingest_state; DROP TABLE IF EXISTS provider_daily;DROP TABLE IF EXISTS task_outcomes; DROP TABLE IF EXISTS file_lineage;DROP TABLE IF EXISTS token_cost; DROP TABLE IF EXISTS daily_counters;DROP TABLE IF EXISTS family_daily; DROP TABLE IF EXISTS agent_daily;DROP TABLE IF EXISTS logical_request_daily; DROP TABLE IF EXISTS file_seen_daily;"
4298
- );
4299
- }
4300
- this.db.exec(`
4301
- CREATE TABLE IF NOT EXISTS ingest_state (
4302
- file TEXT PRIMARY KEY,
4303
- bytes INTEGER NOT NULL
4304
- );
4305
- CREATE TABLE IF NOT EXISTS provider_daily (
4306
- day TEXT NOT NULL,
4307
- provider_id TEXT NOT NULL,
4308
- model_id TEXT NOT NULL,
4309
- attempts INTEGER NOT NULL DEFAULT 0,
4310
- completed INTEGER NOT NULL DEFAULT 0,
4311
- failed INTEGER NOT NULL DEFAULT 0,
4312
- retries INTEGER NOT NULL DEFAULT 0,
4313
- fallbacks INTEGER NOT NULL DEFAULT 0,
4314
- input_tokens INTEGER NOT NULL DEFAULT 0,
4315
- output_tokens INTEGER NOT NULL DEFAULT 0,
4316
- cache_read_tokens INTEGER NOT NULL DEFAULT 0,
4317
- cache_write_tokens INTEGER NOT NULL DEFAULT 0,
4318
- duration_ms_total REAL NOT NULL DEFAULT 0,
4319
- duration_ms_max REAL NOT NULL DEFAULT 0,
4320
- duration_count INTEGER NOT NULL DEFAULT 0,
4321
- PRIMARY KEY (day, provider_id, model_id)
4322
- );
4323
- CREATE TABLE IF NOT EXISTS task_outcomes (
4324
- task_id TEXT PRIMARY KEY,
4325
- run_id TEXT NOT NULL DEFAULT '',
4326
- board_id TEXT NOT NULL DEFAULT '',
4327
- session_id TEXT NOT NULL DEFAULT '',
4328
- agent_id TEXT NOT NULL DEFAULT '',
4329
- status TEXT NOT NULL DEFAULT 'started',
4330
- started_at TEXT,
4331
- ended_at TEXT,
4332
- duration_ms REAL,
4333
- retries INTEGER NOT NULL DEFAULT 0,
4334
- verification_failures INTEGER NOT NULL DEFAULT 0
4335
- );
4336
- CREATE TABLE IF NOT EXISTS file_lineage (
4337
- event_id TEXT PRIMARY KEY,
4338
- path TEXT NOT NULL,
4339
- path_key TEXT NOT NULL,
4340
- operation TEXT NOT NULL,
4341
- occurred_at TEXT NOT NULL,
4342
- session_id TEXT NOT NULL DEFAULT '',
4343
- agent_id TEXT NOT NULL DEFAULT '',
4344
- task_id TEXT NOT NULL DEFAULT '',
4345
- board_id TEXT NOT NULL DEFAULT '',
4346
- run_id TEXT NOT NULL DEFAULT '',
4347
- tool_name TEXT NOT NULL DEFAULT '',
4348
- provider_id TEXT NOT NULL DEFAULT '',
4349
- model_id TEXT NOT NULL DEFAULT '',
4350
- source TEXT NOT NULL DEFAULT ''
4351
- );
4352
- -- Lookups filter on the case-normalized path_key (matching the query
4353
- -- engine); the path column retains original casing for display.
4354
- CREATE INDEX IF NOT EXISTS idx_file_lineage_path ON file_lineage(path_key, occurred_at);
4355
- CREATE INDEX IF NOT EXISTS idx_file_lineage_task ON file_lineage(task_id);
4356
- CREATE TABLE IF NOT EXISTS token_cost (
4357
- scope_key TEXT PRIMARY KEY,
4358
- day TEXT NOT NULL,
4359
- occurred_at TEXT NOT NULL,
4360
- sequence INTEGER NOT NULL,
4361
- cost REAL NOT NULL
4362
- );
4363
- -- Backing store for defaultSummary(): per-day scalar counters plus
4364
- -- dedup sets, populated for every ingested event (not just the
4365
- -- provider/task/file families above).
4366
- CREATE TABLE IF NOT EXISTS daily_counters (
4367
- day TEXT PRIMARY KEY,
4368
- tool_calls INTEGER NOT NULL DEFAULT 0,
4369
- completed_tools INTEGER NOT NULL DEFAULT 0,
4370
- failed_tools INTEGER NOT NULL DEFAULT 0,
4371
- tool_duration_ms_total REAL NOT NULL DEFAULT 0,
4372
- tool_duration_ms_max REAL NOT NULL DEFAULT 0,
4373
- tool_duration_count INTEGER NOT NULL DEFAULT 0,
4374
- processes INTEGER NOT NULL DEFAULT 0,
4375
- failed_processes INTEGER NOT NULL DEFAULT 0,
4376
- file_events_all INTEGER NOT NULL DEFAULT 0,
4377
- decisions INTEGER NOT NULL DEFAULT 0,
4378
- escalations INTEGER NOT NULL DEFAULT 0,
4379
- agent_events INTEGER NOT NULL DEFAULT 0,
4380
- failures INTEGER NOT NULL DEFAULT 0,
4381
- cancellations INTEGER NOT NULL DEFAULT 0
4382
- );
4383
- CREATE TABLE IF NOT EXISTS family_daily (
4384
- day TEXT NOT NULL,
4385
- family TEXT NOT NULL,
4386
- count INTEGER NOT NULL DEFAULT 0,
4387
- failure_count INTEGER NOT NULL DEFAULT 0,
4388
- PRIMARY KEY (day, family)
4389
- );
4390
- CREATE TABLE IF NOT EXISTS agent_daily (day TEXT NOT NULL, agent_id TEXT NOT NULL, PRIMARY KEY (day, agent_id));
4391
- CREATE TABLE IF NOT EXISTS logical_request_daily (day TEXT NOT NULL, logical_request_id TEXT NOT NULL, PRIMARY KEY (day, logical_request_id));
4392
- CREATE TABLE IF NOT EXISTS file_seen_daily (day TEXT NOT NULL, path_key TEXT NOT NULL, PRIMARY KEY (day, path_key));
4393
- PRAGMA user_version = ${SCHEMA_VERSION2};
4394
- `);
4395
- }
4396
- loadOffsets() {
4397
- const rows = this.db.prepare("SELECT file, bytes FROM ingest_state").all();
4398
- return new Map(rows.map((row) => [row.file, Number(row.bytes)]));
4399
- }
4400
- pruneOffsets(existingFiles) {
4401
- const keep = new Set(
4402
- existingFiles.map((file) => normalizeKey(path13.relative(this.directory, file)))
4403
- );
4404
- for (const row of this.db.prepare("SELECT file FROM ingest_state").all()) {
4405
- if (row.file.startsWith(SQLITE_SOURCE_PREFIX)) continue;
4406
- if (!keep.has(row.file))
4407
- this.db.prepare("DELETE FROM ingest_state WHERE file = ?").run(row.file);
4408
- }
4409
- }
4410
- /**
4411
- * Fold everything the SQLite journal holds past this store's per-day cursor.
4412
- *
4413
- * Opened read-only on its own connection: the journal runs in WAL, so this
4414
- * never blocks the daemon writing to it, and metrics are best-effort — a
4415
- * journal that cannot be opened (mid-migration, absent, locked) leaves the
4416
- * cursors untouched and the next refresh retries.
4417
- *
4418
- * Rows the journal has already evicted are simply not seen. That is the
4419
- * intended split of responsibilities: the journal is a bounded ring, and this
4420
- * store is where an aggregate outlives the raw event it came from — which
4421
- * only holds if refresh runs more often than the ring turns over.
4422
- */
4423
- ingestSqliteJournal(offsets, result) {
4424
- const journalPath = path13.join(this.directory, CHRONICLE_SQLITE_FILE);
4425
- let source;
4426
- try {
4427
- source = new (loadDatabaseSync2())(journalPath, { readOnly: true });
4428
- } catch {
4429
- return;
4430
- }
4431
- try {
4432
- const days = source.prepare("SELECT DISTINCT day FROM events ORDER BY day").all();
4433
- const read = source.prepare(
4434
- "SELECT sequence, payload FROM events WHERE day = ? AND sequence > ? ORDER BY sequence LIMIT ?"
4435
- );
4436
- const writeCursor = this.db.prepare(
4437
- "INSERT INTO ingest_state (file, bytes) VALUES (?, ?) ON CONFLICT(file) DO UPDATE SET bytes = excluded.bytes"
4438
- );
4439
- for (const { day } of days) {
4440
- const key = `${SQLITE_SOURCE_PREFIX}${day}`;
4441
- const from = offsets.get(key) ?? 0;
4442
- let cursor = from;
4443
- this.db.exec("BEGIN");
4444
- try {
4445
- for (; ; ) {
4446
- const rows = read.all(day, cursor, SQLITE_INGEST_BATCH);
4447
- if (rows.length === 0) break;
4448
- for (const row of rows) {
4449
- try {
4450
- this.ingestEvent(JSON.parse(row.payload));
4451
- result.ingestedEvents++;
4452
- } catch {
4453
- result.invalidLines++;
4454
- }
4455
- result.ingestedBytes += row.payload.length;
4456
- cursor = Number(row.sequence);
4457
- }
4458
- if (rows.length < SQLITE_INGEST_BATCH) break;
4459
- }
4460
- if (cursor > from) writeCursor.run(key, cursor);
4461
- this.db.exec("COMMIT");
4462
- } catch (error) {
4463
- this.db.exec("ROLLBACK");
4464
- throw error;
4465
- }
4466
- if (cursor > from) result.sourceFiles++;
4467
- }
4468
- } catch {
4469
- } finally {
4470
- source.close();
4471
- }
4472
- }
4473
- /** Read complete lines appended after `consumed` bytes. The trailing
4474
- * partial line of an actively-written partition is left for the next
4475
- * refresh — `ingest_state.bytes` only ever advances past full lines. */
4476
- async ingestFile(file, key, consumed, result) {
4477
- let handle;
4478
- try {
4479
- handle = await fs8.open(file, "r");
4480
- } catch {
4481
- return false;
4482
- }
4483
- try {
4484
- const size = (await handle.stat()).size;
4485
- if (size <= consumed) return false;
4486
- let position = consumed;
4487
- let remainder = Buffer.alloc(0);
4488
- let advanced = consumed;
4489
- this.db.exec("BEGIN");
4490
- try {
4491
- while (position < size) {
4492
- const length = Math.min(READ_CHUNK_BYTES, size - position);
4493
- const buffer = Buffer.allocUnsafe(length);
4494
- const { bytesRead } = await handle.read(buffer, 0, length, position);
4495
- if (bytesRead <= 0) break;
4496
- position += bytesRead;
4497
- const data = remainder.length > 0 ? Buffer.concat([remainder, buffer.subarray(0, bytesRead)]) : buffer.subarray(0, bytesRead);
4498
- const lastNewline = data.lastIndexOf(10);
4499
- if (lastNewline < 0) {
4500
- remainder = Buffer.from(data);
4501
- continue;
4502
- }
4503
- for (const line of data.subarray(0, lastNewline).toString("utf8").split("\n")) {
4504
- const trimmed = line.trim();
4505
- if (!trimmed) continue;
4506
- try {
4507
- this.ingestEvent(JSON.parse(trimmed));
4508
- result.ingestedEvents++;
4509
- } catch {
4510
- result.invalidLines++;
4511
- }
4512
- }
4513
- advanced += lastNewline + 1;
4514
- remainder = Buffer.from(data.subarray(lastNewline + 1));
4515
- }
4516
- this.db.prepare(
4517
- "INSERT INTO ingest_state (file, bytes) VALUES (?, ?) ON CONFLICT(file) DO UPDATE SET bytes = excluded.bytes"
4518
- ).run(key, advanced);
4519
- this.db.exec("COMMIT");
4520
- } catch (error) {
4521
- this.db.exec("ROLLBACK");
4522
- throw error;
4523
- }
4524
- result.ingestedBytes += advanced - consumed;
4525
- return advanced > consumed;
4526
- } finally {
4527
- await handle.close();
4528
- }
4529
- }
4530
- ingestEvent(event) {
4531
- if (typeof event?.eventType !== "string" || !event.scope) return;
4532
- this.ingestDailyCounters(event);
4533
- const type = event.eventType;
4534
- if (type.startsWith("provider.attempt.") || type === "provider.fallback") {
4535
- this.ingestProvider(event);
4536
- } else if (type === "token.accounted") {
4537
- this.ingestTokenCost(event);
4538
- } else if (/^(?:sdd|subagent|kanban)\.task[._]/.test(type)) {
4539
- this.ingestTask(event);
4540
- } else if (type === "file.event" || /^file\.(?:tool|external)\./.test(type)) {
4541
- this.ingestFileEvent(event);
4542
- }
4543
- }
4544
- /** Runs for every ingested event (not just the type-specific branches
4545
- * below) — mirrors query.ts's updateSummary() closely enough that
4546
- * defaultSummary() matches what a raw scan of the same window would say. */
4547
- ingestDailyCounters(event) {
4548
- const day = eventDay(event);
4549
- this.db.prepare("INSERT OR IGNORE INTO daily_counters (day) VALUES (?)").run(day);
4550
- const bump = (sql, ...params) => this.db.prepare(`UPDATE daily_counters SET ${sql} WHERE day = ?`).run(...params, day);
4551
- const family = signalFamily(event);
4552
- const failed = isTerminalFailure(event) ? 1 : 0;
4553
- this.db.prepare(
4554
- `INSERT INTO family_daily (day, family, count, failure_count) VALUES (?, ?, 1, ?)
4555
- ON CONFLICT(day, family) DO UPDATE SET count = count + 1, failure_count = failure_count + excluded.failure_count`
4556
- ).run(day, family, failed);
4557
- if (failed) bump("failures = failures + 1");
4558
- if (event.outcome === "cancelled" || event.outcome === "abandoned")
4559
- bump("cancellations = cancellations + 1");
4560
- if (family === "agent") bump("agent_events = agent_events + 1");
4561
- if (event.correlation.logicalRequestId) {
4562
- this.db.prepare(
4563
- "INSERT OR IGNORE INTO logical_request_daily (day, logical_request_id) VALUES (?, ?)"
4564
- ).run(day, event.correlation.logicalRequestId);
4565
- }
4566
- if (event.scope.agentId) {
4567
- this.db.prepare("INSERT OR IGNORE INTO agent_daily (day, agent_id) VALUES (?, ?)").run(day, event.scope.agentId);
4330
+ if (event.scope.agentId) {
4331
+ this.db.prepare("INSERT OR IGNORE INTO agent_daily (day, agent_id) VALUES (?, ?)").run(day, event.scope.agentId);
4568
4332
  }
4569
4333
  const type = event.eventType;
4570
4334
  if (type === "decision.requested") bump("decisions = decisions + 1");
@@ -4662,110 +4426,345 @@ var ChronicleMetricsStore = class _ChronicleMetricsStore {
4662
4426
  for (const [column, value] of lineage) {
4663
4427
  if (value) set(`${column} = ?`, value);
4664
4428
  }
4665
- const base = event.eventType.replace(/^(?:sdd|subagent|kanban)\.task[._]/, "");
4666
- switch (base) {
4667
- case "started":
4668
- set("status = 'started', started_at = COALESCE(started_at, ?)", occurredAt);
4669
- break;
4670
- case "completed":
4671
- set(
4672
- "status = 'completed', ended_at = ?, duration_ms = ?",
4673
- occurredAt,
4674
- numberOrDuration(event, attributes)
4675
- );
4676
- break;
4677
- case "failed":
4678
- set("status = 'failed', ended_at = ?", occurredAt);
4679
- break;
4680
- case "retrying":
4681
- set("retries = retries + 1");
4682
- break;
4683
- case "verification_failed":
4684
- set("verification_failures = verification_failures + 1");
4685
- break;
4686
- case "merged":
4687
- set("status = 'merged'");
4688
- break;
4689
- case "conflict":
4690
- set("status = 'conflict'");
4691
- break;
4692
- default:
4693
- break;
4429
+ const base = event.eventType.replace(/^(?:sdd|subagent|kanban)\.task[._]/, "");
4430
+ switch (base) {
4431
+ case "started":
4432
+ set("status = 'started', started_at = COALESCE(started_at, ?)", occurredAt);
4433
+ break;
4434
+ case "completed":
4435
+ set(
4436
+ "status = 'completed', ended_at = ?, duration_ms = ?",
4437
+ occurredAt,
4438
+ numberOrDuration(event, attributes)
4439
+ );
4440
+ break;
4441
+ case "failed":
4442
+ set("status = 'failed', ended_at = ?", occurredAt);
4443
+ break;
4444
+ case "retrying":
4445
+ set("retries = retries + 1");
4446
+ break;
4447
+ case "verification_failed":
4448
+ set("verification_failures = verification_failures + 1");
4449
+ break;
4450
+ case "merged":
4451
+ set("status = 'merged'");
4452
+ break;
4453
+ case "conflict":
4454
+ set("status = 'conflict'");
4455
+ break;
4456
+ default:
4457
+ break;
4458
+ }
4459
+ }
4460
+ ingestFileEvent(event) {
4461
+ const attributes = event.attributes ?? {};
4462
+ const operation = stringAt(attributes, "operation") ?? "";
4463
+ if (!operation || operation === "read") return;
4464
+ const filePath = event.resource?.path ?? stringAt(attributes, "filePath");
4465
+ if (!filePath) return;
4466
+ this.db.prepare(
4467
+ `INSERT OR IGNORE INTO file_lineage
4468
+ (event_id, path, path_key, operation, occurred_at, session_id, agent_id, task_id, board_id, run_id,
4469
+ tool_name, provider_id, model_id, source)
4470
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
4471
+ ).run(
4472
+ event.eventId,
4473
+ normalizeKey(filePath),
4474
+ normalizePathKey(filePath),
4475
+ operation,
4476
+ event.occurredAt ?? event.observedAt,
4477
+ event.scope.sessionId ?? "",
4478
+ event.scope.agentId ?? "",
4479
+ event.scope.taskId ?? stringAt(attributes, "taskId") ?? "",
4480
+ event.scope.kanbanBoardId ?? stringAt(attributes, "boardId") ?? "",
4481
+ stringAt(attributes, "runId") ?? "",
4482
+ stringAt(attributes, "toolName") ?? "",
4483
+ event.runtime?.providerId ?? stringAt(attributes, "provider") ?? "",
4484
+ event.runtime?.modelId ?? stringAt(attributes, "model") ?? "",
4485
+ stringAt(attributes, "source") ?? (event.eventType === "file.event" ? "tool" : "external")
4486
+ );
4487
+ }
4488
+ };
4489
+
4490
+ // src/chronicle/metrics-store.ts
4491
+ var ChronicleMetricsStore = class _ChronicleMetricsStore {
4492
+ db;
4493
+ directory;
4494
+ dbPath;
4495
+ ingester;
4496
+ constructor(directory) {
4497
+ this.directory = path14.resolve(directory);
4498
+ this.dbPath = path14.join(this.directory, "metrics.db");
4499
+ const Database = loadDatabaseSync2();
4500
+ this.db = new Database(this.dbPath);
4501
+ this.db.exec("PRAGMA journal_mode = WAL");
4502
+ ensureMetricsSchema(this.db);
4503
+ this.ingester = new ChronicleMetricsIngester(this.db, this.directory, this.dbPath);
4504
+ }
4505
+ static open(chronicleDirectory2) {
4506
+ return new _ChronicleMetricsStore(chronicleDirectory2);
4507
+ }
4508
+ close() {
4509
+ this.db.close();
4510
+ }
4511
+ /** Incrementally ingest journal bytes appended since the last refresh.
4512
+ * Safe across processes: guarded by a file lock on the database path. */
4513
+ async refresh() {
4514
+ return this.ingester.refresh();
4515
+ }
4516
+ providerDaily(options = {}) {
4517
+ const clauses = [];
4518
+ const params = [];
4519
+ if (options.from) {
4520
+ clauses.push("day >= ?");
4521
+ params.push(options.from.slice(0, 10));
4522
+ }
4523
+ if (options.to) {
4524
+ clauses.push("day <= ?");
4525
+ params.push(options.to.slice(0, 10));
4526
+ }
4527
+ const where = clauses.length > 0 ? ` WHERE ${clauses.join(" AND ")}` : "";
4528
+ const rows = this.db.prepare(
4529
+ `SELECT day, provider_id, model_id, attempts, completed, failed, retries, fallbacks,
4530
+ input_tokens, output_tokens, cache_read_tokens, cache_write_tokens,
4531
+ duration_ms_total, duration_ms_max, duration_count
4532
+ FROM provider_daily${where} ORDER BY day DESC, provider_id, model_id`
4533
+ ).all(...params);
4534
+ return rows.map((row) => ({
4535
+ day: String(row.day),
4536
+ providerId: String(row.provider_id),
4537
+ modelId: String(row.model_id),
4538
+ attempts: Number(row.attempts),
4539
+ completed: Number(row.completed),
4540
+ failed: Number(row.failed),
4541
+ retries: Number(row.retries),
4542
+ fallbacks: Number(row.fallbacks),
4543
+ inputTokens: Number(row.input_tokens),
4544
+ outputTokens: Number(row.output_tokens),
4545
+ cacheReadTokens: Number(row.cache_read_tokens),
4546
+ cacheWriteTokens: Number(row.cache_write_tokens),
4547
+ avgDurationMs: Number(row.duration_count) > 0 ? Number(row.duration_ms_total) / Number(row.duration_count) : 0,
4548
+ maxDurationMs: Number(row.duration_ms_max)
4549
+ }));
4550
+ }
4551
+ taskOutcomes(options = {}) {
4552
+ const clauses = [];
4553
+ const params = [];
4554
+ if (options.runId) {
4555
+ clauses.push("t.run_id = ?");
4556
+ params.push(options.runId);
4557
+ }
4558
+ if (options.boardId) {
4559
+ clauses.push("t.board_id = ?");
4560
+ params.push(options.boardId);
4561
+ }
4562
+ if (options.sessionId) {
4563
+ clauses.push("t.session_id = ?");
4564
+ params.push(options.sessionId);
4565
+ }
4566
+ if (options.status) {
4567
+ clauses.push("t.status = ?");
4568
+ params.push(options.status);
4569
+ }
4570
+ const where = clauses.length > 0 ? ` WHERE ${clauses.join(" AND ")}` : "";
4571
+ params.push(clampLimit(options.limit, 100));
4572
+ const rows = this.db.prepare(
4573
+ `SELECT t.*, (SELECT COUNT(*) FROM file_lineage f WHERE f.task_id = t.task_id) AS files_touched
4574
+ FROM task_outcomes t${where}
4575
+ ORDER BY COALESCE(t.started_at, '') DESC LIMIT ?`
4576
+ ).all(...params);
4577
+ return rows.map((row) => ({
4578
+ taskId: String(row.task_id),
4579
+ runId: String(row.run_id),
4580
+ boardId: String(row.board_id),
4581
+ sessionId: String(row.session_id),
4582
+ agentId: String(row.agent_id),
4583
+ status: String(row.status),
4584
+ startedAt: row.started_at === null ? null : String(row.started_at),
4585
+ endedAt: row.ended_at === null ? null : String(row.ended_at),
4586
+ durationMs: row.duration_ms === null ? null : Number(row.duration_ms),
4587
+ retries: Number(row.retries),
4588
+ verificationFailures: Number(row.verification_failures),
4589
+ filesTouched: Number(row.files_touched)
4590
+ }));
4591
+ }
4592
+ fileLineage(options = {}) {
4593
+ const clauses = [];
4594
+ const params = [];
4595
+ if (options.path) {
4596
+ clauses.push("path_key = ?");
4597
+ params.push(normalizePathKey(options.path));
4598
+ }
4599
+ if (options.paths) {
4600
+ const pathKeys = [...new Set(options.paths.map(normalizePathKey))];
4601
+ if (pathKeys.length === 0) return [];
4602
+ clauses.push(`path_key IN (${pathKeys.map(() => "?").join(",")})`);
4603
+ params.push(...pathKeys);
4604
+ }
4605
+ if (options.taskId) {
4606
+ clauses.push("task_id = ?");
4607
+ params.push(options.taskId);
4608
+ }
4609
+ if (options.boardId) {
4610
+ clauses.push("board_id = ?");
4611
+ params.push(options.boardId);
4612
+ }
4613
+ if (options.sessionId) {
4614
+ clauses.push("session_id = ?");
4615
+ params.push(options.sessionId);
4616
+ }
4617
+ const where = clauses.length > 0 ? ` WHERE ${clauses.join(" AND ")}` : "";
4618
+ params.push(clampLimit(options.limit, 200));
4619
+ const projection = `path, operation, occurred_at, session_id, agent_id, task_id, board_id, run_id,
4620
+ tool_name, provider_id, model_id, source`;
4621
+ const sql = options.latestPerPath ? `SELECT ${projection} FROM (
4622
+ SELECT ${projection}, ROW_NUMBER() OVER (
4623
+ PARTITION BY path_key ORDER BY occurred_at DESC, event_id DESC
4624
+ ) AS path_rank
4625
+ FROM file_lineage${where}
4626
+ ) WHERE path_rank = 1 ORDER BY occurred_at DESC LIMIT ?` : `SELECT ${projection}
4627
+ FROM file_lineage${where} ORDER BY occurred_at DESC LIMIT ?`;
4628
+ const rows = this.db.prepare(sql).all(...params);
4629
+ return rows.map((row) => ({
4630
+ path: row.path,
4631
+ operation: row.operation,
4632
+ occurredAt: row.occurred_at,
4633
+ sessionId: row.session_id,
4634
+ agentId: row.agent_id,
4635
+ taskId: row.task_id,
4636
+ boardId: row.board_id,
4637
+ runId: row.run_id,
4638
+ toolName: row.tool_name,
4639
+ providerId: row.provider_id,
4640
+ modelId: row.model_id,
4641
+ source: row.source
4642
+ }));
4643
+ }
4644
+ summary() {
4645
+ const provider = this.db.prepare(
4646
+ "SELECT COALESCE(SUM(attempts),0) a, COALESCE(SUM(completed),0) c, COALESCE(SUM(failed),0) f FROM provider_daily"
4647
+ ).get();
4648
+ const tasks = {};
4649
+ for (const row of this.db.prepare("SELECT status, COUNT(*) n FROM task_outcomes GROUP BY status").all()) {
4650
+ tasks[row.status] = Number(row.n);
4694
4651
  }
4652
+ const files = this.db.prepare("SELECT COUNT(*) n, COUNT(DISTINCT path) p FROM file_lineage").get();
4653
+ const cost = this.db.prepare("SELECT COALESCE(SUM(cost),0) c FROM token_cost").get();
4654
+ const terminal = Number(provider.c) + Number(provider.f);
4655
+ return {
4656
+ providers: {
4657
+ attempts: Number(provider.a),
4658
+ completed: Number(provider.c),
4659
+ failed: Number(provider.f),
4660
+ successRate: terminal > 0 ? Number(provider.c) / terminal : 0
4661
+ },
4662
+ tasks,
4663
+ files: { mutations: Number(files.n), uniquePaths: Number(files.p) },
4664
+ estimatedCostUsd: Number(cost.c)
4665
+ };
4695
4666
  }
4696
- ingestFileEvent(event) {
4697
- const attributes = event.attributes ?? {};
4698
- const operation = stringAt(attributes, "operation") ?? "";
4699
- if (!operation || operation === "read") return;
4700
- const filePath = event.resource?.path ?? stringAt(attributes, "filePath");
4701
- if (!filePath) return;
4702
- this.db.prepare(
4703
- `INSERT OR IGNORE INTO file_lineage
4704
- (event_id, path, path_key, operation, occurred_at, session_id, agent_id, task_id, board_id, run_id,
4705
- tool_name, provider_id, model_id, source)
4706
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
4707
- ).run(
4708
- event.eventId,
4709
- normalizeKey(filePath),
4710
- normalizePathKey(filePath),
4711
- operation,
4712
- event.occurredAt ?? event.observedAt,
4713
- event.scope.sessionId ?? "",
4714
- event.scope.agentId ?? "",
4715
- event.scope.taskId ?? stringAt(attributes, "taskId") ?? "",
4716
- event.scope.kanbanBoardId ?? stringAt(attributes, "boardId") ?? "",
4717
- stringAt(attributes, "runId") ?? "",
4718
- stringAt(attributes, "toolName") ?? "",
4719
- event.runtime?.providerId ?? stringAt(attributes, "provider") ?? "",
4720
- event.runtime?.modelId ?? stringAt(attributes, "model") ?? "",
4721
- stringAt(attributes, "source") ?? (event.eventType === "file.event" ? "tool" : "external")
4722
- );
4667
+ /**
4668
+ * A `ChronicleSummary` for the default/unfiltered dashboard view — only
4669
+ * `from`/`to` (day-precision) narrow it.
4670
+ */
4671
+ defaultSummary(options = {}) {
4672
+ const fromDay = options.from?.slice(0, 10);
4673
+ const toDay = options.to?.slice(0, 10);
4674
+ const dayFilter = (column) => {
4675
+ const clauses = [];
4676
+ const params = [];
4677
+ if (fromDay) {
4678
+ clauses.push(`${column} >= ?`);
4679
+ params.push(fromDay);
4680
+ }
4681
+ if (toDay) {
4682
+ clauses.push(`${column} <= ?`);
4683
+ params.push(toDay);
4684
+ }
4685
+ return { where: clauses.length > 0 ? ` WHERE ${clauses.join(" AND ")}` : "", params };
4686
+ };
4687
+ const providerRange = dayFilter("day");
4688
+ const provider = this.db.prepare(
4689
+ `SELECT COALESCE(SUM(attempts),0) attempts, COALESCE(SUM(completed),0) completed, COALESCE(SUM(failed),0) failed,
4690
+ COALESCE(SUM(retries),0) retries, COALESCE(SUM(fallbacks),0) fallbacks,
4691
+ COUNT(DISTINCT provider_id) providers, COUNT(DISTINCT model_id) models,
4692
+ COALESCE(SUM(input_tokens),0) inputTokens, COALESCE(SUM(output_tokens),0) outputTokens,
4693
+ COALESCE(SUM(cache_read_tokens),0) cacheReadTokens, COALESCE(SUM(cache_write_tokens),0) cacheWriteTokens,
4694
+ COALESCE(SUM(duration_ms_total),0) durationTotal, COALESCE(MAX(duration_ms_max),0) durationMax,
4695
+ COALESCE(SUM(duration_count),0) durationCount
4696
+ FROM provider_daily${providerRange.where}`
4697
+ ).get(...providerRange.params);
4698
+ const counterRange = dayFilter("day");
4699
+ const counters = this.db.prepare(
4700
+ `SELECT COALESCE(SUM(tool_calls),0) toolCalls, COALESCE(SUM(completed_tools),0) completedTools,
4701
+ COALESCE(SUM(failed_tools),0) failedTools, COALESCE(SUM(tool_duration_ms_total),0) toolDurationTotal,
4702
+ COALESCE(SUM(tool_duration_count),0) toolDurationCount, COALESCE(SUM(processes),0) processes,
4703
+ COALESCE(SUM(failed_processes),0) failedProcesses, COALESCE(SUM(file_events_all),0) fileEvents,
4704
+ COALESCE(SUM(decisions),0) decisions, COALESCE(SUM(escalations),0) escalations,
4705
+ COALESCE(SUM(agent_events),0) agentEvents, COALESCE(SUM(failures),0) failures,
4706
+ COALESCE(SUM(cancellations),0) cancellations
4707
+ FROM daily_counters${counterRange.where}`
4708
+ ).get(...counterRange.params);
4709
+ const familyRange = dayFilter("day");
4710
+ const familyRows = this.db.prepare(`SELECT family, count, failure_count FROM family_daily${familyRange.where}`).all(...familyRange.params);
4711
+ const families = { ...EMPTY_FAMILIES };
4712
+ const failuresByFamily = { ...EMPTY_FAMILIES };
4713
+ for (const row of familyRows) {
4714
+ const family = row.family;
4715
+ families[family] = Number(row.count);
4716
+ failuresByFamily[family] = Number(row.failure_count);
4717
+ }
4718
+ const agentRange = dayFilter("day");
4719
+ const uniqueAgents = this.db.prepare(`SELECT COUNT(DISTINCT agent_id) n FROM agent_daily${agentRange.where}`).get(...agentRange.params).n;
4720
+ const requestRange = dayFilter("day");
4721
+ const logicalRequests = this.db.prepare(
4722
+ `SELECT COUNT(DISTINCT logical_request_id) n FROM logical_request_daily${requestRange.where}`
4723
+ ).get(...requestRange.params).n;
4724
+ const fileRange = dayFilter("day");
4725
+ const uniqueFiles = this.db.prepare(`SELECT COUNT(DISTINCT path_key) n FROM file_seen_daily${fileRange.where}`).get(...fileRange.params).n;
4726
+ const costRange = dayFilter("day");
4727
+ const cost = this.db.prepare(`SELECT COALESCE(SUM(cost),0) c FROM token_cost${costRange.where}`).get(...costRange.params).c;
4728
+ return {
4729
+ logicalRequests: Number(logicalRequests),
4730
+ modelAttempts: Number(provider.attempts),
4731
+ completedAttempts: Number(provider.completed),
4732
+ failedAttempts: Number(provider.failed),
4733
+ scheduledRetries: Number(provider.retries),
4734
+ fallbacks: Number(provider.fallbacks),
4735
+ providers: Number(provider.providers),
4736
+ models: Number(provider.models),
4737
+ inputTokens: Number(provider.inputTokens),
4738
+ outputTokens: Number(provider.outputTokens),
4739
+ cacheReadTokens: Number(provider.cacheReadTokens),
4740
+ cacheWriteTokens: Number(provider.cacheWriteTokens),
4741
+ estimatedCostUsd: Number(cost),
4742
+ providerAvgDurationMs: Number(provider.durationCount) > 0 ? Number(provider.durationTotal) / Number(provider.durationCount) : 0,
4743
+ providerP95DurationMs: Number(provider.durationMax),
4744
+ toolCalls: Number(counters.toolCalls),
4745
+ completedTools: Number(counters.completedTools),
4746
+ failedTools: Number(counters.failedTools),
4747
+ toolAvgDurationMs: Number(counters.toolDurationCount) > 0 ? Number(counters.toolDurationTotal) / Number(counters.toolDurationCount) : 0,
4748
+ processes: Number(counters.processes),
4749
+ failedProcesses: Number(counters.failedProcesses),
4750
+ fileEvents: Number(counters.fileEvents),
4751
+ uniqueFiles: Number(uniqueFiles),
4752
+ agentEvents: Number(counters.agentEvents),
4753
+ uniqueAgents: Number(uniqueAgents),
4754
+ decisions: Number(counters.decisions),
4755
+ escalations: Number(counters.escalations),
4756
+ failures: Number(counters.failures),
4757
+ cancellations: Number(counters.cancellations),
4758
+ families,
4759
+ failuresByFamily
4760
+ };
4723
4761
  }
4724
4762
  };
4725
- function eventDay(event) {
4726
- return (event.occurredAt ?? event.observedAt).slice(0, 10);
4727
- }
4728
- function durationMs2(event) {
4729
- const value = Number(event.durationNs ?? 0) / 1e6;
4730
- return Number.isFinite(value) && value > 0 ? value : 0;
4731
- }
4732
- function numberOrDuration(event, attributes) {
4733
- const explicit = attributes.durationMs;
4734
- if (typeof explicit === "number" && Number.isFinite(explicit)) return explicit;
4735
- return durationMs2(event);
4736
- }
4737
- function numberAt2(event, dotPath) {
4738
- const value = readPath2(event.attributes ?? {}, dotPath);
4739
- return typeof value === "number" && Number.isFinite(value) ? value : 0;
4740
- }
4741
- function readPath2(value, key) {
4742
- return key.split(".").reduce(
4743
- (current, part) => current && typeof current === "object" ? current[part] : void 0,
4744
- value
4745
- );
4746
- }
4747
- function stringAt(record, key) {
4748
- const value = record[key];
4749
- return typeof value === "string" && value.length > 0 ? value : void 0;
4750
- }
4751
- function asString(value) {
4752
- return typeof value === "string" && value.length > 0 ? value : void 0;
4753
- }
4754
- function clampLimit(limit, fallback) {
4755
- if (typeof limit !== "number" || !Number.isFinite(limit) || limit <= 0) return fallback;
4756
- return Math.min(Math.floor(limit), 1e4);
4757
- }
4758
- function normalizeKey(value) {
4759
- return value.replaceAll("\\", "/");
4760
- }
4761
- function normalizePathKey(value) {
4762
- return value.replaceAll("\\", "/").replace(/^\.\//, "").toLowerCase();
4763
- }
4764
4763
 
4765
4764
  // src/chronicle/project-server-endpoint.ts
4766
4765
  import { createHash as createHash5 } from "node:crypto";
4767
4766
  import * as os3 from "node:os";
4768
- import * as path14 from "node:path";
4767
+ import * as path15 from "node:path";
4769
4768
 
4770
4769
  // src/chronicle/project-server-protocol.ts
4771
4770
  var CHRONICLE_PROJECT_SERVER_PROTOCOL_VERSION = 2;
@@ -4779,25 +4778,25 @@ function encodeChronicleProjectServerMessage(message) {
4779
4778
  // src/chronicle/project-server-endpoint.ts
4780
4779
  var CHRONICLE_PROJECT_SERVER_METADATA_FILE = "server.json";
4781
4780
  function normalizedPath(value) {
4782
- const resolved = path14.resolve(value);
4781
+ const resolved = path15.resolve(value);
4783
4782
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
4784
4783
  }
4785
4784
  function chronicleProjectServerKey(projectDir) {
4786
- return createHash5("sha256").update(normalizedPath(path14.join(projectDir, "chronicle"))).digest("hex").slice(0, 24);
4785
+ return createHash5("sha256").update(normalizedPath(path15.join(projectDir, "chronicle"))).digest("hex").slice(0, 24);
4787
4786
  }
4788
4787
  function chronicleProjectServerEndpoint(projectDir) {
4789
4788
  const key = chronicleProjectServerKey(projectDir);
4790
4789
  if (process.platform === "win32") {
4791
4790
  return `\\\\.\\pipe\\wrongstack-chronicle-v${CHRONICLE_PROJECT_SERVER_PROTOCOL_VERSION}-${key}`;
4792
4791
  }
4793
- return path14.join(
4792
+ return path15.join(
4794
4793
  os3.tmpdir(),
4795
4794
  `wsch-v${CHRONICLE_PROJECT_SERVER_PROTOCOL_VERSION}`,
4796
4795
  `${key}.sock`
4797
4796
  );
4798
4797
  }
4799
4798
  function chronicleProjectServerMetadataPath(projectDir) {
4800
- return path14.join(projectDir, "chronicle", CHRONICLE_PROJECT_SERVER_METADATA_FILE);
4799
+ return path15.join(projectDir, "chronicle", CHRONICLE_PROJECT_SERVER_METADATA_FILE);
4801
4800
  }
4802
4801
 
4803
4802
  // src/chronicle/project-server.ts
@@ -4824,17 +4823,17 @@ function parseArgs(argv) {
4824
4823
  }
4825
4824
  const retentionInput = Number(values.get("--retention-days"));
4826
4825
  return {
4827
- projectRoot: path15.resolve(values.get("--project-root")),
4828
- globalRoot: path15.resolve(values.get("--global-root")),
4826
+ projectRoot: path16.resolve(values.get("--project-root")),
4827
+ globalRoot: path16.resolve(values.get("--global-root")),
4829
4828
  projectId: values.get("--project-id"),
4830
- projectDir: path15.resolve(values.get("--project-dir")),
4829
+ projectDir: path16.resolve(values.get("--project-dir")),
4831
4830
  workspaceId: values.get("--workspace-id"),
4832
4831
  retentionDays: Number.isFinite(retentionInput) && retentionInput > 0 ? retentionInput : 30
4833
4832
  };
4834
4833
  }
4835
4834
  useDaemonPerfDefaults();
4836
4835
  var parsed = parseArgs(process.argv.slice(2));
4837
- var chronicleDirectory = path15.join(parsed.projectDir, "chronicle");
4836
+ var chronicleDirectory = path16.join(parsed.projectDir, "chronicle");
4838
4837
  var endpoint = chronicleProjectServerEndpoint(parsed.projectDir);
4839
4838
  var metadataPath = chronicleProjectServerMetadataPath(parsed.projectDir);
4840
4839
  var idleInput = Number(process.env["WRONGSTACK_CHRONICLE_SERVER_IDLE_MS"]);
@@ -4854,7 +4853,7 @@ var serverInfo = {
4854
4853
  endpoint,
4855
4854
  startedAt
4856
4855
  };
4857
- process.title = `wrongstack-chronicle:${path15.basename(parsed.projectRoot)}`;
4856
+ process.title = `wrongstack-chronicle:${path16.basename(parsed.projectRoot)}`;
4858
4857
  var clients = /* @__PURE__ */ new Set();
4859
4858
  var journals = /* @__PURE__ */ new Map();
4860
4859
  var activeRequests = 0;
@@ -4892,7 +4891,7 @@ function pruneJournals(currentDay) {
4892
4891
  }
4893
4892
  }
4894
4893
  function legacyPartitionPath(location) {
4895
- return path15.join(location.chronicleDirectory, `${location.day}.events.jsonl`);
4894
+ return path16.join(location.chronicleDirectory, `${location.day}.events.jsonl`);
4896
4895
  }
4897
4896
  function useSqliteStore() {
4898
4897
  return process.env["WRONGSTACK_CHRONICLE_STORE"] !== "jsonl";
@@ -5218,7 +5217,7 @@ function scheduleIdleStop() {
5218
5217
  idleTimer.unref?.();
5219
5218
  }
5220
5219
  async function writeMetadata() {
5221
- await fsp4.mkdir(path15.dirname(metadataPath), { recursive: true });
5220
+ await fsp4.mkdir(path16.dirname(metadataPath), { recursive: true });
5222
5221
  const metadata = { ...serverInfo, authToken };
5223
5222
  await atomicWrite(metadataPath, `${JSON.stringify(metadata, null, 2)}
5224
5223
  `, { mode: 384 });