@meterbility/collector 0.3.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/LICENSE +33 -0
- package/README.md +11 -0
- package/dist/index.d.ts +497 -0
- package/dist/index.js +1475 -0
- package/package.json +51 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1475 @@
|
|
|
1
|
+
// src/store.ts
|
|
2
|
+
import { mkdir as mkdir2 } from "fs/promises";
|
|
3
|
+
import { mkdirSync } from "fs";
|
|
4
|
+
import { dirname as dirname2 } from "path";
|
|
5
|
+
import Database from "better-sqlite3";
|
|
6
|
+
import { dbPath, meterHome } from "@meterbility/shared";
|
|
7
|
+
|
|
8
|
+
// src/schema.ts
|
|
9
|
+
var SCHEMA_VERSION = 5;
|
|
10
|
+
function ensureSchema(db) {
|
|
11
|
+
db.pragma("journal_mode = WAL");
|
|
12
|
+
db.pragma("synchronous = NORMAL");
|
|
13
|
+
db.pragma("foreign_keys = ON");
|
|
14
|
+
db.exec(`
|
|
15
|
+
CREATE TABLE IF NOT EXISTS meta (
|
|
16
|
+
key TEXT PRIMARY KEY,
|
|
17
|
+
value TEXT NOT NULL
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
21
|
+
project_id TEXT PRIMARY KEY,
|
|
22
|
+
name TEXT NOT NULL,
|
|
23
|
+
cwd TEXT NOT NULL UNIQUE,
|
|
24
|
+
created_at TEXT NOT NULL
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
CREATE TABLE IF NOT EXISTS agents (
|
|
28
|
+
agent_id TEXT PRIMARY KEY,
|
|
29
|
+
project_id TEXT NOT NULL REFERENCES projects(project_id),
|
|
30
|
+
name TEXT NOT NULL,
|
|
31
|
+
created_at TEXT NOT NULL,
|
|
32
|
+
UNIQUE(project_id, name)
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
CREATE TABLE IF NOT EXISTS runs (
|
|
36
|
+
run_id TEXT PRIMARY KEY,
|
|
37
|
+
agent_id TEXT NOT NULL REFERENCES agents(agent_id),
|
|
38
|
+
project_id TEXT NOT NULL REFERENCES projects(project_id),
|
|
39
|
+
source_session_id TEXT,
|
|
40
|
+
source_runtime TEXT NOT NULL,
|
|
41
|
+
title TEXT,
|
|
42
|
+
status TEXT NOT NULL,
|
|
43
|
+
started_at TEXT NOT NULL,
|
|
44
|
+
ended_at TEXT,
|
|
45
|
+
git_branch TEXT,
|
|
46
|
+
cwd TEXT,
|
|
47
|
+
fork_origin_run_id TEXT REFERENCES runs(run_id),
|
|
48
|
+
fork_origin_step_id TEXT,
|
|
49
|
+
tokens_total_input INTEGER NOT NULL DEFAULT 0,
|
|
50
|
+
tokens_total_output INTEGER NOT NULL DEFAULT 0,
|
|
51
|
+
tokens_total_cached INTEGER NOT NULL DEFAULT 0,
|
|
52
|
+
cost_cents REAL NOT NULL DEFAULT 0,
|
|
53
|
+
step_count INTEGER NOT NULL DEFAULT 0,
|
|
54
|
+
tags TEXT NOT NULL DEFAULT '[]'
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
CREATE INDEX IF NOT EXISTS idx_runs_project_started
|
|
58
|
+
ON runs(project_id, started_at DESC);
|
|
59
|
+
CREATE INDEX IF NOT EXISTS idx_runs_session
|
|
60
|
+
ON runs(source_session_id);
|
|
61
|
+
CREATE INDEX IF NOT EXISTS idx_runs_fork_origin
|
|
62
|
+
ON runs(fork_origin_run_id);
|
|
63
|
+
|
|
64
|
+
CREATE TABLE IF NOT EXISTS steps (
|
|
65
|
+
step_id TEXT PRIMARY KEY,
|
|
66
|
+
run_id TEXT NOT NULL REFERENCES runs(run_id) ON DELETE CASCADE,
|
|
67
|
+
parent_step_id TEXT,
|
|
68
|
+
fork_origin_id TEXT,
|
|
69
|
+
sequence INTEGER NOT NULL,
|
|
70
|
+
timestamp TEXT NOT NULL,
|
|
71
|
+
model TEXT NOT NULL,
|
|
72
|
+
context_snapshot_id TEXT NOT NULL,
|
|
73
|
+
decision_ref TEXT NOT NULL,
|
|
74
|
+
action_json TEXT NOT NULL,
|
|
75
|
+
outcome_json TEXT NOT NULL,
|
|
76
|
+
tokens_input INTEGER NOT NULL DEFAULT 0,
|
|
77
|
+
tokens_output INTEGER NOT NULL DEFAULT 0,
|
|
78
|
+
tokens_cached_read INTEGER NOT NULL DEFAULT 0,
|
|
79
|
+
tokens_cache_creation INTEGER NOT NULL DEFAULT 0,
|
|
80
|
+
tokens_cache_creation_1h INTEGER NOT NULL DEFAULT 0,
|
|
81
|
+
tokens_reasoning INTEGER,
|
|
82
|
+
latency_ms INTEGER NOT NULL DEFAULT 0,
|
|
83
|
+
cost_cents REAL NOT NULL DEFAULT 0,
|
|
84
|
+
status TEXT NOT NULL,
|
|
85
|
+
tags TEXT NOT NULL DEFAULT '[]',
|
|
86
|
+
UNIQUE(run_id, sequence)
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
CREATE INDEX IF NOT EXISTS idx_steps_run
|
|
90
|
+
ON steps(run_id, sequence);
|
|
91
|
+
CREATE INDEX IF NOT EXISTS idx_steps_context
|
|
92
|
+
ON steps(context_snapshot_id);
|
|
93
|
+
|
|
94
|
+
-- Stored context snapshots. The 'id' is the SHA256 of the canonicalized
|
|
95
|
+
-- snapshot JSON; the snapshot JSON itself lives in the blob store.
|
|
96
|
+
CREATE TABLE IF NOT EXISTS context_snapshots (
|
|
97
|
+
snapshot_id TEXT PRIMARY KEY,
|
|
98
|
+
blob_ref TEXT NOT NULL,
|
|
99
|
+
component_count INTEGER NOT NULL DEFAULT 0,
|
|
100
|
+
created_at TEXT NOT NULL
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
CREATE TABLE IF NOT EXISTS forks (
|
|
104
|
+
fork_id TEXT PRIMARY KEY,
|
|
105
|
+
origin_run_id TEXT NOT NULL REFERENCES runs(run_id),
|
|
106
|
+
origin_step_id TEXT NOT NULL,
|
|
107
|
+
fork_run_id TEXT NOT NULL REFERENCES runs(run_id),
|
|
108
|
+
edit_type TEXT NOT NULL,
|
|
109
|
+
edit_payload_json TEXT NOT NULL,
|
|
110
|
+
created_at TEXT NOT NULL
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
CREATE INDEX IF NOT EXISTS idx_forks_origin
|
|
114
|
+
ON forks(origin_run_id);
|
|
115
|
+
|
|
116
|
+
CREATE TABLE IF NOT EXISTS annotations (
|
|
117
|
+
annotation_id TEXT PRIMARY KEY,
|
|
118
|
+
target_kind TEXT NOT NULL,
|
|
119
|
+
target_id TEXT NOT NULL,
|
|
120
|
+
author TEXT NOT NULL,
|
|
121
|
+
verdict TEXT,
|
|
122
|
+
note TEXT,
|
|
123
|
+
kind TEXT NOT NULL DEFAULT 'comment'
|
|
124
|
+
CHECK (kind IN ('comment', 'probe_pause', 'probe_edit', 'capture_skipped')),
|
|
125
|
+
created_at TEXT NOT NULL
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
CREATE INDEX IF NOT EXISTS idx_annotations_target
|
|
129
|
+
ON annotations(target_kind, target_id);
|
|
130
|
+
-- idx_annotations_kind is created AFTER ensureColumn below so
|
|
131
|
+
-- legacy v4 DBs (which lack the kind column at executescript
|
|
132
|
+
-- time) don't trip on referencing a column that doesn't exist
|
|
133
|
+
-- yet. For fresh DBs the column is created above so the index
|
|
134
|
+
-- still fires on the same call path, just a few statements later.
|
|
135
|
+
|
|
136
|
+
-- Idempotency aid for the Claude Code adapter: remember the last byte
|
|
137
|
+
-- offset we ingested per session file so we can resume cheaply.
|
|
138
|
+
CREATE TABLE IF NOT EXISTS ingest_progress (
|
|
139
|
+
source_runtime TEXT NOT NULL,
|
|
140
|
+
source_path TEXT NOT NULL,
|
|
141
|
+
last_offset INTEGER NOT NULL,
|
|
142
|
+
last_ingested_at TEXT NOT NULL,
|
|
143
|
+
PRIMARY KEY (source_runtime, source_path)
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
CREATE TABLE IF NOT EXISTS settings (
|
|
147
|
+
key TEXT PRIMARY KEY,
|
|
148
|
+
value TEXT NOT NULL,
|
|
149
|
+
updated_at TEXT NOT NULL
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
CREATE TABLE IF NOT EXISTS redaction_log (
|
|
153
|
+
blob_ref TEXT NOT NULL,
|
|
154
|
+
rule TEXT NOT NULL,
|
|
155
|
+
count INTEGER NOT NULL,
|
|
156
|
+
created_at TEXT NOT NULL
|
|
157
|
+
);
|
|
158
|
+
CREATE INDEX IF NOT EXISTS idx_redaction_blob
|
|
159
|
+
ON redaction_log(blob_ref);
|
|
160
|
+
|
|
161
|
+
-- \u2500\u2500\u2500 v0.3 Track A: per-step file change capture \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
162
|
+
-- One row per file mutation attributed to a Step. Vendor-neutral \u2014
|
|
163
|
+
-- Claude Code hook adapter writes v0.3; Codex / SDK / proxy / file
|
|
164
|
+
-- watcher fill in additional source paths in v0.4. Path-keyed
|
|
165
|
+
-- queries are core (per v0.3 \xA73.1.2: "show me every change to
|
|
166
|
+
-- src/auth.ts across this run"), so indexes cover (run_id, path)
|
|
167
|
+
-- as well as the step join.
|
|
168
|
+
--
|
|
169
|
+
-- before_blob_ref / after_blob_ref are content addresses into the
|
|
170
|
+
-- blob store. NULL sentinels carry meaning: NULL before = 'create'
|
|
171
|
+
-- (or partial); NULL after = 'delete' (or partial). The
|
|
172
|
+
-- partial_diff flag distinguishes "we didn't capture this" from
|
|
173
|
+
-- "this side genuinely doesn't exist" so the UI can render the
|
|
174
|
+
-- right affordance.
|
|
175
|
+
--
|
|
176
|
+
-- CHECK constraints declare the full enum even where v0.3 only
|
|
177
|
+
-- writes a subset \u2014 see the additive-only note above the schema
|
|
178
|
+
-- version constant.
|
|
179
|
+
CREATE TABLE IF NOT EXISTS file_change (
|
|
180
|
+
file_change_id TEXT PRIMARY KEY,
|
|
181
|
+
run_id TEXT NOT NULL REFERENCES runs(run_id) ON DELETE CASCADE,
|
|
182
|
+
step_id TEXT NOT NULL REFERENCES steps(step_id) ON DELETE CASCADE,
|
|
183
|
+
sequence INTEGER NOT NULL,
|
|
184
|
+
tool_call_id TEXT,
|
|
185
|
+
derived_from TEXT NOT NULL
|
|
186
|
+
CHECK (derived_from IN ('tool_call','filesystem_watch','git_diff')),
|
|
187
|
+
path TEXT NOT NULL,
|
|
188
|
+
old_path TEXT,
|
|
189
|
+
op TEXT NOT NULL
|
|
190
|
+
CHECK (op IN ('create','modify','delete','rename','chmod')),
|
|
191
|
+
before_blob_ref TEXT,
|
|
192
|
+
after_blob_ref TEXT,
|
|
193
|
+
partial_diff INTEGER NOT NULL DEFAULT 0,
|
|
194
|
+
gitignored INTEGER NOT NULL DEFAULT 0,
|
|
195
|
+
patch_text TEXT,
|
|
196
|
+
patch_format TEXT
|
|
197
|
+
CHECK (patch_format IN ('unified','binary','notebook_cell')
|
|
198
|
+
OR patch_format IS NULL),
|
|
199
|
+
encoding TEXT,
|
|
200
|
+
bom INTEGER NOT NULL DEFAULT 0,
|
|
201
|
+
line_endings TEXT,
|
|
202
|
+
mime TEXT,
|
|
203
|
+
language TEXT,
|
|
204
|
+
size_before INTEGER,
|
|
205
|
+
size_after INTEGER,
|
|
206
|
+
line_count_before INTEGER,
|
|
207
|
+
line_count_after INTEGER,
|
|
208
|
+
lines_added INTEGER NOT NULL DEFAULT 0,
|
|
209
|
+
lines_removed INTEGER NOT NULL DEFAULT 0,
|
|
210
|
+
mode_before INTEGER,
|
|
211
|
+
mode_after INTEGER,
|
|
212
|
+
source_tool_name TEXT,
|
|
213
|
+
source_tool_input TEXT,
|
|
214
|
+
redacted INTEGER NOT NULL DEFAULT 0,
|
|
215
|
+
normalizer_notes TEXT,
|
|
216
|
+
created_at TEXT NOT NULL,
|
|
217
|
+
UNIQUE(step_id, sequence)
|
|
218
|
+
);
|
|
219
|
+
CREATE INDEX IF NOT EXISTS idx_fc_run_step
|
|
220
|
+
ON file_change(run_id, step_id);
|
|
221
|
+
CREATE INDEX IF NOT EXISTS idx_fc_run_path
|
|
222
|
+
ON file_change(run_id, path);
|
|
223
|
+
CREATE INDEX IF NOT EXISTS idx_fc_step
|
|
224
|
+
ON file_change(step_id);
|
|
225
|
+
CREATE INDEX IF NOT EXISTS idx_fc_path_seq
|
|
226
|
+
ON file_change(run_id, path, step_id);
|
|
227
|
+
|
|
228
|
+
-- \u2500\u2500\u2500 v0.3 Track A: baseline working tree \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
229
|
+
-- Content-addressed snapshot of the project's files at Run start,
|
|
230
|
+
-- captured lazily on the first FileChange (per v0.3 \xA73.5). The
|
|
231
|
+
-- replay algorithm (\xA73.6) layers FileChange rows on top of this to
|
|
232
|
+
-- compute working_tree_at(run, step) in O(touched_paths) instead of
|
|
233
|
+
-- O(total_files_in_repo).
|
|
234
|
+
--
|
|
235
|
+
-- manifest_blob_ref points at a sorted, NUL-separated, newline-
|
|
236
|
+
-- delimited blob (each record is path + NUL + mode + NUL + blob_ref
|
|
237
|
+
-- followed by a newline). Sortedness gives byte-identical manifests
|
|
238
|
+
-- for identical trees, which dedups via SHA naturally \u2014 two runs
|
|
239
|
+
-- against the same git HEAD share one baseline blob.
|
|
240
|
+
--
|
|
241
|
+
-- git_head / git_dirty are advisory metadata only. Meterbility never
|
|
242
|
+
-- depends on git being present.
|
|
243
|
+
CREATE TABLE IF NOT EXISTS baseline_tree (
|
|
244
|
+
baseline_tree_id TEXT PRIMARY KEY,
|
|
245
|
+
project_id TEXT NOT NULL REFERENCES projects(project_id),
|
|
246
|
+
manifest_blob_ref TEXT NOT NULL,
|
|
247
|
+
git_head TEXT,
|
|
248
|
+
git_dirty INTEGER NOT NULL DEFAULT 0,
|
|
249
|
+
captured_at TEXT NOT NULL
|
|
250
|
+
);
|
|
251
|
+
CREATE INDEX IF NOT EXISTS idx_bt_project
|
|
252
|
+
ON baseline_tree(project_id);
|
|
253
|
+
CREATE INDEX IF NOT EXISTS idx_bt_git
|
|
254
|
+
ON baseline_tree(project_id, git_head);
|
|
255
|
+
|
|
256
|
+
-- Regression suite (v0.1). A test = name + assertion list.
|
|
257
|
+
-- Optionally references a canonical run that originated the test.
|
|
258
|
+
CREATE TABLE IF NOT EXISTS regression_tests (
|
|
259
|
+
test_id TEXT PRIMARY KEY,
|
|
260
|
+
name TEXT NOT NULL UNIQUE,
|
|
261
|
+
description TEXT,
|
|
262
|
+
assertions_json TEXT NOT NULL,
|
|
263
|
+
canonical_run_id TEXT REFERENCES runs(run_id),
|
|
264
|
+
created_at TEXT NOT NULL
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
CREATE TABLE IF NOT EXISTS regression_results (
|
|
268
|
+
result_id TEXT PRIMARY KEY,
|
|
269
|
+
test_id TEXT NOT NULL REFERENCES regression_tests(test_id) ON DELETE CASCADE,
|
|
270
|
+
run_id TEXT NOT NULL REFERENCES runs(run_id) ON DELETE CASCADE,
|
|
271
|
+
passed INTEGER NOT NULL,
|
|
272
|
+
details_json TEXT NOT NULL,
|
|
273
|
+
created_at TEXT NOT NULL
|
|
274
|
+
);
|
|
275
|
+
CREATE INDEX IF NOT EXISTS idx_regression_results_test
|
|
276
|
+
ON regression_results(test_id, created_at DESC);
|
|
277
|
+
`);
|
|
278
|
+
ensureColumn(
|
|
279
|
+
db,
|
|
280
|
+
"steps",
|
|
281
|
+
"tokens_cache_creation_1h",
|
|
282
|
+
"INTEGER NOT NULL DEFAULT 0"
|
|
283
|
+
);
|
|
284
|
+
ensureColumn(db, "runs", "baseline_tree_id", "TEXT");
|
|
285
|
+
ensureColumn(db, "runs", "probe_state", "TEXT");
|
|
286
|
+
ensureColumn(
|
|
287
|
+
db,
|
|
288
|
+
"annotations",
|
|
289
|
+
"kind",
|
|
290
|
+
"TEXT NOT NULL DEFAULT 'comment'"
|
|
291
|
+
);
|
|
292
|
+
db.exec(
|
|
293
|
+
"CREATE INDEX IF NOT EXISTS idx_annotations_kind ON annotations(kind)"
|
|
294
|
+
);
|
|
295
|
+
const row = db.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get();
|
|
296
|
+
if (!row) {
|
|
297
|
+
db.prepare("INSERT INTO meta(key,value) VALUES(?,?)").run(
|
|
298
|
+
"schema_version",
|
|
299
|
+
String(SCHEMA_VERSION)
|
|
300
|
+
);
|
|
301
|
+
} else if (Number(row.value) < SCHEMA_VERSION) {
|
|
302
|
+
db.prepare("UPDATE meta SET value = ? WHERE key = 'schema_version'").run(
|
|
303
|
+
String(SCHEMA_VERSION)
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
function ensureColumn(db, table, column, ddl) {
|
|
308
|
+
const cols = db.pragma(`table_info(${table})`);
|
|
309
|
+
if (cols.find((c) => c.name === column)) return;
|
|
310
|
+
db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${ddl}`);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// src/blobs.ts
|
|
314
|
+
import { mkdir, writeFile, readFile, stat } from "fs/promises";
|
|
315
|
+
import { dirname } from "path";
|
|
316
|
+
import {
|
|
317
|
+
blobPath,
|
|
318
|
+
blobRoot,
|
|
319
|
+
hashJson,
|
|
320
|
+
redactBuffer,
|
|
321
|
+
sha256
|
|
322
|
+
} from "@meterbility/shared";
|
|
323
|
+
function isProbablyText(buf) {
|
|
324
|
+
for (let i = 0; i < buf.length; i++) {
|
|
325
|
+
if (buf[i] === 0) return false;
|
|
326
|
+
}
|
|
327
|
+
if (buf.length > 0 && Buffer.byteLength(buf.toString("utf-8"), "utf-8") !== buf.length) {
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
332
|
+
var BlobStore = class {
|
|
333
|
+
constructor(db) {
|
|
334
|
+
this.db = db;
|
|
335
|
+
}
|
|
336
|
+
db;
|
|
337
|
+
async putString(content, opts) {
|
|
338
|
+
return this.putBuffer(Buffer.from(content, "utf-8"), opts);
|
|
339
|
+
}
|
|
340
|
+
async putJson(value, opts) {
|
|
341
|
+
return this.putString(JSON.stringify(value), opts);
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Hash without persisting — useful when you want a snapshot id before
|
|
345
|
+
* deciding whether to write.
|
|
346
|
+
*/
|
|
347
|
+
hashJson(value) {
|
|
348
|
+
return hashJson(value);
|
|
349
|
+
}
|
|
350
|
+
async putBuffer(buf, opts) {
|
|
351
|
+
const shouldSkip = opts?.skipRedact === true || !isProbablyText(buf);
|
|
352
|
+
const { buffer: scrubbed, redactions } = shouldSkip ? { buffer: buf, redactions: [] } : redactBuffer(buf);
|
|
353
|
+
const hash = sha256(scrubbed);
|
|
354
|
+
const path = blobPath(hash);
|
|
355
|
+
try {
|
|
356
|
+
await stat(path);
|
|
357
|
+
return hash;
|
|
358
|
+
} catch {
|
|
359
|
+
}
|
|
360
|
+
await mkdir(dirname(path), { recursive: true });
|
|
361
|
+
await writeFile(path, scrubbed);
|
|
362
|
+
if (redactions.length) {
|
|
363
|
+
const stmt = this.db.prepare(
|
|
364
|
+
"INSERT INTO redaction_log(blob_ref, rule, count, created_at) VALUES (?,?,?,?)"
|
|
365
|
+
);
|
|
366
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
367
|
+
for (const r of redactions) stmt.run(hash, r.rule, r.count, now);
|
|
368
|
+
}
|
|
369
|
+
return hash;
|
|
370
|
+
}
|
|
371
|
+
async getString(hash) {
|
|
372
|
+
const buf = await this.getBuffer(hash);
|
|
373
|
+
return buf.toString("utf-8");
|
|
374
|
+
}
|
|
375
|
+
async getJson(hash) {
|
|
376
|
+
const text = await this.getString(hash);
|
|
377
|
+
return JSON.parse(text);
|
|
378
|
+
}
|
|
379
|
+
async getBuffer(hash) {
|
|
380
|
+
return readFile(blobPath(hash));
|
|
381
|
+
}
|
|
382
|
+
async tryGetString(hash) {
|
|
383
|
+
try {
|
|
384
|
+
return await this.getString(hash);
|
|
385
|
+
} catch {
|
|
386
|
+
return void 0;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Binary-safe variant of `tryGetString`. Used by /api/blob/:hash/render
|
|
391
|
+
* which has to look at raw bytes for MIME sniffing before deciding
|
|
392
|
+
* whether to render text or serve as image/octet-stream.
|
|
393
|
+
*/
|
|
394
|
+
async tryGetBuffer(hash) {
|
|
395
|
+
try {
|
|
396
|
+
return await this.getBuffer(hash);
|
|
397
|
+
} catch {
|
|
398
|
+
return void 0;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Write pre-computed HTML (or other text) under a caller-chosen
|
|
403
|
+
* synthetic key, bypassing the content-addressed hash. Used by the
|
|
404
|
+
* blob_render cache to store rendered HTML under a key derived
|
|
405
|
+
* from `sha(blob_hash + lang + RENDER_VERSION)` rather than the
|
|
406
|
+
* content's own hash — this way two different renders of the same
|
|
407
|
+
* source code (e.g. lang=auto vs lang=typescript) cache separately
|
|
408
|
+
* but predictably.
|
|
409
|
+
*
|
|
410
|
+
* Skips redaction (renders are derived from already-stored blobs
|
|
411
|
+
* that went through redaction at put time).
|
|
412
|
+
*/
|
|
413
|
+
async putWithKey(content, key) {
|
|
414
|
+
const path = blobPath(key);
|
|
415
|
+
try {
|
|
416
|
+
await stat(path);
|
|
417
|
+
return;
|
|
418
|
+
} catch {
|
|
419
|
+
}
|
|
420
|
+
await mkdir(dirname(path), { recursive: true });
|
|
421
|
+
await writeFile(path, Buffer.from(content, "utf-8"));
|
|
422
|
+
}
|
|
423
|
+
rootDir() {
|
|
424
|
+
return blobRoot();
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
// src/store.ts
|
|
429
|
+
var Store = class _Store {
|
|
430
|
+
db;
|
|
431
|
+
blobs;
|
|
432
|
+
constructor(db) {
|
|
433
|
+
this.db = db;
|
|
434
|
+
this.blobs = new BlobStore(db);
|
|
435
|
+
}
|
|
436
|
+
static open(opts) {
|
|
437
|
+
const path = opts?.path ?? dbPath();
|
|
438
|
+
mkdirSync(meterHome(), { recursive: true });
|
|
439
|
+
mkdirSync(dirname2(path), { recursive: true });
|
|
440
|
+
const db = new Database(path);
|
|
441
|
+
ensureSchema(db);
|
|
442
|
+
return new _Store(db);
|
|
443
|
+
}
|
|
444
|
+
static async openAsync(opts) {
|
|
445
|
+
const path = opts?.path ?? dbPath();
|
|
446
|
+
await mkdir2(meterHome(), { recursive: true });
|
|
447
|
+
await mkdir2(dirname2(path), { recursive: true });
|
|
448
|
+
return this.open(opts);
|
|
449
|
+
}
|
|
450
|
+
close() {
|
|
451
|
+
this.db.close();
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
// src/queries.ts
|
|
456
|
+
import { randomUUID } from "crypto";
|
|
457
|
+
function rowToRun(row) {
|
|
458
|
+
return {
|
|
459
|
+
run_id: row.run_id,
|
|
460
|
+
agent_id: row.agent_id,
|
|
461
|
+
project_id: row.project_id,
|
|
462
|
+
source_session_id: row.source_session_id ?? void 0,
|
|
463
|
+
source_runtime: row.source_runtime,
|
|
464
|
+
title: row.title ?? void 0,
|
|
465
|
+
status: row.status,
|
|
466
|
+
started_at: row.started_at,
|
|
467
|
+
ended_at: row.ended_at ?? void 0,
|
|
468
|
+
git_branch: row.git_branch ?? void 0,
|
|
469
|
+
cwd: row.cwd ?? void 0,
|
|
470
|
+
fork_origin_run_id: row.fork_origin_run_id ?? void 0,
|
|
471
|
+
fork_origin_step_id: row.fork_origin_step_id ?? void 0,
|
|
472
|
+
tokens_total_input: row.tokens_total_input,
|
|
473
|
+
tokens_total_output: row.tokens_total_output,
|
|
474
|
+
tokens_total_cached: row.tokens_total_cached,
|
|
475
|
+
cost_cents: row.cost_cents,
|
|
476
|
+
step_count: row.step_count,
|
|
477
|
+
tags: JSON.parse(row.tags),
|
|
478
|
+
baseline_tree_id: row.baseline_tree_id ?? void 0,
|
|
479
|
+
probe_state: row.probe_state === "paused" || row.probe_state === "resumed" ? row.probe_state : void 0
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
function rowToStep(row) {
|
|
483
|
+
return {
|
|
484
|
+
step_id: row.step_id,
|
|
485
|
+
run_id: row.run_id,
|
|
486
|
+
parent_step_id: row.parent_step_id ?? void 0,
|
|
487
|
+
fork_origin_id: row.fork_origin_id ?? void 0,
|
|
488
|
+
sequence: row.sequence,
|
|
489
|
+
timestamp: row.timestamp,
|
|
490
|
+
model: row.model,
|
|
491
|
+
context_snapshot_id: row.context_snapshot_id,
|
|
492
|
+
decision_ref: row.decision_ref,
|
|
493
|
+
action: JSON.parse(row.action_json),
|
|
494
|
+
outcome: JSON.parse(row.outcome_json),
|
|
495
|
+
tokens: {
|
|
496
|
+
input: row.tokens_input,
|
|
497
|
+
output: row.tokens_output,
|
|
498
|
+
cached_read: row.tokens_cached_read,
|
|
499
|
+
cache_creation: row.tokens_cache_creation,
|
|
500
|
+
cache_creation_1h: row.tokens_cache_creation_1h ?? 0,
|
|
501
|
+
reasoning: row.tokens_reasoning ?? void 0
|
|
502
|
+
},
|
|
503
|
+
latency_ms: row.latency_ms,
|
|
504
|
+
cost_cents: row.cost_cents,
|
|
505
|
+
tags: JSON.parse(row.tags),
|
|
506
|
+
status: row.status
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
function upsertProjectByCwd(store, cwd, name) {
|
|
510
|
+
const existing = store.db.prepare("SELECT * FROM projects WHERE cwd = ?").get(cwd);
|
|
511
|
+
if (existing) return existing;
|
|
512
|
+
const project = {
|
|
513
|
+
project_id: `prj_${randomUUID()}`,
|
|
514
|
+
cwd,
|
|
515
|
+
name: name ?? cwd.split("/").pop() ?? cwd,
|
|
516
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
517
|
+
};
|
|
518
|
+
store.db.prepare(
|
|
519
|
+
"INSERT INTO projects(project_id, name, cwd, created_at) VALUES (?,?,?,?)"
|
|
520
|
+
).run(project.project_id, project.name, project.cwd, project.created_at);
|
|
521
|
+
return project;
|
|
522
|
+
}
|
|
523
|
+
function upsertAgent(store, projectId, name) {
|
|
524
|
+
const existing = store.db.prepare("SELECT * FROM agents WHERE project_id = ? AND name = ?").get(projectId, name);
|
|
525
|
+
if (existing) return existing;
|
|
526
|
+
const agent = {
|
|
527
|
+
agent_id: `agt_${randomUUID()}`,
|
|
528
|
+
project_id: projectId,
|
|
529
|
+
name,
|
|
530
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
531
|
+
};
|
|
532
|
+
store.db.prepare("INSERT INTO agents(agent_id, project_id, name, created_at) VALUES (?,?,?,?)").run(agent.agent_id, agent.project_id, agent.name, agent.created_at);
|
|
533
|
+
return agent;
|
|
534
|
+
}
|
|
535
|
+
function insertRun(store, run) {
|
|
536
|
+
store.db.prepare(
|
|
537
|
+
`INSERT INTO runs(
|
|
538
|
+
run_id, agent_id, project_id, source_session_id, source_runtime,
|
|
539
|
+
title, status, started_at, ended_at, git_branch, cwd,
|
|
540
|
+
fork_origin_run_id, fork_origin_step_id,
|
|
541
|
+
tokens_total_input, tokens_total_output, tokens_total_cached,
|
|
542
|
+
cost_cents, step_count, tags,
|
|
543
|
+
baseline_tree_id, probe_state
|
|
544
|
+
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`
|
|
545
|
+
).run(
|
|
546
|
+
run.run_id,
|
|
547
|
+
run.agent_id,
|
|
548
|
+
run.project_id,
|
|
549
|
+
run.source_session_id ?? null,
|
|
550
|
+
run.source_runtime,
|
|
551
|
+
run.title ?? null,
|
|
552
|
+
run.status,
|
|
553
|
+
run.started_at,
|
|
554
|
+
run.ended_at ?? null,
|
|
555
|
+
run.git_branch ?? null,
|
|
556
|
+
run.cwd ?? null,
|
|
557
|
+
run.fork_origin_run_id ?? null,
|
|
558
|
+
run.fork_origin_step_id ?? null,
|
|
559
|
+
run.tokens_total_input,
|
|
560
|
+
run.tokens_total_output,
|
|
561
|
+
run.tokens_total_cached,
|
|
562
|
+
run.cost_cents,
|
|
563
|
+
run.step_count,
|
|
564
|
+
JSON.stringify(run.tags ?? []),
|
|
565
|
+
run.baseline_tree_id ?? null,
|
|
566
|
+
run.probe_state ?? null
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
function updateRunTotals(store, runId) {
|
|
570
|
+
store.db.prepare(
|
|
571
|
+
`UPDATE runs SET
|
|
572
|
+
step_count = (SELECT COUNT(*) FROM steps WHERE run_id = runs.run_id),
|
|
573
|
+
tokens_total_input = COALESCE(
|
|
574
|
+
(SELECT SUM(tokens_input) FROM steps WHERE run_id = runs.run_id), 0),
|
|
575
|
+
tokens_total_output = COALESCE(
|
|
576
|
+
(SELECT SUM(tokens_output) FROM steps WHERE run_id = runs.run_id), 0),
|
|
577
|
+
tokens_total_cached = COALESCE(
|
|
578
|
+
(SELECT SUM(tokens_cached_read + tokens_cache_creation + tokens_cache_creation_1h)
|
|
579
|
+
FROM steps WHERE run_id = runs.run_id), 0),
|
|
580
|
+
cost_cents = COALESCE(
|
|
581
|
+
(SELECT SUM(cost_cents) FROM steps WHERE run_id = runs.run_id), 0)
|
|
582
|
+
WHERE run_id = ?`
|
|
583
|
+
).run(runId);
|
|
584
|
+
}
|
|
585
|
+
function setRunStatus(store, runId, status, endedAt) {
|
|
586
|
+
store.db.prepare("UPDATE runs SET status = ?, ended_at = ? WHERE run_id = ?").run(status, endedAt ?? null, runId);
|
|
587
|
+
}
|
|
588
|
+
function insertStep(store, step) {
|
|
589
|
+
store.db.prepare(
|
|
590
|
+
`INSERT OR REPLACE INTO steps(
|
|
591
|
+
step_id, run_id, parent_step_id, fork_origin_id, sequence, timestamp,
|
|
592
|
+
model, context_snapshot_id, decision_ref, action_json, outcome_json,
|
|
593
|
+
tokens_input, tokens_output, tokens_cached_read, tokens_cache_creation,
|
|
594
|
+
tokens_cache_creation_1h, tokens_reasoning, latency_ms, cost_cents,
|
|
595
|
+
status, tags
|
|
596
|
+
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`
|
|
597
|
+
).run(
|
|
598
|
+
step.step_id,
|
|
599
|
+
step.run_id,
|
|
600
|
+
step.parent_step_id ?? null,
|
|
601
|
+
step.fork_origin_id ?? null,
|
|
602
|
+
step.sequence,
|
|
603
|
+
step.timestamp,
|
|
604
|
+
step.model,
|
|
605
|
+
step.context_snapshot_id,
|
|
606
|
+
step.decision_ref,
|
|
607
|
+
JSON.stringify(step.action),
|
|
608
|
+
JSON.stringify(step.outcome),
|
|
609
|
+
step.tokens.input,
|
|
610
|
+
step.tokens.output,
|
|
611
|
+
step.tokens.cached_read,
|
|
612
|
+
step.tokens.cache_creation,
|
|
613
|
+
step.tokens.cache_creation_1h ?? 0,
|
|
614
|
+
step.tokens.reasoning ?? null,
|
|
615
|
+
step.latency_ms,
|
|
616
|
+
step.cost_cents,
|
|
617
|
+
step.status,
|
|
618
|
+
JSON.stringify(step.tags ?? [])
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
function recordContextSnapshot(store, snapshotId, blobRef, componentCount) {
|
|
622
|
+
store.db.prepare(
|
|
623
|
+
`INSERT OR IGNORE INTO context_snapshots(snapshot_id, blob_ref, component_count, created_at)
|
|
624
|
+
VALUES (?,?,?,?)`
|
|
625
|
+
).run(snapshotId, blobRef, componentCount, (/* @__PURE__ */ new Date()).toISOString());
|
|
626
|
+
}
|
|
627
|
+
function resolveSnapshotBlobRef(store, snapshotId) {
|
|
628
|
+
const row = store.db.prepare("SELECT blob_ref FROM context_snapshots WHERE snapshot_id = ?").get(snapshotId);
|
|
629
|
+
return row?.blob_ref ?? snapshotId;
|
|
630
|
+
}
|
|
631
|
+
function getRun(store, runId) {
|
|
632
|
+
const exact = store.db.prepare("SELECT * FROM runs WHERE run_id = ?").get(runId);
|
|
633
|
+
if (exact) return rowToRun(exact);
|
|
634
|
+
if (runId.length < 6) return void 0;
|
|
635
|
+
const prefix = store.db.prepare("SELECT * FROM runs WHERE run_id LIKE ? LIMIT 2").all(`${runId}%`);
|
|
636
|
+
if (prefix.length === 1) return rowToRun(prefix[0]);
|
|
637
|
+
return void 0;
|
|
638
|
+
}
|
|
639
|
+
function listRuns(store, opts = {}) {
|
|
640
|
+
const params = [];
|
|
641
|
+
let where = "1=1";
|
|
642
|
+
if (opts.projectId) {
|
|
643
|
+
where += " AND project_id = ?";
|
|
644
|
+
params.push(opts.projectId);
|
|
645
|
+
}
|
|
646
|
+
if (opts.agentId) {
|
|
647
|
+
where += " AND agent_id = ?";
|
|
648
|
+
params.push(opts.agentId);
|
|
649
|
+
}
|
|
650
|
+
if (opts.status) {
|
|
651
|
+
where += " AND status = ?";
|
|
652
|
+
params.push(opts.status);
|
|
653
|
+
}
|
|
654
|
+
if (opts.containsTool) {
|
|
655
|
+
where += " AND run_id IN (SELECT run_id FROM steps WHERE action_json LIKE ?)";
|
|
656
|
+
params.push(`%"tool_name":"${opts.containsTool}"%`);
|
|
657
|
+
}
|
|
658
|
+
const limit = opts.limit ?? 50;
|
|
659
|
+
const rows = store.db.prepare(
|
|
660
|
+
`SELECT * FROM runs WHERE ${where} ORDER BY started_at DESC LIMIT ?`
|
|
661
|
+
).all(...params, limit);
|
|
662
|
+
return rows.map(rowToRun);
|
|
663
|
+
}
|
|
664
|
+
function listSteps(store, runId) {
|
|
665
|
+
const rows = store.db.prepare("SELECT * FROM steps WHERE run_id = ? ORDER BY sequence ASC").all(runId);
|
|
666
|
+
return rows.map(rowToStep);
|
|
667
|
+
}
|
|
668
|
+
function getStep(store, stepId) {
|
|
669
|
+
const exact = store.db.prepare("SELECT * FROM steps WHERE step_id = ?").get(stepId);
|
|
670
|
+
if (exact) return rowToStep(exact);
|
|
671
|
+
if (stepId.length < 6) return void 0;
|
|
672
|
+
const prefix = store.db.prepare("SELECT * FROM steps WHERE step_id LIKE ? LIMIT 2").all(`${stepId}%`);
|
|
673
|
+
if (prefix.length === 1) return rowToStep(prefix[0]);
|
|
674
|
+
return void 0;
|
|
675
|
+
}
|
|
676
|
+
function getStepBySequence(store, runId, sequence) {
|
|
677
|
+
const row = store.db.prepare("SELECT * FROM steps WHERE run_id = ? AND sequence = ?").get(runId, sequence);
|
|
678
|
+
return row ? rowToStep(row) : void 0;
|
|
679
|
+
}
|
|
680
|
+
function insertFork(store, args) {
|
|
681
|
+
const fork_id = `frk_${randomUUID()}`;
|
|
682
|
+
store.db.prepare(
|
|
683
|
+
`INSERT INTO forks(fork_id, origin_run_id, origin_step_id, fork_run_id,
|
|
684
|
+
edit_type, edit_payload_json, created_at)
|
|
685
|
+
VALUES (?,?,?,?,?,?,?)`
|
|
686
|
+
).run(
|
|
687
|
+
fork_id,
|
|
688
|
+
args.originRunId,
|
|
689
|
+
args.originStepId,
|
|
690
|
+
args.forkRunId,
|
|
691
|
+
args.edit.type,
|
|
692
|
+
JSON.stringify(args.edit.payload ?? null),
|
|
693
|
+
(/* @__PURE__ */ new Date()).toISOString()
|
|
694
|
+
);
|
|
695
|
+
return fork_id;
|
|
696
|
+
}
|
|
697
|
+
function listForks(store, originRunId) {
|
|
698
|
+
return store.db.prepare(
|
|
699
|
+
"SELECT fork_id, origin_run_id, origin_step_id, fork_run_id, edit_type, created_at FROM forks WHERE origin_run_id = ? ORDER BY created_at"
|
|
700
|
+
).all(originRunId);
|
|
701
|
+
}
|
|
702
|
+
function insertAnnotation(store, args) {
|
|
703
|
+
const ann = {
|
|
704
|
+
annotation_id: `ann_${randomUUID()}`,
|
|
705
|
+
target_kind: args.targetKind,
|
|
706
|
+
target_id: args.targetId,
|
|
707
|
+
author: args.author,
|
|
708
|
+
kind: args.kind ?? "comment",
|
|
709
|
+
verdict: args.verdict,
|
|
710
|
+
note: args.note,
|
|
711
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
712
|
+
};
|
|
713
|
+
store.db.prepare(
|
|
714
|
+
`INSERT INTO annotations(annotation_id, target_kind, target_id, author, kind, verdict, note, created_at)
|
|
715
|
+
VALUES (?,?,?,?,?,?,?,?)`
|
|
716
|
+
).run(
|
|
717
|
+
ann.annotation_id,
|
|
718
|
+
ann.target_kind,
|
|
719
|
+
ann.target_id,
|
|
720
|
+
ann.author,
|
|
721
|
+
ann.kind,
|
|
722
|
+
ann.verdict ?? null,
|
|
723
|
+
ann.note ?? null,
|
|
724
|
+
ann.created_at
|
|
725
|
+
);
|
|
726
|
+
return ann;
|
|
727
|
+
}
|
|
728
|
+
function listAnnotations(store, targetKind, targetId) {
|
|
729
|
+
const rows = store.db.prepare(
|
|
730
|
+
"SELECT * FROM annotations WHERE target_kind = ? AND target_id = ? ORDER BY created_at"
|
|
731
|
+
).all(targetKind, targetId);
|
|
732
|
+
return rows.map((r) => ({
|
|
733
|
+
annotation_id: r.annotation_id,
|
|
734
|
+
target_kind: r.target_kind,
|
|
735
|
+
target_id: r.target_id,
|
|
736
|
+
author: r.author,
|
|
737
|
+
kind: r.kind,
|
|
738
|
+
verdict: r.verdict ?? void 0,
|
|
739
|
+
note: r.note ?? void 0,
|
|
740
|
+
created_at: r.created_at
|
|
741
|
+
}));
|
|
742
|
+
}
|
|
743
|
+
function getIngestOffset(store, source_runtime, source_path) {
|
|
744
|
+
const row = store.db.prepare(
|
|
745
|
+
"SELECT last_offset FROM ingest_progress WHERE source_runtime = ? AND source_path = ?"
|
|
746
|
+
).get(source_runtime, source_path);
|
|
747
|
+
return row?.last_offset ?? 0;
|
|
748
|
+
}
|
|
749
|
+
function setIngestOffset(store, source_runtime, source_path, offset) {
|
|
750
|
+
store.db.prepare(
|
|
751
|
+
`INSERT INTO ingest_progress(source_runtime, source_path, last_offset, last_ingested_at)
|
|
752
|
+
VALUES(?,?,?,?)
|
|
753
|
+
ON CONFLICT(source_runtime, source_path) DO UPDATE SET
|
|
754
|
+
last_offset = excluded.last_offset,
|
|
755
|
+
last_ingested_at = excluded.last_ingested_at`
|
|
756
|
+
).run(source_runtime, source_path, offset, (/* @__PURE__ */ new Date()).toISOString());
|
|
757
|
+
}
|
|
758
|
+
function getRunBySessionId(store, sourceSessionId) {
|
|
759
|
+
const row = store.db.prepare("SELECT * FROM runs WHERE source_session_id = ? ORDER BY started_at DESC LIMIT 1").get(sourceSessionId);
|
|
760
|
+
return row ? rowToRun(row) : void 0;
|
|
761
|
+
}
|
|
762
|
+
function aggregateTokens(steps) {
|
|
763
|
+
return steps.reduce(
|
|
764
|
+
(acc, s) => ({
|
|
765
|
+
input: acc.input + s.tokens.input,
|
|
766
|
+
output: acc.output + s.tokens.output,
|
|
767
|
+
cached_read: acc.cached_read + s.tokens.cached_read,
|
|
768
|
+
cache_creation: acc.cache_creation + s.tokens.cache_creation,
|
|
769
|
+
cache_creation_1h: (acc.cache_creation_1h ?? 0) + (s.tokens.cache_creation_1h ?? 0)
|
|
770
|
+
}),
|
|
771
|
+
{
|
|
772
|
+
input: 0,
|
|
773
|
+
output: 0,
|
|
774
|
+
cached_read: 0,
|
|
775
|
+
cache_creation: 0,
|
|
776
|
+
cache_creation_1h: 0
|
|
777
|
+
}
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
function rowToFileChange(row) {
|
|
781
|
+
return {
|
|
782
|
+
file_change_id: row.file_change_id,
|
|
783
|
+
run_id: row.run_id,
|
|
784
|
+
step_id: row.step_id,
|
|
785
|
+
sequence: row.sequence,
|
|
786
|
+
tool_call_id: row.tool_call_id ?? void 0,
|
|
787
|
+
derived_from: row.derived_from,
|
|
788
|
+
path: row.path,
|
|
789
|
+
old_path: row.old_path ?? void 0,
|
|
790
|
+
op: row.op,
|
|
791
|
+
before_blob_ref: row.before_blob_ref ?? void 0,
|
|
792
|
+
after_blob_ref: row.after_blob_ref ?? void 0,
|
|
793
|
+
partial_diff: row.partial_diff === 1,
|
|
794
|
+
gitignored: row.gitignored === 1,
|
|
795
|
+
patch_text: row.patch_text ?? void 0,
|
|
796
|
+
patch_format: row.patch_format ?? void 0,
|
|
797
|
+
encoding: row.encoding ?? void 0,
|
|
798
|
+
bom: row.bom === 1,
|
|
799
|
+
line_endings: row.line_endings ?? void 0,
|
|
800
|
+
mime: row.mime ?? void 0,
|
|
801
|
+
language: row.language ?? void 0,
|
|
802
|
+
size_before: row.size_before ?? void 0,
|
|
803
|
+
size_after: row.size_after ?? void 0,
|
|
804
|
+
line_count_before: row.line_count_before ?? void 0,
|
|
805
|
+
line_count_after: row.line_count_after ?? void 0,
|
|
806
|
+
lines_added: row.lines_added,
|
|
807
|
+
lines_removed: row.lines_removed,
|
|
808
|
+
mode_before: row.mode_before ?? void 0,
|
|
809
|
+
mode_after: row.mode_after ?? void 0,
|
|
810
|
+
source_tool_name: row.source_tool_name ?? void 0,
|
|
811
|
+
source_tool_input: row.source_tool_input ? safeJsonParse(row.source_tool_input) : void 0,
|
|
812
|
+
redacted: row.redacted === 1,
|
|
813
|
+
normalizer_notes: row.normalizer_notes ? safeJsonParse(row.normalizer_notes) : void 0,
|
|
814
|
+
created_at: row.created_at
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
function safeJsonParse(s) {
|
|
818
|
+
try {
|
|
819
|
+
return JSON.parse(s);
|
|
820
|
+
} catch {
|
|
821
|
+
return s;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
var FileChangeInvariantError = class extends Error {
|
|
825
|
+
constructor(message) {
|
|
826
|
+
super(`FileChange invariant violation: ${message}`);
|
|
827
|
+
this.name = "FileChangeInvariantError";
|
|
828
|
+
}
|
|
829
|
+
};
|
|
830
|
+
function assertFileChangeInvariants(fc) {
|
|
831
|
+
if (fc.op === "chmod") return;
|
|
832
|
+
if (fc.partial_diff) {
|
|
833
|
+
if (fc.before_blob_ref !== void 0) {
|
|
834
|
+
throw new FileChangeInvariantError(
|
|
835
|
+
`partial_diff=true requires before_blob_ref to be null (got ${JSON.stringify(fc.before_blob_ref)})`
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
if (fc.after_blob_ref !== void 0) {
|
|
839
|
+
throw new FileChangeInvariantError(
|
|
840
|
+
`partial_diff=true requires after_blob_ref to be null (got ${JSON.stringify(fc.after_blob_ref)})`
|
|
841
|
+
);
|
|
842
|
+
}
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
if (fc.op === "create") {
|
|
846
|
+
if (fc.before_blob_ref !== void 0) {
|
|
847
|
+
throw new FileChangeInvariantError(
|
|
848
|
+
"op='create' requires before_blob_ref to be null"
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
if (fc.after_blob_ref === void 0) {
|
|
852
|
+
throw new FileChangeInvariantError(
|
|
853
|
+
"op='create' requires after_blob_ref to be set"
|
|
854
|
+
);
|
|
855
|
+
}
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
if (fc.op === "delete") {
|
|
859
|
+
if (fc.before_blob_ref === void 0) {
|
|
860
|
+
throw new FileChangeInvariantError(
|
|
861
|
+
"op='delete' requires before_blob_ref to be set"
|
|
862
|
+
);
|
|
863
|
+
}
|
|
864
|
+
if (fc.after_blob_ref !== void 0) {
|
|
865
|
+
throw new FileChangeInvariantError(
|
|
866
|
+
"op='delete' requires after_blob_ref to be null"
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
if (fc.before_blob_ref === void 0) {
|
|
872
|
+
throw new FileChangeInvariantError(
|
|
873
|
+
`op='${fc.op}' requires before_blob_ref to be set (use partial_diff=true if not captured)`
|
|
874
|
+
);
|
|
875
|
+
}
|
|
876
|
+
if (fc.after_blob_ref === void 0) {
|
|
877
|
+
throw new FileChangeInvariantError(
|
|
878
|
+
`op='${fc.op}' requires after_blob_ref to be set (use partial_diff=true if not captured)`
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
if (fc.op === "rename" && fc.old_path === void 0) {
|
|
882
|
+
throw new FileChangeInvariantError("op='rename' requires old_path to be set");
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
function insertFileChange(store, fc) {
|
|
886
|
+
assertFileChangeInvariants(fc);
|
|
887
|
+
const id = fc.file_change_id ?? `fc_${randomUUID()}`;
|
|
888
|
+
const created = fc.created_at ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
889
|
+
store.db.prepare(
|
|
890
|
+
`INSERT INTO file_change(
|
|
891
|
+
file_change_id, run_id, step_id, sequence,
|
|
892
|
+
tool_call_id, derived_from, path, old_path, op,
|
|
893
|
+
before_blob_ref, after_blob_ref,
|
|
894
|
+
partial_diff, gitignored,
|
|
895
|
+
patch_text, patch_format,
|
|
896
|
+
encoding, bom, line_endings, mime, language,
|
|
897
|
+
size_before, size_after, line_count_before, line_count_after,
|
|
898
|
+
lines_added, lines_removed,
|
|
899
|
+
mode_before, mode_after,
|
|
900
|
+
source_tool_name, source_tool_input,
|
|
901
|
+
redacted, normalizer_notes, created_at
|
|
902
|
+
) VALUES (
|
|
903
|
+
?, ?, ?, ?,
|
|
904
|
+
?, ?, ?, ?, ?,
|
|
905
|
+
?, ?,
|
|
906
|
+
?, ?,
|
|
907
|
+
?, ?,
|
|
908
|
+
?, ?, ?, ?, ?,
|
|
909
|
+
?, ?, ?, ?,
|
|
910
|
+
?, ?,
|
|
911
|
+
?, ?,
|
|
912
|
+
?, ?,
|
|
913
|
+
?, ?, ?
|
|
914
|
+
)`
|
|
915
|
+
).run(
|
|
916
|
+
id,
|
|
917
|
+
fc.run_id,
|
|
918
|
+
fc.step_id,
|
|
919
|
+
fc.sequence,
|
|
920
|
+
fc.tool_call_id ?? null,
|
|
921
|
+
fc.derived_from,
|
|
922
|
+
fc.path,
|
|
923
|
+
fc.old_path ?? null,
|
|
924
|
+
fc.op,
|
|
925
|
+
fc.before_blob_ref ?? null,
|
|
926
|
+
fc.after_blob_ref ?? null,
|
|
927
|
+
fc.partial_diff ? 1 : 0,
|
|
928
|
+
fc.gitignored ? 1 : 0,
|
|
929
|
+
fc.patch_text ?? null,
|
|
930
|
+
fc.patch_format ?? null,
|
|
931
|
+
fc.encoding ?? null,
|
|
932
|
+
fc.bom ? 1 : 0,
|
|
933
|
+
fc.line_endings ?? null,
|
|
934
|
+
fc.mime ?? null,
|
|
935
|
+
fc.language ?? null,
|
|
936
|
+
fc.size_before ?? null,
|
|
937
|
+
fc.size_after ?? null,
|
|
938
|
+
fc.line_count_before ?? null,
|
|
939
|
+
fc.line_count_after ?? null,
|
|
940
|
+
fc.lines_added,
|
|
941
|
+
fc.lines_removed,
|
|
942
|
+
fc.mode_before ?? null,
|
|
943
|
+
fc.mode_after ?? null,
|
|
944
|
+
fc.source_tool_name ?? null,
|
|
945
|
+
fc.source_tool_input !== void 0 ? JSON.stringify(fc.source_tool_input) : null,
|
|
946
|
+
fc.redacted ? 1 : 0,
|
|
947
|
+
fc.normalizer_notes !== void 0 ? JSON.stringify(fc.normalizer_notes) : null,
|
|
948
|
+
created
|
|
949
|
+
);
|
|
950
|
+
return {
|
|
951
|
+
file_change_id: id,
|
|
952
|
+
run_id: fc.run_id,
|
|
953
|
+
step_id: fc.step_id,
|
|
954
|
+
sequence: fc.sequence,
|
|
955
|
+
tool_call_id: fc.tool_call_id,
|
|
956
|
+
derived_from: fc.derived_from,
|
|
957
|
+
path: fc.path,
|
|
958
|
+
old_path: fc.old_path,
|
|
959
|
+
op: fc.op,
|
|
960
|
+
before_blob_ref: fc.before_blob_ref,
|
|
961
|
+
after_blob_ref: fc.after_blob_ref,
|
|
962
|
+
partial_diff: fc.partial_diff,
|
|
963
|
+
gitignored: fc.gitignored,
|
|
964
|
+
patch_text: fc.patch_text,
|
|
965
|
+
patch_format: fc.patch_format,
|
|
966
|
+
encoding: fc.encoding,
|
|
967
|
+
bom: fc.bom,
|
|
968
|
+
line_endings: fc.line_endings,
|
|
969
|
+
mime: fc.mime,
|
|
970
|
+
language: fc.language,
|
|
971
|
+
size_before: fc.size_before,
|
|
972
|
+
size_after: fc.size_after,
|
|
973
|
+
line_count_before: fc.line_count_before,
|
|
974
|
+
line_count_after: fc.line_count_after,
|
|
975
|
+
lines_added: fc.lines_added,
|
|
976
|
+
lines_removed: fc.lines_removed,
|
|
977
|
+
mode_before: fc.mode_before,
|
|
978
|
+
mode_after: fc.mode_after,
|
|
979
|
+
source_tool_name: fc.source_tool_name,
|
|
980
|
+
source_tool_input: fc.source_tool_input,
|
|
981
|
+
redacted: fc.redacted,
|
|
982
|
+
normalizer_notes: fc.normalizer_notes,
|
|
983
|
+
created_at: created
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
function listFileChanges(store, opts = {}) {
|
|
987
|
+
const params = [];
|
|
988
|
+
let where = "1=1";
|
|
989
|
+
if (opts.runId) {
|
|
990
|
+
where += " AND fc.run_id = ?";
|
|
991
|
+
params.push(opts.runId);
|
|
992
|
+
}
|
|
993
|
+
if (opts.stepId) {
|
|
994
|
+
where += " AND fc.step_id = ?";
|
|
995
|
+
params.push(opts.stepId);
|
|
996
|
+
}
|
|
997
|
+
if (opts.path) {
|
|
998
|
+
where += " AND (fc.path = ? OR fc.old_path = ?)";
|
|
999
|
+
params.push(opts.path, opts.path);
|
|
1000
|
+
}
|
|
1001
|
+
if (opts.maxStepSeqExclusive !== void 0) {
|
|
1002
|
+
where += " AND s.sequence < ?";
|
|
1003
|
+
params.push(opts.maxStepSeqExclusive);
|
|
1004
|
+
}
|
|
1005
|
+
const rows = store.db.prepare(
|
|
1006
|
+
`SELECT fc.*
|
|
1007
|
+
FROM file_change fc
|
|
1008
|
+
JOIN steps s ON s.step_id = fc.step_id
|
|
1009
|
+
WHERE ${where}
|
|
1010
|
+
ORDER BY s.sequence ASC, fc.sequence ASC`
|
|
1011
|
+
).all(...params);
|
|
1012
|
+
return rows.map(rowToFileChange);
|
|
1013
|
+
}
|
|
1014
|
+
function getFileChange(store, fileChangeId) {
|
|
1015
|
+
const row = store.db.prepare("SELECT * FROM file_change WHERE file_change_id = ?").get(fileChangeId);
|
|
1016
|
+
return row ? rowToFileChange(row) : void 0;
|
|
1017
|
+
}
|
|
1018
|
+
function rowToBaselineTree(row) {
|
|
1019
|
+
return {
|
|
1020
|
+
baseline_tree_id: row.baseline_tree_id,
|
|
1021
|
+
project_id: row.project_id,
|
|
1022
|
+
manifest_blob_ref: row.manifest_blob_ref,
|
|
1023
|
+
git_head: row.git_head ?? void 0,
|
|
1024
|
+
git_dirty: row.git_dirty === 1,
|
|
1025
|
+
captured_at: row.captured_at
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
function insertBaselineTree(store, bt) {
|
|
1029
|
+
const id = bt.baseline_tree_id ?? `bt_${randomUUID()}`;
|
|
1030
|
+
const captured = bt.captured_at ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
1031
|
+
store.db.prepare(
|
|
1032
|
+
`INSERT INTO baseline_tree(
|
|
1033
|
+
baseline_tree_id, project_id, manifest_blob_ref,
|
|
1034
|
+
git_head, git_dirty, captured_at
|
|
1035
|
+
) VALUES (?, ?, ?, ?, ?, ?)`
|
|
1036
|
+
).run(
|
|
1037
|
+
id,
|
|
1038
|
+
bt.project_id,
|
|
1039
|
+
bt.manifest_blob_ref,
|
|
1040
|
+
bt.git_head ?? null,
|
|
1041
|
+
bt.git_dirty ? 1 : 0,
|
|
1042
|
+
captured
|
|
1043
|
+
);
|
|
1044
|
+
return {
|
|
1045
|
+
baseline_tree_id: id,
|
|
1046
|
+
project_id: bt.project_id,
|
|
1047
|
+
manifest_blob_ref: bt.manifest_blob_ref,
|
|
1048
|
+
git_head: bt.git_head,
|
|
1049
|
+
git_dirty: bt.git_dirty,
|
|
1050
|
+
captured_at: captured
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
function getBaselineTree(store, baselineTreeId) {
|
|
1054
|
+
const row = store.db.prepare("SELECT * FROM baseline_tree WHERE baseline_tree_id = ?").get(baselineTreeId);
|
|
1055
|
+
return row ? rowToBaselineTree(row) : void 0;
|
|
1056
|
+
}
|
|
1057
|
+
function findBaselineByManifest(store, projectId, manifestBlobRef) {
|
|
1058
|
+
const row = store.db.prepare(
|
|
1059
|
+
"SELECT * FROM baseline_tree WHERE project_id = ? AND manifest_blob_ref = ? LIMIT 1"
|
|
1060
|
+
).get(projectId, manifestBlobRef);
|
|
1061
|
+
return row ? rowToBaselineTree(row) : void 0;
|
|
1062
|
+
}
|
|
1063
|
+
function setRunBaselineTree(store, runId, baselineTreeId) {
|
|
1064
|
+
store.db.prepare("UPDATE runs SET baseline_tree_id = ? WHERE run_id = ?").run(baselineTreeId, runId);
|
|
1065
|
+
}
|
|
1066
|
+
function setRunProbeState(store, runId, state) {
|
|
1067
|
+
store.db.prepare("UPDATE runs SET probe_state = ? WHERE run_id = ?").run(state, runId);
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
// src/settings.ts
|
|
1071
|
+
function getSetting(store, key) {
|
|
1072
|
+
const row = store.db.prepare("SELECT value FROM settings WHERE key = ?").get(key);
|
|
1073
|
+
return row?.value;
|
|
1074
|
+
}
|
|
1075
|
+
function setSetting(store, key, value) {
|
|
1076
|
+
store.db.prepare(
|
|
1077
|
+
`INSERT INTO settings(key, value, updated_at) VALUES (?, ?, ?)
|
|
1078
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`
|
|
1079
|
+
).run(key, value, (/* @__PURE__ */ new Date()).toISOString());
|
|
1080
|
+
}
|
|
1081
|
+
function deleteSetting(store, key) {
|
|
1082
|
+
store.db.prepare("DELETE FROM settings WHERE key = ?").run(key);
|
|
1083
|
+
}
|
|
1084
|
+
function listSettings(store) {
|
|
1085
|
+
return store.db.prepare("SELECT key, value, updated_at FROM settings ORDER BY key").all();
|
|
1086
|
+
}
|
|
1087
|
+
function resolveSetting(store, key, envVar) {
|
|
1088
|
+
return process.env[envVar] ?? getSetting(store, key);
|
|
1089
|
+
}
|
|
1090
|
+
function isSecret(key) {
|
|
1091
|
+
return /api_key|webhook|password|token|secret|url$/i.test(key);
|
|
1092
|
+
}
|
|
1093
|
+
function maskSecret(value) {
|
|
1094
|
+
if (value.length <= 8) return "\u2022".repeat(value.length);
|
|
1095
|
+
return `${value.slice(0, 7)}${"\u2022".repeat(8)}${value.slice(-4)}`;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
// src/replay.ts
|
|
1099
|
+
var FIELD_SEP = 0;
|
|
1100
|
+
var RECORD_SEP = 10;
|
|
1101
|
+
function serializeManifest(entries) {
|
|
1102
|
+
const sorted = [...entries].sort((a, b) => {
|
|
1103
|
+
const ab = Buffer.from(a.path, "utf-8");
|
|
1104
|
+
const bb = Buffer.from(b.path, "utf-8");
|
|
1105
|
+
return Buffer.compare(ab, bb);
|
|
1106
|
+
});
|
|
1107
|
+
const parts = [];
|
|
1108
|
+
for (const e of sorted) {
|
|
1109
|
+
const pathBuf = Buffer.from(e.path, "utf-8");
|
|
1110
|
+
if (pathBuf.includes(FIELD_SEP) || pathBuf.includes(RECORD_SEP)) {
|
|
1111
|
+
throw new Error(
|
|
1112
|
+
`serializeManifest: path contains illegal byte (NUL or newline): ${JSON.stringify(e.path)}`
|
|
1113
|
+
);
|
|
1114
|
+
}
|
|
1115
|
+
const refBuf = Buffer.from(e.blob_ref, "utf-8");
|
|
1116
|
+
if (refBuf.includes(FIELD_SEP) || refBuf.includes(RECORD_SEP)) {
|
|
1117
|
+
throw new Error(
|
|
1118
|
+
`serializeManifest: blob_ref contains illegal byte: ${JSON.stringify(e.blob_ref)}`
|
|
1119
|
+
);
|
|
1120
|
+
}
|
|
1121
|
+
parts.push(pathBuf);
|
|
1122
|
+
parts.push(Buffer.from([FIELD_SEP]));
|
|
1123
|
+
parts.push(Buffer.from(String(e.mode), "utf-8"));
|
|
1124
|
+
parts.push(Buffer.from([FIELD_SEP]));
|
|
1125
|
+
parts.push(refBuf);
|
|
1126
|
+
parts.push(Buffer.from([RECORD_SEP]));
|
|
1127
|
+
}
|
|
1128
|
+
return Buffer.concat(parts);
|
|
1129
|
+
}
|
|
1130
|
+
function parseManifest(buf) {
|
|
1131
|
+
if (buf.length === 0) return [];
|
|
1132
|
+
const entries = [];
|
|
1133
|
+
let cursor = 0;
|
|
1134
|
+
while (cursor < buf.length) {
|
|
1135
|
+
const nl = buf.indexOf(RECORD_SEP, cursor);
|
|
1136
|
+
const end = nl === -1 ? buf.length : nl;
|
|
1137
|
+
if (end === cursor) {
|
|
1138
|
+
cursor = end + 1;
|
|
1139
|
+
continue;
|
|
1140
|
+
}
|
|
1141
|
+
const record = buf.subarray(cursor, end);
|
|
1142
|
+
const sep1 = record.indexOf(FIELD_SEP);
|
|
1143
|
+
if (sep1 === -1) {
|
|
1144
|
+
throw new Error(
|
|
1145
|
+
`parseManifest: malformed record at offset ${cursor} (no NUL separator)`
|
|
1146
|
+
);
|
|
1147
|
+
}
|
|
1148
|
+
const sep2 = record.indexOf(FIELD_SEP, sep1 + 1);
|
|
1149
|
+
if (sep2 === -1) {
|
|
1150
|
+
throw new Error(
|
|
1151
|
+
`parseManifest: malformed record at offset ${cursor} (missing second NUL)`
|
|
1152
|
+
);
|
|
1153
|
+
}
|
|
1154
|
+
const path = record.subarray(0, sep1).toString("utf-8");
|
|
1155
|
+
const modeStr = record.subarray(sep1 + 1, sep2).toString("utf-8");
|
|
1156
|
+
const blob_ref = record.subarray(sep2 + 1).toString("utf-8");
|
|
1157
|
+
const mode = Number.parseInt(modeStr, 10);
|
|
1158
|
+
if (!Number.isFinite(mode)) {
|
|
1159
|
+
throw new Error(
|
|
1160
|
+
`parseManifest: invalid mode "${modeStr}" at offset ${cursor}`
|
|
1161
|
+
);
|
|
1162
|
+
}
|
|
1163
|
+
entries.push({ path, mode, blob_ref });
|
|
1164
|
+
if (nl === -1) break;
|
|
1165
|
+
cursor = nl + 1;
|
|
1166
|
+
}
|
|
1167
|
+
return entries;
|
|
1168
|
+
}
|
|
1169
|
+
async function loadBaselineTree(store, baseline) {
|
|
1170
|
+
const tree = /* @__PURE__ */ new Map();
|
|
1171
|
+
const buf = await store.blobs.getBuffer(baseline.manifest_blob_ref).catch(
|
|
1172
|
+
() => void 0
|
|
1173
|
+
);
|
|
1174
|
+
if (!buf) return tree;
|
|
1175
|
+
for (const e of parseManifest(buf)) {
|
|
1176
|
+
tree.set(e.path, { blob_ref: e.blob_ref, mode: e.mode });
|
|
1177
|
+
}
|
|
1178
|
+
return tree;
|
|
1179
|
+
}
|
|
1180
|
+
async function workingTreeAt(store, runId, opts = {}) {
|
|
1181
|
+
const run = getRun(store, runId);
|
|
1182
|
+
if (!run) return /* @__PURE__ */ new Map();
|
|
1183
|
+
const tree = run.baseline_tree_id ? await (async () => {
|
|
1184
|
+
const bt = getBaselineTree(store, run.baseline_tree_id);
|
|
1185
|
+
return bt ? await loadBaselineTree(store, bt) : /* @__PURE__ */ new Map();
|
|
1186
|
+
})() : /* @__PURE__ */ new Map();
|
|
1187
|
+
const fcs = listFileChanges(store, {
|
|
1188
|
+
runId: run.run_id,
|
|
1189
|
+
maxStepSeqExclusive: opts.stepSeq
|
|
1190
|
+
});
|
|
1191
|
+
for (const fc of fcs) {
|
|
1192
|
+
applyFileChange(tree, fc);
|
|
1193
|
+
}
|
|
1194
|
+
return tree;
|
|
1195
|
+
}
|
|
1196
|
+
function applyFileChange(tree, fc) {
|
|
1197
|
+
if (fc.partial_diff) return;
|
|
1198
|
+
if (fc.op === "delete") {
|
|
1199
|
+
tree.delete(fc.path);
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1202
|
+
if (fc.op === "rename") {
|
|
1203
|
+
if (fc.old_path) tree.delete(fc.old_path);
|
|
1204
|
+
if (fc.after_blob_ref) {
|
|
1205
|
+
const existing = tree.get(fc.path);
|
|
1206
|
+
tree.set(fc.path, {
|
|
1207
|
+
blob_ref: fc.after_blob_ref,
|
|
1208
|
+
mode: fc.mode_after ?? existing?.mode ?? 33188
|
|
1209
|
+
});
|
|
1210
|
+
}
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
if (fc.op === "chmod") {
|
|
1214
|
+
const existing = tree.get(fc.path);
|
|
1215
|
+
if (existing && fc.mode_after !== void 0) {
|
|
1216
|
+
tree.set(fc.path, { ...existing, mode: fc.mode_after });
|
|
1217
|
+
}
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
if (fc.after_blob_ref) {
|
|
1221
|
+
const existing = tree.get(fc.path);
|
|
1222
|
+
tree.set(fc.path, {
|
|
1223
|
+
blob_ref: fc.after_blob_ref,
|
|
1224
|
+
mode: fc.mode_after ?? existing?.mode ?? 33188
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
// src/baseline.ts
|
|
1230
|
+
import { readdir, readFile as readFile2, stat as stat2 } from "fs/promises";
|
|
1231
|
+
import { spawnSync } from "child_process";
|
|
1232
|
+
import { availableParallelism } from "os";
|
|
1233
|
+
import { join, relative, sep } from "path";
|
|
1234
|
+
import { IgnoreMatcher } from "@meterbility/shared";
|
|
1235
|
+
var MAX_BASELINE_FILE_BYTES = 5 * 1024 * 1024;
|
|
1236
|
+
var HASH_CONCURRENCY = Math.min(8, Math.max(1, availableParallelism()));
|
|
1237
|
+
async function captureBaseline(store, projectId, cwd, opts = {}) {
|
|
1238
|
+
try {
|
|
1239
|
+
const s = await stat2(cwd);
|
|
1240
|
+
if (!s.isDirectory()) return void 0;
|
|
1241
|
+
} catch {
|
|
1242
|
+
return void 0;
|
|
1243
|
+
}
|
|
1244
|
+
const matcher = opts.matcher ?? await buildDefaultMatcher(cwd);
|
|
1245
|
+
const maxBytes = opts.maxFileBytes ?? MAX_BASELINE_FILE_BYTES;
|
|
1246
|
+
const candidates = await walk(cwd, matcher);
|
|
1247
|
+
const entries = [];
|
|
1248
|
+
let skipped = 0;
|
|
1249
|
+
const queue = [...candidates];
|
|
1250
|
+
await Promise.all(
|
|
1251
|
+
Array.from({ length: HASH_CONCURRENCY }, async () => {
|
|
1252
|
+
for (; ; ) {
|
|
1253
|
+
const c = queue.shift();
|
|
1254
|
+
if (!c) return;
|
|
1255
|
+
const absPath = join(cwd, ...c.repoPath.split("/"));
|
|
1256
|
+
try {
|
|
1257
|
+
const buf = await readFile2(absPath);
|
|
1258
|
+
if (buf.length > maxBytes) {
|
|
1259
|
+
skipped += 1;
|
|
1260
|
+
continue;
|
|
1261
|
+
}
|
|
1262
|
+
const blob_ref = await store.blobs.putBuffer(buf);
|
|
1263
|
+
entries.push({ path: c.repoPath, mode: c.mode, blob_ref });
|
|
1264
|
+
} catch {
|
|
1265
|
+
skipped += 1;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
})
|
|
1269
|
+
);
|
|
1270
|
+
const manifestBuf = serializeManifest(entries);
|
|
1271
|
+
const manifest_blob_ref = await store.blobs.putBuffer(manifestBuf, {
|
|
1272
|
+
skipRedact: true
|
|
1273
|
+
});
|
|
1274
|
+
const existing = findBaselineByManifest(store, projectId, manifest_blob_ref);
|
|
1275
|
+
const { git_head, git_dirty } = readGitAdvisory(cwd);
|
|
1276
|
+
if (existing) {
|
|
1277
|
+
return {
|
|
1278
|
+
baseline_tree_id: existing.baseline_tree_id,
|
|
1279
|
+
manifest_blob_ref,
|
|
1280
|
+
file_count: entries.length,
|
|
1281
|
+
skipped,
|
|
1282
|
+
reused_existing: true,
|
|
1283
|
+
git_head: existing.git_head ?? git_head,
|
|
1284
|
+
git_dirty: existing.git_dirty
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1287
|
+
const row = insertBaselineTree(store, {
|
|
1288
|
+
project_id: projectId,
|
|
1289
|
+
manifest_blob_ref,
|
|
1290
|
+
git_head,
|
|
1291
|
+
git_dirty
|
|
1292
|
+
});
|
|
1293
|
+
return {
|
|
1294
|
+
baseline_tree_id: row.baseline_tree_id,
|
|
1295
|
+
manifest_blob_ref,
|
|
1296
|
+
file_count: entries.length,
|
|
1297
|
+
skipped,
|
|
1298
|
+
reused_existing: false,
|
|
1299
|
+
git_head,
|
|
1300
|
+
git_dirty
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
async function walk(rootCwd, matcher) {
|
|
1304
|
+
const out = [];
|
|
1305
|
+
const stack = [rootCwd];
|
|
1306
|
+
while (stack.length > 0) {
|
|
1307
|
+
const dir = stack.pop();
|
|
1308
|
+
let entries;
|
|
1309
|
+
try {
|
|
1310
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
1311
|
+
} catch {
|
|
1312
|
+
continue;
|
|
1313
|
+
}
|
|
1314
|
+
for (const entry of entries) {
|
|
1315
|
+
const abs = join(dir, entry.name);
|
|
1316
|
+
const repoRel = toRepoRelative(rootCwd, abs);
|
|
1317
|
+
const isDir = entry.isDirectory();
|
|
1318
|
+
if (matcher.matches(repoRel, isDir)) continue;
|
|
1319
|
+
if (isDir) {
|
|
1320
|
+
stack.push(abs);
|
|
1321
|
+
} else if (entry.isFile()) {
|
|
1322
|
+
let mode = 33188;
|
|
1323
|
+
try {
|
|
1324
|
+
const s = await stat2(abs);
|
|
1325
|
+
mode = s.mode;
|
|
1326
|
+
} catch {
|
|
1327
|
+
}
|
|
1328
|
+
out.push({ repoPath: repoRel, mode });
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
return out;
|
|
1333
|
+
}
|
|
1334
|
+
function toRepoRelative(rootCwd, abs) {
|
|
1335
|
+
return relative(rootCwd, abs).split(sep).join("/");
|
|
1336
|
+
}
|
|
1337
|
+
async function buildDefaultMatcher(cwd) {
|
|
1338
|
+
const userMeterbility = await readLines(join(cwd, ".meterbilityignore"));
|
|
1339
|
+
const gitignore = await readLines(join(cwd, ".gitignore"));
|
|
1340
|
+
return IgnoreMatcher.fromDefaultsPlus(userMeterbility, gitignore);
|
|
1341
|
+
}
|
|
1342
|
+
async function readLines(path) {
|
|
1343
|
+
try {
|
|
1344
|
+
const text = await readFile2(path, "utf-8");
|
|
1345
|
+
return text.split(/\r?\n/);
|
|
1346
|
+
} catch {
|
|
1347
|
+
return void 0;
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
function readGitAdvisory(cwd) {
|
|
1351
|
+
const head = spawnSync("git", ["rev-parse", "HEAD"], {
|
|
1352
|
+
cwd,
|
|
1353
|
+
encoding: "utf-8",
|
|
1354
|
+
// Suppress noisy "not a git repository" output from stderr.
|
|
1355
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
1356
|
+
});
|
|
1357
|
+
const git_head = head.status === 0 ? head.stdout.trim() || void 0 : void 0;
|
|
1358
|
+
if (!git_head) return { git_head: void 0, git_dirty: false };
|
|
1359
|
+
const status = spawnSync("git", ["status", "--porcelain"], {
|
|
1360
|
+
cwd,
|
|
1361
|
+
encoding: "utf-8",
|
|
1362
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
1363
|
+
});
|
|
1364
|
+
const git_dirty = status.status === 0 && status.stdout.trim().length > 0;
|
|
1365
|
+
return { git_head, git_dirty };
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
// src/file_size_policy.ts
|
|
1369
|
+
var DEFAULT_MAX_PARTIAL_BYTES = 5e6;
|
|
1370
|
+
var DEFAULT_MAX_SKIP_BYTES = 5e7;
|
|
1371
|
+
function enforceFileSizePolicy(input) {
|
|
1372
|
+
const maxPartial = input.settings?.max_partial_bytes ?? DEFAULT_MAX_PARTIAL_BYTES;
|
|
1373
|
+
const maxSkip = input.settings?.max_skip_bytes ?? DEFAULT_MAX_SKIP_BYTES;
|
|
1374
|
+
if (maxSkip <= maxPartial) {
|
|
1375
|
+
throw new Error(
|
|
1376
|
+
`file_size_policy: misconfig \u2014 max_skip_bytes (${maxSkip}) must exceed max_partial_bytes (${maxPartial})`
|
|
1377
|
+
);
|
|
1378
|
+
}
|
|
1379
|
+
const sizeBefore = input.beforeBuf?.length;
|
|
1380
|
+
const sizeAfter = input.afterBuf?.length;
|
|
1381
|
+
const largerSize = Math.max(sizeBefore ?? 0, sizeAfter ?? 0);
|
|
1382
|
+
if (sizeBefore === void 0 && sizeAfter === void 0) {
|
|
1383
|
+
return {
|
|
1384
|
+
beforeBuf: void 0,
|
|
1385
|
+
afterBuf: void 0,
|
|
1386
|
+
partial_diff: false,
|
|
1387
|
+
redacted: false,
|
|
1388
|
+
size_before: void 0,
|
|
1389
|
+
size_after: void 0
|
|
1390
|
+
};
|
|
1391
|
+
}
|
|
1392
|
+
if (largerSize > maxSkip) {
|
|
1393
|
+
return {
|
|
1394
|
+
beforeBuf: void 0,
|
|
1395
|
+
afterBuf: void 0,
|
|
1396
|
+
partial_diff: false,
|
|
1397
|
+
redacted: true,
|
|
1398
|
+
size_before: sizeBefore,
|
|
1399
|
+
size_after: sizeAfter
|
|
1400
|
+
};
|
|
1401
|
+
}
|
|
1402
|
+
if (largerSize > maxPartial) {
|
|
1403
|
+
const dropBefore = (sizeBefore ?? 0) >= (sizeAfter ?? 0);
|
|
1404
|
+
return {
|
|
1405
|
+
beforeBuf: dropBefore ? void 0 : input.beforeBuf,
|
|
1406
|
+
afterBuf: dropBefore ? input.afterBuf : void 0,
|
|
1407
|
+
partial_diff: true,
|
|
1408
|
+
redacted: false,
|
|
1409
|
+
size_before: sizeBefore,
|
|
1410
|
+
size_after: sizeAfter
|
|
1411
|
+
};
|
|
1412
|
+
}
|
|
1413
|
+
return {
|
|
1414
|
+
beforeBuf: input.beforeBuf,
|
|
1415
|
+
afterBuf: input.afterBuf,
|
|
1416
|
+
partial_diff: false,
|
|
1417
|
+
redacted: false,
|
|
1418
|
+
size_before: sizeBefore,
|
|
1419
|
+
size_after: sizeAfter
|
|
1420
|
+
};
|
|
1421
|
+
}
|
|
1422
|
+
export {
|
|
1423
|
+
BlobStore,
|
|
1424
|
+
DEFAULT_MAX_PARTIAL_BYTES,
|
|
1425
|
+
DEFAULT_MAX_SKIP_BYTES,
|
|
1426
|
+
FileChangeInvariantError,
|
|
1427
|
+
SCHEMA_VERSION,
|
|
1428
|
+
Store,
|
|
1429
|
+
aggregateTokens,
|
|
1430
|
+
applyFileChange,
|
|
1431
|
+
assertFileChangeInvariants,
|
|
1432
|
+
captureBaseline,
|
|
1433
|
+
deleteSetting,
|
|
1434
|
+
enforceFileSizePolicy,
|
|
1435
|
+
ensureSchema,
|
|
1436
|
+
findBaselineByManifest,
|
|
1437
|
+
getBaselineTree,
|
|
1438
|
+
getFileChange,
|
|
1439
|
+
getIngestOffset,
|
|
1440
|
+
getRun,
|
|
1441
|
+
getRunBySessionId,
|
|
1442
|
+
getSetting,
|
|
1443
|
+
getStep,
|
|
1444
|
+
getStepBySequence,
|
|
1445
|
+
insertAnnotation,
|
|
1446
|
+
insertBaselineTree,
|
|
1447
|
+
insertFileChange,
|
|
1448
|
+
insertFork,
|
|
1449
|
+
insertRun,
|
|
1450
|
+
insertStep,
|
|
1451
|
+
isProbablyText,
|
|
1452
|
+
isSecret,
|
|
1453
|
+
listAnnotations,
|
|
1454
|
+
listFileChanges,
|
|
1455
|
+
listForks,
|
|
1456
|
+
listRuns,
|
|
1457
|
+
listSettings,
|
|
1458
|
+
listSteps,
|
|
1459
|
+
loadBaselineTree,
|
|
1460
|
+
maskSecret,
|
|
1461
|
+
parseManifest,
|
|
1462
|
+
recordContextSnapshot,
|
|
1463
|
+
resolveSetting,
|
|
1464
|
+
resolveSnapshotBlobRef,
|
|
1465
|
+
serializeManifest,
|
|
1466
|
+
setIngestOffset,
|
|
1467
|
+
setRunBaselineTree,
|
|
1468
|
+
setRunProbeState,
|
|
1469
|
+
setRunStatus,
|
|
1470
|
+
setSetting,
|
|
1471
|
+
updateRunTotals,
|
|
1472
|
+
upsertAgent,
|
|
1473
|
+
upsertProjectByCwd,
|
|
1474
|
+
workingTreeAt
|
|
1475
|
+
};
|