@wrongstack/core 0.310.1 → 0.313.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chronicle/index.js +84 -6391
- package/dist/chronicle/project-server.js +84 -4868
- package/dist/chunk-2BUUIMQB.js +225 -0
- package/dist/chunk-2FK4WTFF.js +207 -0
- package/dist/chunk-2FQL4PDC.js +292 -0
- package/dist/chunk-2KKODIQU.js +2256 -0
- package/dist/chunk-2YU4WVBW.js +134 -0
- package/dist/chunk-3E244UDA.js +356 -0
- package/dist/chunk-3XPVB7BA.js +135 -0
- package/dist/chunk-4LJJYBAU.js +388 -0
- package/dist/chunk-4VWO3ZXL.js +342 -0
- package/dist/chunk-4XV6IR37.js +187 -0
- package/dist/chunk-4ZHTKJHM.js +183 -0
- package/dist/chunk-52RTVH4M.js +361 -0
- package/dist/chunk-57GEWK5X.js +26 -0
- package/dist/chunk-5DSMEVA4.js +931 -0
- package/dist/chunk-5RP3QTRL.js +1223 -0
- package/dist/chunk-5S437PRC.js +50 -0
- package/dist/chunk-5XDUGTKN.js +511 -0
- package/dist/chunk-62V2AFVG.js +422 -0
- package/dist/chunk-65RDIOR2.js +2470 -0
- package/dist/chunk-6AKOTGLM.js +597 -0
- package/dist/chunk-7ZHZE5EO.js +28 -0
- package/dist/chunk-A455ATZB.js +140 -0
- package/dist/chunk-AD7ZFZCS.js +46 -0
- package/dist/chunk-AI4LLQ2U.js +856 -0
- package/dist/chunk-ASX6AW36.js +789 -0
- package/dist/chunk-AT4V4XGI.js +3522 -0
- package/dist/chunk-AWNQ6ED6.js +311 -0
- package/dist/chunk-B4AMLZPB.js +9 -0
- package/dist/chunk-BAKVJLEV.js +417 -0
- package/dist/chunk-BJHVULHO.js +55 -0
- package/dist/chunk-CB2HFKTP.js +9 -0
- package/dist/chunk-CE4ZOCOI.js +1640 -0
- package/dist/chunk-CQQL4ZOQ.js +151 -0
- package/dist/chunk-DEIQMBD2.js +65 -0
- package/dist/chunk-E6I5RXVK.js +58 -0
- package/dist/chunk-EFEHDQGY.js +1589 -0
- package/dist/chunk-EODDVJBM.js +82 -0
- package/dist/chunk-EPC3GZD4.js +308 -0
- package/dist/chunk-FAYRX2AD.js +13 -0
- package/dist/chunk-FFSX2QDQ.js +362 -0
- package/dist/chunk-FHAMQS7Q.js +5856 -0
- package/dist/chunk-FIV4IJL7.js +2263 -0
- package/dist/chunk-FNXVT7HF.js +52 -0
- package/dist/chunk-FSWCWSTS.js +395 -0
- package/dist/chunk-FW5LDDMJ.js +16464 -0
- package/dist/chunk-FXQLIX3K.js +211 -0
- package/dist/chunk-G5AAKEEZ.js +228 -0
- package/dist/chunk-G5S3S5ZN.js +38 -0
- package/dist/chunk-GAZEWQEZ.js +231 -0
- package/dist/chunk-GFJOMSHG.js +125 -0
- package/dist/chunk-GFXZTHNI.js +158 -0
- package/dist/chunk-GM54GJBU.js +960 -0
- package/dist/chunk-GPQ3NJPR.js +900 -0
- package/dist/chunk-GTCU6DZ3.js +1 -0
- package/dist/chunk-H5E77KUJ.js +15 -0
- package/dist/chunk-H72H4XDU.js +1023 -0
- package/dist/chunk-HA4ZYA6T.js +178 -0
- package/dist/chunk-I45KAW2N.js +5020 -0
- package/dist/chunk-IQ35HMQL.js +1396 -0
- package/dist/chunk-IYFBFX34.js +86 -0
- package/dist/chunk-JJPMWPB6.js +109 -0
- package/dist/chunk-JTHKOYWU.js +663 -0
- package/dist/chunk-JVGPR3TS.js +365 -0
- package/dist/chunk-JVKB2TBL.js +48 -0
- package/dist/chunk-JX7VBZDK.js +31 -0
- package/dist/chunk-K3G6JBUC.js +5232 -0
- package/dist/chunk-K4YUNKU7.js +589 -0
- package/dist/chunk-KBZGE6LN.js +710 -0
- package/dist/chunk-KDQJCT2J.js +45 -0
- package/dist/chunk-KI4L2DUL.js +560 -0
- package/dist/chunk-KJ2ZTQSL.js +47 -0
- package/dist/chunk-KSJ5UTP7.js +154 -0
- package/dist/chunk-LHOA5DWA.js +28 -0
- package/dist/chunk-LX5QWLBL.js +27 -0
- package/dist/chunk-LZDDLNCZ.js +279 -0
- package/dist/chunk-M2WF6AXG.js +115 -0
- package/dist/chunk-M7BS2K6U.js +1 -0
- package/dist/chunk-NDBIPFNY.js +102 -0
- package/dist/chunk-NEMIWEXC.js +1199 -0
- package/dist/chunk-NFAYHPRH.js +323 -0
- package/dist/chunk-O37BQ65S.js +21 -0
- package/dist/chunk-O74MJHXU.js +187 -0
- package/dist/chunk-OJZEX73F.js +112 -0
- package/dist/chunk-PAAWLLZX.js +74 -0
- package/dist/chunk-PFXFQQRT.js +339 -0
- package/dist/chunk-PFZO3C6H.js +14 -0
- package/dist/chunk-PHH7LD45.js +113 -0
- package/dist/chunk-QRTG4XTI.js +188 -0
- package/dist/chunk-QVOVMXJR.js +1351 -0
- package/dist/chunk-RCQYT2I4.js +757 -0
- package/dist/chunk-RD6DEE36.js +1178 -0
- package/dist/chunk-RJIZEJ42.js +30 -0
- package/dist/chunk-RPOA7IAE.js +150 -0
- package/dist/chunk-SN2DQRL6.js +1172 -0
- package/dist/chunk-SQJVFEBV.js +41 -0
- package/dist/chunk-SQP64RCS.js +5391 -0
- package/dist/chunk-SVT4RNTR.js +49 -0
- package/dist/chunk-SWUFN7XM.js +43 -0
- package/dist/chunk-SXMVYC5H.js +174 -0
- package/dist/chunk-SZ6AYUIZ.js +23 -0
- package/dist/chunk-T3KGZBU5.js +3171 -0
- package/dist/chunk-TBRL2RCC.js +9 -0
- package/dist/chunk-TRT2HRWZ.js +18 -0
- package/dist/chunk-TYW2GI5I.js +1077 -0
- package/dist/chunk-UIWONWVG.js +4959 -0
- package/dist/chunk-VB3H5WWB.js +151 -0
- package/dist/chunk-W4A6QA3D.js +258 -0
- package/dist/chunk-WBPVKEEE.js +227 -0
- package/dist/chunk-WEBMSQP2.js +123 -0
- package/dist/chunk-WOEZV3Q4.js +333 -0
- package/dist/chunk-WQQON3SV.js +473 -0
- package/dist/chunk-WQTOVMGV.js +119 -0
- package/dist/chunk-WZO23AWO.js +2779 -0
- package/dist/chunk-XBDP4QPW.js +7 -0
- package/dist/chunk-XEBHSJLT.js +103 -0
- package/dist/chunk-XQ2ECNK3.js +1 -0
- package/dist/chunk-XQ2XGIBF.js +2086 -0
- package/dist/chunk-Y2GE5TSI.js +355 -0
- package/dist/chunk-YBUJCZ57.js +351 -0
- package/dist/chunk-YNCVSFGF.js +149 -0
- package/dist/chunk-YUPMXOGP.js +282 -0
- package/dist/chunk-YY6APYUM.js +49 -0
- package/dist/chunk-Z2IJUO45.js +691 -0
- package/dist/chunk-ZEHUCJHY.js +129 -0
- package/dist/chunk-ZYU2YOJ6.js +1496 -0
- package/dist/chunk-ZZ4V3WIZ.js +163 -0
- package/dist/coordination/agents/index.js +145 -6260
- package/dist/coordination/index.js +365 -34323
- package/dist/coordination/mailbox-project-server.js +48 -3220
- package/dist/coordination/mailbox-types.d.ts +13 -1
- package/dist/coordination/provider-status-tracker.d.ts +4 -0
- package/dist/coordination/sqlite-mailbox.d.ts +2 -1
- package/dist/core/agent.d.ts +14 -1
- package/dist/core/index.js +267 -12479
- package/dist/core/model-ref.js +5 -23
- package/dist/core/provider-runner.d.ts +36 -1
- package/dist/design/index.js +50 -1087
- package/dist/execution/index.d.ts +1 -1
- package/dist/execution/index.js +1599 -21887
- package/dist/execution/prompt-enhancer.js +16 -535
- package/dist/execution/retry-policy.d.ts +31 -1
- package/dist/extension/index.js +7 -311
- package/dist/goal/index.js +33 -3435
- package/dist/hooks/index.js +14 -1138
- package/dist/hq/index.js +112 -5139
- package/dist/hq/protocol.js +21 -668
- package/dist/index.js +1410 -92655
- package/dist/infrastructure/index.js +64 -4811
- package/dist/kernel/events/provider-events.d.ts +15 -0
- package/dist/kernel/events/wrongtrace-events.d.ts +26 -0
- package/dist/kernel/events.d.ts +16 -7
- package/dist/kernel/index.js +14 -992
- package/dist/models/index.js +40 -1117
- package/dist/notifications/index.js +3 -108
- package/dist/observability/index.js +17 -279
- package/dist/plugin/index.js +584 -14835
- package/dist/prompts/index.js +14 -948
- package/dist/registry/index.js +9 -822
- package/dist/registry-R23UBE7H.js +14 -0
- package/dist/replay/index.js +6 -84
- package/dist/review-finding-store-DKAV3AS6.js +21 -0
- package/dist/review-report-store-OWY3EC3N.js +19 -0
- package/dist/security/index.js +114 -2202
- package/dist/session-catalog/index.js +36 -2103
- package/dist/session-catalog/project-server.js +42 -1435
- package/dist/skills/index.js +35 -1578
- package/dist/storage/index.js +182 -15600
- package/dist/tasking/index.js +10 -676
- package/dist/tools/index.js +52 -9589
- package/dist/types/config/autonomy.d.ts +5 -0
- package/dist/types/config/tools.d.ts +2 -2
- package/dist/types/config/ui.d.ts +1 -1
- package/dist/types/errors.d.ts +18 -0
- package/dist/types/index.js +121 -1341
- package/dist/types/provider-runner.d.ts +8 -0
- package/dist/types/session-markers.js +8 -144
- package/dist/utils/error.js +3 -4
- package/dist/utils/expect-defined.js +3 -9
- package/dist/utils/heap-watchdog.js +6 -706
- package/dist/utils/index.js +269 -5685
- package/dist/utils/sage-output-block.js +5 -42
- package/dist/wiring/proxy-rewrite.d.ts +92 -4
- package/dist/wiring/proxy-rewrite.js +20 -61
- package/dist/worktree/index.js +6 -887
- package/package.json +4 -4
|
@@ -0,0 +1,3522 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SECRET_FILE_MODE
|
|
3
|
+
} from "./chunk-H5E77KUJ.js";
|
|
4
|
+
import {
|
|
5
|
+
withSqliteExperimentalWarningSuppressed
|
|
6
|
+
} from "./chunk-LX5QWLBL.js";
|
|
7
|
+
import {
|
|
8
|
+
atomicWrite,
|
|
9
|
+
withFileLock
|
|
10
|
+
} from "./chunk-7ZHZE5EO.js";
|
|
11
|
+
|
|
12
|
+
// src/chronicle/identity.ts
|
|
13
|
+
import { createHash } from "node:crypto";
|
|
14
|
+
import * as os from "node:os";
|
|
15
|
+
import * as path from "node:path";
|
|
16
|
+
function resolveChronicleRuntimeLocation(input) {
|
|
17
|
+
const day = (input.now ?? /* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
18
|
+
return {
|
|
19
|
+
installationId: stableId("installation", path.resolve(input.globalRoot)),
|
|
20
|
+
machineId: stableId("machine", `${os.hostname()}\0${os.platform()}\0${os.arch()}`),
|
|
21
|
+
projectId: input.projectId,
|
|
22
|
+
chronicleDirectory: path.join(input.projectDir, "chronicle"),
|
|
23
|
+
day
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function stableId(prefix, value) {
|
|
27
|
+
return `${prefix}_${createHash("sha256").update(value).digest("hex").slice(0, 24)}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// src/chronicle/event-hash.ts
|
|
31
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
32
|
+
var GENESIS_HASH = "0".repeat(64);
|
|
33
|
+
function stableStringify(value) {
|
|
34
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
35
|
+
if (Array.isArray(value)) {
|
|
36
|
+
return `[${value.map((item) => item === void 0 ? "null" : stableStringify(item)).join(",")}]`;
|
|
37
|
+
}
|
|
38
|
+
const obj = value;
|
|
39
|
+
return `{${Object.keys(obj).sort().filter((key) => obj[key] !== void 0).map((key) => `${JSON.stringify(key)}:${stableStringify(obj[key])}`).join(",")}}`;
|
|
40
|
+
}
|
|
41
|
+
function hashValue(value) {
|
|
42
|
+
return createHash2("sha256").update(stableStringify(value), "utf8").digest("hex");
|
|
43
|
+
}
|
|
44
|
+
function chronicleEventHash(event) {
|
|
45
|
+
const { hash: _hash, ...unhashed } = event;
|
|
46
|
+
return hashValue(unhashed);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// src/chronicle/types.ts
|
|
50
|
+
var CHRONICLE_SCHEMA_VERSION = 1;
|
|
51
|
+
|
|
52
|
+
// src/chronicle/journal.ts
|
|
53
|
+
import { randomUUID } from "node:crypto";
|
|
54
|
+
import { createReadStream } from "node:fs";
|
|
55
|
+
import * as fs from "node:fs/promises";
|
|
56
|
+
import * as path3 from "node:path";
|
|
57
|
+
import { createInterface } from "node:readline";
|
|
58
|
+
|
|
59
|
+
// src/chronicle/partition-filename.ts
|
|
60
|
+
import * as path2 from "node:path";
|
|
61
|
+
var PARTITION_FILE_PATTERN = /^(.*\.events)(?:\.(\d{5}))?\.jsonl$/;
|
|
62
|
+
function partitionFamily(filePath) {
|
|
63
|
+
const match = PARTITION_FILE_PATTERN.exec(path2.basename(filePath));
|
|
64
|
+
return path2.join(path2.dirname(filePath), match?.[1] ?? path2.basename(filePath));
|
|
65
|
+
}
|
|
66
|
+
function partitionSequence(filePath) {
|
|
67
|
+
const match = PARTITION_FILE_PATTERN.exec(path2.basename(filePath));
|
|
68
|
+
return Number(match?.[2] ?? 0);
|
|
69
|
+
}
|
|
70
|
+
function comparePartitionPaths(left, right) {
|
|
71
|
+
const familyOrder = partitionFamily(left).localeCompare(partitionFamily(right));
|
|
72
|
+
return familyOrder || partitionSequence(left) - partitionSequence(right);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// src/chronicle/journal.ts
|
|
76
|
+
var DEFAULT_MAX_PARTITION_BYTES = 100 * 1024 * 1024;
|
|
77
|
+
var DEFAULT_ROTATION_WINDOW_MS = 60 * 60 * 1e3;
|
|
78
|
+
var RETENTION_CHECKPOINT_VERSION = 1;
|
|
79
|
+
var ChronicleJournal = class {
|
|
80
|
+
basePath;
|
|
81
|
+
now;
|
|
82
|
+
monotonicNow;
|
|
83
|
+
idFactory;
|
|
84
|
+
maxPending;
|
|
85
|
+
batchWindowMs;
|
|
86
|
+
maxPartitionSizeBytes;
|
|
87
|
+
rotationWindowMs;
|
|
88
|
+
retentionDays;
|
|
89
|
+
autoPurgeIntervalMs;
|
|
90
|
+
pending = [];
|
|
91
|
+
drainPromise;
|
|
92
|
+
drainScheduled = false;
|
|
93
|
+
drainTimer;
|
|
94
|
+
counters = {
|
|
95
|
+
acceptedEvents: 0,
|
|
96
|
+
persistedEvents: 0,
|
|
97
|
+
rejectedEvents: 0,
|
|
98
|
+
failedEvents: 0,
|
|
99
|
+
batches: 0,
|
|
100
|
+
maxObservedPending: 0,
|
|
101
|
+
largestBatch: 0,
|
|
102
|
+
partitionRolls: 0
|
|
103
|
+
};
|
|
104
|
+
lastBatchDurationMs;
|
|
105
|
+
partitionIndex = 0;
|
|
106
|
+
partitionStartedAt;
|
|
107
|
+
partitionSizeBytes = 0;
|
|
108
|
+
stateInitialized = false;
|
|
109
|
+
lastSequence = 0;
|
|
110
|
+
lastHash = GENESIS_HASH;
|
|
111
|
+
lastAutoPurgeAt = 0;
|
|
112
|
+
constructor(options) {
|
|
113
|
+
this.basePath = path3.resolve(options.filePath);
|
|
114
|
+
this.now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
115
|
+
this.monotonicNow = options.monotonicNow ?? (() => process.hrtime.bigint());
|
|
116
|
+
this.idFactory = options.idFactory ?? randomUUID;
|
|
117
|
+
this.maxPending = Math.max(1, options.maxPending ?? 1e5);
|
|
118
|
+
this.batchWindowMs = Math.max(0, options.batchWindowMs ?? 5);
|
|
119
|
+
this.maxPartitionSizeBytes = options.maxPartitionSizeBytes ?? DEFAULT_MAX_PARTITION_BYTES;
|
|
120
|
+
this.rotationWindowMs = options.rotationWindowMs ?? DEFAULT_ROTATION_WINDOW_MS;
|
|
121
|
+
this.retentionDays = options.retentionDays && Number.isFinite(options.retentionDays) && options.retentionDays > 0 ? options.retentionDays : 0;
|
|
122
|
+
this.autoPurgeIntervalMs = Math.max(0, options.autoPurgeIntervalMs ?? 36e5);
|
|
123
|
+
this.partitionStartedAt = Date.now();
|
|
124
|
+
}
|
|
125
|
+
get path() {
|
|
126
|
+
return this.partitionIndex === 0 ? this.basePath : rotatedPath(this.basePath, this.partitionIndex);
|
|
127
|
+
}
|
|
128
|
+
stats() {
|
|
129
|
+
return {
|
|
130
|
+
...this.counters,
|
|
131
|
+
pendingEvents: this.pending.length,
|
|
132
|
+
...this.lastBatchDurationMs !== void 0 ? { lastBatchDurationMs: this.lastBatchDurationMs } : {}
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
append(input) {
|
|
136
|
+
if (this.pending.length >= this.maxPending) {
|
|
137
|
+
this.counters.rejectedEvents++;
|
|
138
|
+
return Promise.reject(
|
|
139
|
+
new Error(`Chronicle backpressure limit reached (${this.maxPending} pending events)`)
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
const promise = new Promise((resolve7, reject) => {
|
|
143
|
+
this.pending.push({ input, resolve: resolve7, reject });
|
|
144
|
+
});
|
|
145
|
+
this.counters.acceptedEvents++;
|
|
146
|
+
this.counters.maxObservedPending = Math.max(
|
|
147
|
+
this.counters.maxObservedPending,
|
|
148
|
+
this.pending.length
|
|
149
|
+
);
|
|
150
|
+
this.scheduleDrain();
|
|
151
|
+
return promise;
|
|
152
|
+
}
|
|
153
|
+
async readAll() {
|
|
154
|
+
await this.flush();
|
|
155
|
+
const files = await collectPartitions(this.basePath);
|
|
156
|
+
const entries = [];
|
|
157
|
+
for (const file of files) entries.push(...await readEntriesStrict(file));
|
|
158
|
+
return entries;
|
|
159
|
+
}
|
|
160
|
+
async flush() {
|
|
161
|
+
if (this.drainTimer) {
|
|
162
|
+
clearTimeout(this.drainTimer);
|
|
163
|
+
this.drainTimer = void 0;
|
|
164
|
+
this.drainScheduled = false;
|
|
165
|
+
}
|
|
166
|
+
while (this.pending.length > 0 || this.drainPromise) {
|
|
167
|
+
if (this.pending.length > 0 && !this.drainPromise) this.startDrain();
|
|
168
|
+
await this.drainPromise;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
async verify() {
|
|
172
|
+
await this.flush();
|
|
173
|
+
const files = await collectPartitions(this.basePath);
|
|
174
|
+
const checkpointResult = await readRetentionCheckpoint(this.basePath);
|
|
175
|
+
if (checkpointResult.error)
|
|
176
|
+
return { ok: false, entries: 0, brokenAt: 0, reason: checkpointResult.error };
|
|
177
|
+
return verifyPartitionFiles(files, checkpointResult.checkpoint);
|
|
178
|
+
}
|
|
179
|
+
async purge(options) {
|
|
180
|
+
await this.flush();
|
|
181
|
+
if (!Number.isFinite(options.retentionDays) || options.retentionDays <= 0) {
|
|
182
|
+
throw new TypeError("Chronicle retentionDays must be a positive finite number");
|
|
183
|
+
}
|
|
184
|
+
await this.refreshStateFromDisk();
|
|
185
|
+
const cutoff = Date.now() - options.retentionDays * 864e5;
|
|
186
|
+
const activePath = path3.resolve(this.path);
|
|
187
|
+
const errors = [];
|
|
188
|
+
let dc = 0, db = 0, sc = 0;
|
|
189
|
+
const suppliedFiles = options.files === void 0 ? await collectJournalPartitions(this.basePath) : [...options.files];
|
|
190
|
+
const eligible = /* @__PURE__ */ new Set();
|
|
191
|
+
for (const suppliedPath of new Set(suppliedFiles)) {
|
|
192
|
+
const file = path3.resolve(suppliedPath);
|
|
193
|
+
if (!isJournalPartition(file, path3.dirname(this.basePath), this.basePath)) {
|
|
194
|
+
errors.push({
|
|
195
|
+
file: suppliedPath,
|
|
196
|
+
reason: "not a Chronicle journal partition in this journal directory"
|
|
197
|
+
});
|
|
198
|
+
sc++;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (file === activePath) {
|
|
202
|
+
sc++;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
let mtimeMs;
|
|
206
|
+
try {
|
|
207
|
+
const fileStat = await fs.lstat(file);
|
|
208
|
+
if (!fileStat.isFile()) {
|
|
209
|
+
sc++;
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
mtimeMs = fileStat.mtimeMs;
|
|
213
|
+
} catch (error) {
|
|
214
|
+
if (isNotFound(error)) continue;
|
|
215
|
+
errors.push({ file, reason: errorMessage(error) });
|
|
216
|
+
sc++;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (mtimeMs > cutoff) {
|
|
220
|
+
sc++;
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
eligible.add(file);
|
|
224
|
+
}
|
|
225
|
+
const candidates = [];
|
|
226
|
+
const allPartitions = await collectJournalPartitions(this.basePath);
|
|
227
|
+
for (const family of groupPartitionsByFamily(allPartitions).values()) {
|
|
228
|
+
for (const file of family) {
|
|
229
|
+
if (file === activePath || !eligible.has(file)) break;
|
|
230
|
+
candidates.push(file);
|
|
231
|
+
eligible.delete(file);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
sc += eligible.size;
|
|
235
|
+
if (!options.dryRun) {
|
|
236
|
+
for (const file of candidates) {
|
|
237
|
+
try {
|
|
238
|
+
const familyBase = partitionFamilyBase(file);
|
|
239
|
+
let deletedBytes;
|
|
240
|
+
await withFileLock(familyBase, async () => {
|
|
241
|
+
const fileStat = await fs.lstat(file);
|
|
242
|
+
if (!fileStat.isFile() || fileStat.mtimeMs > cutoff) return;
|
|
243
|
+
const checkpointResult = await readRetentionCheckpoint(familyBase);
|
|
244
|
+
if (checkpointResult.error) throw new Error(checkpointResult.error);
|
|
245
|
+
const checkpoint = checkpointResult.checkpoint;
|
|
246
|
+
const nextCheckpoint = await verifyRetainedPrefix(
|
|
247
|
+
streamEntriesStrict(file),
|
|
248
|
+
checkpoint
|
|
249
|
+
);
|
|
250
|
+
if (!nextCheckpoint)
|
|
251
|
+
throw new Error("partition does not extend the trusted Chronicle chain");
|
|
252
|
+
if (nextCheckpoint.sequence > (checkpoint?.sequence ?? 0)) {
|
|
253
|
+
await writeRetentionCheckpoint(familyBase, nextCheckpoint);
|
|
254
|
+
}
|
|
255
|
+
deletedBytes = fileStat.size;
|
|
256
|
+
await fs.unlink(file);
|
|
257
|
+
});
|
|
258
|
+
if (deletedBytes === void 0) {
|
|
259
|
+
sc++;
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
db += deletedBytes;
|
|
263
|
+
dc++;
|
|
264
|
+
} catch (error) {
|
|
265
|
+
errors.push({ file, reason: errorMessage(error) });
|
|
266
|
+
sc++;
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
deletedCount: dc,
|
|
273
|
+
deletedBytes: db,
|
|
274
|
+
skippedCount: sc,
|
|
275
|
+
errors,
|
|
276
|
+
...options.dryRun ? { candidates } : {}
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
async maybeAutoPurge() {
|
|
280
|
+
if (this.retentionDays <= 0) return;
|
|
281
|
+
const n = Date.now();
|
|
282
|
+
if (n - this.lastAutoPurgeAt < this.autoPurgeIntervalMs) return;
|
|
283
|
+
this.lastAutoPurgeAt = n;
|
|
284
|
+
try {
|
|
285
|
+
await this.purge({ retentionDays: this.retentionDays });
|
|
286
|
+
} catch {
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
scheduleDrain() {
|
|
290
|
+
if (this.drainScheduled || this.drainPromise) return;
|
|
291
|
+
this.drainScheduled = true;
|
|
292
|
+
if (this.batchWindowMs === 0) {
|
|
293
|
+
queueMicrotask(() => {
|
|
294
|
+
this.drainScheduled = false;
|
|
295
|
+
this.startDrain();
|
|
296
|
+
});
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
this.drainTimer = setTimeout(() => {
|
|
300
|
+
this.drainTimer = void 0;
|
|
301
|
+
this.drainScheduled = false;
|
|
302
|
+
this.startDrain();
|
|
303
|
+
}, this.batchWindowMs);
|
|
304
|
+
}
|
|
305
|
+
startDrain() {
|
|
306
|
+
if (this.drainPromise || this.pending.length === 0) return;
|
|
307
|
+
const batch = this.pending.splice(0);
|
|
308
|
+
const drain = this.persistBatch(batch);
|
|
309
|
+
this.drainPromise = drain.finally(() => {
|
|
310
|
+
this.drainPromise = void 0;
|
|
311
|
+
if (this.pending.length > 0) this.scheduleDrain();
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
async refreshStateFromDisk() {
|
|
315
|
+
const files = await collectPartitions(this.basePath);
|
|
316
|
+
const latest = files[files.length - 1] ?? this.basePath;
|
|
317
|
+
this.partitionIndex = partitionIndex(latest, this.basePath);
|
|
318
|
+
const state = await readLastEntryState(latest);
|
|
319
|
+
const entry = state.entry;
|
|
320
|
+
const checkpointResult = entry ? {} : await readRetentionCheckpoint(this.basePath);
|
|
321
|
+
if (checkpointResult.error) throw new Error(checkpointResult.error);
|
|
322
|
+
const checkpoint = checkpointResult.checkpoint;
|
|
323
|
+
this.lastSequence = entry?.sequence ?? checkpoint?.sequence ?? 0;
|
|
324
|
+
this.lastHash = entry?.hash ?? checkpoint?.hash ?? GENESIS_HASH;
|
|
325
|
+
this.partitionSizeBytes = state.size;
|
|
326
|
+
this.partitionStartedAt = state.birthtimeMs ?? Date.now();
|
|
327
|
+
this.stateInitialized = true;
|
|
328
|
+
}
|
|
329
|
+
async canReuseDiskState() {
|
|
330
|
+
if (!this.stateInitialized) return false;
|
|
331
|
+
const nextPartition = rotatedPath(this.basePath, this.partitionIndex + 1);
|
|
332
|
+
const [currentStat, nextExists] = await Promise.all([
|
|
333
|
+
fs.stat(this.path).catch((error) => {
|
|
334
|
+
if (isNotFound(error)) return void 0;
|
|
335
|
+
throw error;
|
|
336
|
+
}),
|
|
337
|
+
fs.access(nextPartition).then(
|
|
338
|
+
() => true,
|
|
339
|
+
(error) => {
|
|
340
|
+
if (isNotFound(error)) return false;
|
|
341
|
+
throw error;
|
|
342
|
+
}
|
|
343
|
+
)
|
|
344
|
+
]);
|
|
345
|
+
return currentStat?.isFile() === true && currentStat.size === this.partitionSizeBytes && !nextExists;
|
|
346
|
+
}
|
|
347
|
+
async checkRotation() {
|
|
348
|
+
if (this.partitionIndex === 0 && this.lastSequence === 0) return;
|
|
349
|
+
if (Number.isFinite(this.rotationWindowMs) && Date.now() - this.partitionStartedAt >= this.rotationWindowMs) {
|
|
350
|
+
this.rotate();
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
if (Number.isFinite(this.maxPartitionSizeBytes) && this.partitionSizeBytes >= this.maxPartitionSizeBytes)
|
|
354
|
+
this.rotate();
|
|
355
|
+
}
|
|
356
|
+
rotate() {
|
|
357
|
+
this.partitionIndex++;
|
|
358
|
+
this.partitionStartedAt = Date.now();
|
|
359
|
+
this.partitionSizeBytes = 0;
|
|
360
|
+
this.counters.partitionRolls++;
|
|
361
|
+
}
|
|
362
|
+
async persistBatch(batch) {
|
|
363
|
+
const started = performance.now();
|
|
364
|
+
this.counters.batches++;
|
|
365
|
+
this.counters.largestBatch = Math.max(this.counters.largestBatch, batch.length);
|
|
366
|
+
try {
|
|
367
|
+
let recorded = [];
|
|
368
|
+
await withFileLock(this.basePath, async () => {
|
|
369
|
+
if (!await this.canReuseDiskState()) await this.refreshStateFromDisk();
|
|
370
|
+
await this.checkRotation();
|
|
371
|
+
const cp = this.path;
|
|
372
|
+
let prev = this.lastSequence > 0 ? { sequence: this.lastSequence, hash: this.lastHash } : void 0;
|
|
373
|
+
recorded = batch.map(({ input }) => {
|
|
374
|
+
const instant = this.now().toISOString();
|
|
375
|
+
const uh = {
|
|
376
|
+
...input,
|
|
377
|
+
occurredAt: input.occurredAt ?? instant,
|
|
378
|
+
monotonicNs: input.monotonicNs ?? this.monotonicNow().toString(),
|
|
379
|
+
schemaVersion: CHRONICLE_SCHEMA_VERSION,
|
|
380
|
+
eventId: this.idFactory(),
|
|
381
|
+
observedAt: instant,
|
|
382
|
+
persistedAt: instant,
|
|
383
|
+
sequence: (prev?.sequence ?? 0) + 1,
|
|
384
|
+
previousHash: prev?.hash ?? GENESIS_HASH
|
|
385
|
+
};
|
|
386
|
+
const event = { ...uh, hash: hashValue(uh) };
|
|
387
|
+
prev = event;
|
|
388
|
+
return event;
|
|
389
|
+
});
|
|
390
|
+
const serialized = recorded.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
391
|
+
await fs.appendFile(cp, serialized, { encoding: "utf8", mode: SECRET_FILE_MODE });
|
|
392
|
+
this.partitionSizeBytes += Buffer.byteLength(serialized);
|
|
393
|
+
});
|
|
394
|
+
const last = recorded[recorded.length - 1];
|
|
395
|
+
this.lastSequence = last.sequence;
|
|
396
|
+
this.lastHash = last.hash;
|
|
397
|
+
batch.forEach((item, i) => {
|
|
398
|
+
item.resolve(recorded[i]);
|
|
399
|
+
});
|
|
400
|
+
this.counters.persistedEvents += batch.length;
|
|
401
|
+
void this.maybeAutoPurge();
|
|
402
|
+
} catch (error) {
|
|
403
|
+
this.stateInitialized = false;
|
|
404
|
+
this.counters.failedEvents += batch.length;
|
|
405
|
+
batch.forEach((item) => {
|
|
406
|
+
item.reject(error);
|
|
407
|
+
});
|
|
408
|
+
} finally {
|
|
409
|
+
this.lastBatchDurationMs = performance.now() - started;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
function rotatedPath(basePath, index) {
|
|
414
|
+
const dir = path3.dirname(basePath);
|
|
415
|
+
const ext = path3.extname(basePath);
|
|
416
|
+
const base = path3.basename(basePath, ext);
|
|
417
|
+
return path3.join(dir, `${base}.${String(index).padStart(5, "0")}${ext}`);
|
|
418
|
+
}
|
|
419
|
+
async function collectPartitions(basePath) {
|
|
420
|
+
const dir = path3.dirname(basePath);
|
|
421
|
+
const ext = path3.extname(basePath);
|
|
422
|
+
const base = path3.basename(basePath, ext);
|
|
423
|
+
const pattern = new RegExp(`^${escapeRegex(base)}(?:\\.\\d{5})?${escapeRegex(ext)}$`);
|
|
424
|
+
const result = [];
|
|
425
|
+
try {
|
|
426
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
427
|
+
for (const entry of entries)
|
|
428
|
+
if (entry.isFile() && pattern.test(entry.name)) result.push(path3.join(dir, entry.name));
|
|
429
|
+
} catch {
|
|
430
|
+
}
|
|
431
|
+
const baseFile = path3.join(dir, base + ext);
|
|
432
|
+
const rotated = result.filter((file) => file !== baseFile).sort((left, right) => parseIndex(left, base, ext) - parseIndex(right, base, ext));
|
|
433
|
+
return [baseFile, ...rotated];
|
|
434
|
+
}
|
|
435
|
+
async function collectJournalPartitions(basePath) {
|
|
436
|
+
const directory = path3.dirname(basePath);
|
|
437
|
+
const result = [];
|
|
438
|
+
try {
|
|
439
|
+
const entries = await fs.readdir(directory, { withFileTypes: true });
|
|
440
|
+
for (const entry of entries) {
|
|
441
|
+
const file = path3.join(directory, entry.name);
|
|
442
|
+
if (entry.isFile() && isJournalPartition(file, directory, basePath)) result.push(file);
|
|
443
|
+
}
|
|
444
|
+
} catch {
|
|
445
|
+
}
|
|
446
|
+
return result.sort(comparePartitionPaths);
|
|
447
|
+
}
|
|
448
|
+
function isJournalPartition(filePath, directory, basePath) {
|
|
449
|
+
if (path3.dirname(path3.resolve(filePath)) !== path3.resolve(directory)) return false;
|
|
450
|
+
const fileName = path3.basename(filePath);
|
|
451
|
+
if (!basePath) return false;
|
|
452
|
+
const baseName = path3.basename(basePath);
|
|
453
|
+
const dailyFamily = /^\d{4}-\d{2}-\d{2}\.events(?:\.\d{5})?\.jsonl$/;
|
|
454
|
+
if (/^\d{4}-\d{2}-\d{2}\.events\.jsonl$/.test(baseName)) return dailyFamily.test(fileName);
|
|
455
|
+
return partitionFamilyBase(path3.resolve(filePath)) === partitionFamilyBase(path3.resolve(basePath));
|
|
456
|
+
}
|
|
457
|
+
function groupPartitionsByFamily(files) {
|
|
458
|
+
const groups = /* @__PURE__ */ new Map();
|
|
459
|
+
for (const file of files) {
|
|
460
|
+
const family = partitionFamilyBase(file);
|
|
461
|
+
const group = groups.get(family) ?? [];
|
|
462
|
+
group.push(file);
|
|
463
|
+
groups.set(family, group);
|
|
464
|
+
}
|
|
465
|
+
return groups;
|
|
466
|
+
}
|
|
467
|
+
function partitionFamilyBase(filePath) {
|
|
468
|
+
return filePath.replace(/\.\d{5}(?=\.jsonl$)/, "");
|
|
469
|
+
}
|
|
470
|
+
function retentionCheckpointPath(basePath) {
|
|
471
|
+
return `${partitionFamilyBase(basePath)}.retention.json`;
|
|
472
|
+
}
|
|
473
|
+
async function verifyRetainedPrefix(entries, checkpoint) {
|
|
474
|
+
let sequence = checkpoint?.sequence ?? 0;
|
|
475
|
+
let hash = checkpoint?.hash ?? GENESIS_HASH;
|
|
476
|
+
let advanced = false;
|
|
477
|
+
let sawEntry = false;
|
|
478
|
+
for await (const entry of entries) {
|
|
479
|
+
sawEntry = true;
|
|
480
|
+
if (entry.sequence <= sequence) continue;
|
|
481
|
+
if (entry.sequence !== sequence + 1 || entry.previousHash !== hash) return void 0;
|
|
482
|
+
const { hash: recordedHash, ...content } = entry;
|
|
483
|
+
if (hashValue(content) !== recordedHash) return void 0;
|
|
484
|
+
sequence = entry.sequence;
|
|
485
|
+
hash = recordedHash;
|
|
486
|
+
advanced = true;
|
|
487
|
+
}
|
|
488
|
+
if (!sawEntry) return void 0;
|
|
489
|
+
if (!advanced) return checkpoint;
|
|
490
|
+
return { version: RETENTION_CHECKPOINT_VERSION, sequence, hash };
|
|
491
|
+
}
|
|
492
|
+
async function verifyPartitionFiles(files, checkpoint) {
|
|
493
|
+
const checkpointSequence = checkpoint?.sequence ?? 0;
|
|
494
|
+
let previousHash = checkpoint?.hash ?? GENESIS_HASH;
|
|
495
|
+
let entries = 0;
|
|
496
|
+
let lastSequence = checkpointSequence;
|
|
497
|
+
let coveredPrevious;
|
|
498
|
+
for (const file of files) {
|
|
499
|
+
try {
|
|
500
|
+
for await (const entry of streamEntriesStrict(file)) {
|
|
501
|
+
const { hash: recordedHash, ...content } = entry;
|
|
502
|
+
if (entry.sequence <= checkpointSequence) {
|
|
503
|
+
if (hashValue(content) !== recordedHash)
|
|
504
|
+
return { ok: false, entries, brokenAt: entries, reason: "entry hash mismatch" };
|
|
505
|
+
if (coveredPrevious && entry.sequence !== coveredPrevious.sequence + 1) {
|
|
506
|
+
return {
|
|
507
|
+
ok: false,
|
|
508
|
+
entries,
|
|
509
|
+
brokenAt: entries,
|
|
510
|
+
reason: `sequence ${entry.sequence} is not ${coveredPrevious.sequence + 1}`
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
if (coveredPrevious && entry.previousHash !== coveredPrevious.hash) {
|
|
514
|
+
return { ok: false, entries, brokenAt: entries, reason: "previous hash mismatch" };
|
|
515
|
+
}
|
|
516
|
+
if (entry.sequence === checkpointSequence && recordedHash !== checkpoint?.hash) {
|
|
517
|
+
return {
|
|
518
|
+
ok: false,
|
|
519
|
+
entries,
|
|
520
|
+
brokenAt: entries,
|
|
521
|
+
reason: "retention checkpoint hash mismatch"
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
coveredPrevious = entry;
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
const index = entries++;
|
|
528
|
+
if (entry.sequence !== lastSequence + 1)
|
|
529
|
+
return {
|
|
530
|
+
ok: false,
|
|
531
|
+
entries,
|
|
532
|
+
brokenAt: index,
|
|
533
|
+
reason: `sequence ${entry.sequence} is not ${lastSequence + 1}`
|
|
534
|
+
};
|
|
535
|
+
if (entry.previousHash !== previousHash)
|
|
536
|
+
return { ok: false, entries, brokenAt: index, reason: "previous hash mismatch" };
|
|
537
|
+
if (hashValue(content) !== recordedHash)
|
|
538
|
+
return { ok: false, entries, brokenAt: index, reason: "entry hash mismatch" };
|
|
539
|
+
previousHash = recordedHash;
|
|
540
|
+
lastSequence = entry.sequence;
|
|
541
|
+
}
|
|
542
|
+
} catch (error) {
|
|
543
|
+
return { ok: false, entries, brokenAt: entries, reason: errorMessage(error) };
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
if (coveredPrevious && (coveredPrevious.sequence !== checkpointSequence || coveredPrevious.hash !== checkpoint?.hash)) {
|
|
547
|
+
return { ok: false, entries, brokenAt: entries, reason: "retention checkpoint hash mismatch" };
|
|
548
|
+
}
|
|
549
|
+
return { ok: true, entries, lastSequence, lastHash: previousHash };
|
|
550
|
+
}
|
|
551
|
+
async function readRetentionCheckpoint(basePath) {
|
|
552
|
+
const checkpointPath = retentionCheckpointPath(basePath);
|
|
553
|
+
let raw;
|
|
554
|
+
try {
|
|
555
|
+
raw = await fs.readFile(checkpointPath, "utf8");
|
|
556
|
+
} catch (error) {
|
|
557
|
+
return isNotFound(error) ? {} : { error: `cannot read retention checkpoint: ${errorMessage(error)}` };
|
|
558
|
+
}
|
|
559
|
+
try {
|
|
560
|
+
const parsed = JSON.parse(raw);
|
|
561
|
+
if (parsed.version !== RETENTION_CHECKPOINT_VERSION || !Number.isSafeInteger(parsed.sequence) || (parsed.sequence ?? -1) < 0 || !isHash(parsed.hash)) {
|
|
562
|
+
return { error: "invalid Chronicle retention checkpoint" };
|
|
563
|
+
}
|
|
564
|
+
return { checkpoint: parsed };
|
|
565
|
+
} catch {
|
|
566
|
+
return { error: "invalid Chronicle retention checkpoint JSON" };
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
async function writeRetentionCheckpoint(basePath, checkpoint) {
|
|
570
|
+
await atomicWrite(retentionCheckpointPath(basePath), `${JSON.stringify(checkpoint)}
|
|
571
|
+
`, {
|
|
572
|
+
mode: 384
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
function isHash(value) {
|
|
576
|
+
return typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
|
|
577
|
+
}
|
|
578
|
+
function parseIndex(filePath, base, ext) {
|
|
579
|
+
const suffix = path3.basename(filePath).slice(base.length + 1, -ext.length);
|
|
580
|
+
return suffix ? parseInt(suffix, 10) : 0;
|
|
581
|
+
}
|
|
582
|
+
function partitionIndex(filePath, basePath) {
|
|
583
|
+
const ext = path3.extname(basePath);
|
|
584
|
+
return parseIndex(filePath, path3.basename(basePath, ext), ext);
|
|
585
|
+
}
|
|
586
|
+
function escapeRegex(text) {
|
|
587
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
588
|
+
}
|
|
589
|
+
async function readLastEntryState(filePath) {
|
|
590
|
+
let handle;
|
|
591
|
+
try {
|
|
592
|
+
handle = await fs.open(filePath, "r");
|
|
593
|
+
} catch (error) {
|
|
594
|
+
if (isNotFound(error)) return { size: 0 };
|
|
595
|
+
throw error;
|
|
596
|
+
}
|
|
597
|
+
try {
|
|
598
|
+
const stat4 = await handle.stat();
|
|
599
|
+
const size = stat4.size;
|
|
600
|
+
let position = size, suffix = "";
|
|
601
|
+
while (position > 0) {
|
|
602
|
+
const length = Math.min(65536, position);
|
|
603
|
+
position -= length;
|
|
604
|
+
const buf = Buffer.allocUnsafe(length);
|
|
605
|
+
await handle.read(buf, 0, length, position);
|
|
606
|
+
suffix = buf.toString("utf8") + suffix;
|
|
607
|
+
const lines = suffix.split("\n");
|
|
608
|
+
const start = position === 0 ? 0 : 1;
|
|
609
|
+
for (let i = lines.length - 1; i >= start; i--) {
|
|
610
|
+
const trimmed = lines[i].trim();
|
|
611
|
+
if (!trimmed) continue;
|
|
612
|
+
try {
|
|
613
|
+
return {
|
|
614
|
+
entry: JSON.parse(trimmed),
|
|
615
|
+
size,
|
|
616
|
+
birthtimeMs: stat4.birthtimeMs
|
|
617
|
+
};
|
|
618
|
+
} catch {
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
suffix = lines[0] ?? "";
|
|
622
|
+
}
|
|
623
|
+
return { size, birthtimeMs: stat4.birthtimeMs };
|
|
624
|
+
} finally {
|
|
625
|
+
await handle.close();
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
async function* streamEntriesStrict(filePath) {
|
|
629
|
+
let lineNumber = 0;
|
|
630
|
+
let input;
|
|
631
|
+
let lines;
|
|
632
|
+
try {
|
|
633
|
+
input = createReadStream(filePath, { encoding: "utf8", highWaterMark: 256 * 1024 });
|
|
634
|
+
lines = createInterface({ input, crlfDelay: Infinity });
|
|
635
|
+
for await (const line of lines) {
|
|
636
|
+
lineNumber++;
|
|
637
|
+
const trimmed = line.trim();
|
|
638
|
+
if (!trimmed) continue;
|
|
639
|
+
let entry;
|
|
640
|
+
try {
|
|
641
|
+
entry = JSON.parse(trimmed);
|
|
642
|
+
} catch {
|
|
643
|
+
throw new Error(`invalid JSON at line ${lineNumber} in ${path3.basename(filePath)}`);
|
|
644
|
+
}
|
|
645
|
+
yield entry;
|
|
646
|
+
}
|
|
647
|
+
} catch (error) {
|
|
648
|
+
if (isNotFound(error)) return;
|
|
649
|
+
throw error;
|
|
650
|
+
} finally {
|
|
651
|
+
lines?.close();
|
|
652
|
+
input?.destroy();
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
async function readEntriesStrict(filePath) {
|
|
656
|
+
const entries = [];
|
|
657
|
+
for await (const entry of streamEntriesStrict(filePath)) entries.push(entry);
|
|
658
|
+
return entries;
|
|
659
|
+
}
|
|
660
|
+
function isNotFound(error) {
|
|
661
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
662
|
+
}
|
|
663
|
+
function errorMessage(error) {
|
|
664
|
+
return error instanceof Error ? error.message : String(error);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// src/chronicle/partition-range-cache.ts
|
|
668
|
+
import * as fs2 from "node:fs/promises";
|
|
669
|
+
import * as path4 from "node:path";
|
|
670
|
+
var CACHE_VERSION = 1;
|
|
671
|
+
function isRange(value) {
|
|
672
|
+
if (!value || typeof value !== "object") return false;
|
|
673
|
+
const range = value;
|
|
674
|
+
return typeof range.size === "number" && (range.minOccurredAt === void 0 || typeof range.minOccurredAt === "string") && (range.maxOccurredAt === void 0 || typeof range.maxOccurredAt === "string") && (range.empty === void 0 || typeof range.empty === "boolean");
|
|
675
|
+
}
|
|
676
|
+
var ChroniclePartitionRangeCache = class {
|
|
677
|
+
constructor(directory) {
|
|
678
|
+
this.directory = directory;
|
|
679
|
+
this.cachePath = path4.join(directory, "range-cache.json");
|
|
680
|
+
}
|
|
681
|
+
directory;
|
|
682
|
+
cachePath;
|
|
683
|
+
entries;
|
|
684
|
+
dirty = false;
|
|
685
|
+
toKey(filePath) {
|
|
686
|
+
return path4.relative(this.directory, filePath).split(path4.sep).join("/");
|
|
687
|
+
}
|
|
688
|
+
async load() {
|
|
689
|
+
if (this.entries) return this.entries;
|
|
690
|
+
const entries = /* @__PURE__ */ new Map();
|
|
691
|
+
try {
|
|
692
|
+
const raw = await fs2.readFile(this.cachePath, "utf8");
|
|
693
|
+
const parsed = JSON.parse(raw);
|
|
694
|
+
if (parsed.version === CACHE_VERSION && parsed.entries && typeof parsed.entries === "object") {
|
|
695
|
+
for (const [key, range] of Object.entries(parsed.entries)) {
|
|
696
|
+
if (isRange(range)) entries.set(key, range);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
} catch {
|
|
700
|
+
}
|
|
701
|
+
this.entries = entries;
|
|
702
|
+
return entries;
|
|
703
|
+
}
|
|
704
|
+
async get(filePath, currentSize) {
|
|
705
|
+
const entries = await this.load();
|
|
706
|
+
const range = entries.get(this.toKey(filePath));
|
|
707
|
+
return range && range.size === currentSize ? range : void 0;
|
|
708
|
+
}
|
|
709
|
+
set(filePath, range) {
|
|
710
|
+
this.entries ??= /* @__PURE__ */ new Map();
|
|
711
|
+
this.entries.set(this.toKey(filePath), range);
|
|
712
|
+
this.dirty = true;
|
|
713
|
+
}
|
|
714
|
+
/** Drop entries for files no longer present (e.g. removed by retention purge). */
|
|
715
|
+
prune(existingFiles) {
|
|
716
|
+
if (!this.entries) return;
|
|
717
|
+
const keep = new Set(existingFiles.map((file) => this.toKey(file)));
|
|
718
|
+
for (const key of this.entries.keys()) {
|
|
719
|
+
if (!keep.has(key)) {
|
|
720
|
+
this.entries.delete(key);
|
|
721
|
+
this.dirty = true;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
async flush() {
|
|
726
|
+
if (!this.dirty || !this.entries) return;
|
|
727
|
+
const payload = { version: CACHE_VERSION, entries: Object.fromEntries(this.entries) };
|
|
728
|
+
try {
|
|
729
|
+
await withFileLock(this.cachePath, async () => {
|
|
730
|
+
await atomicWrite(this.cachePath, `${JSON.stringify(payload)}
|
|
731
|
+
`, { mode: 384 });
|
|
732
|
+
});
|
|
733
|
+
this.dirty = false;
|
|
734
|
+
} catch {
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
};
|
|
738
|
+
|
|
739
|
+
// src/chronicle/query.ts
|
|
740
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
741
|
+
import { createReadStream as createReadStream2 } from "node:fs";
|
|
742
|
+
import * as fs3 from "node:fs/promises";
|
|
743
|
+
import * as path5 from "node:path";
|
|
744
|
+
import { createInterface as createInterface2 } from "node:readline";
|
|
745
|
+
var CHRONICLE_FACET_FIELDS = /* @__PURE__ */ new Set([
|
|
746
|
+
"eventType",
|
|
747
|
+
"outcome",
|
|
748
|
+
"projectId",
|
|
749
|
+
"sessionId",
|
|
750
|
+
"agentId",
|
|
751
|
+
"taskId",
|
|
752
|
+
"providerId",
|
|
753
|
+
"modelId",
|
|
754
|
+
"resourceKind",
|
|
755
|
+
"resourcePath",
|
|
756
|
+
"toolCallId"
|
|
757
|
+
]);
|
|
758
|
+
var MAX_CURSOR_SNAPSHOT_ENTRIES = 1e4;
|
|
759
|
+
async function* streamLines(filePath, maxBytes) {
|
|
760
|
+
const stream = createReadStream2(filePath, {
|
|
761
|
+
encoding: "utf8",
|
|
762
|
+
highWaterMark: 256 * 1024,
|
|
763
|
+
...maxBytes !== void 0 ? { end: maxBytes - 1 } : {}
|
|
764
|
+
});
|
|
765
|
+
const rl = createInterface2({ input: stream, crlfDelay: Infinity });
|
|
766
|
+
try {
|
|
767
|
+
for await (const line of rl) {
|
|
768
|
+
yield line;
|
|
769
|
+
}
|
|
770
|
+
} finally {
|
|
771
|
+
rl.close();
|
|
772
|
+
stream.destroy();
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
async function computeOccurredAtRange(file, size) {
|
|
776
|
+
let min;
|
|
777
|
+
let max;
|
|
778
|
+
for await (const line of streamLines(file, size)) {
|
|
779
|
+
if (!line.trim()) continue;
|
|
780
|
+
let event;
|
|
781
|
+
try {
|
|
782
|
+
event = JSON.parse(line);
|
|
783
|
+
if (!isChronicleEvent(event)) continue;
|
|
784
|
+
} catch {
|
|
785
|
+
continue;
|
|
786
|
+
}
|
|
787
|
+
const occurredAt = event.occurredAt ?? event.observedAt;
|
|
788
|
+
if (min === void 0 || occurredAt < min) min = occurredAt;
|
|
789
|
+
if (max === void 0 || occurredAt > max) max = occurredAt;
|
|
790
|
+
}
|
|
791
|
+
return min === void 0 ? void 0 : { min, max };
|
|
792
|
+
}
|
|
793
|
+
function rangeOverlaps(range, query) {
|
|
794
|
+
if (range.empty) return false;
|
|
795
|
+
if (query.from && range.maxOccurredAt < query.from) return false;
|
|
796
|
+
if (query.to && range.minOccurredAt > query.to) return false;
|
|
797
|
+
return true;
|
|
798
|
+
}
|
|
799
|
+
var ChronicleQueryEngine = class _ChronicleQueryEngine {
|
|
800
|
+
constructor(files, diagnostics, rangeCache, activeFile) {
|
|
801
|
+
this.diagnostics = diagnostics;
|
|
802
|
+
this.partitionFiles = files;
|
|
803
|
+
this.rangeCache = rangeCache;
|
|
804
|
+
this.activeFile = activeFile;
|
|
805
|
+
}
|
|
806
|
+
diagnostics;
|
|
807
|
+
partitionFiles;
|
|
808
|
+
rangeCache;
|
|
809
|
+
/** The one file (if any) that could still be actively appended to — never cache-checked or skipped. */
|
|
810
|
+
activeFile;
|
|
811
|
+
static async fromDirectory(directory, options) {
|
|
812
|
+
const resolved = path5.resolve(directory);
|
|
813
|
+
const files = await findPartitions(resolved);
|
|
814
|
+
const rangeCache = options?.rangeCache ?? new ChroniclePartitionRangeCache(resolved);
|
|
815
|
+
return new _ChronicleQueryEngine(
|
|
816
|
+
files,
|
|
817
|
+
{ sourceFiles: files.length, invalidLines: 0 },
|
|
818
|
+
rangeCache,
|
|
819
|
+
files.at(-1)
|
|
820
|
+
);
|
|
821
|
+
}
|
|
822
|
+
static async fromFiles(files) {
|
|
823
|
+
return new _ChronicleQueryEngine(files, { sourceFiles: files.length, invalidLines: 0 });
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* Returns the file's observed (min, max) `occurredAt` range, computing and
|
|
827
|
+
* caching it on first use. Only ever called for closed/immutable files
|
|
828
|
+
* (never `this.activeFile`) — see the skip-check call sites.
|
|
829
|
+
*/
|
|
830
|
+
async getOrComputeRange(file, size) {
|
|
831
|
+
const cached = await this.rangeCache.get(file, size);
|
|
832
|
+
if (cached) return cached;
|
|
833
|
+
const computed = await computeOccurredAtRange(file, size);
|
|
834
|
+
const range = computed ? { size, minOccurredAt: computed.min, maxOccurredAt: computed.max } : { size, empty: true };
|
|
835
|
+
this.rangeCache.set(file, range);
|
|
836
|
+
return range;
|
|
837
|
+
}
|
|
838
|
+
/** True when a closed file's cached range provably cannot contain a match for `query`. */
|
|
839
|
+
async canSkip(file, size, query) {
|
|
840
|
+
if (!this.rangeCache || !query.from && !query.to || file === this.activeFile) return false;
|
|
841
|
+
const range = await this.getOrComputeRange(file, size);
|
|
842
|
+
return !rangeOverlaps(range, query);
|
|
843
|
+
}
|
|
844
|
+
/** Stream all partitions, filter, and return the requested page + summary. */
|
|
845
|
+
async query(query = {}) {
|
|
846
|
+
const order = query.order ?? "desc";
|
|
847
|
+
const limit = Math.max(1, Math.min(query.limit ?? 100, 1e4));
|
|
848
|
+
const queryHash = hashQuery(query);
|
|
849
|
+
const cursor = decodeCursor(query.cursor, order, queryHash);
|
|
850
|
+
const snapshotFiles = cursor ? await resolveSnapshotFiles(this.partitionFiles, cursor.snapshot) : await captureSnapshot(this.partitionFiles);
|
|
851
|
+
const files = order === "asc" ? snapshotFiles : snapshotFiles.slice().reverse();
|
|
852
|
+
const summaryAcc = createSummaryAccumulator();
|
|
853
|
+
const orderedCandidates = [];
|
|
854
|
+
const pageOrder = (left, right) => compareEvents(left, right) * (order === "asc" ? 1 : -1);
|
|
855
|
+
let totalCount = 0;
|
|
856
|
+
let remainingCount = 0;
|
|
857
|
+
let scannedEvents = 0;
|
|
858
|
+
let invalidLines = 0;
|
|
859
|
+
for (const snapshotFile of files) {
|
|
860
|
+
try {
|
|
861
|
+
if (snapshotFile.size === 0) continue;
|
|
862
|
+
if (await this.canSkip(snapshotFile.file, snapshotFile.size, query)) continue;
|
|
863
|
+
const lines = order === "asc" ? streamLines(snapshotFile.file, snapshotFile.size) : reverseLines(snapshotFile.file, snapshotFile.size);
|
|
864
|
+
for await (const line of lines) {
|
|
865
|
+
if (!line.trim()) continue;
|
|
866
|
+
let event;
|
|
867
|
+
try {
|
|
868
|
+
event = JSON.parse(line);
|
|
869
|
+
if (!isChronicleEvent(event)) {
|
|
870
|
+
invalidLines++;
|
|
871
|
+
continue;
|
|
872
|
+
}
|
|
873
|
+
} catch {
|
|
874
|
+
invalidLines++;
|
|
875
|
+
continue;
|
|
876
|
+
}
|
|
877
|
+
scannedEvents++;
|
|
878
|
+
if (!matches(event, query)) continue;
|
|
879
|
+
totalCount++;
|
|
880
|
+
updateSummary(summaryAcc, event);
|
|
881
|
+
if (cursor && compareEventToKey(event, cursor.after) * (order === "asc" ? 1 : -1) <= 0) {
|
|
882
|
+
continue;
|
|
883
|
+
}
|
|
884
|
+
remainingCount++;
|
|
885
|
+
const insertionIndex = findInsertionIndex(orderedCandidates, event, pageOrder);
|
|
886
|
+
if (insertionIndex < limit) {
|
|
887
|
+
orderedCandidates.splice(insertionIndex, 0, event);
|
|
888
|
+
if (orderedCandidates.length > limit) orderedCandidates.pop();
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
} catch {
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
const pageEvents = orderedCandidates;
|
|
895
|
+
const lastEvent = pageEvents.at(-1);
|
|
896
|
+
if (this.rangeCache) {
|
|
897
|
+
this.rangeCache.prune(this.partitionFiles);
|
|
898
|
+
await this.rangeCache.flush();
|
|
899
|
+
}
|
|
900
|
+
return {
|
|
901
|
+
events: pageEvents,
|
|
902
|
+
total: totalCount,
|
|
903
|
+
summary: finalizeSummary(summaryAcc),
|
|
904
|
+
...lastEvent && pageEvents.length < remainingCount ? { nextCursor: encodeCursor({
|
|
905
|
+
version: 1,
|
|
906
|
+
order,
|
|
907
|
+
queryHash,
|
|
908
|
+
after: orderKey(lastEvent),
|
|
909
|
+
snapshot: snapshotFiles.map(({ id, size }) => ({ id, size }))
|
|
910
|
+
}) } : {},
|
|
911
|
+
scannedEvents,
|
|
912
|
+
sourceFiles: snapshotFiles.length,
|
|
913
|
+
invalidLines
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
/** Stream all partitions once and compute value counts for every requested facet. */
|
|
917
|
+
async facets(fields, query = {}, limit = 100) {
|
|
918
|
+
const uniqueFields = [...new Set(fields)];
|
|
919
|
+
if (uniqueFields.length === 0) return {};
|
|
920
|
+
const counts = new Map(uniqueFields.map((field) => [field, /* @__PURE__ */ new Map()]));
|
|
921
|
+
const snapshotFiles = await captureSnapshot(this.partitionFiles);
|
|
922
|
+
let invalidLines = 0;
|
|
923
|
+
for (const snapshotFile of snapshotFiles) {
|
|
924
|
+
try {
|
|
925
|
+
if (snapshotFile.size === 0) continue;
|
|
926
|
+
if (await this.canSkip(snapshotFile.file, snapshotFile.size, query)) continue;
|
|
927
|
+
for await (const line of streamLines(snapshotFile.file, snapshotFile.size)) {
|
|
928
|
+
if (!line.trim()) continue;
|
|
929
|
+
let event;
|
|
930
|
+
try {
|
|
931
|
+
event = JSON.parse(line);
|
|
932
|
+
if (!isChronicleEvent(event)) {
|
|
933
|
+
invalidLines++;
|
|
934
|
+
continue;
|
|
935
|
+
}
|
|
936
|
+
} catch {
|
|
937
|
+
invalidLines++;
|
|
938
|
+
continue;
|
|
939
|
+
}
|
|
940
|
+
if (!matches(event, query)) continue;
|
|
941
|
+
for (const field of uniqueFields) {
|
|
942
|
+
const value = facetValue(event, field);
|
|
943
|
+
const fieldCounts = counts.get(field);
|
|
944
|
+
if (value !== void 0) fieldCounts.set(value, (fieldCounts.get(value) ?? 0) + 1);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
} catch {
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
this.diagnostics.invalidLines = invalidLines;
|
|
951
|
+
if (this.rangeCache) {
|
|
952
|
+
this.rangeCache.prune(this.partitionFiles);
|
|
953
|
+
await this.rangeCache.flush();
|
|
954
|
+
}
|
|
955
|
+
const result = {};
|
|
956
|
+
for (const field of uniqueFields) {
|
|
957
|
+
result[field] = [...counts.get(field)].map(([value, count]) => ({ value, count })).sort((a, b) => b.count - a.count || a.value.localeCompare(b.value)).slice(0, Math.max(0, limit));
|
|
958
|
+
}
|
|
959
|
+
return result;
|
|
960
|
+
}
|
|
961
|
+
/** Stream all partitions and compute one facet's value counts. */
|
|
962
|
+
async facet(field, query = {}, limit = 100) {
|
|
963
|
+
return (await this.facets([field], query, limit))[field] ?? [];
|
|
964
|
+
}
|
|
965
|
+
/** Expand explicit and typed correlation edges; temporal proximity alone never creates causality. */
|
|
966
|
+
async graph(seed = {}, hops = 2, maxNodes = 1e3) {
|
|
967
|
+
const nodeLimit = Math.max(0, Math.floor(maxNodes));
|
|
968
|
+
const selected = /* @__PURE__ */ new Map();
|
|
969
|
+
let seedCount = 0;
|
|
970
|
+
for await (const event of streamEvents(this.partitionFiles)) {
|
|
971
|
+
if (!matches(event, seed)) continue;
|
|
972
|
+
seedCount++;
|
|
973
|
+
if (selected.size < nodeLimit) selected.set(event.eventId, event);
|
|
974
|
+
}
|
|
975
|
+
let frontier = [...selected.values()];
|
|
976
|
+
const depthLimit = Math.max(0, Math.min(hops, 10));
|
|
977
|
+
for (let depth = 0; depth < depthLimit && frontier.length > 0 && selected.size < nodeLimit; depth++) {
|
|
978
|
+
const frontierKeys = new Set(frontier.flatMap((event) => relationKeys(event).map((relation) => relation.key)));
|
|
979
|
+
const next = [];
|
|
980
|
+
for await (const event of streamEvents(this.partitionFiles)) {
|
|
981
|
+
if (selected.has(event.eventId)) continue;
|
|
982
|
+
if (!relationKeys(event).some((relation) => frontierKeys.has(relation.key))) continue;
|
|
983
|
+
selected.set(event.eventId, event);
|
|
984
|
+
next.push(event);
|
|
985
|
+
if (selected.size >= nodeLimit) break;
|
|
986
|
+
}
|
|
987
|
+
frontier = next;
|
|
988
|
+
}
|
|
989
|
+
const nodes = [...selected.values()].sort(compareEvents);
|
|
990
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
991
|
+
for (const node of nodes) for (const relation of relationKeys(node)) {
|
|
992
|
+
const related = byKey.get(relation.key) ?? [];
|
|
993
|
+
related.push(node);
|
|
994
|
+
byKey.set(relation.key, related);
|
|
995
|
+
}
|
|
996
|
+
const edges = [];
|
|
997
|
+
const seen = /* @__PURE__ */ new Set();
|
|
998
|
+
for (const node of nodes) for (const relation of relationKeys(node)) for (const candidate of byKey.get(relation.key) ?? []) {
|
|
999
|
+
if (candidate.eventId === node.eventId) continue;
|
|
1000
|
+
const [from, to] = compareEvents(node, candidate) <= 0 ? [node, candidate] : [candidate, node];
|
|
1001
|
+
const id = `${from.eventId}:${to.eventId}:${relation.kind}`;
|
|
1002
|
+
if (!seen.has(id)) {
|
|
1003
|
+
seen.add(id);
|
|
1004
|
+
edges.push({ from: from.eventId, to: to.eventId, kind: relation.kind, confidence: relation.confidence });
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
return { nodes, edges, truncated: seedCount > nodeLimit || selected.size >= nodeLimit };
|
|
1008
|
+
}
|
|
1009
|
+
};
|
|
1010
|
+
async function* streamEvents(files) {
|
|
1011
|
+
for (const file of files) {
|
|
1012
|
+
try {
|
|
1013
|
+
for await (const line of streamLines(file)) {
|
|
1014
|
+
if (!line.trim()) continue;
|
|
1015
|
+
try {
|
|
1016
|
+
const event = JSON.parse(line);
|
|
1017
|
+
if (isChronicleEvent(event)) yield event;
|
|
1018
|
+
} catch {
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
} catch {
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
async function* reverseLines(filePath, maxBytes) {
|
|
1026
|
+
const CHUNK = 64 * 1024;
|
|
1027
|
+
const NEWLINE = 10;
|
|
1028
|
+
let handle;
|
|
1029
|
+
try {
|
|
1030
|
+
handle = await fs3.open(filePath, "r");
|
|
1031
|
+
} catch {
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
try {
|
|
1035
|
+
const fileSize = (await handle.stat()).size;
|
|
1036
|
+
const size = Math.min(fileSize, maxBytes ?? fileSize);
|
|
1037
|
+
let position = size;
|
|
1038
|
+
let suffix = Buffer.alloc(0);
|
|
1039
|
+
while (position > 0) {
|
|
1040
|
+
const length = Math.min(CHUNK, position);
|
|
1041
|
+
position -= length;
|
|
1042
|
+
const buffer = Buffer.allocUnsafe(length);
|
|
1043
|
+
await handle.read(buffer, 0, length, position);
|
|
1044
|
+
const data = suffix.length === 0 ? buffer : Buffer.concat([buffer, suffix]);
|
|
1045
|
+
let lineEnd = data.length;
|
|
1046
|
+
let firstNewline = -1;
|
|
1047
|
+
for (let index = data.length - 1; index >= 0; index--) {
|
|
1048
|
+
if (data[index] !== NEWLINE) continue;
|
|
1049
|
+
const trimmed2 = data.subarray(index + 1, lineEnd).toString("utf8").trim();
|
|
1050
|
+
if (trimmed2) yield trimmed2;
|
|
1051
|
+
lineEnd = index;
|
|
1052
|
+
firstNewline = index;
|
|
1053
|
+
}
|
|
1054
|
+
suffix = firstNewline >= 0 ? Buffer.from(data.subarray(0, firstNewline)) : data;
|
|
1055
|
+
}
|
|
1056
|
+
const trimmed = suffix.toString("utf8").trim();
|
|
1057
|
+
if (trimmed) yield trimmed;
|
|
1058
|
+
} finally {
|
|
1059
|
+
await handle.close();
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
function createSummaryAccumulator() {
|
|
1063
|
+
return {
|
|
1064
|
+
logicalRequestIds: /* @__PURE__ */ new Set(),
|
|
1065
|
+
modelAttempts: 0,
|
|
1066
|
+
completedAttempts: 0,
|
|
1067
|
+
failedAttempts: 0,
|
|
1068
|
+
scheduledRetries: 0,
|
|
1069
|
+
fallbacks: 0,
|
|
1070
|
+
providers: /* @__PURE__ */ new Set(),
|
|
1071
|
+
models: /* @__PURE__ */ new Set(),
|
|
1072
|
+
inputTokens: 0,
|
|
1073
|
+
outputTokens: 0,
|
|
1074
|
+
cacheReadTokens: 0,
|
|
1075
|
+
cacheWriteTokens: 0,
|
|
1076
|
+
costByScope: /* @__PURE__ */ new Map(),
|
|
1077
|
+
providerDurations: [],
|
|
1078
|
+
toolCalls: 0,
|
|
1079
|
+
completedTools: 0,
|
|
1080
|
+
failedTools: 0,
|
|
1081
|
+
toolDurations: [],
|
|
1082
|
+
processes: 0,
|
|
1083
|
+
failedProcesses: 0,
|
|
1084
|
+
fileEvents: 0,
|
|
1085
|
+
uniqueFiles: /* @__PURE__ */ new Set(),
|
|
1086
|
+
agentEvents: 0,
|
|
1087
|
+
uniqueAgents: /* @__PURE__ */ new Set(),
|
|
1088
|
+
decisions: 0,
|
|
1089
|
+
escalations: 0,
|
|
1090
|
+
failures: 0,
|
|
1091
|
+
cancellations: 0,
|
|
1092
|
+
families: { llm: 0, agent: 0, tool: 0, file: 0, memory: 0, task: 0, decision: 0, runtime: 0, finding: 0 },
|
|
1093
|
+
failuresByFamily: { llm: 0, agent: 0, tool: 0, file: 0, memory: 0, task: 0, decision: 0, runtime: 0, finding: 0 }
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
function updateSummary(acc, event) {
|
|
1097
|
+
const family = signalFamily(event);
|
|
1098
|
+
acc.families[family]++;
|
|
1099
|
+
if (isTerminalFailure(event)) acc.failuresByFamily[family]++;
|
|
1100
|
+
if (event.correlation.logicalRequestId) acc.logicalRequestIds.add(event.correlation.logicalRequestId);
|
|
1101
|
+
if (event.runtime?.providerId) acc.providers.add(event.runtime.providerId);
|
|
1102
|
+
if (event.runtime?.modelId) acc.models.add(event.runtime.modelId);
|
|
1103
|
+
if (event.eventType === "provider.attempt.started") acc.modelAttempts++;
|
|
1104
|
+
else if (event.eventType === "provider.attempt.completed") {
|
|
1105
|
+
acc.completedAttempts++;
|
|
1106
|
+
acc.inputTokens += numberAt(event, "usage.input");
|
|
1107
|
+
acc.outputTokens += numberAt(event, "usage.output");
|
|
1108
|
+
acc.cacheReadTokens += numberAt(event, "usage.cacheRead");
|
|
1109
|
+
acc.cacheWriteTokens += numberAt(event, "usage.cacheWrite");
|
|
1110
|
+
const dur = durationMs(event);
|
|
1111
|
+
if (dur > 0) acc.providerDurations.push(dur);
|
|
1112
|
+
} else if (event.eventType === "provider.attempt.failed") {
|
|
1113
|
+
acc.failedAttempts++;
|
|
1114
|
+
if (event.attributes?.retryScheduled === true) acc.scheduledRetries++;
|
|
1115
|
+
const dur = durationMs(event);
|
|
1116
|
+
if (dur > 0) acc.providerDurations.push(dur);
|
|
1117
|
+
} else if (event.eventType === "provider.fallback") acc.fallbacks++;
|
|
1118
|
+
else if (event.eventType === "tool.started") acc.toolCalls++;
|
|
1119
|
+
else if (event.eventType === "tool.executed") {
|
|
1120
|
+
acc.completedTools++;
|
|
1121
|
+
const dur = durationMs(event);
|
|
1122
|
+
if (dur > 0) acc.toolDurations.push(dur);
|
|
1123
|
+
} else if (event.eventType === "tool.failed") {
|
|
1124
|
+
acc.failedTools++;
|
|
1125
|
+
const dur = durationMs(event);
|
|
1126
|
+
if (dur > 0) acc.toolDurations.push(dur);
|
|
1127
|
+
} else if (event.eventType === "process.started") acc.processes++;
|
|
1128
|
+
else if (event.eventType === "process.completed" && event.outcome === "failure") acc.failedProcesses++;
|
|
1129
|
+
else if (event.eventType === "decision.requested") acc.decisions++;
|
|
1130
|
+
else if (event.eventType === "decision.escalated") acc.escalations++;
|
|
1131
|
+
if (event.eventType === "token.accounted") {
|
|
1132
|
+
const cost = readPath(event.attributes ?? {}, "cost.total");
|
|
1133
|
+
if (typeof cost === "number" && Number.isFinite(cost)) {
|
|
1134
|
+
const key = scopeKey(event);
|
|
1135
|
+
const existing = acc.costByScope.get(key);
|
|
1136
|
+
if (!existing || compareEvents(event, existing.event) > 0) {
|
|
1137
|
+
acc.costByScope.set(key, { cost, event });
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
if (event.resource?.kind === "file" || event.eventType.startsWith("file.")) {
|
|
1142
|
+
acc.fileEvents++;
|
|
1143
|
+
if (event.resource?.path) acc.uniqueFiles.add(event.resource.path);
|
|
1144
|
+
}
|
|
1145
|
+
if (family === "agent") acc.agentEvents++;
|
|
1146
|
+
if (event.scope.agentId) acc.uniqueAgents.add(event.scope.agentId);
|
|
1147
|
+
if (isTerminalFailure(event)) acc.failures++;
|
|
1148
|
+
if (event.outcome === "cancelled" || event.outcome === "abandoned") acc.cancellations++;
|
|
1149
|
+
}
|
|
1150
|
+
function finalizeSummary(acc) {
|
|
1151
|
+
const sortedProviderDurations = acc.providerDurations.slice().sort((a, b) => a - b);
|
|
1152
|
+
const sortedToolDurations = acc.toolDurations.slice().sort((a, b) => a - b);
|
|
1153
|
+
const totalCost = [...acc.costByScope.values()].reduce((sum, entry) => sum + entry.cost, 0);
|
|
1154
|
+
return {
|
|
1155
|
+
logicalRequests: acc.logicalRequestIds.size,
|
|
1156
|
+
modelAttempts: acc.modelAttempts,
|
|
1157
|
+
completedAttempts: acc.completedAttempts,
|
|
1158
|
+
failedAttempts: acc.failedAttempts,
|
|
1159
|
+
scheduledRetries: acc.scheduledRetries,
|
|
1160
|
+
fallbacks: acc.fallbacks,
|
|
1161
|
+
providers: acc.providers.size,
|
|
1162
|
+
models: acc.models.size,
|
|
1163
|
+
inputTokens: acc.inputTokens,
|
|
1164
|
+
outputTokens: acc.outputTokens,
|
|
1165
|
+
cacheReadTokens: acc.cacheReadTokens,
|
|
1166
|
+
cacheWriteTokens: acc.cacheWriteTokens,
|
|
1167
|
+
estimatedCostUsd: totalCost,
|
|
1168
|
+
providerAvgDurationMs: average(sortedProviderDurations),
|
|
1169
|
+
providerP95DurationMs: percentile(sortedProviderDurations, 0.95),
|
|
1170
|
+
toolCalls: acc.toolCalls,
|
|
1171
|
+
completedTools: acc.completedTools,
|
|
1172
|
+
failedTools: acc.failedTools,
|
|
1173
|
+
toolAvgDurationMs: average(sortedToolDurations),
|
|
1174
|
+
processes: acc.processes,
|
|
1175
|
+
failedProcesses: acc.failedProcesses,
|
|
1176
|
+
fileEvents: acc.fileEvents,
|
|
1177
|
+
uniqueFiles: acc.uniqueFiles.size,
|
|
1178
|
+
agentEvents: acc.agentEvents,
|
|
1179
|
+
uniqueAgents: acc.uniqueAgents.size,
|
|
1180
|
+
decisions: acc.decisions,
|
|
1181
|
+
escalations: acc.escalations,
|
|
1182
|
+
failures: acc.failures,
|
|
1183
|
+
cancellations: acc.cancellations,
|
|
1184
|
+
families: acc.families,
|
|
1185
|
+
failuresByFamily: acc.failuresByFamily
|
|
1186
|
+
};
|
|
1187
|
+
}
|
|
1188
|
+
Object.defineProperty(ChronicleQueryEngine.prototype, "partitionFiles", {
|
|
1189
|
+
get() {
|
|
1190
|
+
throw new Error("ChronicleQueryEngine no longer loads events on construction. Use async query().");
|
|
1191
|
+
},
|
|
1192
|
+
set(_val) {
|
|
1193
|
+
Object.defineProperty(this, "partitionFiles", { value: _val, writable: false, configurable: false });
|
|
1194
|
+
}
|
|
1195
|
+
});
|
|
1196
|
+
async function findChroniclePartitions(root) {
|
|
1197
|
+
return findPartitions(root);
|
|
1198
|
+
}
|
|
1199
|
+
async function findPartitions(root) {
|
|
1200
|
+
const result = [];
|
|
1201
|
+
const visit = async (directory) => {
|
|
1202
|
+
let entries;
|
|
1203
|
+
try {
|
|
1204
|
+
entries = await fs3.readdir(directory, { withFileTypes: true });
|
|
1205
|
+
} catch {
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1208
|
+
for (const entry of entries) {
|
|
1209
|
+
const full = path5.join(directory, entry.name);
|
|
1210
|
+
if (entry.isDirectory()) await visit(full);
|
|
1211
|
+
else if (entry.isFile() && PARTITION_FILE_PATTERN.test(entry.name)) result.push(full);
|
|
1212
|
+
}
|
|
1213
|
+
};
|
|
1214
|
+
await visit(root);
|
|
1215
|
+
return result.sort(comparePartitionPaths);
|
|
1216
|
+
}
|
|
1217
|
+
function numberAt(event, dotPath) {
|
|
1218
|
+
const value = readPath(event.attributes ?? {}, dotPath);
|
|
1219
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
1220
|
+
}
|
|
1221
|
+
function durationMs(event) {
|
|
1222
|
+
const value = Number(event.durationNs ?? 0) / 1e6;
|
|
1223
|
+
return Number.isFinite(value) ? value : 0;
|
|
1224
|
+
}
|
|
1225
|
+
function average(values) {
|
|
1226
|
+
return values.length ? values.reduce((sum, v) => sum + v, 0) / values.length : 0;
|
|
1227
|
+
}
|
|
1228
|
+
function percentile(sorted, quantile) {
|
|
1229
|
+
return sorted.length ? sorted[Math.min(sorted.length - 1, Math.max(0, Math.ceil(sorted.length * quantile) - 1))] : 0;
|
|
1230
|
+
}
|
|
1231
|
+
function scopeKey(event) {
|
|
1232
|
+
return `${event.scope.projectId ?? ""}\0${event.scope.sessionId ?? ""}\0${event.scope.agentId ?? ""}`;
|
|
1233
|
+
}
|
|
1234
|
+
function signalFamily(event) {
|
|
1235
|
+
if (/^(?:decision|brain|permission)\./.test(event.eventType)) return "decision";
|
|
1236
|
+
if (event.resource?.kind === "file" || event.resource?.kind === "symbol" || /^(?:file|worktree)\./.test(event.eventType)) return "file";
|
|
1237
|
+
if (/^(?:provider|token|context|ctx|compaction)\./.test(event.eventType)) return "llm";
|
|
1238
|
+
if (/^(?:agent|subagent|delegate|fleet|concurrency)\./.test(event.eventType)) return "agent";
|
|
1239
|
+
if (/^(?:tool|process|mcp|network)\./.test(event.eventType)) return "tool";
|
|
1240
|
+
if (/^(?:memory|storage|trust)\./.test(event.eventType)) return "memory";
|
|
1241
|
+
if (/^(?:sdd|task|kanban|checkpoint|session|iteration|in_flight)\./.test(event.eventType)) return "task";
|
|
1242
|
+
if (/^(?:finding|review)\./.test(event.eventType)) return "finding";
|
|
1243
|
+
return "runtime";
|
|
1244
|
+
}
|
|
1245
|
+
function isTerminalFailure(event) {
|
|
1246
|
+
if (event.eventType === "provider.attempt.failed") return event.attributes?.retryScheduled !== true;
|
|
1247
|
+
return event.eventType === "tool.failed" || event.eventType === "process.completed" && event.outcome === "failure" || /^(?:agent\.run\.error|sdd\.task\.failed|compaction\.failed|network\.request\.failed)$/.test(event.eventType);
|
|
1248
|
+
}
|
|
1249
|
+
function relationKeys(event) {
|
|
1250
|
+
const result = [];
|
|
1251
|
+
const add = (kind, value, confidence) => {
|
|
1252
|
+
if (typeof value === "string" && value) result.push({ key: `${kind}:${value}`, kind, confidence });
|
|
1253
|
+
};
|
|
1254
|
+
add("trace", event.correlation.traceId, "correlated");
|
|
1255
|
+
add("tool_call", event.correlation.toolCallId, "explicit");
|
|
1256
|
+
add("logical_request", event.correlation.logicalRequestId, "explicit");
|
|
1257
|
+
add("attempt", event.correlation.attemptId, "explicit");
|
|
1258
|
+
add("decision", event.attributes?.decisionId, "explicit");
|
|
1259
|
+
add("network_request", event.attributes?.requestId, "explicit");
|
|
1260
|
+
add(
|
|
1261
|
+
"prompt_manifest",
|
|
1262
|
+
event.correlation.promptManifestId ?? event.attributes?.promptManifest?.manifestId,
|
|
1263
|
+
"explicit"
|
|
1264
|
+
);
|
|
1265
|
+
add("resource_lineage", event.resource?.id, "inferred");
|
|
1266
|
+
if (event.eventType === "metrics.rollup" && event.attributes?.signal === "tool.resource.observed") {
|
|
1267
|
+
const resources = event.attributes.resources;
|
|
1268
|
+
for (const resource of resources ?? []) {
|
|
1269
|
+
if (typeof resource?.id === "string") add("resource_lineage", resource.id, "inferred");
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
if (event.correlation.parentSpanId) add("parent_span", event.correlation.parentSpanId, "explicit");
|
|
1273
|
+
add("parent_span", event.correlation.spanId, "explicit");
|
|
1274
|
+
return result;
|
|
1275
|
+
}
|
|
1276
|
+
function matches(event, query) {
|
|
1277
|
+
if (query.eventId && event.eventId !== query.eventId) return false;
|
|
1278
|
+
if (query.eventTypes && !query.eventTypes.includes(event.eventType)) return false;
|
|
1279
|
+
if (query.outcomes && (!event.outcome || !query.outcomes.includes(event.outcome))) return false;
|
|
1280
|
+
const occurredAt = event.occurredAt ?? event.observedAt;
|
|
1281
|
+
if (query.from && occurredAt < query.from || query.to && occurredAt > query.to) return false;
|
|
1282
|
+
if (!equal(query.projectId, event.scope.projectId) || !equal(query.sessionId, event.scope.sessionId)) return false;
|
|
1283
|
+
if (!equal(query.agentId, event.scope.agentId) || !equal(query.taskId, event.scope.taskId)) return false;
|
|
1284
|
+
if (!equal(query.providerId, event.runtime?.providerId) || !equal(query.modelId, event.runtime?.modelId)) return false;
|
|
1285
|
+
if (!equal(query.traceId, event.correlation.traceId) || !equal(query.logicalRequestId, event.correlation.logicalRequestId)) return false;
|
|
1286
|
+
if (!equal(query.promptManifestId, event.correlation.promptManifestId)) return false;
|
|
1287
|
+
if (!equal(query.attemptId, event.correlation.attemptId) || !equal(query.toolCallId, event.correlation.toolCallId)) return false;
|
|
1288
|
+
if (!equal(query.resourceKind, event.resource?.kind) || !equal(query.resourceId, event.resource?.id)) return false;
|
|
1289
|
+
if (query.path && normalize(event.resource?.path) !== normalize(query.path)) return false;
|
|
1290
|
+
if (query.line !== void 0 && !lineContains(event, query.line)) return false;
|
|
1291
|
+
if (query.tags && !objectContains(event.tags, query.tags)) return false;
|
|
1292
|
+
if (query.attributes && !objectContains(event.attributes, query.attributes)) return false;
|
|
1293
|
+
if (query.text && !JSON.stringify(event).toLocaleLowerCase().includes(query.text.toLocaleLowerCase())) return false;
|
|
1294
|
+
return true;
|
|
1295
|
+
}
|
|
1296
|
+
function equal(expected, actual) {
|
|
1297
|
+
return expected === void 0 || expected === actual;
|
|
1298
|
+
}
|
|
1299
|
+
function normalize(value) {
|
|
1300
|
+
return value?.replaceAll("\\", "/").toLocaleLowerCase();
|
|
1301
|
+
}
|
|
1302
|
+
function lineContains(event, line) {
|
|
1303
|
+
const start = event.resource?.lineStart;
|
|
1304
|
+
const end = event.resource?.lineEnd ?? start;
|
|
1305
|
+
return start !== void 0 && end !== void 0 && line >= start && line <= end;
|
|
1306
|
+
}
|
|
1307
|
+
function objectContains(actual, expected) {
|
|
1308
|
+
return Boolean(actual && Object.entries(expected).every(([key, value]) => deepEqual(readPath(actual, key), value)));
|
|
1309
|
+
}
|
|
1310
|
+
function readPath(value, key) {
|
|
1311
|
+
return key.split(".").reduce((current, part) => current && typeof current === "object" ? current[part] : void 0, value);
|
|
1312
|
+
}
|
|
1313
|
+
function deepEqual(left, right) {
|
|
1314
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
1315
|
+
}
|
|
1316
|
+
function findInsertionIndex(events, event, compare) {
|
|
1317
|
+
let low = 0;
|
|
1318
|
+
let high = events.length;
|
|
1319
|
+
while (low < high) {
|
|
1320
|
+
const middle = low + high >>> 1;
|
|
1321
|
+
if (compare(events[middle], event) <= 0) low = middle + 1;
|
|
1322
|
+
else high = middle;
|
|
1323
|
+
}
|
|
1324
|
+
return low;
|
|
1325
|
+
}
|
|
1326
|
+
function compareEvents(a, b) {
|
|
1327
|
+
return compareEventToKey(a, orderKey(b));
|
|
1328
|
+
}
|
|
1329
|
+
function compareEventToKey(event, key) {
|
|
1330
|
+
return (event.occurredAt ?? event.observedAt).localeCompare(key.occurredAt) || event.persistedAt.localeCompare(key.persistedAt) || event.sequence - key.sequence || event.eventId.localeCompare(key.eventId);
|
|
1331
|
+
}
|
|
1332
|
+
function orderKey(event) {
|
|
1333
|
+
return {
|
|
1334
|
+
occurredAt: event.occurredAt ?? event.observedAt,
|
|
1335
|
+
persistedAt: event.persistedAt,
|
|
1336
|
+
sequence: event.sequence,
|
|
1337
|
+
eventId: event.eventId
|
|
1338
|
+
};
|
|
1339
|
+
}
|
|
1340
|
+
function hashQuery(query) {
|
|
1341
|
+
const { cursor: _cursor, limit: _limit, order: _order, ...filters } = query;
|
|
1342
|
+
return createHash3("sha256").update(stableStringify2(filters), "utf8").digest("base64url");
|
|
1343
|
+
}
|
|
1344
|
+
function encodeCursor(cursor) {
|
|
1345
|
+
return Buffer.from(JSON.stringify(cursor), "utf8").toString("base64url");
|
|
1346
|
+
}
|
|
1347
|
+
function decodeCursor(encoded, order, queryHash) {
|
|
1348
|
+
if (!encoded) return void 0;
|
|
1349
|
+
if (encoded.length > 1e6) throw new Error("Invalid Chronicle cursor");
|
|
1350
|
+
try {
|
|
1351
|
+
const parsed = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
|
|
1352
|
+
if (!isCursor(parsed) || parsed.order !== order || parsed.queryHash !== queryHash) {
|
|
1353
|
+
throw new Error("cursor does not match the query");
|
|
1354
|
+
}
|
|
1355
|
+
return parsed;
|
|
1356
|
+
} catch (error) {
|
|
1357
|
+
throw new Error(`Invalid Chronicle cursor: ${error instanceof Error ? error.message : String(error)}`);
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
function isCursor(value) {
|
|
1361
|
+
if (!value || typeof value !== "object") return false;
|
|
1362
|
+
const cursor = value;
|
|
1363
|
+
const after = cursor.after;
|
|
1364
|
+
if (cursor.version !== 1 || cursor.order !== "asc" && cursor.order !== "desc" || typeof cursor.queryHash !== "string" || !after || typeof after.occurredAt !== "string" || typeof after.persistedAt !== "string" || !Number.isSafeInteger(after.sequence) || typeof after.eventId !== "string" || !Array.isArray(cursor.snapshot) || cursor.snapshot.length > MAX_CURSOR_SNAPSHOT_ENTRIES) return false;
|
|
1365
|
+
const snapshotIds = /* @__PURE__ */ new Set();
|
|
1366
|
+
for (const entry of cursor.snapshot) {
|
|
1367
|
+
if (!entry || typeof entry.id !== "string" || !entry.id || !Number.isSafeInteger(entry.size) || entry.size < 0 || snapshotIds.has(entry.id)) return false;
|
|
1368
|
+
snapshotIds.add(entry.id);
|
|
1369
|
+
}
|
|
1370
|
+
return true;
|
|
1371
|
+
}
|
|
1372
|
+
async function captureSnapshot(files) {
|
|
1373
|
+
if (files.length > MAX_CURSOR_SNAPSHOT_ENTRIES) {
|
|
1374
|
+
throw new Error("Chronicle snapshot contains too many partitions");
|
|
1375
|
+
}
|
|
1376
|
+
return Promise.all(files.map(async (file) => {
|
|
1377
|
+
let size = 0;
|
|
1378
|
+
try {
|
|
1379
|
+
size = (await fs3.stat(file)).size;
|
|
1380
|
+
} catch {
|
|
1381
|
+
}
|
|
1382
|
+
return { file, id: fileId(file), size };
|
|
1383
|
+
}));
|
|
1384
|
+
}
|
|
1385
|
+
async function resolveSnapshotFiles(files, snapshot) {
|
|
1386
|
+
const currentFiles = new Map(files.map((file) => [fileId(file), file]));
|
|
1387
|
+
return Promise.all(snapshot.map(async (entry) => {
|
|
1388
|
+
const file = currentFiles.get(entry.id);
|
|
1389
|
+
if (!file) throw new Error("Chronicle cursor snapshot has expired");
|
|
1390
|
+
let currentSize;
|
|
1391
|
+
try {
|
|
1392
|
+
currentSize = (await fs3.stat(file)).size;
|
|
1393
|
+
} catch {
|
|
1394
|
+
throw new Error("Chronicle cursor snapshot has expired");
|
|
1395
|
+
}
|
|
1396
|
+
if (currentSize < entry.size) throw new Error("Chronicle cursor snapshot has expired");
|
|
1397
|
+
return { file, ...entry };
|
|
1398
|
+
}));
|
|
1399
|
+
}
|
|
1400
|
+
function fileId(file) {
|
|
1401
|
+
return createHash3("sha256").update(path5.resolve(file), "utf8").digest("base64url");
|
|
1402
|
+
}
|
|
1403
|
+
function stableStringify2(value) {
|
|
1404
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
1405
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify2).join(",")}]`;
|
|
1406
|
+
const object = value;
|
|
1407
|
+
return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${stableStringify2(object[key])}`).join(",")}}`;
|
|
1408
|
+
}
|
|
1409
|
+
function isChronicleEvent(value) {
|
|
1410
|
+
if (!isRecord(value) || !isRecord(value.scope) || !isRecord(value.correlation)) return false;
|
|
1411
|
+
return value.schemaVersion === 1 && typeof value.eventId === "string" && typeof value.eventType === "string" && typeof value.occurredAt === "string" && typeof value.observedAt === "string" && typeof value.persistedAt === "string" && Number.isSafeInteger(value.sequence) && value.sequence >= 0 && typeof value.previousHash === "string" && typeof value.hash === "string" && typeof value.scope.installationId === "string" && typeof value.scope.machineId === "string" && optionalStrings(value.scope, [
|
|
1412
|
+
"projectId",
|
|
1413
|
+
"repositoryId",
|
|
1414
|
+
"workspaceId",
|
|
1415
|
+
"worktreeId",
|
|
1416
|
+
"sessionId",
|
|
1417
|
+
"turnId",
|
|
1418
|
+
"iterationId",
|
|
1419
|
+
"agentId",
|
|
1420
|
+
"goalId",
|
|
1421
|
+
"planId",
|
|
1422
|
+
"taskId",
|
|
1423
|
+
"kanbanBoardId"
|
|
1424
|
+
]) && typeof value.correlation.traceId === "string" && typeof value.correlation.spanId === "string" && optionalStrings(value.correlation, [
|
|
1425
|
+
"parentSpanId",
|
|
1426
|
+
"logicalRequestId",
|
|
1427
|
+
"promptManifestId",
|
|
1428
|
+
"attemptId",
|
|
1429
|
+
"toolCallId"
|
|
1430
|
+
]) && isRuntime(value.runtime) && isResource(value.resource) && (value.attributes === void 0 || isRecord(value.attributes)) && (value.tags === void 0 || isStringRecord(value.tags));
|
|
1431
|
+
}
|
|
1432
|
+
function isRuntime(value) {
|
|
1433
|
+
return value === void 0 || isRecord(value) && optionalStrings(value, ["providerId", "modelId", "modelRevision"]) && optionalFiniteNumbers(value, ["processId", "parentProcessId"]);
|
|
1434
|
+
}
|
|
1435
|
+
function isResource(value) {
|
|
1436
|
+
if (value === void 0) return true;
|
|
1437
|
+
if (!isRecord(value) || ![
|
|
1438
|
+
"file",
|
|
1439
|
+
"symbol",
|
|
1440
|
+
"memory",
|
|
1441
|
+
"task",
|
|
1442
|
+
"kanban",
|
|
1443
|
+
"process",
|
|
1444
|
+
"network",
|
|
1445
|
+
"artifact",
|
|
1446
|
+
"other"
|
|
1447
|
+
].includes(String(value.kind)) || typeof value.id !== "string") return false;
|
|
1448
|
+
return optionalStrings(value, ["path", "contentHashBefore", "contentHashAfter"]) && optionalFiniteNumbers(value, ["lineStart", "lineEnd"]);
|
|
1449
|
+
}
|
|
1450
|
+
function optionalStrings(value, keys) {
|
|
1451
|
+
return keys.every((key) => value[key] === void 0 || typeof value[key] === "string");
|
|
1452
|
+
}
|
|
1453
|
+
function optionalFiniteNumbers(value, keys) {
|
|
1454
|
+
return keys.every((key) => value[key] === void 0 || typeof value[key] === "number" && Number.isFinite(value[key]));
|
|
1455
|
+
}
|
|
1456
|
+
function isStringRecord(value) {
|
|
1457
|
+
return isRecord(value) && Object.values(value).every((entry) => typeof entry === "string");
|
|
1458
|
+
}
|
|
1459
|
+
function isRecord(value) {
|
|
1460
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
1461
|
+
}
|
|
1462
|
+
function facetValue(event, field) {
|
|
1463
|
+
const values = {
|
|
1464
|
+
eventType: event.eventType,
|
|
1465
|
+
outcome: event.outcome,
|
|
1466
|
+
projectId: event.scope.projectId,
|
|
1467
|
+
sessionId: event.scope.sessionId,
|
|
1468
|
+
agentId: event.scope.agentId,
|
|
1469
|
+
taskId: event.scope.taskId,
|
|
1470
|
+
providerId: event.runtime?.providerId,
|
|
1471
|
+
modelId: event.runtime?.modelId,
|
|
1472
|
+
resourceKind: event.resource?.kind,
|
|
1473
|
+
resourcePath: event.resource?.path,
|
|
1474
|
+
toolCallId: event.correlation.toolCallId
|
|
1475
|
+
};
|
|
1476
|
+
return values[field];
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
// src/chronicle/sqlite-journal-quota.ts
|
|
1480
|
+
import * as fs4 from "node:fs";
|
|
1481
|
+
var SQLITE_FIXED_OVERHEAD_BYTES = 16 * 1024 * 1024;
|
|
1482
|
+
var MIN_SQLITE_PAGE_BUDGET_BYTES = 64 * 1024;
|
|
1483
|
+
var MAX_WAL_RESERVE_BYTES = 32 * 1024 * 1024;
|
|
1484
|
+
var WAL_AUTOCHECKPOINT_PAGES = 2e3;
|
|
1485
|
+
var WAL_SIZE_LIMIT_BYTES = 16 * 1024 * 1024;
|
|
1486
|
+
var ChronicleStorageQuotaError = class extends Error {
|
|
1487
|
+
currentBytes;
|
|
1488
|
+
batchBytes;
|
|
1489
|
+
maxBytes;
|
|
1490
|
+
path;
|
|
1491
|
+
constructor(details) {
|
|
1492
|
+
super(
|
|
1493
|
+
`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)`
|
|
1494
|
+
);
|
|
1495
|
+
this.name = "ChronicleStorageQuotaError";
|
|
1496
|
+
this.currentBytes = details.currentBytes;
|
|
1497
|
+
this.batchBytes = details.batchBytes;
|
|
1498
|
+
this.maxBytes = details.maxBytes;
|
|
1499
|
+
this.path = details.path;
|
|
1500
|
+
}
|
|
1501
|
+
};
|
|
1502
|
+
var ChronicleQuotaManager = class {
|
|
1503
|
+
constructor(db, dbPath, maxBytes) {
|
|
1504
|
+
this.db = db;
|
|
1505
|
+
this.dbPath = dbPath;
|
|
1506
|
+
this.maxBytes = maxBytes;
|
|
1507
|
+
}
|
|
1508
|
+
db;
|
|
1509
|
+
dbPath;
|
|
1510
|
+
maxBytes;
|
|
1511
|
+
cachedPageSize;
|
|
1512
|
+
quotaHeadroomBytes = 0;
|
|
1513
|
+
bytesSinceQuotaCheck = Number.POSITIVE_INFINITY;
|
|
1514
|
+
configure() {
|
|
1515
|
+
if (this.maxBytes === void 0) {
|
|
1516
|
+
this.db.exec("PRAGMA max_page_count = 2147483646");
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1519
|
+
const halfSplit = Math.floor((this.maxBytes - SQLITE_FIXED_OVERHEAD_BYTES) / 2);
|
|
1520
|
+
const sidecarReserve = Math.min(MAX_WAL_RESERVE_BYTES, halfSplit);
|
|
1521
|
+
const mainBudget = this.maxBytes - SQLITE_FIXED_OVERHEAD_BYTES - sidecarReserve;
|
|
1522
|
+
if (mainBudget < MIN_SQLITE_PAGE_BUDGET_BYTES) {
|
|
1523
|
+
throw new Error(
|
|
1524
|
+
`maxBytes must be at least ${SQLITE_FIXED_OVERHEAD_BYTES + 2 * MIN_SQLITE_PAGE_BUDGET_BYTES}`
|
|
1525
|
+
);
|
|
1526
|
+
}
|
|
1527
|
+
const maxPages = Math.max(1, Math.floor(mainBudget / this.pageSizeBytes()));
|
|
1528
|
+
this.db.exec(`PRAGMA max_page_count = ${maxPages}`);
|
|
1529
|
+
}
|
|
1530
|
+
pageSizeBytes() {
|
|
1531
|
+
if (this.cachedPageSize === void 0) {
|
|
1532
|
+
this.cachedPageSize = Number(
|
|
1533
|
+
this.db.prepare("PRAGMA page_size").get().page_size
|
|
1534
|
+
);
|
|
1535
|
+
}
|
|
1536
|
+
return this.cachedPageSize;
|
|
1537
|
+
}
|
|
1538
|
+
aggregateLiveBytes() {
|
|
1539
|
+
let total = 0;
|
|
1540
|
+
for (const file of [`${this.dbPath}-journal`, `${this.dbPath}-wal`, `${this.dbPath}-shm`]) {
|
|
1541
|
+
try {
|
|
1542
|
+
total += fs4.statSync(file).size;
|
|
1543
|
+
} catch (error) {
|
|
1544
|
+
if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) throw error;
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
return total + this.mainDatabaseLiveBytes();
|
|
1548
|
+
}
|
|
1549
|
+
mainDatabaseLiveBytes() {
|
|
1550
|
+
const pageCount = Number(
|
|
1551
|
+
this.db.prepare("PRAGMA page_count").get().page_count
|
|
1552
|
+
);
|
|
1553
|
+
const freelist = Number(
|
|
1554
|
+
this.db.prepare("PRAGMA freelist_count").get().freelist_count
|
|
1555
|
+
);
|
|
1556
|
+
return Math.max(0, pageCount - freelist) * this.pageSizeBytes();
|
|
1557
|
+
}
|
|
1558
|
+
invalidateEstimate() {
|
|
1559
|
+
this.quotaHeadroomBytes = 0;
|
|
1560
|
+
this.bytesSinceQuotaCheck = Number.POSITIVE_INFINITY;
|
|
1561
|
+
}
|
|
1562
|
+
recordAppendedBytes(bytes) {
|
|
1563
|
+
this.bytesSinceQuotaCheck += bytes;
|
|
1564
|
+
}
|
|
1565
|
+
assertWithinByteQuota(batchBytes) {
|
|
1566
|
+
if (this.maxBytes === void 0) return;
|
|
1567
|
+
if (!this.shouldMeasureQuota(batchBytes)) return;
|
|
1568
|
+
const currentBytes = this.aggregateLiveBytes();
|
|
1569
|
+
this.recordQuotaHeadroom(currentBytes);
|
|
1570
|
+
if (currentBytes + batchBytes <= this.maxBytes) return;
|
|
1571
|
+
throw new ChronicleStorageQuotaError({
|
|
1572
|
+
currentBytes,
|
|
1573
|
+
batchBytes,
|
|
1574
|
+
maxBytes: this.maxBytes,
|
|
1575
|
+
path: this.dbPath
|
|
1576
|
+
});
|
|
1577
|
+
}
|
|
1578
|
+
assertActualAllocationWithinQuota() {
|
|
1579
|
+
if (this.maxBytes === void 0) return;
|
|
1580
|
+
if (!this.shouldMeasureQuota()) return;
|
|
1581
|
+
const currentBytes = this.aggregateLiveBytes();
|
|
1582
|
+
this.recordQuotaHeadroom(currentBytes);
|
|
1583
|
+
if (currentBytes <= this.maxBytes) return;
|
|
1584
|
+
throw new ChronicleStorageQuotaError({
|
|
1585
|
+
currentBytes,
|
|
1586
|
+
batchBytes: 0,
|
|
1587
|
+
maxBytes: this.maxBytes,
|
|
1588
|
+
path: this.dbPath
|
|
1589
|
+
});
|
|
1590
|
+
}
|
|
1591
|
+
normalizeQuotaError(error) {
|
|
1592
|
+
if (this.maxBytes === void 0 || !(error instanceof Error)) return error;
|
|
1593
|
+
const sqliteError = error;
|
|
1594
|
+
const quotaExhausted = sqliteError.code === "SQLITE_FULL" || sqliteError.code === 13 || sqliteError.errcode === 13 || /database or disk is full/i.test(sqliteError.message);
|
|
1595
|
+
if (!quotaExhausted) return error;
|
|
1596
|
+
return new ChronicleStorageQuotaError({
|
|
1597
|
+
currentBytes: this.aggregateLiveBytes(),
|
|
1598
|
+
batchBytes: 0,
|
|
1599
|
+
maxBytes: this.maxBytes,
|
|
1600
|
+
path: this.dbPath
|
|
1601
|
+
});
|
|
1602
|
+
}
|
|
1603
|
+
shouldMeasureQuota(batchBytes = 0) {
|
|
1604
|
+
return this.bytesSinceQuotaCheck + batchBytes >= this.quotaHeadroomBytes / 2;
|
|
1605
|
+
}
|
|
1606
|
+
recordQuotaHeadroom(currentBytes) {
|
|
1607
|
+
this.quotaHeadroomBytes = Math.max(0, (this.maxBytes ?? 0) - currentBytes);
|
|
1608
|
+
this.bytesSinceQuotaCheck = 0;
|
|
1609
|
+
}
|
|
1610
|
+
};
|
|
1611
|
+
|
|
1612
|
+
// src/chronicle/sqlite-journal.ts
|
|
1613
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
1614
|
+
import * as path6 from "node:path";
|
|
1615
|
+
|
|
1616
|
+
// src/chronicle/sqlite-journal-schema.ts
|
|
1617
|
+
import { loadRuntimeDatabaseSync } from "@wrongstack/persistence";
|
|
1618
|
+
var CHRONICLE_SQLITE_FILE = "chronicle.sqlite";
|
|
1619
|
+
var LEGACY_JSONL_MIGRATION_KEY = "legacy-jsonl-v1";
|
|
1620
|
+
var LEGACY_JSONL_QUARANTINE_KEY = "legacy-jsonl-v1:quarantine";
|
|
1621
|
+
var LEGACY_JSONL_BOUNDARY_KEY = "legacy-jsonl-v1:boundary";
|
|
1622
|
+
var SCHEMA_VERSION = 3;
|
|
1623
|
+
var Ctor;
|
|
1624
|
+
function loadDatabaseSync() {
|
|
1625
|
+
if (Ctor) return Ctor;
|
|
1626
|
+
if (Ctor === null) throw new Error("SQLite is unavailable in this runtime");
|
|
1627
|
+
try {
|
|
1628
|
+
Ctor = withSqliteExperimentalWarningSuppressed(loadRuntimeDatabaseSync);
|
|
1629
|
+
return Ctor;
|
|
1630
|
+
} catch (error) {
|
|
1631
|
+
Ctor = null;
|
|
1632
|
+
throw new Error(
|
|
1633
|
+
"The Chronicle journal needs node:sqlite (Node >= 22.5) or bun:sqlite: " + (error instanceof Error ? error.message : String(error))
|
|
1634
|
+
);
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
function projectEvent(event) {
|
|
1638
|
+
const occurredAt = event.occurredAt ?? event.observedAt;
|
|
1639
|
+
return {
|
|
1640
|
+
occurredAt,
|
|
1641
|
+
outcome: event.outcome ?? null,
|
|
1642
|
+
projectId: event.scope.projectId ?? null,
|
|
1643
|
+
sessionId: event.scope.sessionId ?? null,
|
|
1644
|
+
agentId: event.scope.agentId ?? null,
|
|
1645
|
+
taskId: event.scope.taskId ?? null,
|
|
1646
|
+
traceId: event.correlation?.traceId ?? null,
|
|
1647
|
+
logicalRequestId: event.correlation?.logicalRequestId ?? null,
|
|
1648
|
+
promptManifestId: event.correlation?.promptManifestId ?? null,
|
|
1649
|
+
resourceKind: event.resource?.kind ?? null,
|
|
1650
|
+
resourceId: event.resource?.id ?? null,
|
|
1651
|
+
resourcePath: event.resource?.path ?? null,
|
|
1652
|
+
durationNs: event.durationNs ?? null
|
|
1653
|
+
};
|
|
1654
|
+
}
|
|
1655
|
+
function ensureChronicleSchema(db) {
|
|
1656
|
+
const version = db.prepare("PRAGMA user_version").get().user_version;
|
|
1657
|
+
db.exec(`
|
|
1658
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
1659
|
+
day TEXT NOT NULL,
|
|
1660
|
+
sequence INTEGER NOT NULL,
|
|
1661
|
+
event_id TEXT NOT NULL UNIQUE,
|
|
1662
|
+
hash TEXT NOT NULL,
|
|
1663
|
+
previous_hash TEXT NOT NULL,
|
|
1664
|
+
occurred_at TEXT NOT NULL,
|
|
1665
|
+
event_type TEXT NOT NULL,
|
|
1666
|
+
outcome TEXT,
|
|
1667
|
+
project_id TEXT,
|
|
1668
|
+
session_id TEXT,
|
|
1669
|
+
agent_id TEXT,
|
|
1670
|
+
task_id TEXT,
|
|
1671
|
+
trace_id TEXT,
|
|
1672
|
+
logical_request_id TEXT,
|
|
1673
|
+
prompt_manifest_id TEXT,
|
|
1674
|
+
resource_kind TEXT,
|
|
1675
|
+
resource_id TEXT,
|
|
1676
|
+
resource_path TEXT,
|
|
1677
|
+
duration_ns TEXT,
|
|
1678
|
+
payload TEXT NOT NULL,
|
|
1679
|
+
PRIMARY KEY (day, sequence)
|
|
1680
|
+
);
|
|
1681
|
+
CREATE INDEX IF NOT EXISTS events_occurred_at ON events(occurred_at);
|
|
1682
|
+
CREATE INDEX IF NOT EXISTS events_type_outcome ON events(event_type, outcome);
|
|
1683
|
+
CREATE INDEX IF NOT EXISTS events_session ON events(session_id, day, sequence);
|
|
1684
|
+
CREATE INDEX IF NOT EXISTS events_trace ON events(trace_id);
|
|
1685
|
+
CREATE INDEX IF NOT EXISTS events_logical_request ON events(logical_request_id);
|
|
1686
|
+
|
|
1687
|
+
CREATE TABLE IF NOT EXISTS chain_checkpoint (
|
|
1688
|
+
day TEXT PRIMARY KEY,
|
|
1689
|
+
sequence INTEGER NOT NULL,
|
|
1690
|
+
hash TEXT NOT NULL
|
|
1691
|
+
);
|
|
1692
|
+
|
|
1693
|
+
CREATE TABLE IF NOT EXISTS chronicle_meta (
|
|
1694
|
+
key TEXT PRIMARY KEY,
|
|
1695
|
+
value TEXT NOT NULL
|
|
1696
|
+
);
|
|
1697
|
+
`);
|
|
1698
|
+
if (version < 2) {
|
|
1699
|
+
db.exec("DROP INDEX IF EXISTS events_resource_path");
|
|
1700
|
+
}
|
|
1701
|
+
if (version > 0 && version < 3) {
|
|
1702
|
+
db.exec("ALTER TABLE events ADD COLUMN prompt_manifest_id TEXT");
|
|
1703
|
+
}
|
|
1704
|
+
db.exec("CREATE INDEX IF NOT EXISTS events_prompt_manifest ON events(prompt_manifest_id)");
|
|
1705
|
+
if (version !== SCHEMA_VERSION) {
|
|
1706
|
+
db.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`);
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
// src/chronicle/sqlite-query.ts
|
|
1711
|
+
var MAX_LIMIT = 1e4;
|
|
1712
|
+
function encodeCursor2(cursor) {
|
|
1713
|
+
return Buffer.from(`${cursor.day}:${cursor.sequence}`, "utf8").toString("base64url");
|
|
1714
|
+
}
|
|
1715
|
+
function decodeCursor2(raw) {
|
|
1716
|
+
if (!raw) return void 0;
|
|
1717
|
+
try {
|
|
1718
|
+
const [day, sequence] = Buffer.from(raw, "base64url").toString("utf8").split(":");
|
|
1719
|
+
if (!day || sequence === void 0) return void 0;
|
|
1720
|
+
const parsed = Number(sequence);
|
|
1721
|
+
return Number.isSafeInteger(parsed) ? { day, sequence: parsed } : void 0;
|
|
1722
|
+
} catch {
|
|
1723
|
+
return void 0;
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
function pushDown(query) {
|
|
1727
|
+
const clauses = [];
|
|
1728
|
+
const params = [];
|
|
1729
|
+
const eq = (column, value) => {
|
|
1730
|
+
if (value === void 0) return;
|
|
1731
|
+
clauses.push(`${column} = ?`);
|
|
1732
|
+
params.push(value);
|
|
1733
|
+
};
|
|
1734
|
+
eq("event_id", query.eventId);
|
|
1735
|
+
eq("project_id", query.projectId);
|
|
1736
|
+
eq("session_id", query.sessionId);
|
|
1737
|
+
eq("agent_id", query.agentId);
|
|
1738
|
+
eq("task_id", query.taskId);
|
|
1739
|
+
eq("trace_id", query.traceId);
|
|
1740
|
+
eq("logical_request_id", query.logicalRequestId);
|
|
1741
|
+
eq("prompt_manifest_id", query.promptManifestId);
|
|
1742
|
+
eq("resource_kind", query.resourceKind);
|
|
1743
|
+
eq("resource_id", query.resourceId);
|
|
1744
|
+
if (query.eventTypes?.length) {
|
|
1745
|
+
clauses.push(`event_type IN (${query.eventTypes.map(() => "?").join(",")})`);
|
|
1746
|
+
params.push(...query.eventTypes);
|
|
1747
|
+
}
|
|
1748
|
+
if (query.outcomes?.length) {
|
|
1749
|
+
clauses.push(`outcome IN (${query.outcomes.map(() => "?").join(",")})`);
|
|
1750
|
+
params.push(...query.outcomes);
|
|
1751
|
+
}
|
|
1752
|
+
if (query.from) {
|
|
1753
|
+
clauses.push("occurred_at >= ?");
|
|
1754
|
+
params.push(query.from);
|
|
1755
|
+
}
|
|
1756
|
+
if (query.to) {
|
|
1757
|
+
clauses.push("occurred_at <= ?");
|
|
1758
|
+
params.push(query.to);
|
|
1759
|
+
}
|
|
1760
|
+
return { clause: clauses.length ? clauses.join(" AND ") : "1=1", params };
|
|
1761
|
+
}
|
|
1762
|
+
var ChronicleSqliteQueryEngine = class {
|
|
1763
|
+
constructor(db, options = {}) {
|
|
1764
|
+
this.db = db;
|
|
1765
|
+
this.batchSize = Math.max(1, options.batchSize ?? 1e3);
|
|
1766
|
+
}
|
|
1767
|
+
db;
|
|
1768
|
+
diagnostics = { sourceFiles: 1, invalidLines: 0 };
|
|
1769
|
+
batchSize;
|
|
1770
|
+
async query(query = {}) {
|
|
1771
|
+
const order = query.order ?? "desc";
|
|
1772
|
+
const limit = Math.max(1, Math.min(query.limit ?? 100, MAX_LIMIT));
|
|
1773
|
+
const cursor = decodeCursor2(query.cursor);
|
|
1774
|
+
const pushed = pushDown(query);
|
|
1775
|
+
const direction = order === "asc" ? "ASC" : "DESC";
|
|
1776
|
+
const comparison = order === "asc" ? ">" : "<";
|
|
1777
|
+
const keyset = cursor ? ` AND (day, sequence) ${comparison} (?, ?)` : "";
|
|
1778
|
+
const sql = `SELECT day, sequence, payload FROM events WHERE ${pushed.clause}${keyset} ORDER BY day ${direction}, sequence ${direction} LIMIT ? OFFSET ?`;
|
|
1779
|
+
const summary = createSummaryAccumulator();
|
|
1780
|
+
const page = [];
|
|
1781
|
+
let total = 0;
|
|
1782
|
+
let scannedEvents = 0;
|
|
1783
|
+
let last;
|
|
1784
|
+
let offset = 0;
|
|
1785
|
+
for (; ; ) {
|
|
1786
|
+
const params = [...pushed.params];
|
|
1787
|
+
if (cursor) params.push(cursor.day, cursor.sequence);
|
|
1788
|
+
params.push(this.batchSize, offset);
|
|
1789
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
1790
|
+
if (rows.length === 0) break;
|
|
1791
|
+
offset += rows.length;
|
|
1792
|
+
for (const row of rows) {
|
|
1793
|
+
scannedEvents++;
|
|
1794
|
+
let event;
|
|
1795
|
+
try {
|
|
1796
|
+
event = JSON.parse(row.payload);
|
|
1797
|
+
} catch {
|
|
1798
|
+
this.diagnostics.invalidLines++;
|
|
1799
|
+
continue;
|
|
1800
|
+
}
|
|
1801
|
+
if (!matches(event, query)) continue;
|
|
1802
|
+
total++;
|
|
1803
|
+
updateSummary(summary, event);
|
|
1804
|
+
if (page.length < limit) {
|
|
1805
|
+
page.push(event);
|
|
1806
|
+
last = { day: row.day, sequence: row.sequence };
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
if (rows.length < this.batchSize) break;
|
|
1810
|
+
}
|
|
1811
|
+
page.sort((left, right) => compareEvents(left, right) * (order === "asc" ? 1 : -1));
|
|
1812
|
+
const result = {
|
|
1813
|
+
events: page,
|
|
1814
|
+
total,
|
|
1815
|
+
scannedEvents,
|
|
1816
|
+
sourceFiles: this.diagnostics.sourceFiles,
|
|
1817
|
+
invalidLines: this.diagnostics.invalidLines,
|
|
1818
|
+
summary: finalizeSummary(summary)
|
|
1819
|
+
};
|
|
1820
|
+
if (total > page.length && last) {
|
|
1821
|
+
return { ...result, nextCursor: encodeCursor2(last) };
|
|
1822
|
+
}
|
|
1823
|
+
return result;
|
|
1824
|
+
}
|
|
1825
|
+
/**
|
|
1826
|
+
* Value counts per facet field.
|
|
1827
|
+
*
|
|
1828
|
+
* Not a SQL `GROUP BY`: `facetValue()` reads fields that live inside the
|
|
1829
|
+
* payload — provider, model, tool call, tag — so grouping in SQL would only
|
|
1830
|
+
* work for the handful that happen to be columns and would need a second,
|
|
1831
|
+
* divergent definition for the rest. The narrowing is still done by SQL; the
|
|
1832
|
+
* counting uses the JSONL engine's own projection.
|
|
1833
|
+
*/
|
|
1834
|
+
async facets(fields, query = {}, limit = 100) {
|
|
1835
|
+
const uniqueFields = [...new Set(fields)];
|
|
1836
|
+
if (uniqueFields.length === 0) return {};
|
|
1837
|
+
const counts = new Map(uniqueFields.map((field) => [field, /* @__PURE__ */ new Map()]));
|
|
1838
|
+
for (const event of this.eachMatch(query)) {
|
|
1839
|
+
for (const field of uniqueFields) {
|
|
1840
|
+
const value = facetValue(event, field);
|
|
1841
|
+
if (value === void 0) continue;
|
|
1842
|
+
const fieldCounts = counts.get(field);
|
|
1843
|
+
fieldCounts?.set(value, (fieldCounts.get(value) ?? 0) + 1);
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
const result = {};
|
|
1847
|
+
for (const field of uniqueFields) {
|
|
1848
|
+
result[field] = [...counts.get(field) ?? /* @__PURE__ */ new Map()].map(([value, count]) => ({ value, count })).sort((left, right) => right.count - left.count || left.value.localeCompare(right.value)).slice(0, Math.max(0, limit));
|
|
1849
|
+
}
|
|
1850
|
+
return result;
|
|
1851
|
+
}
|
|
1852
|
+
async facet(field, query = {}, limit = 100) {
|
|
1853
|
+
return (await this.facets([field], query, limit))[field] ?? [];
|
|
1854
|
+
}
|
|
1855
|
+
/**
|
|
1856
|
+
* Expand explicit and typed correlation edges around a seed set.
|
|
1857
|
+
*
|
|
1858
|
+
* The traversal is the JSONL engine's, moved onto rows: `relationKeys()`
|
|
1859
|
+
* defines the edges, `compareEvents` orders the nodes, and each hop is
|
|
1860
|
+
* another ordered pass. Order is load-bearing rather than cosmetic — both
|
|
1861
|
+
* `maxNodes` truncations stop at whatever they reach first, so a differently
|
|
1862
|
+
* ordered scan would return a different subgraph rather than the same one
|
|
1863
|
+
* shuffled. `ORDER BY day, sequence` reproduces `comparePartitionPaths`
|
|
1864
|
+
* (family, then rotation index) followed by line order within a partition.
|
|
1865
|
+
*/
|
|
1866
|
+
async graph(seed = {}, hops = 2, maxNodes = 1e3) {
|
|
1867
|
+
const nodeLimit = Math.max(0, Math.floor(maxNodes));
|
|
1868
|
+
const selected = /* @__PURE__ */ new Map();
|
|
1869
|
+
let seedCount = 0;
|
|
1870
|
+
for (const event of this.eachMatch(seed)) {
|
|
1871
|
+
seedCount++;
|
|
1872
|
+
if (selected.size < nodeLimit) selected.set(event.eventId, event);
|
|
1873
|
+
}
|
|
1874
|
+
let frontier = [...selected.values()];
|
|
1875
|
+
const depthLimit = Math.max(0, Math.min(hops, 10));
|
|
1876
|
+
for (let depth = 0; depth < depthLimit && frontier.length > 0 && selected.size < nodeLimit; depth++) {
|
|
1877
|
+
const frontierKeys = new Set(
|
|
1878
|
+
frontier.flatMap((event) => relationKeys(event).map((relation) => relation.key))
|
|
1879
|
+
);
|
|
1880
|
+
const next = [];
|
|
1881
|
+
for (const event of this.eachMatch({})) {
|
|
1882
|
+
if (selected.has(event.eventId)) continue;
|
|
1883
|
+
if (!relationKeys(event).some((relation) => frontierKeys.has(relation.key))) continue;
|
|
1884
|
+
selected.set(event.eventId, event);
|
|
1885
|
+
next.push(event);
|
|
1886
|
+
if (selected.size >= nodeLimit) break;
|
|
1887
|
+
}
|
|
1888
|
+
frontier = next;
|
|
1889
|
+
}
|
|
1890
|
+
const nodes = [...selected.values()].sort(compareEvents);
|
|
1891
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
1892
|
+
for (const node of nodes) {
|
|
1893
|
+
for (const relation of relationKeys(node)) {
|
|
1894
|
+
const related = byKey.get(relation.key) ?? [];
|
|
1895
|
+
related.push(node);
|
|
1896
|
+
byKey.set(relation.key, related);
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
const edges = [];
|
|
1900
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1901
|
+
for (const node of nodes) {
|
|
1902
|
+
for (const relation of relationKeys(node)) {
|
|
1903
|
+
for (const candidate of byKey.get(relation.key) ?? []) {
|
|
1904
|
+
if (candidate.eventId === node.eventId) continue;
|
|
1905
|
+
const [from, to] = compareEvents(node, candidate) <= 0 ? [node, candidate] : [candidate, node];
|
|
1906
|
+
const id = `${from.eventId}:${to.eventId}:${relation.kind}`;
|
|
1907
|
+
if (seen.has(id)) continue;
|
|
1908
|
+
seen.add(id);
|
|
1909
|
+
edges.push({
|
|
1910
|
+
from: from.eventId,
|
|
1911
|
+
to: to.eventId,
|
|
1912
|
+
kind: relation.kind,
|
|
1913
|
+
confidence: relation.confidence
|
|
1914
|
+
});
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
return {
|
|
1919
|
+
nodes,
|
|
1920
|
+
edges,
|
|
1921
|
+
truncated: seedCount > nodeLimit || selected.size >= nodeLimit
|
|
1922
|
+
};
|
|
1923
|
+
}
|
|
1924
|
+
/** Every event satisfying `query`, pulled in batches so a wide scan stays bounded. */
|
|
1925
|
+
*eachMatch(query) {
|
|
1926
|
+
const pushed = pushDown(query);
|
|
1927
|
+
const sql = `SELECT payload FROM events WHERE ${pushed.clause} ORDER BY day, sequence LIMIT ? OFFSET ?`;
|
|
1928
|
+
let offset = 0;
|
|
1929
|
+
for (; ; ) {
|
|
1930
|
+
const rows = this.db.prepare(sql).all(...pushed.params, this.batchSize, offset);
|
|
1931
|
+
if (rows.length === 0) return;
|
|
1932
|
+
offset += rows.length;
|
|
1933
|
+
for (const row of rows) {
|
|
1934
|
+
let event;
|
|
1935
|
+
try {
|
|
1936
|
+
event = JSON.parse(row.payload);
|
|
1937
|
+
} catch {
|
|
1938
|
+
this.diagnostics.invalidLines++;
|
|
1939
|
+
continue;
|
|
1940
|
+
}
|
|
1941
|
+
if (matches(event, query)) yield event;
|
|
1942
|
+
}
|
|
1943
|
+
if (rows.length < this.batchSize) return;
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
};
|
|
1947
|
+
|
|
1948
|
+
// src/chronicle/sqlite-journal.ts
|
|
1949
|
+
var TRIM_SLACK_RATIO = 0.02;
|
|
1950
|
+
var MAX_TRIM_SLACK_EVENTS = 2e3;
|
|
1951
|
+
var ChronicleSqliteJournal = class {
|
|
1952
|
+
db;
|
|
1953
|
+
dbPath;
|
|
1954
|
+
now;
|
|
1955
|
+
monotonicNow;
|
|
1956
|
+
idFactory;
|
|
1957
|
+
retentionDays;
|
|
1958
|
+
maxEvents;
|
|
1959
|
+
retentionCheckIntervalMs;
|
|
1960
|
+
/** Overshoot allowed above `maxEvents` before eviction runs; see TRIM_SLACK_RATIO. */
|
|
1961
|
+
trimSlack;
|
|
1962
|
+
quotaManager;
|
|
1963
|
+
nextRetentionCheckAt = 0;
|
|
1964
|
+
retainedEventCount = 0;
|
|
1965
|
+
/**
|
|
1966
|
+
* Statements reused across appends.
|
|
1967
|
+
*
|
|
1968
|
+
* `db.prepare` re-parses the SQL every call, and the append path used to
|
|
1969
|
+
* prepare five statements per batch. They are held rather than re-prepared
|
|
1970
|
+
* because the schema cannot change under an open journal.
|
|
1971
|
+
*/
|
|
1972
|
+
statements;
|
|
1973
|
+
/**
|
|
1974
|
+
* Cached chain head per day. Cleared wholesale on any write failure so the
|
|
1975
|
+
* next append rebuilds from the database rather than trusting a counter that
|
|
1976
|
+
* may have advanced past what actually committed.
|
|
1977
|
+
*/
|
|
1978
|
+
anchors = /* @__PURE__ */ new Map();
|
|
1979
|
+
counters = {
|
|
1980
|
+
acceptedEvents: 0,
|
|
1981
|
+
persistedEvents: 0,
|
|
1982
|
+
rejectedEvents: 0,
|
|
1983
|
+
failedEvents: 0,
|
|
1984
|
+
batches: 0,
|
|
1985
|
+
maxObservedPending: 0,
|
|
1986
|
+
largestBatch: 0
|
|
1987
|
+
};
|
|
1988
|
+
lastBatchDurationMs;
|
|
1989
|
+
constructor(options) {
|
|
1990
|
+
this.dbPath = path6.join(path6.resolve(options.directory), CHRONICLE_SQLITE_FILE);
|
|
1991
|
+
this.now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
1992
|
+
this.monotonicNow = options.monotonicNow ?? (() => process.hrtime.bigint());
|
|
1993
|
+
this.idFactory = options.idFactory ?? (() => randomUUID2());
|
|
1994
|
+
if (options.retentionDays !== void 0 && (!Number.isFinite(options.retentionDays) || options.retentionDays <= 0)) {
|
|
1995
|
+
throw new RangeError("retentionDays must be a positive finite number");
|
|
1996
|
+
}
|
|
1997
|
+
if (options.retentionCheckIntervalMs !== void 0 && (!Number.isFinite(options.retentionCheckIntervalMs) || options.retentionCheckIntervalMs <= 0)) {
|
|
1998
|
+
throw new RangeError("retentionCheckIntervalMs must be a positive finite number");
|
|
1999
|
+
}
|
|
2000
|
+
if (options.maxEvents !== void 0 && (!Number.isInteger(options.maxEvents) || options.maxEvents < 1)) {
|
|
2001
|
+
throw new RangeError("maxEvents must be a positive integer");
|
|
2002
|
+
}
|
|
2003
|
+
if (options.maxBytes !== void 0 && (!Number.isSafeInteger(options.maxBytes) || options.maxBytes < 1)) {
|
|
2004
|
+
throw new RangeError("maxBytes must be a positive safe integer");
|
|
2005
|
+
}
|
|
2006
|
+
const minimumQuotaBytes = SQLITE_FIXED_OVERHEAD_BYTES + 2 * MIN_SQLITE_PAGE_BUDGET_BYTES;
|
|
2007
|
+
if (options.maxBytes !== void 0 && options.maxBytes < minimumQuotaBytes) {
|
|
2008
|
+
throw new RangeError(`maxBytes must be at least ${minimumQuotaBytes}`);
|
|
2009
|
+
}
|
|
2010
|
+
this.retentionDays = options.retentionDays;
|
|
2011
|
+
this.maxEvents = options.maxEvents;
|
|
2012
|
+
this.retentionCheckIntervalMs = options.retentionCheckIntervalMs ?? 60 * 60 * 1e3;
|
|
2013
|
+
this.trimSlack = this.maxEvents === void 0 ? 0 : Math.min(MAX_TRIM_SLACK_EVENTS, Math.floor(this.maxEvents * TRIM_SLACK_RATIO));
|
|
2014
|
+
const Database = loadDatabaseSync();
|
|
2015
|
+
this.db = new Database(this.dbPath);
|
|
2016
|
+
this.db.exec("PRAGMA journal_mode = WAL");
|
|
2017
|
+
this.db.exec("PRAGMA synchronous = FULL");
|
|
2018
|
+
this.db.exec(`PRAGMA wal_autocheckpoint = ${WAL_AUTOCHECKPOINT_PAGES}`);
|
|
2019
|
+
this.db.exec(`PRAGMA journal_size_limit = ${WAL_SIZE_LIMIT_BYTES}`);
|
|
2020
|
+
ensureChronicleSchema(this.db);
|
|
2021
|
+
this.quotaManager = new ChronicleQuotaManager(this.db, this.dbPath, options.maxBytes);
|
|
2022
|
+
this.quotaManager.configure();
|
|
2023
|
+
if (this.maxEvents !== void 0) {
|
|
2024
|
+
const row = this.db.prepare("SELECT COUNT(*) AS count FROM events").get();
|
|
2025
|
+
this.retainedEventCount = row.count;
|
|
2026
|
+
this.enforceEventLimitAtStartup();
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
close() {
|
|
2030
|
+
this.db.close();
|
|
2031
|
+
}
|
|
2032
|
+
stats() {
|
|
2033
|
+
return {
|
|
2034
|
+
...this.counters,
|
|
2035
|
+
pendingEvents: 0,
|
|
2036
|
+
partitionRolls: 0,
|
|
2037
|
+
...this.lastBatchDurationMs !== void 0 ? { lastBatchDurationMs: this.lastBatchDurationMs } : {}
|
|
2038
|
+
};
|
|
2039
|
+
}
|
|
2040
|
+
async flush() {
|
|
2041
|
+
return Promise.resolve();
|
|
2042
|
+
}
|
|
2043
|
+
async append(input) {
|
|
2044
|
+
const [event] = await this.appendBatch([input]);
|
|
2045
|
+
return event;
|
|
2046
|
+
}
|
|
2047
|
+
async appendBatch(inputs) {
|
|
2048
|
+
if (inputs.length === 0) return [];
|
|
2049
|
+
const started = performance.now();
|
|
2050
|
+
this.counters.acceptedEvents += inputs.length;
|
|
2051
|
+
this.counters.batches += 1;
|
|
2052
|
+
this.counters.largestBatch = Math.max(this.counters.largestBatch, inputs.length);
|
|
2053
|
+
const instant = this.now().toISOString();
|
|
2054
|
+
const day = instant.slice(0, 10);
|
|
2055
|
+
const events = [];
|
|
2056
|
+
let previous = this.readAnchor(day);
|
|
2057
|
+
for (const input of inputs) {
|
|
2058
|
+
const unhashed = {
|
|
2059
|
+
...input,
|
|
2060
|
+
occurredAt: input.occurredAt ?? instant,
|
|
2061
|
+
monotonicNs: input.monotonicNs ?? this.monotonicNow().toString(),
|
|
2062
|
+
schemaVersion: CHRONICLE_SCHEMA_VERSION,
|
|
2063
|
+
eventId: this.idFactory(),
|
|
2064
|
+
observedAt: instant,
|
|
2065
|
+
persistedAt: instant,
|
|
2066
|
+
sequence: previous.sequence + 1,
|
|
2067
|
+
previousHash: previous.hash
|
|
2068
|
+
};
|
|
2069
|
+
const event = { ...unhashed, hash: hashValue(unhashed) };
|
|
2070
|
+
events.push(event);
|
|
2071
|
+
previous = { sequence: event.sequence, hash: event.hash };
|
|
2072
|
+
}
|
|
2073
|
+
const payloads = events.map((event) => JSON.stringify(event));
|
|
2074
|
+
let batchBytes = 0;
|
|
2075
|
+
for (const payload of payloads) batchBytes += Buffer.byteLength(payload, "utf8");
|
|
2076
|
+
let retainedCountAfterCommit = this.retainedEventCount;
|
|
2077
|
+
try {
|
|
2078
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
2079
|
+
this.quotaManager.assertWithinByteQuota(batchBytes);
|
|
2080
|
+
const { insert } = this.preparedStatements();
|
|
2081
|
+
for (const [index, event] of events.entries()) {
|
|
2082
|
+
const row = projectEvent(event);
|
|
2083
|
+
insert.run(
|
|
2084
|
+
day,
|
|
2085
|
+
event.sequence,
|
|
2086
|
+
event.eventId,
|
|
2087
|
+
event.hash,
|
|
2088
|
+
event.previousHash,
|
|
2089
|
+
row.occurredAt,
|
|
2090
|
+
event.eventType,
|
|
2091
|
+
row.outcome,
|
|
2092
|
+
row.projectId,
|
|
2093
|
+
row.sessionId,
|
|
2094
|
+
row.agentId,
|
|
2095
|
+
row.taskId,
|
|
2096
|
+
row.traceId,
|
|
2097
|
+
row.logicalRequestId,
|
|
2098
|
+
row.promptManifestId,
|
|
2099
|
+
row.resourceKind,
|
|
2100
|
+
row.resourceId,
|
|
2101
|
+
row.resourcePath,
|
|
2102
|
+
row.durationNs,
|
|
2103
|
+
payloads[index]
|
|
2104
|
+
);
|
|
2105
|
+
}
|
|
2106
|
+
retainedCountAfterCommit = this.enforceEventLimitWithinTransaction(
|
|
2107
|
+
this.retainedEventCount + events.length
|
|
2108
|
+
);
|
|
2109
|
+
this.quotaManager.assertActualAllocationWithinQuota();
|
|
2110
|
+
this.db.exec("COMMIT");
|
|
2111
|
+
} catch (error) {
|
|
2112
|
+
try {
|
|
2113
|
+
this.db.exec("ROLLBACK");
|
|
2114
|
+
} catch {
|
|
2115
|
+
}
|
|
2116
|
+
this.anchors.clear();
|
|
2117
|
+
this.quotaManager.invalidateEstimate();
|
|
2118
|
+
this.counters.failedEvents += inputs.length;
|
|
2119
|
+
this.lastBatchDurationMs = performance.now() - started;
|
|
2120
|
+
throw this.quotaManager.normalizeQuotaError(error);
|
|
2121
|
+
}
|
|
2122
|
+
this.anchors.set(day, previous);
|
|
2123
|
+
this.counters.persistedEvents += events.length;
|
|
2124
|
+
this.retainedEventCount = retainedCountAfterCommit;
|
|
2125
|
+
this.quotaManager.recordAppendedBytes(batchBytes);
|
|
2126
|
+
this.lastBatchDurationMs = performance.now() - started;
|
|
2127
|
+
await this.enforceRetentionIfDue();
|
|
2128
|
+
return events;
|
|
2129
|
+
}
|
|
2130
|
+
async readAll() {
|
|
2131
|
+
const rows = this.db.prepare("SELECT payload FROM events ORDER BY day, sequence").all();
|
|
2132
|
+
return rows.map((row) => JSON.parse(row.payload));
|
|
2133
|
+
}
|
|
2134
|
+
async verify() {
|
|
2135
|
+
let entries = 0;
|
|
2136
|
+
let lastSequence = 0;
|
|
2137
|
+
let lastHash = GENESIS_HASH;
|
|
2138
|
+
for (const day of this.days()) {
|
|
2139
|
+
const checkpoint = this.readCheckpoint(day);
|
|
2140
|
+
let expectedSequence = (checkpoint?.sequence ?? 0) + 1;
|
|
2141
|
+
let previousHash = checkpoint?.hash ?? GENESIS_HASH;
|
|
2142
|
+
const rows = this.db.prepare(
|
|
2143
|
+
"SELECT sequence, hash, previous_hash, payload FROM events WHERE day = ? ORDER BY sequence"
|
|
2144
|
+
).all(day);
|
|
2145
|
+
for (const row of rows) {
|
|
2146
|
+
if (row.sequence !== expectedSequence) {
|
|
2147
|
+
return {
|
|
2148
|
+
ok: false,
|
|
2149
|
+
entries,
|
|
2150
|
+
brokenAt: entries,
|
|
2151
|
+
reason: `sequence gap in ${day}: expected ${expectedSequence}, found ${row.sequence}`
|
|
2152
|
+
};
|
|
2153
|
+
}
|
|
2154
|
+
if (row.previous_hash !== previousHash) {
|
|
2155
|
+
return { ok: false, entries, brokenAt: entries, reason: "previous hash mismatch" };
|
|
2156
|
+
}
|
|
2157
|
+
let event;
|
|
2158
|
+
try {
|
|
2159
|
+
event = JSON.parse(row.payload);
|
|
2160
|
+
} catch {
|
|
2161
|
+
return { ok: false, entries, brokenAt: entries, reason: "invalid payload JSON" };
|
|
2162
|
+
}
|
|
2163
|
+
if (chronicleEventHash(event) !== row.hash) {
|
|
2164
|
+
return { ok: false, entries, brokenAt: entries, reason: "entry hash mismatch" };
|
|
2165
|
+
}
|
|
2166
|
+
entries += 1;
|
|
2167
|
+
expectedSequence = row.sequence + 1;
|
|
2168
|
+
previousHash = row.hash;
|
|
2169
|
+
}
|
|
2170
|
+
lastSequence = expectedSequence - 1;
|
|
2171
|
+
lastHash = previousHash;
|
|
2172
|
+
}
|
|
2173
|
+
return { ok: true, entries, lastSequence, lastHash };
|
|
2174
|
+
}
|
|
2175
|
+
async enforceRetentionIfDue() {
|
|
2176
|
+
if (this.retentionDays === void 0) return;
|
|
2177
|
+
const now = this.now();
|
|
2178
|
+
if (now.getTime() < this.nextRetentionCheckAt) return;
|
|
2179
|
+
this.nextRetentionCheckAt = now.getTime() + this.retentionCheckIntervalMs;
|
|
2180
|
+
try {
|
|
2181
|
+
await this.purge({ retentionDays: this.retentionDays });
|
|
2182
|
+
} catch {
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
enforceEventLimitWithinTransaction(count, slack = this.trimSlack) {
|
|
2186
|
+
if (this.maxEvents === void 0 || count <= this.maxEvents + slack) return count;
|
|
2187
|
+
const excess = count - this.maxEvents;
|
|
2188
|
+
const statements = this.preparedStatements();
|
|
2189
|
+
const boundary = statements.trimBoundary.get(excess - 1);
|
|
2190
|
+
if (!boundary) return count;
|
|
2191
|
+
statements.writeCheckpoint.run(boundary.day, boundary.sequence, boundary.hash);
|
|
2192
|
+
statements.deletePrefix.run(boundary.day, boundary.day, boundary.sequence);
|
|
2193
|
+
statements.deleteCheckpoints.run(boundary.day);
|
|
2194
|
+
for (const day of this.anchors.keys()) {
|
|
2195
|
+
if (day <= boundary.day) this.anchors.delete(day);
|
|
2196
|
+
}
|
|
2197
|
+
return this.maxEvents;
|
|
2198
|
+
}
|
|
2199
|
+
enforceEventLimitAtStartup() {
|
|
2200
|
+
if (this.maxEvents === void 0 || this.retainedEventCount <= this.maxEvents) return;
|
|
2201
|
+
try {
|
|
2202
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
2203
|
+
const retainedCount = this.enforceEventLimitWithinTransaction(this.retainedEventCount, 0);
|
|
2204
|
+
this.db.exec("COMMIT");
|
|
2205
|
+
this.retainedEventCount = retainedCount;
|
|
2206
|
+
} catch (error) {
|
|
2207
|
+
try {
|
|
2208
|
+
this.db.exec("ROLLBACK");
|
|
2209
|
+
} catch {
|
|
2210
|
+
}
|
|
2211
|
+
throw error;
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
async purge(options) {
|
|
2215
|
+
const empty = {
|
|
2216
|
+
deletedCount: 0,
|
|
2217
|
+
deletedBytes: 0,
|
|
2218
|
+
skippedCount: 0,
|
|
2219
|
+
errors: []
|
|
2220
|
+
};
|
|
2221
|
+
if (!Number.isFinite(options.retentionDays) || options.retentionDays <= 0) return empty;
|
|
2222
|
+
const cutoff = new Date(this.now().getTime() - options.retentionDays * 864e5).toISOString().slice(0, 10);
|
|
2223
|
+
const count = this.db.prepare("SELECT COUNT(*) AS n FROM events WHERE day < ?").get(cutoff).n;
|
|
2224
|
+
if (count === 0) return empty;
|
|
2225
|
+
if (options.dryRun) {
|
|
2226
|
+
const days = this.db.prepare("SELECT DISTINCT day FROM events WHERE day < ? ORDER BY day").all(cutoff);
|
|
2227
|
+
return { ...empty, deletedCount: count, candidates: days.map((row) => row.day) };
|
|
2228
|
+
}
|
|
2229
|
+
try {
|
|
2230
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
2231
|
+
this.db.prepare("DELETE FROM events WHERE day < ?").run(cutoff);
|
|
2232
|
+
this.db.prepare("DELETE FROM chain_checkpoint WHERE day < ?").run(cutoff);
|
|
2233
|
+
this.db.exec("COMMIT");
|
|
2234
|
+
} catch (error) {
|
|
2235
|
+
try {
|
|
2236
|
+
this.db.exec("ROLLBACK");
|
|
2237
|
+
} catch {
|
|
2238
|
+
}
|
|
2239
|
+
return {
|
|
2240
|
+
...empty,
|
|
2241
|
+
errors: [
|
|
2242
|
+
{
|
|
2243
|
+
file: this.dbPath,
|
|
2244
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
2245
|
+
}
|
|
2246
|
+
]
|
|
2247
|
+
};
|
|
2248
|
+
}
|
|
2249
|
+
this.anchors.clear();
|
|
2250
|
+
this.retainedEventCount = this.countRows();
|
|
2251
|
+
this.quotaManager.invalidateEstimate();
|
|
2252
|
+
return { ...empty, deletedCount: count };
|
|
2253
|
+
}
|
|
2254
|
+
async runFamilyImport(load) {
|
|
2255
|
+
const insert = this.db.prepare(
|
|
2256
|
+
`INSERT INTO events (
|
|
2257
|
+
day, sequence, event_id, hash, previous_hash, occurred_at, event_type, outcome,
|
|
2258
|
+
project_id, session_id, agent_id, task_id, trace_id, logical_request_id, prompt_manifest_id,
|
|
2259
|
+
resource_kind, resource_id, resource_path, duration_ns, payload
|
|
2260
|
+
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`
|
|
2261
|
+
);
|
|
2262
|
+
const checkpoint = this.db.prepare(
|
|
2263
|
+
`INSERT INTO chain_checkpoint (day, sequence, hash) VALUES (?, ?, ?)
|
|
2264
|
+
ON CONFLICT(day) DO UPDATE SET sequence = excluded.sequence, hash = excluded.hash`
|
|
2265
|
+
);
|
|
2266
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
2267
|
+
let retainedCountAfterCommit = this.retainedEventCount;
|
|
2268
|
+
try {
|
|
2269
|
+
this.quotaManager.invalidateEstimate();
|
|
2270
|
+
this.quotaManager.assertWithinByteQuota(0);
|
|
2271
|
+
await load({
|
|
2272
|
+
insert: (day, event) => {
|
|
2273
|
+
const row = projectEvent(event);
|
|
2274
|
+
insert.run(
|
|
2275
|
+
day,
|
|
2276
|
+
event.sequence,
|
|
2277
|
+
event.eventId,
|
|
2278
|
+
event.hash,
|
|
2279
|
+
event.previousHash,
|
|
2280
|
+
row.occurredAt,
|
|
2281
|
+
event.eventType,
|
|
2282
|
+
row.outcome,
|
|
2283
|
+
row.projectId,
|
|
2284
|
+
row.sessionId,
|
|
2285
|
+
row.agentId,
|
|
2286
|
+
row.taskId,
|
|
2287
|
+
row.traceId,
|
|
2288
|
+
row.logicalRequestId,
|
|
2289
|
+
row.promptManifestId,
|
|
2290
|
+
row.resourceKind,
|
|
2291
|
+
row.resourceId,
|
|
2292
|
+
row.resourcePath,
|
|
2293
|
+
row.durationNs,
|
|
2294
|
+
JSON.stringify(event)
|
|
2295
|
+
);
|
|
2296
|
+
},
|
|
2297
|
+
checkpoint: (day, sequence, hash) => {
|
|
2298
|
+
checkpoint.run(day, sequence, hash);
|
|
2299
|
+
}
|
|
2300
|
+
});
|
|
2301
|
+
retainedCountAfterCommit = this.enforceEventLimitWithinTransaction(this.countRows(), 0);
|
|
2302
|
+
this.quotaManager.invalidateEstimate();
|
|
2303
|
+
this.quotaManager.assertActualAllocationWithinQuota();
|
|
2304
|
+
this.db.exec("COMMIT");
|
|
2305
|
+
} catch (error) {
|
|
2306
|
+
try {
|
|
2307
|
+
this.db.exec("ROLLBACK");
|
|
2308
|
+
} catch {
|
|
2309
|
+
}
|
|
2310
|
+
this.quotaManager.invalidateEstimate();
|
|
2311
|
+
throw this.quotaManager.normalizeQuotaError(error);
|
|
2312
|
+
} finally {
|
|
2313
|
+
this.anchors.clear();
|
|
2314
|
+
}
|
|
2315
|
+
this.retainedEventCount = retainedCountAfterCommit;
|
|
2316
|
+
}
|
|
2317
|
+
queryEngine(options) {
|
|
2318
|
+
return new ChronicleSqliteQueryEngine(this.db, options);
|
|
2319
|
+
}
|
|
2320
|
+
hasImportedLegacyJournal() {
|
|
2321
|
+
return this.readMeta(LEGACY_JSONL_MIGRATION_KEY) !== void 0;
|
|
2322
|
+
}
|
|
2323
|
+
markLegacyJournalImported() {
|
|
2324
|
+
this.db.prepare(
|
|
2325
|
+
`INSERT INTO chronicle_meta (key, value) VALUES (?, 'done')
|
|
2326
|
+
ON CONFLICT(key) DO UPDATE SET value = 'done'`
|
|
2327
|
+
).run(LEGACY_JSONL_MIGRATION_KEY);
|
|
2328
|
+
}
|
|
2329
|
+
recordQuarantinedFamilies(families) {
|
|
2330
|
+
if (families.length === 0) return;
|
|
2331
|
+
this.db.prepare(
|
|
2332
|
+
`INSERT INTO chronicle_meta (key, value) VALUES (?, ?)
|
|
2333
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value`
|
|
2334
|
+
).run(LEGACY_JSONL_QUARANTINE_KEY, JSON.stringify(families));
|
|
2335
|
+
}
|
|
2336
|
+
/** Record per-partition-file JSONL byte offsets at import time (merged — the
|
|
2337
|
+
* import persists per family, so a crash mid-run keeps earlier boundaries).
|
|
2338
|
+
* The metrics ingester folds only bytes beyond these boundaries after the
|
|
2339
|
+
* migration, so post-migration appends (the jsonl-store fallback) are
|
|
2340
|
+
* ingested while the migrated bytes are never re-counted. Files absent from
|
|
2341
|
+
* the map were quarantined (or created post-migration) — their events live
|
|
2342
|
+
* only in JSONL. */
|
|
2343
|
+
recordLegacyJsonlBoundary(boundary) {
|
|
2344
|
+
let merged = boundary;
|
|
2345
|
+
try {
|
|
2346
|
+
const row = this.db.prepare("SELECT value FROM chronicle_meta WHERE key = ?").get(LEGACY_JSONL_BOUNDARY_KEY);
|
|
2347
|
+
if (row?.value) {
|
|
2348
|
+
merged = { ...JSON.parse(row.value), ...boundary };
|
|
2349
|
+
}
|
|
2350
|
+
} catch {
|
|
2351
|
+
}
|
|
2352
|
+
this.db.prepare(
|
|
2353
|
+
`INSERT INTO chronicle_meta (key, value) VALUES (?, ?)
|
|
2354
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value`
|
|
2355
|
+
).run(LEGACY_JSONL_BOUNDARY_KEY, JSON.stringify(merged));
|
|
2356
|
+
}
|
|
2357
|
+
hasImportedDay(day) {
|
|
2358
|
+
const row = this.db.prepare("SELECT 1 AS present FROM events WHERE day = ? LIMIT 1").get(day);
|
|
2359
|
+
return row !== void 0;
|
|
2360
|
+
}
|
|
2361
|
+
quarantinedFamilies() {
|
|
2362
|
+
const raw = this.readMeta(LEGACY_JSONL_QUARANTINE_KEY);
|
|
2363
|
+
if (!raw) return [];
|
|
2364
|
+
try {
|
|
2365
|
+
const parsed = JSON.parse(raw);
|
|
2366
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
2367
|
+
} catch {
|
|
2368
|
+
return [];
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
preparedStatements() {
|
|
2372
|
+
this.statements ??= {
|
|
2373
|
+
insert: this.db.prepare(
|
|
2374
|
+
`INSERT INTO events (
|
|
2375
|
+
day, sequence, event_id, hash, previous_hash, occurred_at, event_type, outcome,
|
|
2376
|
+
project_id, session_id, agent_id, task_id, trace_id, logical_request_id, prompt_manifest_id,
|
|
2377
|
+
resource_kind, resource_id, resource_path, duration_ns, payload
|
|
2378
|
+
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`
|
|
2379
|
+
),
|
|
2380
|
+
trimBoundary: this.db.prepare(
|
|
2381
|
+
"SELECT day, sequence, hash FROM events ORDER BY day, sequence LIMIT 1 OFFSET ?"
|
|
2382
|
+
),
|
|
2383
|
+
writeCheckpoint: this.db.prepare(
|
|
2384
|
+
`INSERT INTO chain_checkpoint (day, sequence, hash) VALUES (?, ?, ?)
|
|
2385
|
+
ON CONFLICT(day) DO UPDATE SET sequence = excluded.sequence, hash = excluded.hash`
|
|
2386
|
+
),
|
|
2387
|
+
deletePrefix: this.db.prepare(
|
|
2388
|
+
"DELETE FROM events WHERE day < ? OR (day = ? AND sequence <= ?)"
|
|
2389
|
+
),
|
|
2390
|
+
deleteCheckpoints: this.db.prepare("DELETE FROM chain_checkpoint WHERE day < ?")
|
|
2391
|
+
};
|
|
2392
|
+
return this.statements;
|
|
2393
|
+
}
|
|
2394
|
+
countRows() {
|
|
2395
|
+
return this.db.prepare("SELECT COUNT(*) AS count FROM events").get().count;
|
|
2396
|
+
}
|
|
2397
|
+
readMeta(key) {
|
|
2398
|
+
const row = this.db.prepare("SELECT value FROM chronicle_meta WHERE key = ?").get(key);
|
|
2399
|
+
return row?.value;
|
|
2400
|
+
}
|
|
2401
|
+
readCheckpoint(day) {
|
|
2402
|
+
return this.db.prepare("SELECT sequence, hash FROM chain_checkpoint WHERE day = ?").get(day);
|
|
2403
|
+
}
|
|
2404
|
+
days() {
|
|
2405
|
+
const rows = this.db.prepare("SELECT DISTINCT day FROM events ORDER BY day").all();
|
|
2406
|
+
return rows.map((row) => row.day);
|
|
2407
|
+
}
|
|
2408
|
+
readAnchor(day) {
|
|
2409
|
+
const cached = this.anchors.get(day);
|
|
2410
|
+
if (cached) return cached;
|
|
2411
|
+
const last = this.db.prepare("SELECT sequence, hash FROM events WHERE day = ? ORDER BY sequence DESC LIMIT 1").get(day);
|
|
2412
|
+
const anchor = last ?? this.readCheckpoint(day) ?? { sequence: 0, hash: GENESIS_HASH };
|
|
2413
|
+
this.anchors.set(day, anchor);
|
|
2414
|
+
return anchor;
|
|
2415
|
+
}
|
|
2416
|
+
};
|
|
2417
|
+
|
|
2418
|
+
// src/chronicle/metrics-schema.ts
|
|
2419
|
+
import { loadRuntimeDatabaseSync as loadRuntimeDatabaseSync2 } from "@wrongstack/persistence";
|
|
2420
|
+
var SCHEMA_VERSION2 = 4;
|
|
2421
|
+
var READ_CHUNK_BYTES = 1024 * 1024;
|
|
2422
|
+
var SQLITE_SOURCE_PREFIX = "sqlite:";
|
|
2423
|
+
var SQLITE_INGEST_BATCH = 2e3;
|
|
2424
|
+
var EMPTY_FAMILIES = {
|
|
2425
|
+
llm: 0,
|
|
2426
|
+
agent: 0,
|
|
2427
|
+
tool: 0,
|
|
2428
|
+
file: 0,
|
|
2429
|
+
memory: 0,
|
|
2430
|
+
task: 0,
|
|
2431
|
+
decision: 0,
|
|
2432
|
+
runtime: 0,
|
|
2433
|
+
finding: 0
|
|
2434
|
+
};
|
|
2435
|
+
var Ctor2;
|
|
2436
|
+
function loadDatabaseSync2() {
|
|
2437
|
+
if (Ctor2) return Ctor2;
|
|
2438
|
+
if (Ctor2 === null) throw new Error("SQLite is unavailable in this runtime");
|
|
2439
|
+
try {
|
|
2440
|
+
Ctor2 = withSqliteExperimentalWarningSuppressed(loadRuntimeDatabaseSync2);
|
|
2441
|
+
return Ctor2;
|
|
2442
|
+
} catch (error) {
|
|
2443
|
+
Ctor2 = null;
|
|
2444
|
+
throw new Error(
|
|
2445
|
+
"Chronicle metrics need node:sqlite (Node >= 22.5) or bun:sqlite: " + (error instanceof Error ? error.message : String(error))
|
|
2446
|
+
);
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
function isChronicleMetricsAvailable() {
|
|
2450
|
+
try {
|
|
2451
|
+
loadDatabaseSync2();
|
|
2452
|
+
return true;
|
|
2453
|
+
} catch {
|
|
2454
|
+
return false;
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
function ensureMetricsSchema(db) {
|
|
2458
|
+
const version = db.prepare("PRAGMA user_version").get().user_version;
|
|
2459
|
+
if (version !== 0 && version !== SCHEMA_VERSION2) {
|
|
2460
|
+
db.exec(
|
|
2461
|
+
"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;"
|
|
2462
|
+
);
|
|
2463
|
+
}
|
|
2464
|
+
db.exec(`
|
|
2465
|
+
CREATE TABLE IF NOT EXISTS ingest_state (
|
|
2466
|
+
file TEXT PRIMARY KEY,
|
|
2467
|
+
bytes INTEGER NOT NULL
|
|
2468
|
+
);
|
|
2469
|
+
CREATE TABLE IF NOT EXISTS provider_daily (
|
|
2470
|
+
day TEXT NOT NULL,
|
|
2471
|
+
provider_id TEXT NOT NULL,
|
|
2472
|
+
model_id TEXT NOT NULL,
|
|
2473
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
2474
|
+
completed INTEGER NOT NULL DEFAULT 0,
|
|
2475
|
+
failed INTEGER NOT NULL DEFAULT 0,
|
|
2476
|
+
retries INTEGER NOT NULL DEFAULT 0,
|
|
2477
|
+
fallbacks INTEGER NOT NULL DEFAULT 0,
|
|
2478
|
+
input_tokens INTEGER NOT NULL DEFAULT 0,
|
|
2479
|
+
output_tokens INTEGER NOT NULL DEFAULT 0,
|
|
2480
|
+
cache_read_tokens INTEGER NOT NULL DEFAULT 0,
|
|
2481
|
+
cache_write_tokens INTEGER NOT NULL DEFAULT 0,
|
|
2482
|
+
duration_ms_total REAL NOT NULL DEFAULT 0,
|
|
2483
|
+
duration_ms_max REAL NOT NULL DEFAULT 0,
|
|
2484
|
+
duration_count INTEGER NOT NULL DEFAULT 0,
|
|
2485
|
+
PRIMARY KEY (day, provider_id, model_id)
|
|
2486
|
+
);
|
|
2487
|
+
CREATE TABLE IF NOT EXISTS task_outcomes (
|
|
2488
|
+
task_id TEXT PRIMARY KEY,
|
|
2489
|
+
run_id TEXT NOT NULL DEFAULT '',
|
|
2490
|
+
board_id TEXT NOT NULL DEFAULT '',
|
|
2491
|
+
session_id TEXT NOT NULL DEFAULT '',
|
|
2492
|
+
agent_id TEXT NOT NULL DEFAULT '',
|
|
2493
|
+
status TEXT NOT NULL DEFAULT 'started',
|
|
2494
|
+
started_at TEXT,
|
|
2495
|
+
ended_at TEXT,
|
|
2496
|
+
duration_ms REAL,
|
|
2497
|
+
retries INTEGER NOT NULL DEFAULT 0,
|
|
2498
|
+
verification_failures INTEGER NOT NULL DEFAULT 0
|
|
2499
|
+
);
|
|
2500
|
+
CREATE TABLE IF NOT EXISTS file_lineage (
|
|
2501
|
+
event_id TEXT PRIMARY KEY,
|
|
2502
|
+
path TEXT NOT NULL,
|
|
2503
|
+
path_key TEXT NOT NULL,
|
|
2504
|
+
operation TEXT NOT NULL,
|
|
2505
|
+
occurred_at TEXT NOT NULL,
|
|
2506
|
+
session_id TEXT NOT NULL DEFAULT '',
|
|
2507
|
+
agent_id TEXT NOT NULL DEFAULT '',
|
|
2508
|
+
task_id TEXT NOT NULL DEFAULT '',
|
|
2509
|
+
board_id TEXT NOT NULL DEFAULT '',
|
|
2510
|
+
run_id TEXT NOT NULL DEFAULT '',
|
|
2511
|
+
tool_name TEXT NOT NULL DEFAULT '',
|
|
2512
|
+
provider_id TEXT NOT NULL DEFAULT '',
|
|
2513
|
+
model_id TEXT NOT NULL DEFAULT '',
|
|
2514
|
+
logical_request_id TEXT NOT NULL DEFAULT '',
|
|
2515
|
+
prompt_manifest_id TEXT NOT NULL DEFAULT '',
|
|
2516
|
+
provenance_confidence TEXT NOT NULL DEFAULT 'unknown',
|
|
2517
|
+
source TEXT NOT NULL DEFAULT ''
|
|
2518
|
+
);
|
|
2519
|
+
CREATE INDEX IF NOT EXISTS idx_file_lineage_path ON file_lineage(path_key, occurred_at);
|
|
2520
|
+
CREATE INDEX IF NOT EXISTS idx_file_lineage_task ON file_lineage(task_id);
|
|
2521
|
+
CREATE TABLE IF NOT EXISTS token_cost (
|
|
2522
|
+
scope_key TEXT PRIMARY KEY,
|
|
2523
|
+
day TEXT NOT NULL,
|
|
2524
|
+
occurred_at TEXT NOT NULL,
|
|
2525
|
+
sequence INTEGER NOT NULL,
|
|
2526
|
+
cost REAL NOT NULL
|
|
2527
|
+
);
|
|
2528
|
+
CREATE TABLE IF NOT EXISTS daily_counters (
|
|
2529
|
+
day TEXT PRIMARY KEY,
|
|
2530
|
+
tool_calls INTEGER NOT NULL DEFAULT 0,
|
|
2531
|
+
completed_tools INTEGER NOT NULL DEFAULT 0,
|
|
2532
|
+
failed_tools INTEGER NOT NULL DEFAULT 0,
|
|
2533
|
+
tool_duration_ms_total REAL NOT NULL DEFAULT 0,
|
|
2534
|
+
tool_duration_ms_max REAL NOT NULL DEFAULT 0,
|
|
2535
|
+
tool_duration_count INTEGER NOT NULL DEFAULT 0,
|
|
2536
|
+
processes INTEGER NOT NULL DEFAULT 0,
|
|
2537
|
+
failed_processes INTEGER NOT NULL DEFAULT 0,
|
|
2538
|
+
file_events_all INTEGER NOT NULL DEFAULT 0,
|
|
2539
|
+
decisions INTEGER NOT NULL DEFAULT 0,
|
|
2540
|
+
escalations INTEGER NOT NULL DEFAULT 0,
|
|
2541
|
+
agent_events INTEGER NOT NULL DEFAULT 0,
|
|
2542
|
+
failures INTEGER NOT NULL DEFAULT 0,
|
|
2543
|
+
cancellations INTEGER NOT NULL DEFAULT 0
|
|
2544
|
+
);
|
|
2545
|
+
CREATE TABLE IF NOT EXISTS family_daily (
|
|
2546
|
+
day TEXT NOT NULL,
|
|
2547
|
+
family TEXT NOT NULL,
|
|
2548
|
+
count INTEGER NOT NULL DEFAULT 0,
|
|
2549
|
+
failure_count INTEGER NOT NULL DEFAULT 0,
|
|
2550
|
+
PRIMARY KEY (day, family)
|
|
2551
|
+
);
|
|
2552
|
+
CREATE TABLE IF NOT EXISTS agent_daily (day TEXT NOT NULL, agent_id TEXT NOT NULL, PRIMARY KEY (day, agent_id));
|
|
2553
|
+
CREATE TABLE IF NOT EXISTS logical_request_daily (day TEXT NOT NULL, logical_request_id TEXT NOT NULL, PRIMARY KEY (day, logical_request_id));
|
|
2554
|
+
CREATE TABLE IF NOT EXISTS file_seen_daily (day TEXT NOT NULL, path_key TEXT NOT NULL, PRIMARY KEY (day, path_key));
|
|
2555
|
+
PRAGMA user_version = ${SCHEMA_VERSION2};
|
|
2556
|
+
`);
|
|
2557
|
+
}
|
|
2558
|
+
function eventDay(event) {
|
|
2559
|
+
return (event.occurredAt ?? event.observedAt).slice(0, 10);
|
|
2560
|
+
}
|
|
2561
|
+
function durationMs2(event) {
|
|
2562
|
+
const value = Number(event.durationNs ?? 0) / 1e6;
|
|
2563
|
+
return Number.isFinite(value) && value > 0 ? value : 0;
|
|
2564
|
+
}
|
|
2565
|
+
function numberOrDuration(event, attributes) {
|
|
2566
|
+
const explicit = attributes.durationMs;
|
|
2567
|
+
if (typeof explicit === "number" && Number.isFinite(explicit)) return explicit;
|
|
2568
|
+
return durationMs2(event);
|
|
2569
|
+
}
|
|
2570
|
+
function numberAt2(event, dotPath) {
|
|
2571
|
+
const value = readPath2(event.attributes ?? {}, dotPath);
|
|
2572
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
2573
|
+
}
|
|
2574
|
+
function readPath2(value, key) {
|
|
2575
|
+
return key.split(".").reduce(
|
|
2576
|
+
(current, part) => current && typeof current === "object" ? current[part] : void 0,
|
|
2577
|
+
value
|
|
2578
|
+
);
|
|
2579
|
+
}
|
|
2580
|
+
function stringAt(record, key) {
|
|
2581
|
+
const value = record[key];
|
|
2582
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
2583
|
+
}
|
|
2584
|
+
function asString(value) {
|
|
2585
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
2586
|
+
}
|
|
2587
|
+
function clampLimit(limit, fallback) {
|
|
2588
|
+
if (typeof limit !== "number" || !Number.isFinite(limit) || limit <= 0) return fallback;
|
|
2589
|
+
return Math.min(Math.floor(limit), 1e4);
|
|
2590
|
+
}
|
|
2591
|
+
function normalizeKey(value) {
|
|
2592
|
+
return value.replaceAll("\\", "/");
|
|
2593
|
+
}
|
|
2594
|
+
function normalizePathKey(value) {
|
|
2595
|
+
return value.replaceAll("\\", "/").replace(/^\.\//, "").toLowerCase();
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2598
|
+
// src/chronicle/metrics-store.ts
|
|
2599
|
+
import * as path8 from "node:path";
|
|
2600
|
+
|
|
2601
|
+
// src/chronicle/metrics-ingest.ts
|
|
2602
|
+
import * as fs5 from "node:fs/promises";
|
|
2603
|
+
import * as path7 from "node:path";
|
|
2604
|
+
var REBUILD_MARKER_KEY = `${SQLITE_SOURCE_PREFIX}rebuild_done`;
|
|
2605
|
+
var ChronicleMetricsIngester = class {
|
|
2606
|
+
constructor(db, directory, dbPath) {
|
|
2607
|
+
this.db = db;
|
|
2608
|
+
this.directory = directory;
|
|
2609
|
+
this.dbPath = dbPath;
|
|
2610
|
+
}
|
|
2611
|
+
db;
|
|
2612
|
+
directory;
|
|
2613
|
+
dbPath;
|
|
2614
|
+
/**
|
|
2615
|
+
* Prepared-statement cache for the per-event ingest hot paths. Preparing
|
|
2616
|
+
* the same SQL on every event was the dominant ingest cost; node:sqlite
|
|
2617
|
+
* statements are reusable, so each distinct SQL string is prepared once.
|
|
2618
|
+
*/
|
|
2619
|
+
stmtCache = /* @__PURE__ */ new Map();
|
|
2620
|
+
stmt(sql) {
|
|
2621
|
+
let s = this.stmtCache.get(sql);
|
|
2622
|
+
if (!s) {
|
|
2623
|
+
s = this.db.prepare(sql);
|
|
2624
|
+
this.stmtCache.set(sql, s);
|
|
2625
|
+
}
|
|
2626
|
+
return s;
|
|
2627
|
+
}
|
|
2628
|
+
async refresh() {
|
|
2629
|
+
const result = {
|
|
2630
|
+
ingestedEvents: 0,
|
|
2631
|
+
ingestedBytes: 0,
|
|
2632
|
+
sourceFiles: 0,
|
|
2633
|
+
invalidLines: 0
|
|
2634
|
+
};
|
|
2635
|
+
await withFileLock(this.dbPath, async () => {
|
|
2636
|
+
const files = await findChroniclePartitions(this.directory);
|
|
2637
|
+
const offsets = this.loadOffsets();
|
|
2638
|
+
const journalPath = path7.join(this.directory, CHRONICLE_SQLITE_FILE);
|
|
2639
|
+
let source = null;
|
|
2640
|
+
try {
|
|
2641
|
+
source = new (loadDatabaseSync2())(journalPath, { readOnly: true });
|
|
2642
|
+
} catch {
|
|
2643
|
+
}
|
|
2644
|
+
try {
|
|
2645
|
+
const migrated = source !== null && this.legacyJsonlConsumed(source);
|
|
2646
|
+
const boundary = migrated && source !== null ? this.loadJsonlBoundary(source) : null;
|
|
2647
|
+
const rebuilt = migrated && this.needsSqliteRebuild(offsets);
|
|
2648
|
+
if (rebuilt && source !== null) {
|
|
2649
|
+
this.rebuildFromSqliteJournal(source, result);
|
|
2650
|
+
}
|
|
2651
|
+
if (!migrated || boundary !== null) {
|
|
2652
|
+
for (const file of files) {
|
|
2653
|
+
const key = normalizeKey(path7.relative(this.directory, file));
|
|
2654
|
+
let base;
|
|
2655
|
+
if (!migrated) {
|
|
2656
|
+
base = offsets.get(key) ?? 0;
|
|
2657
|
+
} else if (boundary?.has(key)) {
|
|
2658
|
+
base = Math.max(offsets.get(key) ?? 0, boundary.get(key));
|
|
2659
|
+
} else {
|
|
2660
|
+
base = rebuilt ? 0 : offsets.get(key) ?? 0;
|
|
2661
|
+
}
|
|
2662
|
+
const ingested = await this.ingestFile(file, key, base, result);
|
|
2663
|
+
if (ingested) result.sourceFiles++;
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
this.pruneOffsets(files);
|
|
2667
|
+
if (source !== null && !rebuilt) {
|
|
2668
|
+
this.ingestSqliteJournal(source, offsets, result, false);
|
|
2669
|
+
if (migrated) this.ensureRebuildMarker();
|
|
2670
|
+
}
|
|
2671
|
+
} finally {
|
|
2672
|
+
if (source !== null) source.close();
|
|
2673
|
+
}
|
|
2674
|
+
});
|
|
2675
|
+
return result;
|
|
2676
|
+
}
|
|
2677
|
+
loadOffsets() {
|
|
2678
|
+
const rows = this.db.prepare("SELECT file, bytes FROM ingest_state").all();
|
|
2679
|
+
return new Map(rows.map((row) => [row.file, Number(row.bytes)]));
|
|
2680
|
+
}
|
|
2681
|
+
/** True once the legacy JSONL journal was imported into the SQLite journal
|
|
2682
|
+
* (`importLegacyChronicleJournal` records the marker in `chronicle_meta`).
|
|
2683
|
+
* After that point the SQLite `events` table already holds every JSONL
|
|
2684
|
+
* event, so folding the leftover partition files again would double-count
|
|
2685
|
+
* every migrated event in the aggregates. `source` is the already-opened
|
|
2686
|
+
* read-only journal handle (null when no SQLite journal exists yet). */
|
|
2687
|
+
legacyJsonlConsumed(source) {
|
|
2688
|
+
if (source === null) return false;
|
|
2689
|
+
try {
|
|
2690
|
+
const row = source.prepare("SELECT value FROM chronicle_meta WHERE key = ?").get(LEGACY_JSONL_MIGRATION_KEY);
|
|
2691
|
+
return row?.value !== void 0;
|
|
2692
|
+
} catch {
|
|
2693
|
+
return false;
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
/** Per-partition-file JSONL byte offsets recorded by
|
|
2697
|
+
* `importLegacyChronicleJournal` at import time (keyed by the same relative
|
|
2698
|
+
* path the fold uses). Files absent from the map were quarantined (or
|
|
2699
|
+
* created after the migration) — their events live only in JSONL. */
|
|
2700
|
+
loadJsonlBoundary(source) {
|
|
2701
|
+
try {
|
|
2702
|
+
const row = source.prepare("SELECT value FROM chronicle_meta WHERE key = ?").get(LEGACY_JSONL_BOUNDARY_KEY);
|
|
2703
|
+
if (!row?.value) return null;
|
|
2704
|
+
const parsed = JSON.parse(row.value);
|
|
2705
|
+
return new Map(Object.entries(parsed));
|
|
2706
|
+
} catch {
|
|
2707
|
+
return null;
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
/** Persist the rebuild sentinel idempotently. Its presence makes
|
|
2711
|
+
* `needsSqliteRebuild` return false, so post-migration jsonl-store offsets
|
|
2712
|
+
* are never mistaken for pre-migration progress. */
|
|
2713
|
+
ensureRebuildMarker() {
|
|
2714
|
+
this.db.prepare(
|
|
2715
|
+
"INSERT INTO ingest_state (file, bytes) VALUES (?, ?) ON CONFLICT(file) DO UPDATE SET bytes = excluded.bytes"
|
|
2716
|
+
).run(REBUILD_MARKER_KEY, 1);
|
|
2717
|
+
}
|
|
2718
|
+
/** Upgrade path: `metrics.db` already aggregated the legacy JSONL, then the
|
|
2719
|
+
* one-shot import copied those same events into the SQLite journal. There
|
|
2720
|
+
* is no `sqlite:` cursor yet, so a plain incremental fold would replay the
|
|
2721
|
+
* entire migrated history on top of the existing aggregates. The metrics
|
|
2722
|
+
* projection is disposable (fully derived from the journal), so rebuild it
|
|
2723
|
+
* from the SQLite source exactly once — the rebuild writes the cursors and
|
|
2724
|
+
* subsequent refreshes go back to incremental folding. */
|
|
2725
|
+
needsSqliteRebuild(offsets) {
|
|
2726
|
+
if (offsets.has(REBUILD_MARKER_KEY)) return false;
|
|
2727
|
+
for (const [key, bytes] of offsets) {
|
|
2728
|
+
if (!key.startsWith(SQLITE_SOURCE_PREFIX) && bytes > 0) return true;
|
|
2729
|
+
}
|
|
2730
|
+
return false;
|
|
2731
|
+
}
|
|
2732
|
+
rebuildFromSqliteJournal(source, result) {
|
|
2733
|
+
this.db.exec(
|
|
2734
|
+
"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;"
|
|
2735
|
+
);
|
|
2736
|
+
this.ingestSqliteJournal(source, /* @__PURE__ */ new Map(), result, true);
|
|
2737
|
+
this.ensureRebuildMarker();
|
|
2738
|
+
}
|
|
2739
|
+
pruneOffsets(existingFiles) {
|
|
2740
|
+
const keep = new Set(
|
|
2741
|
+
existingFiles.map((file) => normalizeKey(path7.relative(this.directory, file)))
|
|
2742
|
+
);
|
|
2743
|
+
for (const row of this.db.prepare("SELECT file FROM ingest_state").all()) {
|
|
2744
|
+
if (row.file.startsWith(SQLITE_SOURCE_PREFIX)) continue;
|
|
2745
|
+
if (!keep.has(row.file))
|
|
2746
|
+
this.db.prepare("DELETE FROM ingest_state WHERE file = ?").run(row.file);
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
ingestSqliteJournal(source, offsets, result, propagateErrors) {
|
|
2750
|
+
try {
|
|
2751
|
+
const days = source.prepare("SELECT DISTINCT day FROM events ORDER BY day").all();
|
|
2752
|
+
const read = source.prepare(
|
|
2753
|
+
"SELECT sequence, payload FROM events WHERE day = ? AND sequence > ? ORDER BY sequence LIMIT ?"
|
|
2754
|
+
);
|
|
2755
|
+
const writeCursor = this.db.prepare(
|
|
2756
|
+
"INSERT INTO ingest_state (file, bytes) VALUES (?, ?) ON CONFLICT(file) DO UPDATE SET bytes = excluded.bytes"
|
|
2757
|
+
);
|
|
2758
|
+
for (const { day } of days) {
|
|
2759
|
+
const key = `${SQLITE_SOURCE_PREFIX}${day}`;
|
|
2760
|
+
const from = offsets.get(key) ?? 0;
|
|
2761
|
+
let cursor = from;
|
|
2762
|
+
this.db.exec("BEGIN");
|
|
2763
|
+
try {
|
|
2764
|
+
for (; ; ) {
|
|
2765
|
+
const rows = read.all(day, cursor, SQLITE_INGEST_BATCH);
|
|
2766
|
+
if (rows.length === 0) break;
|
|
2767
|
+
for (const row of rows) {
|
|
2768
|
+
try {
|
|
2769
|
+
this.ingestEventAtomically(JSON.parse(row.payload), result);
|
|
2770
|
+
} catch {
|
|
2771
|
+
result.invalidLines++;
|
|
2772
|
+
}
|
|
2773
|
+
result.ingestedBytes += row.payload.length;
|
|
2774
|
+
cursor = Number(row.sequence);
|
|
2775
|
+
}
|
|
2776
|
+
if (rows.length < SQLITE_INGEST_BATCH) break;
|
|
2777
|
+
}
|
|
2778
|
+
if (cursor > from) writeCursor.run(key, cursor);
|
|
2779
|
+
this.db.exec("COMMIT");
|
|
2780
|
+
} catch (error) {
|
|
2781
|
+
this.db.exec("ROLLBACK");
|
|
2782
|
+
throw error;
|
|
2783
|
+
}
|
|
2784
|
+
if (cursor > from) result.sourceFiles++;
|
|
2785
|
+
}
|
|
2786
|
+
} catch (error) {
|
|
2787
|
+
if (propagateErrors) throw error;
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2790
|
+
async ingestFile(file, key, consumed, result) {
|
|
2791
|
+
let handle;
|
|
2792
|
+
try {
|
|
2793
|
+
handle = await fs5.open(file, "r");
|
|
2794
|
+
} catch {
|
|
2795
|
+
return false;
|
|
2796
|
+
}
|
|
2797
|
+
try {
|
|
2798
|
+
const size = (await handle.stat()).size;
|
|
2799
|
+
if (size <= consumed) return false;
|
|
2800
|
+
let position = consumed;
|
|
2801
|
+
let remainder = Buffer.alloc(0);
|
|
2802
|
+
let advanced = consumed;
|
|
2803
|
+
this.db.exec("BEGIN");
|
|
2804
|
+
try {
|
|
2805
|
+
while (position < size) {
|
|
2806
|
+
const length = Math.min(READ_CHUNK_BYTES, size - position);
|
|
2807
|
+
const buffer = Buffer.allocUnsafe(length);
|
|
2808
|
+
const { bytesRead } = await handle.read(buffer, 0, length, position);
|
|
2809
|
+
if (bytesRead <= 0) break;
|
|
2810
|
+
position += bytesRead;
|
|
2811
|
+
const data = remainder.length > 0 ? Buffer.concat([remainder, buffer.subarray(0, bytesRead)]) : buffer.subarray(0, bytesRead);
|
|
2812
|
+
const lastNewline = data.lastIndexOf(10);
|
|
2813
|
+
if (lastNewline < 0) {
|
|
2814
|
+
remainder = Buffer.from(data);
|
|
2815
|
+
continue;
|
|
2816
|
+
}
|
|
2817
|
+
for (const line of data.subarray(0, lastNewline).toString("utf8").split("\n")) {
|
|
2818
|
+
const trimmed = line.trim();
|
|
2819
|
+
if (!trimmed) continue;
|
|
2820
|
+
try {
|
|
2821
|
+
this.ingestEventAtomically(JSON.parse(trimmed), result);
|
|
2822
|
+
} catch {
|
|
2823
|
+
result.invalidLines++;
|
|
2824
|
+
}
|
|
2825
|
+
}
|
|
2826
|
+
advanced += lastNewline + 1;
|
|
2827
|
+
remainder = Buffer.from(data.subarray(lastNewline + 1));
|
|
2828
|
+
}
|
|
2829
|
+
this.db.prepare(
|
|
2830
|
+
"INSERT INTO ingest_state (file, bytes) VALUES (?, ?) ON CONFLICT(file) DO UPDATE SET bytes = excluded.bytes"
|
|
2831
|
+
).run(key, advanced);
|
|
2832
|
+
this.db.exec("COMMIT");
|
|
2833
|
+
} catch (error) {
|
|
2834
|
+
this.db.exec("ROLLBACK");
|
|
2835
|
+
throw error;
|
|
2836
|
+
}
|
|
2837
|
+
result.ingestedBytes += advanced - consumed;
|
|
2838
|
+
return advanced > consumed;
|
|
2839
|
+
} finally {
|
|
2840
|
+
await handle.close();
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
/** Fold one event into the aggregates atomically. `ingestEvent` runs several
|
|
2844
|
+
* statements; when one throws mid-way (e.g. a malformed resource path), the
|
|
2845
|
+
* statements before it already mutated the aggregates. The savepoint rolls
|
|
2846
|
+
* those partial updates back: `ROLLBACK TO` rewinds, `RELEASE` (reached
|
|
2847
|
+
* before the re-throw) pops the savepoint so the next event in the batch
|
|
2848
|
+
* starts from a clean stack, and the batch commits only fully-succeeded
|
|
2849
|
+
* events. Dropping the `RELEASE` would leak the savepoint into the next
|
|
2850
|
+
* event's stack — the outer transaction ROLLBACK is never reached because
|
|
2851
|
+
* the per-event catch swallows the error and continues the batch. */
|
|
2852
|
+
ingestEventAtomically(event, result) {
|
|
2853
|
+
this.db.exec("SAVEPOINT ingest_event");
|
|
2854
|
+
try {
|
|
2855
|
+
this.ingestEvent(event);
|
|
2856
|
+
this.db.exec("RELEASE ingest_event");
|
|
2857
|
+
result.ingestedEvents++;
|
|
2858
|
+
} catch (error) {
|
|
2859
|
+
this.db.exec("ROLLBACK TO ingest_event");
|
|
2860
|
+
this.db.exec("RELEASE ingest_event");
|
|
2861
|
+
throw error;
|
|
2862
|
+
}
|
|
2863
|
+
}
|
|
2864
|
+
ingestEvent(event) {
|
|
2865
|
+
if (typeof event?.eventType !== "string" || !event.scope) return;
|
|
2866
|
+
this.ingestDailyCounters(event);
|
|
2867
|
+
const type = event.eventType;
|
|
2868
|
+
if (type.startsWith("provider.attempt.") || type === "provider.fallback") {
|
|
2869
|
+
this.ingestProvider(event);
|
|
2870
|
+
} else if (type === "token.accounted") {
|
|
2871
|
+
this.ingestTokenCost(event);
|
|
2872
|
+
} else if (/^(?:sdd|subagent|kanban)\.task[._]/.test(type)) {
|
|
2873
|
+
this.ingestTask(event);
|
|
2874
|
+
} else if (type === "file.event" || /^file\.(?:tool|external)\./.test(type)) {
|
|
2875
|
+
this.ingestFileEvent(event);
|
|
2876
|
+
}
|
|
2877
|
+
}
|
|
2878
|
+
ingestDailyCounters(event) {
|
|
2879
|
+
const day = eventDay(event);
|
|
2880
|
+
this.stmt("INSERT OR IGNORE INTO daily_counters (day) VALUES (?)").run(day);
|
|
2881
|
+
const bump = (sql, ...params) => this.stmt(`UPDATE daily_counters SET ${sql} WHERE day = ?`).run(...params, day);
|
|
2882
|
+
const family = signalFamily(event);
|
|
2883
|
+
const failed = isTerminalFailure(event) ? 1 : 0;
|
|
2884
|
+
this.stmt(
|
|
2885
|
+
`INSERT INTO family_daily (day, family, count, failure_count) VALUES (?, ?, 1, ?)
|
|
2886
|
+
ON CONFLICT(day, family) DO UPDATE SET count = count + 1, failure_count = failure_count + excluded.failure_count`
|
|
2887
|
+
).run(day, family, failed);
|
|
2888
|
+
if (failed) bump("failures = failures + 1");
|
|
2889
|
+
if (event.outcome === "cancelled" || event.outcome === "abandoned")
|
|
2890
|
+
bump("cancellations = cancellations + 1");
|
|
2891
|
+
if (family === "agent") bump("agent_events = agent_events + 1");
|
|
2892
|
+
if (event.correlation.logicalRequestId) {
|
|
2893
|
+
this.stmt(
|
|
2894
|
+
"INSERT OR IGNORE INTO logical_request_daily (day, logical_request_id) VALUES (?, ?)"
|
|
2895
|
+
).run(day, event.correlation.logicalRequestId);
|
|
2896
|
+
}
|
|
2897
|
+
if (event.scope.agentId) {
|
|
2898
|
+
this.stmt("INSERT OR IGNORE INTO agent_daily (day, agent_id) VALUES (?, ?)").run(
|
|
2899
|
+
day,
|
|
2900
|
+
event.scope.agentId
|
|
2901
|
+
);
|
|
2902
|
+
}
|
|
2903
|
+
const type = event.eventType;
|
|
2904
|
+
if (type === "decision.requested") bump("decisions = decisions + 1");
|
|
2905
|
+
else if (type === "decision.escalated") bump("escalations = escalations + 1");
|
|
2906
|
+
else if (type === "tool.started") bump("tool_calls = tool_calls + 1");
|
|
2907
|
+
else if (type === "tool.executed" || type === "tool.failed") {
|
|
2908
|
+
const dur = durationMs2(event);
|
|
2909
|
+
const durationCount = dur > 0 ? 1 : 0;
|
|
2910
|
+
bump(
|
|
2911
|
+
`${type === "tool.executed" ? "completed_tools" : "failed_tools"} = ${type === "tool.executed" ? "completed_tools" : "failed_tools"} + 1,
|
|
2912
|
+
tool_duration_ms_total = tool_duration_ms_total + ?, tool_duration_ms_max = MAX(tool_duration_ms_max, ?), tool_duration_count = tool_duration_count + ?`,
|
|
2913
|
+
dur,
|
|
2914
|
+
dur,
|
|
2915
|
+
durationCount
|
|
2916
|
+
);
|
|
2917
|
+
} else if (type === "process.started") bump("processes = processes + 1");
|
|
2918
|
+
else if (type === "process.completed" && event.outcome === "failure")
|
|
2919
|
+
bump("failed_processes = failed_processes + 1");
|
|
2920
|
+
if (event.resource?.kind === "file" || type.startsWith("file.")) {
|
|
2921
|
+
bump("file_events_all = file_events_all + 1");
|
|
2922
|
+
if (event.resource?.path) {
|
|
2923
|
+
this.stmt("INSERT OR IGNORE INTO file_seen_daily (day, path_key) VALUES (?, ?)").run(
|
|
2924
|
+
day,
|
|
2925
|
+
normalizePathKey(event.resource.path)
|
|
2926
|
+
);
|
|
2927
|
+
}
|
|
2928
|
+
}
|
|
2929
|
+
}
|
|
2930
|
+
ingestProvider(event) {
|
|
2931
|
+
const day = eventDay(event);
|
|
2932
|
+
const providerId = event.runtime?.providerId ?? asString(readPath2(event.attributes ?? {}, "from.providerId")) ?? "";
|
|
2933
|
+
const modelId = event.runtime?.modelId ?? asString(readPath2(event.attributes ?? {}, "from.model")) ?? "";
|
|
2934
|
+
if (!providerId && !modelId) return;
|
|
2935
|
+
this.stmt(
|
|
2936
|
+
"INSERT OR IGNORE INTO provider_daily (day, provider_id, model_id) VALUES (?, ?, ?)"
|
|
2937
|
+
).run(day, providerId, modelId);
|
|
2938
|
+
const update = (sql, ...params) => this.stmt(
|
|
2939
|
+
`UPDATE provider_daily SET ${sql} WHERE day = ? AND provider_id = ? AND model_id = ?`
|
|
2940
|
+
).run(...params, day, providerId, modelId);
|
|
2941
|
+
const duration = durationMs2(event);
|
|
2942
|
+
switch (event.eventType) {
|
|
2943
|
+
case "provider.attempt.started":
|
|
2944
|
+
update("attempts = attempts + 1");
|
|
2945
|
+
break;
|
|
2946
|
+
case "provider.attempt.completed":
|
|
2947
|
+
update(
|
|
2948
|
+
"completed = completed + 1, input_tokens = input_tokens + ?, output_tokens = output_tokens + ?, cache_read_tokens = cache_read_tokens + ?, cache_write_tokens = cache_write_tokens + ?, duration_ms_total = duration_ms_total + ?, duration_ms_max = MAX(duration_ms_max, ?), duration_count = duration_count + ?",
|
|
2949
|
+
numberAt2(event, "usage.input"),
|
|
2950
|
+
numberAt2(event, "usage.output"),
|
|
2951
|
+
numberAt2(event, "usage.cacheRead"),
|
|
2952
|
+
numberAt2(event, "usage.cacheWrite"),
|
|
2953
|
+
duration,
|
|
2954
|
+
duration,
|
|
2955
|
+
duration > 0 ? 1 : 0
|
|
2956
|
+
);
|
|
2957
|
+
break;
|
|
2958
|
+
case "provider.attempt.failed":
|
|
2959
|
+
update(
|
|
2960
|
+
"failed = failed + 1, retries = retries + ?, duration_ms_total = duration_ms_total + ?, duration_ms_max = MAX(duration_ms_max, ?), duration_count = duration_count + ?",
|
|
2961
|
+
event.attributes?.retryScheduled === true ? 1 : 0,
|
|
2962
|
+
duration,
|
|
2963
|
+
duration,
|
|
2964
|
+
duration > 0 ? 1 : 0
|
|
2965
|
+
);
|
|
2966
|
+
break;
|
|
2967
|
+
case "provider.fallback":
|
|
2968
|
+
update("fallbacks = fallbacks + 1");
|
|
2969
|
+
break;
|
|
2970
|
+
default:
|
|
2971
|
+
break;
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2974
|
+
ingestTokenCost(event) {
|
|
2975
|
+
const cost = readPath2(event.attributes ?? {}, "cost.total");
|
|
2976
|
+
if (typeof cost !== "number" || !Number.isFinite(cost)) return;
|
|
2977
|
+
const scopeKey2 = `${event.scope.projectId ?? ""}\0${event.scope.sessionId ?? ""}\0${event.scope.agentId ?? ""}`;
|
|
2978
|
+
const occurredAt = event.occurredAt ?? event.observedAt;
|
|
2979
|
+
this.stmt(
|
|
2980
|
+
`INSERT INTO token_cost (scope_key, day, occurred_at, sequence, cost) VALUES (?, ?, ?, ?, ?)
|
|
2981
|
+
ON CONFLICT(scope_key) DO UPDATE SET
|
|
2982
|
+
day = excluded.day, occurred_at = excluded.occurred_at,
|
|
2983
|
+
sequence = excluded.sequence, cost = excluded.cost
|
|
2984
|
+
WHERE excluded.occurred_at > token_cost.occurred_at
|
|
2985
|
+
OR (excluded.occurred_at = token_cost.occurred_at AND excluded.sequence > token_cost.sequence)`
|
|
2986
|
+
).run(scopeKey2, eventDay(event), occurredAt, event.sequence, cost);
|
|
2987
|
+
}
|
|
2988
|
+
ingestTask(event) {
|
|
2989
|
+
const attributes = event.attributes ?? {};
|
|
2990
|
+
const taskId = event.scope.taskId ?? stringAt(attributes, "taskId");
|
|
2991
|
+
if (!taskId) return;
|
|
2992
|
+
const occurredAt = event.occurredAt ?? event.observedAt;
|
|
2993
|
+
this.stmt("INSERT OR IGNORE INTO task_outcomes (task_id) VALUES (?)").run(taskId);
|
|
2994
|
+
const set = (sql, ...params) => this.stmt(`UPDATE task_outcomes SET ${sql} WHERE task_id = ?`).run(...params, taskId);
|
|
2995
|
+
const lineage = [
|
|
2996
|
+
["run_id", stringAt(attributes, "runId")],
|
|
2997
|
+
["board_id", event.scope.kanbanBoardId ?? stringAt(attributes, "boardId")],
|
|
2998
|
+
["session_id", event.scope.sessionId],
|
|
2999
|
+
["agent_id", event.scope.agentId ?? stringAt(attributes, "subagentId")]
|
|
3000
|
+
];
|
|
3001
|
+
for (const [column, value] of lineage) {
|
|
3002
|
+
if (value) set(`${column} = ?`, value);
|
|
3003
|
+
}
|
|
3004
|
+
const base = event.eventType.replace(/^(?:sdd|subagent|kanban)\.task[._]/, "");
|
|
3005
|
+
switch (base) {
|
|
3006
|
+
case "started":
|
|
3007
|
+
set("status = 'started', started_at = COALESCE(started_at, ?)", occurredAt);
|
|
3008
|
+
break;
|
|
3009
|
+
case "completed":
|
|
3010
|
+
set(
|
|
3011
|
+
"status = 'completed', ended_at = ?, duration_ms = ?",
|
|
3012
|
+
occurredAt,
|
|
3013
|
+
numberOrDuration(event, attributes)
|
|
3014
|
+
);
|
|
3015
|
+
break;
|
|
3016
|
+
case "failed":
|
|
3017
|
+
set("status = 'failed', ended_at = ?", occurredAt);
|
|
3018
|
+
break;
|
|
3019
|
+
case "retrying":
|
|
3020
|
+
set("retries = retries + 1");
|
|
3021
|
+
break;
|
|
3022
|
+
case "verification_failed":
|
|
3023
|
+
set("verification_failures = verification_failures + 1");
|
|
3024
|
+
break;
|
|
3025
|
+
case "merged":
|
|
3026
|
+
set("status = 'merged'");
|
|
3027
|
+
break;
|
|
3028
|
+
case "conflict":
|
|
3029
|
+
set("status = 'conflict'");
|
|
3030
|
+
break;
|
|
3031
|
+
default:
|
|
3032
|
+
break;
|
|
3033
|
+
}
|
|
3034
|
+
}
|
|
3035
|
+
ingestFileEvent(event) {
|
|
3036
|
+
const attributes = event.attributes ?? {};
|
|
3037
|
+
const operation = stringAt(attributes, "operation") ?? "";
|
|
3038
|
+
if (!operation || operation === "read") return;
|
|
3039
|
+
const filePath = event.resource?.path ?? stringAt(attributes, "filePath");
|
|
3040
|
+
if (!filePath) return;
|
|
3041
|
+
this.stmt(
|
|
3042
|
+
`INSERT OR IGNORE INTO file_lineage
|
|
3043
|
+
(event_id, path, path_key, operation, occurred_at, session_id, agent_id, task_id, board_id, run_id,
|
|
3044
|
+
tool_name, provider_id, model_id, logical_request_id, prompt_manifest_id,
|
|
3045
|
+
provenance_confidence, source)
|
|
3046
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
3047
|
+
).run(
|
|
3048
|
+
event.eventId,
|
|
3049
|
+
normalizeKey(filePath),
|
|
3050
|
+
normalizePathKey(filePath),
|
|
3051
|
+
operation,
|
|
3052
|
+
event.occurredAt ?? event.observedAt,
|
|
3053
|
+
event.scope.sessionId ?? "",
|
|
3054
|
+
event.scope.agentId ?? "",
|
|
3055
|
+
event.scope.taskId ?? stringAt(attributes, "taskId") ?? "",
|
|
3056
|
+
event.scope.kanbanBoardId ?? stringAt(attributes, "boardId") ?? "",
|
|
3057
|
+
stringAt(attributes, "runId") ?? "",
|
|
3058
|
+
stringAt(attributes, "toolName") ?? "",
|
|
3059
|
+
event.runtime?.providerId ?? stringAt(attributes, "provider") ?? "",
|
|
3060
|
+
event.runtime?.modelId ?? stringAt(attributes, "model") ?? "",
|
|
3061
|
+
event.correlation.logicalRequestId ?? stringAt(attributes, "logicalRequestId") ?? "",
|
|
3062
|
+
event.correlation.promptManifestId ?? stringAt(attributes, "promptManifestId") ?? "",
|
|
3063
|
+
provenanceConfidence(attributes),
|
|
3064
|
+
stringAt(attributes, "source") ?? (event.eventType === "file.event" ? "tool" : "external")
|
|
3065
|
+
);
|
|
3066
|
+
}
|
|
3067
|
+
};
|
|
3068
|
+
function provenanceConfidence(attributes) {
|
|
3069
|
+
const value = attributes["provenanceConfidence"];
|
|
3070
|
+
return value === "explicit" || value === "correlated" || value === "inferred" ? value : "unknown";
|
|
3071
|
+
}
|
|
3072
|
+
|
|
3073
|
+
// src/chronicle/metrics-store.ts
|
|
3074
|
+
var ChronicleMetricsStore = class _ChronicleMetricsStore {
|
|
3075
|
+
db;
|
|
3076
|
+
directory;
|
|
3077
|
+
dbPath;
|
|
3078
|
+
ingester;
|
|
3079
|
+
constructor(directory) {
|
|
3080
|
+
this.directory = path8.resolve(directory);
|
|
3081
|
+
this.dbPath = path8.join(this.directory, "metrics.db");
|
|
3082
|
+
const Database = loadDatabaseSync2();
|
|
3083
|
+
this.db = new Database(this.dbPath);
|
|
3084
|
+
this.db.exec("PRAGMA journal_mode = WAL");
|
|
3085
|
+
ensureMetricsSchema(this.db);
|
|
3086
|
+
this.ingester = new ChronicleMetricsIngester(this.db, this.directory, this.dbPath);
|
|
3087
|
+
}
|
|
3088
|
+
static open(chronicleDirectory) {
|
|
3089
|
+
return new _ChronicleMetricsStore(chronicleDirectory);
|
|
3090
|
+
}
|
|
3091
|
+
close() {
|
|
3092
|
+
this.db.close();
|
|
3093
|
+
}
|
|
3094
|
+
/** Incrementally ingest journal bytes appended since the last refresh.
|
|
3095
|
+
* Safe across processes: guarded by a file lock on the database path. */
|
|
3096
|
+
async refresh() {
|
|
3097
|
+
return this.ingester.refresh();
|
|
3098
|
+
}
|
|
3099
|
+
providerDaily(options = {}) {
|
|
3100
|
+
const clauses = [];
|
|
3101
|
+
const params = [];
|
|
3102
|
+
if (options.from) {
|
|
3103
|
+
clauses.push("day >= ?");
|
|
3104
|
+
params.push(options.from.slice(0, 10));
|
|
3105
|
+
}
|
|
3106
|
+
if (options.to) {
|
|
3107
|
+
clauses.push("day <= ?");
|
|
3108
|
+
params.push(options.to.slice(0, 10));
|
|
3109
|
+
}
|
|
3110
|
+
const where = clauses.length > 0 ? ` WHERE ${clauses.join(" AND ")}` : "";
|
|
3111
|
+
const rows = this.db.prepare(
|
|
3112
|
+
`SELECT day, provider_id, model_id, attempts, completed, failed, retries, fallbacks,
|
|
3113
|
+
input_tokens, output_tokens, cache_read_tokens, cache_write_tokens,
|
|
3114
|
+
duration_ms_total, duration_ms_max, duration_count
|
|
3115
|
+
FROM provider_daily${where} ORDER BY day DESC, provider_id, model_id`
|
|
3116
|
+
).all(...params);
|
|
3117
|
+
return rows.map((row) => ({
|
|
3118
|
+
day: String(row.day),
|
|
3119
|
+
providerId: String(row.provider_id),
|
|
3120
|
+
modelId: String(row.model_id),
|
|
3121
|
+
attempts: Number(row.attempts),
|
|
3122
|
+
completed: Number(row.completed),
|
|
3123
|
+
failed: Number(row.failed),
|
|
3124
|
+
retries: Number(row.retries),
|
|
3125
|
+
fallbacks: Number(row.fallbacks),
|
|
3126
|
+
inputTokens: Number(row.input_tokens),
|
|
3127
|
+
outputTokens: Number(row.output_tokens),
|
|
3128
|
+
cacheReadTokens: Number(row.cache_read_tokens),
|
|
3129
|
+
cacheWriteTokens: Number(row.cache_write_tokens),
|
|
3130
|
+
avgDurationMs: Number(row.duration_count) > 0 ? Number(row.duration_ms_total) / Number(row.duration_count) : 0,
|
|
3131
|
+
maxDurationMs: Number(row.duration_ms_max)
|
|
3132
|
+
}));
|
|
3133
|
+
}
|
|
3134
|
+
taskOutcomes(options = {}) {
|
|
3135
|
+
const clauses = [];
|
|
3136
|
+
const params = [];
|
|
3137
|
+
if (options.runId) {
|
|
3138
|
+
clauses.push("t.run_id = ?");
|
|
3139
|
+
params.push(options.runId);
|
|
3140
|
+
}
|
|
3141
|
+
if (options.boardId) {
|
|
3142
|
+
clauses.push("t.board_id = ?");
|
|
3143
|
+
params.push(options.boardId);
|
|
3144
|
+
}
|
|
3145
|
+
if (options.sessionId) {
|
|
3146
|
+
clauses.push("t.session_id = ?");
|
|
3147
|
+
params.push(options.sessionId);
|
|
3148
|
+
}
|
|
3149
|
+
if (options.status) {
|
|
3150
|
+
clauses.push("t.status = ?");
|
|
3151
|
+
params.push(options.status);
|
|
3152
|
+
}
|
|
3153
|
+
const where = clauses.length > 0 ? ` WHERE ${clauses.join(" AND ")}` : "";
|
|
3154
|
+
params.push(clampLimit(options.limit, 100));
|
|
3155
|
+
const rows = this.db.prepare(
|
|
3156
|
+
`SELECT t.*, (SELECT COUNT(*) FROM file_lineage f WHERE f.task_id = t.task_id) AS files_touched
|
|
3157
|
+
FROM task_outcomes t${where}
|
|
3158
|
+
ORDER BY COALESCE(t.started_at, '') DESC LIMIT ?`
|
|
3159
|
+
).all(...params);
|
|
3160
|
+
return rows.map((row) => ({
|
|
3161
|
+
taskId: String(row.task_id),
|
|
3162
|
+
runId: String(row.run_id),
|
|
3163
|
+
boardId: String(row.board_id),
|
|
3164
|
+
sessionId: String(row.session_id),
|
|
3165
|
+
agentId: String(row.agent_id),
|
|
3166
|
+
status: String(row.status),
|
|
3167
|
+
startedAt: row.started_at === null ? null : String(row.started_at),
|
|
3168
|
+
endedAt: row.ended_at === null ? null : String(row.ended_at),
|
|
3169
|
+
durationMs: row.duration_ms === null ? null : Number(row.duration_ms),
|
|
3170
|
+
retries: Number(row.retries),
|
|
3171
|
+
verificationFailures: Number(row.verification_failures),
|
|
3172
|
+
filesTouched: Number(row.files_touched)
|
|
3173
|
+
}));
|
|
3174
|
+
}
|
|
3175
|
+
fileLineage(options = {}) {
|
|
3176
|
+
const clauses = [];
|
|
3177
|
+
const params = [];
|
|
3178
|
+
if (options.path) {
|
|
3179
|
+
clauses.push("path_key = ?");
|
|
3180
|
+
params.push(normalizePathKey(options.path));
|
|
3181
|
+
}
|
|
3182
|
+
if (options.paths) {
|
|
3183
|
+
const pathKeys = [...new Set(options.paths.map(normalizePathKey))];
|
|
3184
|
+
if (pathKeys.length === 0) return [];
|
|
3185
|
+
clauses.push(`path_key IN (${pathKeys.map(() => "?").join(",")})`);
|
|
3186
|
+
params.push(...pathKeys);
|
|
3187
|
+
}
|
|
3188
|
+
if (options.taskId) {
|
|
3189
|
+
clauses.push("task_id = ?");
|
|
3190
|
+
params.push(options.taskId);
|
|
3191
|
+
}
|
|
3192
|
+
if (options.boardId) {
|
|
3193
|
+
clauses.push("board_id = ?");
|
|
3194
|
+
params.push(options.boardId);
|
|
3195
|
+
}
|
|
3196
|
+
if (options.sessionId) {
|
|
3197
|
+
clauses.push("session_id = ?");
|
|
3198
|
+
params.push(options.sessionId);
|
|
3199
|
+
}
|
|
3200
|
+
const where = clauses.length > 0 ? ` WHERE ${clauses.join(" AND ")}` : "";
|
|
3201
|
+
params.push(clampLimit(options.limit, 200));
|
|
3202
|
+
const projection = `path, operation, occurred_at, session_id, agent_id, task_id, board_id, run_id,
|
|
3203
|
+
tool_name, provider_id, model_id, logical_request_id, prompt_manifest_id,
|
|
3204
|
+
provenance_confidence, source`;
|
|
3205
|
+
const sql = options.latestPerPath ? `SELECT ${projection} FROM (
|
|
3206
|
+
SELECT ${projection}, ROW_NUMBER() OVER (
|
|
3207
|
+
PARTITION BY path_key ORDER BY occurred_at DESC, event_id DESC
|
|
3208
|
+
) AS path_rank
|
|
3209
|
+
FROM file_lineage${where}
|
|
3210
|
+
) WHERE path_rank = 1 ORDER BY occurred_at DESC LIMIT ?` : `SELECT ${projection}
|
|
3211
|
+
FROM file_lineage${where} ORDER BY occurred_at DESC LIMIT ?`;
|
|
3212
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
3213
|
+
return rows.map((row) => ({
|
|
3214
|
+
path: row.path,
|
|
3215
|
+
operation: row.operation,
|
|
3216
|
+
occurredAt: row.occurred_at,
|
|
3217
|
+
sessionId: row.session_id,
|
|
3218
|
+
agentId: row.agent_id,
|
|
3219
|
+
taskId: row.task_id,
|
|
3220
|
+
boardId: row.board_id,
|
|
3221
|
+
runId: row.run_id,
|
|
3222
|
+
toolName: row.tool_name,
|
|
3223
|
+
providerId: row.provider_id,
|
|
3224
|
+
modelId: row.model_id,
|
|
3225
|
+
logicalRequestId: row.logical_request_id,
|
|
3226
|
+
promptManifestId: row.prompt_manifest_id,
|
|
3227
|
+
provenanceConfidence: row.provenance_confidence,
|
|
3228
|
+
source: row.source
|
|
3229
|
+
}));
|
|
3230
|
+
}
|
|
3231
|
+
summary() {
|
|
3232
|
+
const provider = this.db.prepare(
|
|
3233
|
+
"SELECT COALESCE(SUM(attempts),0) a, COALESCE(SUM(completed),0) c, COALESCE(SUM(failed),0) f FROM provider_daily"
|
|
3234
|
+
).get();
|
|
3235
|
+
const tasks = {};
|
|
3236
|
+
for (const row of this.db.prepare("SELECT status, COUNT(*) n FROM task_outcomes GROUP BY status").all()) {
|
|
3237
|
+
tasks[row.status] = Number(row.n);
|
|
3238
|
+
}
|
|
3239
|
+
const files = this.db.prepare("SELECT COUNT(*) n, COUNT(DISTINCT path) p FROM file_lineage").get();
|
|
3240
|
+
const cost = this.db.prepare("SELECT COALESCE(SUM(cost),0) c FROM token_cost").get();
|
|
3241
|
+
const terminal = Number(provider.c) + Number(provider.f);
|
|
3242
|
+
return {
|
|
3243
|
+
providers: {
|
|
3244
|
+
attempts: Number(provider.a),
|
|
3245
|
+
completed: Number(provider.c),
|
|
3246
|
+
failed: Number(provider.f),
|
|
3247
|
+
successRate: terminal > 0 ? Number(provider.c) / terminal : 0
|
|
3248
|
+
},
|
|
3249
|
+
tasks,
|
|
3250
|
+
files: { mutations: Number(files.n), uniquePaths: Number(files.p) },
|
|
3251
|
+
estimatedCostUsd: Number(cost.c)
|
|
3252
|
+
};
|
|
3253
|
+
}
|
|
3254
|
+
/**
|
|
3255
|
+
* A `ChronicleSummary` for the default/unfiltered dashboard view — only
|
|
3256
|
+
* `from`/`to` (day-precision) narrow it.
|
|
3257
|
+
*/
|
|
3258
|
+
defaultSummary(options = {}) {
|
|
3259
|
+
const fromDay = options.from?.slice(0, 10);
|
|
3260
|
+
const toDay = options.to?.slice(0, 10);
|
|
3261
|
+
const dayFilter = (column) => {
|
|
3262
|
+
const clauses = [];
|
|
3263
|
+
const params = [];
|
|
3264
|
+
if (fromDay) {
|
|
3265
|
+
clauses.push(`${column} >= ?`);
|
|
3266
|
+
params.push(fromDay);
|
|
3267
|
+
}
|
|
3268
|
+
if (toDay) {
|
|
3269
|
+
clauses.push(`${column} <= ?`);
|
|
3270
|
+
params.push(toDay);
|
|
3271
|
+
}
|
|
3272
|
+
return { where: clauses.length > 0 ? ` WHERE ${clauses.join(" AND ")}` : "", params };
|
|
3273
|
+
};
|
|
3274
|
+
const providerRange = dayFilter("day");
|
|
3275
|
+
const provider = this.db.prepare(
|
|
3276
|
+
`SELECT COALESCE(SUM(attempts),0) attempts, COALESCE(SUM(completed),0) completed, COALESCE(SUM(failed),0) failed,
|
|
3277
|
+
COALESCE(SUM(retries),0) retries, COALESCE(SUM(fallbacks),0) fallbacks,
|
|
3278
|
+
COUNT(DISTINCT provider_id) providers, COUNT(DISTINCT model_id) models,
|
|
3279
|
+
COALESCE(SUM(input_tokens),0) inputTokens, COALESCE(SUM(output_tokens),0) outputTokens,
|
|
3280
|
+
COALESCE(SUM(cache_read_tokens),0) cacheReadTokens, COALESCE(SUM(cache_write_tokens),0) cacheWriteTokens,
|
|
3281
|
+
COALESCE(SUM(duration_ms_total),0) durationTotal, COALESCE(MAX(duration_ms_max),0) durationMax,
|
|
3282
|
+
COALESCE(SUM(duration_count),0) durationCount
|
|
3283
|
+
FROM provider_daily${providerRange.where}`
|
|
3284
|
+
).get(...providerRange.params);
|
|
3285
|
+
const counterRange = dayFilter("day");
|
|
3286
|
+
const counters = this.db.prepare(
|
|
3287
|
+
`SELECT COALESCE(SUM(tool_calls),0) toolCalls, COALESCE(SUM(completed_tools),0) completedTools,
|
|
3288
|
+
COALESCE(SUM(failed_tools),0) failedTools, COALESCE(SUM(tool_duration_ms_total),0) toolDurationTotal,
|
|
3289
|
+
COALESCE(SUM(tool_duration_count),0) toolDurationCount, COALESCE(SUM(processes),0) processes,
|
|
3290
|
+
COALESCE(SUM(failed_processes),0) failedProcesses, COALESCE(SUM(file_events_all),0) fileEvents,
|
|
3291
|
+
COALESCE(SUM(decisions),0) decisions, COALESCE(SUM(escalations),0) escalations,
|
|
3292
|
+
COALESCE(SUM(agent_events),0) agentEvents, COALESCE(SUM(failures),0) failures,
|
|
3293
|
+
COALESCE(SUM(cancellations),0) cancellations
|
|
3294
|
+
FROM daily_counters${counterRange.where}`
|
|
3295
|
+
).get(...counterRange.params);
|
|
3296
|
+
const familyRange = dayFilter("day");
|
|
3297
|
+
const familyRows = this.db.prepare(`SELECT family, count, failure_count FROM family_daily${familyRange.where}`).all(...familyRange.params);
|
|
3298
|
+
const families = { ...EMPTY_FAMILIES };
|
|
3299
|
+
const failuresByFamily = { ...EMPTY_FAMILIES };
|
|
3300
|
+
for (const row of familyRows) {
|
|
3301
|
+
const family = row.family;
|
|
3302
|
+
families[family] = Number(row.count);
|
|
3303
|
+
failuresByFamily[family] = Number(row.failure_count);
|
|
3304
|
+
}
|
|
3305
|
+
const agentRange = dayFilter("day");
|
|
3306
|
+
const uniqueAgents = this.db.prepare(`SELECT COUNT(DISTINCT agent_id) n FROM agent_daily${agentRange.where}`).get(...agentRange.params).n;
|
|
3307
|
+
const requestRange = dayFilter("day");
|
|
3308
|
+
const logicalRequests = this.db.prepare(
|
|
3309
|
+
`SELECT COUNT(DISTINCT logical_request_id) n FROM logical_request_daily${requestRange.where}`
|
|
3310
|
+
).get(...requestRange.params).n;
|
|
3311
|
+
const fileRange = dayFilter("day");
|
|
3312
|
+
const uniqueFiles = this.db.prepare(`SELECT COUNT(DISTINCT path_key) n FROM file_seen_daily${fileRange.where}`).get(...fileRange.params).n;
|
|
3313
|
+
const costRange = dayFilter("day");
|
|
3314
|
+
const cost = this.db.prepare(`SELECT COALESCE(SUM(cost),0) c FROM token_cost${costRange.where}`).get(...costRange.params).c;
|
|
3315
|
+
return {
|
|
3316
|
+
logicalRequests: Number(logicalRequests),
|
|
3317
|
+
modelAttempts: Number(provider.attempts),
|
|
3318
|
+
completedAttempts: Number(provider.completed),
|
|
3319
|
+
failedAttempts: Number(provider.failed),
|
|
3320
|
+
scheduledRetries: Number(provider.retries),
|
|
3321
|
+
fallbacks: Number(provider.fallbacks),
|
|
3322
|
+
providers: Number(provider.providers),
|
|
3323
|
+
models: Number(provider.models),
|
|
3324
|
+
inputTokens: Number(provider.inputTokens),
|
|
3325
|
+
outputTokens: Number(provider.outputTokens),
|
|
3326
|
+
cacheReadTokens: Number(provider.cacheReadTokens),
|
|
3327
|
+
cacheWriteTokens: Number(provider.cacheWriteTokens),
|
|
3328
|
+
estimatedCostUsd: Number(cost),
|
|
3329
|
+
providerAvgDurationMs: Number(provider.durationCount) > 0 ? Number(provider.durationTotal) / Number(provider.durationCount) : 0,
|
|
3330
|
+
providerP95DurationMs: Number(provider.durationMax),
|
|
3331
|
+
toolCalls: Number(counters.toolCalls),
|
|
3332
|
+
completedTools: Number(counters.completedTools),
|
|
3333
|
+
failedTools: Number(counters.failedTools),
|
|
3334
|
+
toolAvgDurationMs: Number(counters.toolDurationCount) > 0 ? Number(counters.toolDurationTotal) / Number(counters.toolDurationCount) : 0,
|
|
3335
|
+
processes: Number(counters.processes),
|
|
3336
|
+
failedProcesses: Number(counters.failedProcesses),
|
|
3337
|
+
fileEvents: Number(counters.fileEvents),
|
|
3338
|
+
uniqueFiles: Number(uniqueFiles),
|
|
3339
|
+
agentEvents: Number(counters.agentEvents),
|
|
3340
|
+
uniqueAgents: Number(uniqueAgents),
|
|
3341
|
+
decisions: Number(counters.decisions),
|
|
3342
|
+
escalations: Number(counters.escalations),
|
|
3343
|
+
failures: Number(counters.failures),
|
|
3344
|
+
cancellations: Number(counters.cancellations),
|
|
3345
|
+
families,
|
|
3346
|
+
failuresByFamily
|
|
3347
|
+
};
|
|
3348
|
+
}
|
|
3349
|
+
};
|
|
3350
|
+
|
|
3351
|
+
// src/chronicle/legacy-journal-import.ts
|
|
3352
|
+
import * as fs6 from "node:fs/promises";
|
|
3353
|
+
import * as path9 from "node:path";
|
|
3354
|
+
var ChronicleImportError = class extends Error {
|
|
3355
|
+
constructor(message, day, sequence) {
|
|
3356
|
+
super(message);
|
|
3357
|
+
this.day = day;
|
|
3358
|
+
this.sequence = sequence;
|
|
3359
|
+
this.name = "ChronicleImportError";
|
|
3360
|
+
}
|
|
3361
|
+
day;
|
|
3362
|
+
sequence;
|
|
3363
|
+
};
|
|
3364
|
+
async function discoverFamilies(directory) {
|
|
3365
|
+
let entries;
|
|
3366
|
+
try {
|
|
3367
|
+
entries = await fs6.readdir(directory);
|
|
3368
|
+
} catch {
|
|
3369
|
+
return [];
|
|
3370
|
+
}
|
|
3371
|
+
const bases = /* @__PURE__ */ new Set();
|
|
3372
|
+
for (const entry of entries) {
|
|
3373
|
+
const match = PARTITION_FILE_PATTERN.exec(entry);
|
|
3374
|
+
if (match?.[1]) bases.add(match[1]);
|
|
3375
|
+
}
|
|
3376
|
+
return [...bases].sort();
|
|
3377
|
+
}
|
|
3378
|
+
function dayOfFamily(familyBase) {
|
|
3379
|
+
return familyBase.replace(/\.events$/u, "");
|
|
3380
|
+
}
|
|
3381
|
+
async function importLegacyChronicleJournal(journal, directory) {
|
|
3382
|
+
if (journal.hasImportedLegacyJournal()) {
|
|
3383
|
+
return {
|
|
3384
|
+
alreadyImported: true,
|
|
3385
|
+
families: 0,
|
|
3386
|
+
events: 0,
|
|
3387
|
+
quarantined: journal.quarantinedFamilies()
|
|
3388
|
+
};
|
|
3389
|
+
}
|
|
3390
|
+
const families = await discoverFamilies(directory);
|
|
3391
|
+
const quarantined = [];
|
|
3392
|
+
const boundary = {};
|
|
3393
|
+
let importedEvents = 0;
|
|
3394
|
+
let importedFamilies = 0;
|
|
3395
|
+
for (const familyBase of families) {
|
|
3396
|
+
const day = dayOfFamily(familyBase);
|
|
3397
|
+
const basePath = path9.join(directory, `${familyBase}.jsonl`);
|
|
3398
|
+
let familyEvents = 0;
|
|
3399
|
+
const familyBoundary = {};
|
|
3400
|
+
if (journal.hasImportedDay(day)) continue;
|
|
3401
|
+
try {
|
|
3402
|
+
await journal.runFamilyImport(async (sink) => {
|
|
3403
|
+
familyEvents = 0;
|
|
3404
|
+
const checkpointResult = await readRetentionCheckpoint(basePath);
|
|
3405
|
+
if (checkpointResult.error) {
|
|
3406
|
+
throw new ChronicleImportError(checkpointResult.error, day, 0);
|
|
3407
|
+
}
|
|
3408
|
+
const checkpoint = checkpointResult.checkpoint;
|
|
3409
|
+
if (checkpoint) sink.checkpoint(day, checkpoint.sequence, checkpoint.hash);
|
|
3410
|
+
let expectedSequence = (checkpoint?.sequence ?? 0) + 1;
|
|
3411
|
+
let previousHash = checkpoint?.hash ?? GENESIS_HASH;
|
|
3412
|
+
for (const partition of await collectPartitions(basePath)) {
|
|
3413
|
+
familyBoundary[path9.relative(directory, partition).replaceAll("\\", "/")] = (await fs6.stat(partition)).size;
|
|
3414
|
+
for await (const event of streamEntriesStrict(partition)) {
|
|
3415
|
+
if (event.sequence !== expectedSequence) {
|
|
3416
|
+
throw new ChronicleImportError(
|
|
3417
|
+
`sequence gap in ${day}: expected ${expectedSequence}, found ${event.sequence}`,
|
|
3418
|
+
day,
|
|
3419
|
+
event.sequence
|
|
3420
|
+
);
|
|
3421
|
+
}
|
|
3422
|
+
if (event.previousHash !== previousHash) {
|
|
3423
|
+
throw new ChronicleImportError(
|
|
3424
|
+
`previous hash mismatch in ${day} at sequence ${event.sequence}`,
|
|
3425
|
+
day,
|
|
3426
|
+
event.sequence
|
|
3427
|
+
);
|
|
3428
|
+
}
|
|
3429
|
+
if (chronicleEventHash(event) !== event.hash) {
|
|
3430
|
+
throw new ChronicleImportError(
|
|
3431
|
+
`entry hash mismatch in ${day} at sequence ${event.sequence}`,
|
|
3432
|
+
day,
|
|
3433
|
+
event.sequence
|
|
3434
|
+
);
|
|
3435
|
+
}
|
|
3436
|
+
sink.insert(day, event);
|
|
3437
|
+
familyEvents += 1;
|
|
3438
|
+
expectedSequence = event.sequence + 1;
|
|
3439
|
+
previousHash = event.hash;
|
|
3440
|
+
}
|
|
3441
|
+
}
|
|
3442
|
+
});
|
|
3443
|
+
} catch (error) {
|
|
3444
|
+
if (!(error instanceof ChronicleImportError)) throw error;
|
|
3445
|
+
quarantined.push({ day, sequence: error.sequence, reason: error.message });
|
|
3446
|
+
continue;
|
|
3447
|
+
}
|
|
3448
|
+
Object.assign(boundary, familyBoundary);
|
|
3449
|
+
if (familyEvents > 0) importedFamilies += 1;
|
|
3450
|
+
importedEvents += familyEvents;
|
|
3451
|
+
journal.recordLegacyJsonlBoundary(boundary);
|
|
3452
|
+
}
|
|
3453
|
+
journal.recordQuarantinedFamilies(quarantined);
|
|
3454
|
+
journal.recordLegacyJsonlBoundary(boundary);
|
|
3455
|
+
journal.markLegacyJournalImported();
|
|
3456
|
+
return {
|
|
3457
|
+
alreadyImported: false,
|
|
3458
|
+
families: importedFamilies,
|
|
3459
|
+
events: importedEvents,
|
|
3460
|
+
quarantined
|
|
3461
|
+
};
|
|
3462
|
+
}
|
|
3463
|
+
|
|
3464
|
+
// src/chronicle/project-server-protocol.ts
|
|
3465
|
+
var CHRONICLE_PROJECT_SERVER_PROTOCOL_VERSION = 2;
|
|
3466
|
+
var CHRONICLE_PROJECT_SERVER_MAX_FRAME_CHARS = 64 * 1024 * 1024;
|
|
3467
|
+
var CHRONICLE_MAX_APPEND_BATCH = 1e4;
|
|
3468
|
+
function encodeChronicleProjectServerMessage(message) {
|
|
3469
|
+
return `${JSON.stringify(message)}
|
|
3470
|
+
`;
|
|
3471
|
+
}
|
|
3472
|
+
|
|
3473
|
+
// src/chronicle/project-server-endpoint.ts
|
|
3474
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
3475
|
+
import * as os2 from "node:os";
|
|
3476
|
+
import * as path10 from "node:path";
|
|
3477
|
+
var CHRONICLE_PROJECT_SERVER_METADATA_FILE = "server.json";
|
|
3478
|
+
function normalizedPath(value) {
|
|
3479
|
+
const resolved = path10.resolve(value);
|
|
3480
|
+
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
3481
|
+
}
|
|
3482
|
+
function chronicleProjectServerKey(projectDir) {
|
|
3483
|
+
return createHash4("sha256").update(normalizedPath(path10.join(projectDir, "chronicle"))).digest("hex").slice(0, 24);
|
|
3484
|
+
}
|
|
3485
|
+
function chronicleProjectServerEndpoint(projectDir) {
|
|
3486
|
+
const key = chronicleProjectServerKey(projectDir);
|
|
3487
|
+
if (process.platform === "win32") {
|
|
3488
|
+
return `\\\\.\\pipe\\wrongstack-chronicle-v${CHRONICLE_PROJECT_SERVER_PROTOCOL_VERSION}-${key}`;
|
|
3489
|
+
}
|
|
3490
|
+
return path10.join(
|
|
3491
|
+
os2.tmpdir(),
|
|
3492
|
+
`wsch-v${CHRONICLE_PROJECT_SERVER_PROTOCOL_VERSION}`,
|
|
3493
|
+
`${key}.sock`
|
|
3494
|
+
);
|
|
3495
|
+
}
|
|
3496
|
+
function chronicleProjectServerMetadataPath(projectDir) {
|
|
3497
|
+
return path10.join(projectDir, "chronicle", CHRONICLE_PROJECT_SERVER_METADATA_FILE);
|
|
3498
|
+
}
|
|
3499
|
+
|
|
3500
|
+
export {
|
|
3501
|
+
resolveChronicleRuntimeLocation,
|
|
3502
|
+
GENESIS_HASH,
|
|
3503
|
+
CHRONICLE_SCHEMA_VERSION,
|
|
3504
|
+
ChronicleJournal,
|
|
3505
|
+
ChroniclePartitionRangeCache,
|
|
3506
|
+
CHRONICLE_FACET_FIELDS,
|
|
3507
|
+
ChronicleQueryEngine,
|
|
3508
|
+
findChroniclePartitions,
|
|
3509
|
+
ChronicleStorageQuotaError,
|
|
3510
|
+
ChronicleSqliteJournal,
|
|
3511
|
+
isChronicleMetricsAvailable,
|
|
3512
|
+
ChronicleMetricsStore,
|
|
3513
|
+
importLegacyChronicleJournal,
|
|
3514
|
+
CHRONICLE_PROJECT_SERVER_PROTOCOL_VERSION,
|
|
3515
|
+
CHRONICLE_PROJECT_SERVER_MAX_FRAME_CHARS,
|
|
3516
|
+
CHRONICLE_MAX_APPEND_BATCH,
|
|
3517
|
+
encodeChronicleProjectServerMessage,
|
|
3518
|
+
chronicleProjectServerKey,
|
|
3519
|
+
chronicleProjectServerEndpoint,
|
|
3520
|
+
chronicleProjectServerMetadataPath
|
|
3521
|
+
};
|
|
3522
|
+
//# sourceMappingURL=chunk-AT4V4XGI.js.map
|