@wrongstack/core 0.306.3 → 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.
- package/dist/chronicle/index.js +957 -953
- package/dist/chronicle/metrics-ingest.d.ts +55 -0
- package/dist/chronicle/metrics-schema.d.ts +86 -0
- package/dist/chronicle/metrics-store.d.ts +5 -120
- package/dist/chronicle/project-server.js +1083 -1084
- package/dist/chronicle/sqlite-journal-quota.d.ts +39 -0
- package/dist/chronicle/sqlite-journal-schema.d.ts +25 -0
- package/dist/chronicle/sqlite-journal.d.ts +12 -177
- package/dist/coordination/agents/index.js +1738 -1690
- package/dist/coordination/agents/project-agent-capture.d.ts +48 -0
- package/dist/coordination/agents/project-agent-identity.d.ts +3 -129
- package/dist/coordination/agents/project-agent-roster.d.ts +17 -0
- package/dist/coordination/index.js +6103 -5890
- package/dist/coordination/mailbox-http-actor-query.d.ts +16 -0
- package/dist/coordination/mailbox-http-router.d.ts +0 -48
- package/dist/coordination/mailbox-http-sse.d.ts +12 -0
- package/dist/coordination/mailbox-message-types.d.ts +71 -0
- package/dist/coordination/mailbox-predicates.d.ts +13 -0
- package/dist/coordination/mailbox-project-server.js +1 -1
- package/dist/coordination/mailbox-session-sync.d.ts +8 -0
- package/dist/coordination/mailbox-types.d.ts +6 -793
- package/dist/core/agent-loop-context.d.ts +28 -0
- package/dist/core/agent-loop-detector.d.ts +25 -0
- package/dist/core/conversation-state.d.ts +1 -2
- package/dist/core/fallback-doctor.d.ts +72 -0
- package/dist/core/fallback-model.d.ts +19 -1
- package/dist/core/fallback-profile-manager.d.ts +21 -3
- package/dist/core/index.d.ts +2 -1
- package/dist/core/index.js +1143 -766
- package/dist/defaults/index.js +3833 -3595
- package/dist/execution/auto-compaction-middleware.d.ts +64 -0
- package/dist/execution/autonomy-brain-llm.d.ts +55 -0
- package/dist/execution/autonomy-brain.d.ts +3 -156
- package/dist/execution/compaction-budget.d.ts +38 -0
- package/dist/execution/compaction-core.d.ts +3 -166
- package/dist/execution/compaction-elision.d.ts +58 -0
- package/dist/execution/council-orchestrator-helpers.d.ts +39 -0
- package/dist/execution/council-orchestrator.d.ts +3 -42
- package/dist/execution/council-response-parser.d.ts +56 -0
- package/dist/execution/index.d.ts +1 -1
- package/dist/execution/index.js +10016 -9903
- package/dist/execution/prompt-enhancer.js +11 -3
- package/dist/execution/retry-policy.d.ts +27 -0
- package/dist/execution/tool-executor-guard.d.ts +18 -0
- package/dist/execution/tool-executor-runner.d.ts +12 -0
- package/dist/execution/tool-executor.d.ts +0 -75
- package/dist/goal/index.js +19 -6
- package/dist/hq/index.js +58 -17
- package/dist/index.d.ts +2 -1
- package/dist/index.js +8731 -7756
- package/dist/infrastructure/index.js +24 -1
- package/dist/kernel/events/agent-events.d.ts +2 -0
- package/dist/kernel/events/provider-events.d.ts +13 -0
- package/dist/plugin/index.js +2123 -1918
- package/dist/plugins/auto-review-config.d.ts +53 -0
- package/dist/plugins/auto-review-git.d.ts +19 -0
- package/dist/plugins/auto-review-plugin.d.ts +2 -140
- package/dist/plugins/review-finding-verification.d.ts +7 -0
- package/dist/prompts/index.d.ts +1 -0
- package/dist/prompts/prompt-journal.d.ts +94 -0
- package/dist/security/index.js +677 -667
- package/dist/security/permission-explain.d.ts +19 -0
- package/dist/security/permission-policy.d.ts +0 -101
- package/dist/security/yolo-risk.d.ts +5 -4
- package/dist/session-catalog/index.js +270 -227
- package/dist/session-catalog/project-server.js +277 -234
- package/dist/session-catalog/store-rebuild.d.ts +9 -0
- package/dist/session-catalog/store-schema.d.ts +48 -0
- package/dist/session-catalog/store.d.ts +0 -17
- package/dist/storage/cloud-config-sync/sanitize.d.ts +18 -0
- package/dist/storage/cloud-config-sync.d.ts +1 -1
- package/dist/storage/director-state.d.ts +6 -0
- package/dist/storage/file-session-writer.d.ts +2 -58
- package/dist/storage/index.d.ts +2 -1
- package/dist/storage/index.js +2121 -1736
- package/dist/storage/orphan-lock-cleaner.d.ts +15 -0
- package/dist/storage/provider-config-watcher.d.ts +9 -0
- package/dist/storage/session-recovery.d.ts +9 -0
- package/dist/storage/session-store/rename-session.d.ts +16 -0
- package/dist/storage/session-store/resume-session.d.ts +26 -0
- package/dist/storage/session-store/session-store-clear.d.ts +11 -0
- package/dist/storage/session-store/session-store-index.d.ts +13 -0
- package/dist/storage/session-store/strict-empty-check.d.ts +7 -0
- package/dist/storage/session-store.d.ts +2 -129
- package/dist/storage/session-summary-tracker.d.ts +39 -0
- package/dist/storage/session-write-buffer.d.ts +43 -0
- package/dist/storage/session-writer/session-writer-flush.d.ts +4 -0
- package/dist/storage/session-writer/session-writer-summary-tracker.d.ts +28 -0
- package/dist/tools/fallback-agent-model-assign-tool.d.ts +18 -0
- package/dist/tools/fallback-leader-model-set-tool.d.ts +18 -0
- package/dist/tools/fallback-manage-tools.d.ts +8 -45
- package/dist/tools/fallback-profile-manage-tool.d.ts +16 -0
- package/dist/tools/fallback-provider-key-set-tool.d.ts +17 -0
- package/dist/tools/fallback-provider-manage-tool.d.ts +26 -0
- package/dist/tools/index.d.ts +1 -1
- package/dist/tools/index.js +5150 -5090
- package/dist/types/config/root.d.ts +21 -2
- package/dist/types/default-config.d.ts +1 -1
- package/dist/types/index.d.ts +54 -55
- package/dist/types/index.js +953 -932
- package/dist/types/runtime-capability-manifest.d.ts +5 -5
- package/dist/types/session-markers.d.ts +12 -0
- package/dist/types/session-markers.js +19 -0
- package/dist/types/session.d.ts +7 -0
- package/dist/worktree/worktree-manager.d.ts +1 -1
- package/instructions/coordination/subagent-baseline.md +2 -2
- package/instructions/llm/prompt-enhancer.md +2 -1
- package/instructions/sections/tool/common-patterns.md +21 -0
- package/instructions/sections/tool/delegation-compact.md +3 -1
- package/instructions/sections/tool/delegation-full.md +5 -1
- package/instructions/system-lite.md +28 -3
- package/instructions/system-pro.md +37 -8
- package/instructions/system.md +4 -2
- 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
|
|
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
|
|
331
|
-
return { fullPath, 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
|
|
989
|
-
if (
|
|
990
|
-
recentToolMutations.set(
|
|
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
|
|
1053
|
-
if (!
|
|
1054
|
-
pending.add(
|
|
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
|
-
(
|
|
1062
|
+
(relative5) => !isExcluded(relative5, excluded, excludedPaths)
|
|
1063
1063
|
);
|
|
1064
1064
|
const changes = [];
|
|
1065
|
-
for (const
|
|
1066
|
-
const before = known.get(
|
|
1067
|
-
const cached = fullScan?.files.get(
|
|
1068
|
-
const after = cached ?? (fullScan?.complete ? void 0 : await fingerprint(path5.join(root,
|
|
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:
|
|
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
|
|
1265
|
-
if (isExcluded(
|
|
1264
|
+
const relative5 = normalizeRelative(path5.join(relativeDir, entry.name));
|
|
1265
|
+
if (isExcluded(relative5, excluded, excludedPaths)) continue;
|
|
1266
1266
|
if (entry.isDirectory()) {
|
|
1267
|
-
dirs.push(
|
|
1267
|
+
dirs.push(relative5);
|
|
1268
1268
|
} else if (entry.isFile()) {
|
|
1269
|
-
filePaths.push(
|
|
1269
|
+
filePaths.push(relative5);
|
|
1270
1270
|
}
|
|
1271
1271
|
}
|
|
1272
1272
|
const fingerprints = await mapWithConcurrency(
|
|
1273
1273
|
filePaths,
|
|
1274
1274
|
SCAN_HASH_CONCURRENCY,
|
|
1275
|
-
async (
|
|
1275
|
+
async (relative5) => {
|
|
1276
1276
|
try {
|
|
1277
1277
|
const value = await fingerprint(
|
|
1278
|
-
path5.join(root,
|
|
1278
|
+
path5.join(root, relative5),
|
|
1279
1279
|
maxHashBytes,
|
|
1280
|
-
previous?.get(
|
|
1280
|
+
previous?.get(relative5)
|
|
1281
1281
|
);
|
|
1282
|
-
return value ? [
|
|
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
|
|
1303
|
-
if (!
|
|
1304
|
-
const base = { size:
|
|
1305
|
-
if (previous?.hash !== void 0 && previous.size ===
|
|
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 (
|
|
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(
|
|
1325
|
-
const normalized = normalizeRelative(
|
|
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
|
|
1336
|
-
const normalized = normalizeRelative(
|
|
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
|
|
1939
|
-
const 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:
|
|
1956
|
+
birthtimeMs: stat6.birthtimeMs
|
|
1957
1957
|
};
|
|
1958
1958
|
} catch {
|
|
1959
1959
|
}
|
|
1960
1960
|
}
|
|
1961
1961
|
suffix = lines[0] ?? "";
|
|
1962
1962
|
}
|
|
1963
|
-
return { size, 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
|
-
|
|
1972
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
3292
|
-
this.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
*
|
|
3823
|
-
*
|
|
3824
|
-
*
|
|
3825
|
-
*
|
|
3826
|
-
*
|
|
3827
|
-
*
|
|
3828
|
-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
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
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
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
|
-
|
|
4040
|
-
const
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
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
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
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
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
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
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
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
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
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
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
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
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
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
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
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
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
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 (
|
|
4137
|
-
|
|
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
|
-
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
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
|
|
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 =
|
|
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(
|
|
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
|
|
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
|
|
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:
|
|
4828
|
-
globalRoot:
|
|
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:
|
|
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 =
|
|
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:${
|
|
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
|
|
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(
|
|
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 });
|