@meterbility/server 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/dist/index.js ADDED
@@ -0,0 +1,3886 @@
1
+ import {
2
+ DECISION_PREVIEW_LIMIT,
3
+ prettyMultilineString,
4
+ prettyTab,
5
+ prettyValue,
6
+ reformatJsonString,
7
+ renderContext,
8
+ renderDiff,
9
+ renderFleet,
10
+ renderProbePanel,
11
+ renderRun,
12
+ renderRunList,
13
+ renderShell,
14
+ renderStepCardFragment,
15
+ renderTests
16
+ } from "./chunk-VULWJFYO.js";
17
+
18
+ // src/replay.ts
19
+ import { randomUUID } from "crypto";
20
+ import { hashJson } from "@meterbility/shared";
21
+ import { costCents } from "@meterbility/spec";
22
+ import {
23
+ getRun,
24
+ getStep,
25
+ insertRun,
26
+ insertStep,
27
+ listSteps,
28
+ recordContextSnapshot,
29
+ resolveSnapshotBlobRef,
30
+ setRunStatus,
31
+ updateRunTotals,
32
+ upsertAgent,
33
+ upsertProjectByCwd
34
+ } from "@meterbility/collector";
35
+ async function materializePrefix(store, originRunId, forkStepId, edit) {
36
+ const origin = getRun(store, originRunId);
37
+ if (!origin) throw new Error(`unknown run: ${originRunId}`);
38
+ const forkStep = getStep(store, forkStepId);
39
+ if (!forkStep || forkStep.run_id !== originRunId) {
40
+ throw new Error(
41
+ `step ${forkStepId} does not belong to run ${originRunId}`
42
+ );
43
+ }
44
+ const allSteps = listSteps(store, originRunId);
45
+ const prefix = allSteps.filter((s) => s.sequence <= forkStep.sequence);
46
+ const newRunId = `run_${randomUUID()}`;
47
+ const project = upsertProjectByCwd(store, origin.cwd ?? "(unknown)");
48
+ const agent = upsertAgent(store, project.project_id, "fork");
49
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
50
+ const newRun = {
51
+ run_id: newRunId,
52
+ agent_id: agent.agent_id,
53
+ project_id: project.project_id,
54
+ source_session_id: void 0,
55
+ source_runtime: "fork",
56
+ title: forkTitle(origin, edit),
57
+ status: "in_progress",
58
+ started_at: startedAt,
59
+ git_branch: origin.git_branch,
60
+ cwd: origin.cwd,
61
+ fork_origin_run_id: originRunId,
62
+ fork_origin_step_id: forkStepId,
63
+ tokens_total_input: 0,
64
+ tokens_total_output: 0,
65
+ tokens_total_cached: 0,
66
+ cost_cents: 0,
67
+ step_count: 0,
68
+ tags: ["fork", `fork-edit:${edit.type}`]
69
+ };
70
+ insertRun(store, newRun);
71
+ let prevId;
72
+ for (const orig of prefix) {
73
+ const isForkPoint = orig.step_id === forkStepId;
74
+ const { step, snapshot } = isForkPoint ? await applyEditToStep(store, orig, edit) : copyStep(orig);
75
+ step.run_id = newRunId;
76
+ step.step_id = `stp_${randomUUID()}`;
77
+ step.parent_step_id = prevId;
78
+ step.fork_origin_id = isForkPoint ? orig.step_id : void 0;
79
+ if (snapshot) {
80
+ const ref = await store.blobs.putJson(snapshot);
81
+ recordContextSnapshot(
82
+ store,
83
+ snapshot.id,
84
+ ref,
85
+ snapshot.components.length
86
+ );
87
+ step.context_snapshot_id = snapshot.id;
88
+ }
89
+ if (isForkPoint) {
90
+ step.outcome = { status: "pending" };
91
+ step.status = "in_progress";
92
+ }
93
+ insertStep(store, step);
94
+ prevId = step.step_id;
95
+ }
96
+ updateRunTotals(store, newRunId);
97
+ setRunStatus(store, newRunId, "in_progress");
98
+ return { run_id: newRunId, prefix_steps: prefix.length };
99
+ }
100
+ function forkTitle(origin, edit) {
101
+ const base = origin.title ?? origin.run_id;
102
+ return `Fork[${edit.type}] of ${base}`.slice(0, 120);
103
+ }
104
+ function copyStep(orig) {
105
+ return {
106
+ step: {
107
+ ...orig,
108
+ tokens: { ...orig.tokens },
109
+ tags: [...orig.tags]
110
+ }
111
+ };
112
+ }
113
+ async function applyEditToStep(store, step, edit) {
114
+ const snapshot = await rewriteSnapshot(store, step.context_snapshot_id, edit);
115
+ return { step: { ...step, tokens: { ...step.tokens }, tags: [...step.tags] }, snapshot };
116
+ }
117
+ async function rewriteSnapshot(store, snapshotId, edit) {
118
+ const ref = resolveSnapshotBlobRef(store, snapshotId);
119
+ const base = await store.blobs.getJson(ref);
120
+ const components = [];
121
+ let injected = false;
122
+ for (const c of base.components) {
123
+ if (edit.type === "replace_system_prompt" && c.type === "system_prompt") {
124
+ const text = String(
125
+ edit.payload?.text ?? ""
126
+ );
127
+ const ref2 = await store.blobs.putString(text);
128
+ components.push({ type: "system_prompt", content_ref: ref2 });
129
+ injected = true;
130
+ } else if (edit.type === "replace_user_message" && c.type === "conversation_history") {
131
+ const stepRef = edit.payload?.step_ref;
132
+ const text = String(
133
+ edit.payload?.text ?? ""
134
+ );
135
+ const ref2 = await store.blobs.putString(text);
136
+ const newHistory = c.messages.map((m) => {
137
+ if (stepRef && m.step_ref === stepRef && m.role === "user") {
138
+ return { ...m, content_ref: ref2 };
139
+ }
140
+ return m;
141
+ });
142
+ if (!stepRef && newHistory.length > 0) {
143
+ for (let i = newHistory.length - 1; i >= 0; i--) {
144
+ if (newHistory[i].role === "user") {
145
+ newHistory[i] = { ...newHistory[i], content_ref: ref2 };
146
+ break;
147
+ }
148
+ }
149
+ }
150
+ components.push({ type: "conversation_history", messages: newHistory });
151
+ } else if (edit.type === "inject_message" && c.type === "conversation_history") {
152
+ const text = String(
153
+ edit.payload?.text ?? ""
154
+ );
155
+ const role = (edit.payload?.role ?? "user") === "assistant" ? "assistant" : "user";
156
+ const ref2 = await store.blobs.putString(text);
157
+ components.push({
158
+ type: "conversation_history",
159
+ messages: [...c.messages, { role, content_ref: ref2 }]
160
+ });
161
+ } else {
162
+ components.push(c);
163
+ }
164
+ }
165
+ if (edit.type === "replace_system_prompt" && !injected) {
166
+ const text = String(edit.payload?.text ?? "");
167
+ const ref2 = await store.blobs.putString(text);
168
+ components.unshift({ type: "system_prompt", content_ref: ref2 });
169
+ }
170
+ if (edit.type === "inject_message" && !components.some((c) => c.type === "conversation_history")) {
171
+ const text = String(edit.payload?.text ?? "");
172
+ const role = (edit.payload?.role ?? "user") === "assistant" ? "assistant" : "user";
173
+ const ref2 = await store.blobs.putString(text);
174
+ components.push({
175
+ type: "conversation_history",
176
+ messages: [{ role, content_ref: ref2 }]
177
+ });
178
+ }
179
+ return { id: hashJson(components), components };
180
+ }
181
+ async function appendLiveStep(store, runId, args) {
182
+ const run = getRun(store, runId);
183
+ if (!run) throw new Error(`unknown run: ${runId}`);
184
+ const prior = listSteps(store, run.run_id);
185
+ const sequence = prior.length;
186
+ const decisionRef = await store.blobs.putJson(args.decisionContent);
187
+ const snapshotRef = await store.blobs.putJson(args.snapshot);
188
+ recordContextSnapshot(
189
+ store,
190
+ args.snapshot.id,
191
+ snapshotRef,
192
+ args.snapshot.components.length
193
+ );
194
+ const { cost_cents, approx } = costCents(args.model, {
195
+ input: args.tokens.input,
196
+ output: args.tokens.output,
197
+ cached_read: args.tokens.cached_read,
198
+ cache_creation: args.tokens.cache_creation,
199
+ cache_creation_1h: args.tokens.cache_creation_1h
200
+ });
201
+ const tags = ["live-suffix"];
202
+ if (approx) tags.push("cost:approx");
203
+ const step = {
204
+ step_id: `stp_${randomUUID()}`,
205
+ run_id: runId,
206
+ parent_step_id: args.parent_step_id ?? prior[prior.length - 1]?.step_id,
207
+ sequence,
208
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
209
+ model: args.model,
210
+ context_snapshot_id: args.snapshot.id,
211
+ decision_ref: decisionRef,
212
+ action: args.action,
213
+ outcome: args.outcome,
214
+ tokens: args.tokens,
215
+ latency_ms: args.latency_ms,
216
+ cost_cents,
217
+ tags,
218
+ status: args.outcome.status === "error" ? "error" : args.outcome.status === "pending" ? "in_progress" : "ok"
219
+ };
220
+ insertStep(store, step);
221
+ updateRunTotals(store, runId);
222
+ return step;
223
+ }
224
+
225
+ // src/fork.ts
226
+ import {
227
+ getRun as getRun2,
228
+ getStep as getStep2,
229
+ getStepBySequence,
230
+ insertFork,
231
+ listSteps as listSteps2
232
+ } from "@meterbility/collector";
233
+ async function forkRun(store, args, liveSuffix) {
234
+ const origin = getRun2(store, args.origin_run_id);
235
+ if (!origin) throw new Error(`unknown origin run: ${args.origin_run_id}`);
236
+ const targetStep = typeof args.at === "number" ? getStepBySequence(store, origin.run_id, args.at) : getStep2(store, args.at);
237
+ if (!targetStep || targetStep.run_id !== origin.run_id) {
238
+ throw new Error(`fork target step not found in run ${origin.run_id}`);
239
+ }
240
+ validateEdit(args.edit);
241
+ const { run_id: fork_run_id, prefix_steps } = await materializePrefix(
242
+ store,
243
+ origin.run_id,
244
+ targetStep.step_id,
245
+ args.edit
246
+ );
247
+ const fork_id = insertFork(store, {
248
+ originRunId: origin.run_id,
249
+ originStepId: targetStep.step_id,
250
+ forkRunId: fork_run_id,
251
+ edit: args.edit
252
+ });
253
+ let live = false;
254
+ if (liveSuffix) {
255
+ const forkSteps = listSteps2(store, fork_run_id);
256
+ const lastForkStep = forkSteps[forkSteps.length - 1];
257
+ const history = lastForkStep.context_snapshot_id;
258
+ const result = await liveSuffix({
259
+ origin_step: targetStep,
260
+ context_snapshot_id: history,
261
+ edit: args.edit
262
+ });
263
+ await appendLiveStep(store, fork_run_id, {
264
+ model: result.model,
265
+ action: result.action,
266
+ outcome: result.outcome ?? { status: "ok" },
267
+ tokens: result.tokens,
268
+ latency_ms: result.latency_ms ?? 0,
269
+ parent_step_id: lastForkStep.step_id,
270
+ snapshot: {
271
+ id: lastForkStep.context_snapshot_id,
272
+ components: []
273
+ },
274
+ decisionContent: result.decision_content
275
+ });
276
+ live = true;
277
+ }
278
+ return { fork_id, fork_run_id, prefix_steps, live };
279
+ }
280
+ function validateEdit(edit) {
281
+ const allowed = [
282
+ "replace_system_prompt",
283
+ "add_context",
284
+ "remove_tool",
285
+ "modify_tool_description",
286
+ "replace_user_message",
287
+ "inject_message",
288
+ "change_model"
289
+ ];
290
+ if (!allowed.includes(edit.type)) {
291
+ throw new Error(`unsupported edit type: ${edit.type}`);
292
+ }
293
+ }
294
+ function fakeResponder(text) {
295
+ return async () => ({
296
+ model: "fake",
297
+ action: { kind: "message", text },
298
+ outcome: { status: "ok" },
299
+ tokens: { input: 0, output: 0, cached_read: 0, cache_creation: 0 },
300
+ latency_ms: 1,
301
+ decision_content: [{ type: "text", text }]
302
+ });
303
+ }
304
+ function anthropicResponder(store, opts) {
305
+ return async (args) => {
306
+ const { default: Anthropic } = await import("@anthropic-ai/sdk");
307
+ const client = new Anthropic({ apiKey: opts.apiKey });
308
+ const snapshot = await store.blobs.getJson(args.context_snapshot_id);
309
+ const history = snapshot.components.find(
310
+ (c) => c.type === "conversation_history"
311
+ );
312
+ const messages = [];
313
+ if (history?.messages) {
314
+ for (const m of history.messages) {
315
+ if (m.role === "tool") continue;
316
+ const text2 = await store.blobs.tryGetString(m.content_ref);
317
+ messages.push({ role: m.role, content: text2 ?? "" });
318
+ }
319
+ }
320
+ let systemPrompt = opts.systemPrompt;
321
+ const sys = snapshot.components.find((c) => c.type === "system_prompt");
322
+ if (sys?.content_ref && !systemPrompt) {
323
+ systemPrompt = await store.blobs.tryGetString(sys.content_ref);
324
+ }
325
+ const t0 = Date.now();
326
+ const resp = await client.messages.create({
327
+ model: opts.model,
328
+ max_tokens: opts.maxTokens ?? 4096,
329
+ system: systemPrompt,
330
+ messages: messages.length ? messages : [{ role: "user", content: "(no history)" }]
331
+ });
332
+ const t1 = Date.now();
333
+ const text = (resp.content ?? []).filter((b) => b.type === "text").map((b) => b.text).join("\n");
334
+ const usage = resp.usage;
335
+ const cc = usage?.cache_creation;
336
+ const tokens5m = cc ? cc.ephemeral_5m_input_tokens ?? 0 : usage?.cache_creation_input_tokens ?? 0;
337
+ const tokens1h = cc?.ephemeral_1h_input_tokens ?? 0;
338
+ return {
339
+ model: resp.model,
340
+ action: { kind: "message", text },
341
+ outcome: { status: "ok" },
342
+ tokens: {
343
+ input: resp.usage?.input_tokens ?? 0,
344
+ output: resp.usage?.output_tokens ?? 0,
345
+ cached_read: usage?.cache_read_input_tokens ?? 0,
346
+ cache_creation: tokens5m,
347
+ cache_creation_1h: tokens1h
348
+ },
349
+ latency_ms: t1 - t0,
350
+ decision_content: resp.content
351
+ };
352
+ };
353
+ }
354
+
355
+ // src/diff.ts
356
+ import { listSteps as listSteps3 } from "@meterbility/collector";
357
+ function digest(s) {
358
+ return {
359
+ step_id: s.step_id,
360
+ model: s.model,
361
+ context_snapshot_id: s.context_snapshot_id,
362
+ decision_ref: s.decision_ref,
363
+ action_kind: s.action.kind,
364
+ tool_name: s.action.tool_name,
365
+ outcome_status: s.outcome.status,
366
+ outcome_summary: s.outcome.summary,
367
+ cost_cents: s.cost_cents,
368
+ tokens_total: s.tokens.input + s.tokens.output,
369
+ status: s.status
370
+ };
371
+ }
372
+ function diffRuns(store, runA, runB) {
373
+ const stepsA = listSteps3(store, runA);
374
+ const stepsB = listSteps3(store, runB);
375
+ const rows = [];
376
+ const max = Math.max(stepsA.length, stepsB.length);
377
+ let sharedPrefixLength = 0;
378
+ let firstDivergence;
379
+ let diverged = false;
380
+ for (let i = 0; i < max; i++) {
381
+ const a = stepsA[i];
382
+ const b = stepsB[i];
383
+ if (!a && b) {
384
+ rows.push({ sequence: i, kind: "only_b", b: digest(b) });
385
+ if (firstDivergence === void 0) firstDivergence = i;
386
+ diverged = true;
387
+ continue;
388
+ }
389
+ if (a && !b) {
390
+ rows.push({ sequence: i, kind: "only_a", a: digest(a) });
391
+ if (firstDivergence === void 0) firstDivergence = i;
392
+ diverged = true;
393
+ continue;
394
+ }
395
+ if (!a || !b) continue;
396
+ if (diverged) {
397
+ rows.push({ sequence: i, kind: "diverged", a: digest(a), b: digest(b) });
398
+ continue;
399
+ }
400
+ const kind = classify(a, b);
401
+ rows.push({ sequence: i, kind, a: digest(a), b: digest(b) });
402
+ if (kind === "shared") {
403
+ sharedPrefixLength = i + 1;
404
+ } else {
405
+ if (firstDivergence === void 0) firstDivergence = i;
406
+ diverged = true;
407
+ }
408
+ }
409
+ return {
410
+ run_a_id: runA,
411
+ run_b_id: runB,
412
+ total_steps_a: stepsA.length,
413
+ total_steps_b: stepsB.length,
414
+ shared_prefix_length: sharedPrefixLength,
415
+ first_divergence_sequence: firstDivergence,
416
+ rows
417
+ };
418
+ }
419
+ function classify(a, b) {
420
+ if (a.context_snapshot_id !== b.context_snapshot_id) {
421
+ return "context_diff";
422
+ }
423
+ if (a.decision_ref !== b.decision_ref) {
424
+ return "decision_diff";
425
+ }
426
+ if (a.action.kind !== b.action.kind || a.action.tool_name !== b.action.tool_name) {
427
+ return "action_diff";
428
+ }
429
+ if (a.outcome.status !== b.outcome.status || a.outcome.is_error !== b.outcome.is_error) {
430
+ return "outcome_diff";
431
+ }
432
+ return "shared";
433
+ }
434
+
435
+ // src/web.ts
436
+ import { Hono } from "hono";
437
+ import { stream } from "hono/streaming";
438
+ import { serve } from "@hono/node-server";
439
+ import {
440
+ clearProbe,
441
+ consumeInject as consumeProbeInject,
442
+ readState as readProbeState2,
443
+ requestPause as probeRequestPause,
444
+ requestResume as probeRequestResume,
445
+ setInject as probeSetInject
446
+ } from "@meterbility/shared";
447
+ import {
448
+ getBaselineTree,
449
+ getFileChange,
450
+ getRun as getRun6,
451
+ getStep as getStep3,
452
+ getStepBySequence as getStepBySequence2,
453
+ listAnnotations,
454
+ listFileChanges as listFileChanges2,
455
+ listForks,
456
+ listRuns as listRuns3,
457
+ listSteps as listSteps7,
458
+ insertAnnotation as insertAnnotation2,
459
+ resolveSnapshotBlobRef as resolveSnapshotBlobRef3,
460
+ setRunStatus as setRunStatus3,
461
+ updateRunTotals as updateRunTotals3,
462
+ getSetting,
463
+ setSetting,
464
+ deleteSetting,
465
+ resolveSetting
466
+ } from "@meterbility/collector";
467
+ import { TRACE_FORMAT_VERSION } from "@meterbility/spec";
468
+
469
+ // src/probe_annotations.ts
470
+ import { insertAnnotation } from "@meterbility/collector";
471
+ var probeAnnotationFailedCount = 0;
472
+ function recordProbeIntervention(store, runId, kind, payload) {
473
+ try {
474
+ insertAnnotation(store, {
475
+ targetKind: "run",
476
+ targetId: runId,
477
+ author: "system:probe",
478
+ kind,
479
+ note: buildProbeNote(kind, payload)
480
+ });
481
+ } catch (err) {
482
+ probeAnnotationFailedCount += 1;
483
+ console.error(
484
+ `[probe_annotation] failed to record ${kind} for ${runId}: ${err.message} (total failures: ${probeAnnotationFailedCount})`
485
+ );
486
+ }
487
+ }
488
+ function buildProbeNote(kind, payload) {
489
+ if (kind === "probe_pause") {
490
+ const ts2 = String(payload.paused_at ?? (/* @__PURE__ */ new Date()).toISOString());
491
+ return `paused at ${ts2}`;
492
+ }
493
+ const bytes = Number(payload.inject_bytes ?? 0);
494
+ const ts = String(payload.resumed_at ?? (/* @__PURE__ */ new Date()).toISOString());
495
+ return `inject staged (${bytes} bytes) \u2014 resumed at ${ts}`;
496
+ }
497
+
498
+ // src/blob_render.ts
499
+ import { createHash } from "crypto";
500
+ var RENDER_VERSION = "v1";
501
+ var SHIKI_THEME = "github-dark-dimmed";
502
+ function sniffMimeAndLang(buf, pathHint) {
503
+ if (buf.length >= 8) {
504
+ if (buf[0] === 137 && buf[1] === 80 && buf[2] === 78 && buf[3] === 71) {
505
+ return { mime: "image/png", binary: true };
506
+ }
507
+ if (buf[0] === 255 && buf[1] === 216 && buf[2] === 255) {
508
+ return { mime: "image/jpeg", binary: true };
509
+ }
510
+ if (buf[0] === 71 && buf[1] === 73 && buf[2] === 70 && buf[3] === 56 && (buf[4] === 55 || buf[4] === 57) && buf[5] === 97) {
511
+ return { mime: "image/gif", binary: true };
512
+ }
513
+ if (buf.length >= 12 && buf[0] === 82 && buf[1] === 73 && buf[2] === 70 && buf[3] === 70 && buf[8] === 87 && buf[9] === 69 && buf[10] === 66 && buf[11] === 80) {
514
+ return { mime: "image/webp", binary: true };
515
+ }
516
+ }
517
+ const window = Math.min(buf.length, 8192);
518
+ for (let i = 0; i < window; i++) {
519
+ if (buf[i] === 0) {
520
+ return { mime: "application/octet-stream", binary: true };
521
+ }
522
+ }
523
+ const lang = pathHint ? langFromPath(pathHint) : void 0;
524
+ return { mime: "text/plain", lang, binary: false };
525
+ }
526
+ function langFromPath(path) {
527
+ const lower = path.toLowerCase();
528
+ const basename = lower.includes("/") ? lower.slice(lower.lastIndexOf("/") + 1) : lower;
529
+ if (basename === "dockerfile" || basename.startsWith("dockerfile."))
530
+ return "dockerfile";
531
+ if (basename === "makefile" || basename === "gnumakefile") return "makefile";
532
+ if (basename === "rakefile" || basename === "gemfile") return "ruby";
533
+ if (basename === "podfile") return "ruby";
534
+ if (basename === "vagrantfile") return "ruby";
535
+ if (basename === "jenkinsfile") return "groovy";
536
+ if (basename === "cmakelists.txt") return "cmake";
537
+ if (basename === "license" || basename === "license.txt") return void 0;
538
+ if (basename === "readme" || basename === "authors") return void 0;
539
+ const ext = basename.includes(".") ? basename.slice(basename.lastIndexOf(".") + 1) : "";
540
+ return EXT_TO_LANG[ext];
541
+ }
542
+ var EXT_TO_LANG = {
543
+ ts: "typescript",
544
+ tsx: "tsx",
545
+ js: "javascript",
546
+ jsx: "jsx",
547
+ mjs: "javascript",
548
+ cjs: "javascript",
549
+ py: "python",
550
+ rb: "ruby",
551
+ go: "go",
552
+ rs: "rust",
553
+ java: "java",
554
+ kt: "kotlin",
555
+ swift: "swift",
556
+ c: "c",
557
+ h: "c",
558
+ cpp: "cpp",
559
+ cc: "cpp",
560
+ hpp: "cpp",
561
+ cs: "csharp",
562
+ php: "php",
563
+ sh: "bash",
564
+ bash: "bash",
565
+ zsh: "bash",
566
+ fish: "fish",
567
+ ps1: "powershell",
568
+ sql: "sql",
569
+ json: "json",
570
+ jsonc: "jsonc",
571
+ yaml: "yaml",
572
+ yml: "yaml",
573
+ toml: "toml",
574
+ xml: "xml",
575
+ html: "html",
576
+ htm: "html",
577
+ css: "css",
578
+ scss: "scss",
579
+ sass: "sass",
580
+ less: "less",
581
+ md: "markdown",
582
+ mdx: "mdx",
583
+ rst: "rst",
584
+ tex: "latex",
585
+ vue: "vue",
586
+ svelte: "svelte",
587
+ dart: "dart",
588
+ ex: "elixir",
589
+ exs: "elixir",
590
+ erl: "erlang",
591
+ hs: "haskell",
592
+ lua: "lua",
593
+ pl: "perl",
594
+ r: "r",
595
+ scala: "scala",
596
+ zig: "zig",
597
+ proto: "proto",
598
+ graphql: "graphql",
599
+ gql: "graphql",
600
+ env: "bash",
601
+ ini: "ini",
602
+ conf: "ini",
603
+ diff: "diff",
604
+ patch: "diff"
605
+ };
606
+ function stripBom(buf) {
607
+ if (buf.length >= 3 && buf[0] === 239 && buf[1] === 187 && buf[2] === 191) {
608
+ return buf.subarray(3);
609
+ }
610
+ return buf;
611
+ }
612
+ function renderCacheKey(blobHash, lang) {
613
+ return "render_" + createHash("sha256").update(`${blobHash}|${lang}|${RENDER_VERSION}`).digest("hex");
614
+ }
615
+ var shikiHighlighterPromise;
616
+ function getShikiHighlighter() {
617
+ if (!shikiHighlighterPromise) {
618
+ shikiHighlighterPromise = (async () => {
619
+ const shiki = await import("shiki");
620
+ const highlighter = await shiki.createHighlighter({
621
+ themes: [SHIKI_THEME],
622
+ langs: [
623
+ "typescript",
624
+ "tsx",
625
+ "javascript",
626
+ "jsx",
627
+ "python",
628
+ "json",
629
+ "yaml",
630
+ "markdown",
631
+ "bash",
632
+ "go",
633
+ "rust",
634
+ "html",
635
+ "css",
636
+ "sql",
637
+ "diff"
638
+ ]
639
+ });
640
+ return highlighter;
641
+ })();
642
+ }
643
+ return shikiHighlighterPromise;
644
+ }
645
+ async function renderHighlighted(buf, lang) {
646
+ const cleaned = stripBom(buf);
647
+ const code = cleaned.toString("utf-8");
648
+ const effLang = lang ?? "plaintext";
649
+ try {
650
+ const highlighter = await getShikiHighlighter();
651
+ if (effLang !== "plaintext" && typeof highlighter.getLoadedLanguages === "function" && !highlighter.getLoadedLanguages().includes(effLang)) {
652
+ try {
653
+ await highlighter.loadLanguage(effLang);
654
+ } catch {
655
+ return renderPlaintext(code);
656
+ }
657
+ }
658
+ return highlighter.codeToHtml(code, {
659
+ lang: effLang,
660
+ theme: SHIKI_THEME
661
+ });
662
+ } catch {
663
+ return renderPlaintext(code);
664
+ }
665
+ }
666
+ function renderPlaintext(code) {
667
+ const escaped = code.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
668
+ return `<pre class="shiki"><code>${escaped}</code></pre>`;
669
+ }
670
+
671
+ // src/file_view.ts
672
+ var OP_RANK = {
673
+ delete: 0,
674
+ rename: 1,
675
+ modify: 2,
676
+ create: 3,
677
+ chmod: 4
678
+ };
679
+ function compareOpsByRisk(a, b) {
680
+ const ra = a !== void 0 ? OP_RANK[a] : 99;
681
+ const rb = b !== void 0 ? OP_RANK[b] : 99;
682
+ return ra - rb;
683
+ }
684
+ function worstOpAmong(fcs) {
685
+ if (fcs.length === 0) return void 0;
686
+ let best = fcs[0].op;
687
+ for (let i = 1; i < fcs.length; i++) {
688
+ if (compareOpsByRisk(fcs[i].op, best) < 0) best = fcs[i].op;
689
+ }
690
+ return best;
691
+ }
692
+ function buildFileTree(fcs) {
693
+ const byPath = /* @__PURE__ */ new Map();
694
+ for (const fc of fcs) {
695
+ const list = byPath.get(fc.path);
696
+ if (list) list.push(fc);
697
+ else byPath.set(fc.path, [fc]);
698
+ }
699
+ for (const list of byPath.values()) {
700
+ list.sort((a, b) => a.sequence - b.sequence);
701
+ }
702
+ const root = {
703
+ name: "",
704
+ path: "",
705
+ isDir: true,
706
+ dirs: /* @__PURE__ */ new Map(),
707
+ files: []
708
+ };
709
+ for (const [path, touches] of byPath) {
710
+ const segments = path.split("/").filter(Boolean);
711
+ let cursor = root;
712
+ for (let i = 0; i < segments.length - 1; i++) {
713
+ const seg = segments[i];
714
+ let next = cursor.dirs.get(seg);
715
+ if (!next) {
716
+ const dirPath = cursor.path ? `${cursor.path}/${seg}` : seg;
717
+ next = {
718
+ name: seg,
719
+ path: dirPath,
720
+ isDir: true,
721
+ dirs: /* @__PURE__ */ new Map(),
722
+ files: []
723
+ };
724
+ cursor.dirs.set(seg, next);
725
+ }
726
+ cursor = next;
727
+ }
728
+ const fileName = segments[segments.length - 1] ?? path;
729
+ const leaf = {
730
+ name: fileName,
731
+ path,
732
+ isDir: false,
733
+ worstOp: worstOpAmong(touches),
734
+ anyPartial: touches.some((t) => t.partial_diff),
735
+ anyRedacted: touches.some((t) => t.redacted),
736
+ anyBinary: touches.some(
737
+ (t) => !t.partial_diff && !t.redacted && t.patch_format === "binary"
738
+ ),
739
+ children: [],
740
+ changes: touches
741
+ };
742
+ cursor.files.push(leaf);
743
+ }
744
+ function materialize(dir) {
745
+ const childDirs = [];
746
+ for (const [, sub] of dir.dirs) {
747
+ const subChildren = materialize(sub);
748
+ const allLeaves = collectLeaves(subChildren);
749
+ childDirs.push({
750
+ name: sub.name,
751
+ path: sub.path,
752
+ isDir: true,
753
+ worstOp: worstOpAmong(allLeaves.flatMap((l) => l.changes)),
754
+ anyPartial: allLeaves.some((l) => l.anyPartial),
755
+ anyRedacted: allLeaves.some((l) => l.anyRedacted),
756
+ anyBinary: allLeaves.some((l) => l.anyBinary),
757
+ children: subChildren,
758
+ changes: []
759
+ });
760
+ }
761
+ childDirs.sort(
762
+ (a, b) => compareOpsByRisk(a.worstOp, b.worstOp) || a.name.localeCompare(b.name)
763
+ );
764
+ const childFiles = [...dir.files].sort(
765
+ (a, b) => compareOpsByRisk(a.worstOp, b.worstOp) || a.name.localeCompare(b.name)
766
+ );
767
+ return [...childDirs, ...childFiles];
768
+ }
769
+ return materialize(root);
770
+ }
771
+ function collectLeaves(nodes) {
772
+ const out = [];
773
+ for (const n of nodes) {
774
+ if (n.isDir) out.push(...collectLeaves(n.children));
775
+ else out.push(n);
776
+ }
777
+ return out;
778
+ }
779
+ function flattenForDefaultSelection(tree) {
780
+ const out = [];
781
+ function walk(nodes) {
782
+ for (const n of nodes) {
783
+ if (n.isDir) walk(n.children);
784
+ else out.push(n);
785
+ }
786
+ }
787
+ walk(tree);
788
+ return out;
789
+ }
790
+ function pickEmptyReason(args) {
791
+ if (!args.captureEnabled) return "capture_disabled";
792
+ if (args.totalFileChanges === 0) return "no_writes";
793
+ if (args.redactedStubs > 0 && args.redactedStubs === args.totalFileChanges) {
794
+ return "all_skipped";
795
+ }
796
+ return "unknown";
797
+ }
798
+ function renderEmptyState(reason) {
799
+ switch (reason) {
800
+ case "capture_disabled":
801
+ return `<div class="files-empty">
802
+ <p class="files-empty-headline">File capture is disabled for this run.</p>
803
+ <p class="files-empty-sub">
804
+ Re-enable via <code>capture.files.enabled</code> in
805
+ <a href="/settings">Settings \u2192</a>
806
+ </p>
807
+ </div>`;
808
+ case "no_writes":
809
+ return `<div class="files-empty">
810
+ <p class="files-empty-headline">
811
+ This run didn't modify any files.
812
+ </p>
813
+ <p class="files-empty-sub">All tool calls were read-only.</p>
814
+ </div>`;
815
+ case "all_skipped":
816
+ return `<div class="files-empty">
817
+ <p class="files-empty-headline">
818
+ All files in this run exceeded the 50MB capture cap.
819
+ </p>
820
+ <p class="files-empty-sub">
821
+ Adjust <code>capture.files.max_skip_bytes</code> in
822
+ <a href="/settings">Settings \u2192</a>
823
+ </p>
824
+ </div>`;
825
+ case "unknown":
826
+ default:
827
+ return `<div class="files-empty">
828
+ <p class="files-empty-headline">No files captured for this run.</p>
829
+ </div>`;
830
+ }
831
+ }
832
+ function esc(s) {
833
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
834
+ }
835
+ var OP_BADGE = {
836
+ create: { letter: "A", cls: "file-op-create", label: "Created" },
837
+ modify: { letter: "M", cls: "file-op-modify", label: "Modified" },
838
+ delete: { letter: "D", cls: "file-op-delete", label: "Deleted" },
839
+ rename: { letter: "R", cls: "file-op-rename", label: "Renamed" },
840
+ chmod: { letter: "P", cls: "file-op-chmod", label: "Permissions" }
841
+ };
842
+ function renderRunHeaderStrip(args) {
843
+ const totals = args.fileChanges.reduce(
844
+ (acc, fc) => ({
845
+ added: acc.added + (fc.lines_added ?? 0),
846
+ removed: acc.removed + (fc.lines_removed ?? 0),
847
+ partial: acc.partial + (fc.partial_diff ? 1 : 0),
848
+ redacted: acc.redacted + (fc.redacted ? 1 : 0),
849
+ uniquePaths: acc.uniquePaths
850
+ }),
851
+ { added: 0, removed: 0, partial: 0, redacted: 0, uniquePaths: 0 }
852
+ );
853
+ totals.uniquePaths = new Set(args.fileChanges.map((fc) => fc.path)).size;
854
+ const runShort = args.run.run_id.slice(0, 12);
855
+ const flagChips = [];
856
+ if (totals.partial > 0) {
857
+ flagChips.push(
858
+ `<span class="file-flag flag-partial">${totals.partial} partial</span>`
859
+ );
860
+ }
861
+ if (totals.redacted > 0) {
862
+ flagChips.push(
863
+ `<span class="file-flag flag-redacted">${totals.redacted} redacted</span>`
864
+ );
865
+ }
866
+ return `<div class="files-header-strip" role="region" aria-label="Run files summary">
867
+ <a class="files-back-link" href="/runs/${esc(args.run.run_id)}">\u2190 Run ${esc(runShort)}</a>
868
+ <div class="files-summary">
869
+ <span class="files-stat-add">+${totals.added}</span>
870
+ <span class="files-stat-rm">\u2212${totals.removed}</span>
871
+ <span class="files-stat-count">${totals.uniquePaths} file${totals.uniquePaths === 1 ? "" : "s"}</span>
872
+ <span class="files-stat-count">${args.stepCount} step${args.stepCount === 1 ? "" : "s"} touched</span>
873
+ ${flagChips.join(" ")}
874
+ </div>
875
+ </div>`;
876
+ }
877
+ function renderTreeNode(node, selectedPath, expanded, depth) {
878
+ if (node.isDir) {
879
+ const isOpen = expanded.has(node.path);
880
+ const children = isOpen ? `<div class="file-tree-children">${node.children.map((c) => renderTreeNode(c, selectedPath, expanded, depth + 1)).join("")}</div>` : "";
881
+ const badge2 = node.worstOp ? `<span class="file-op-badge ${OP_BADGE[node.worstOp].cls}" aria-label="${OP_BADGE[node.worstOp].label}">${OP_BADGE[node.worstOp].letter}</span>` : "";
882
+ return `<div class="file-tree-dir" data-dir-path="${esc(node.path)}">
883
+ <button class="file-tree-row file-tree-row-dir"
884
+ data-action="toggle-dir"
885
+ data-path="${esc(node.path)}"
886
+ aria-expanded="${isOpen}"
887
+ style="padding-left: ${depth * 12 + 8}px">
888
+ <span class="file-tree-chevron${isOpen ? " is-open" : ""}" aria-hidden="true">\u25B8</span>
889
+ ${badge2}
890
+ <span class="file-tree-name">${esc(node.name)}</span>
891
+ </button>
892
+ ${children}
893
+ </div>`;
894
+ }
895
+ const isSelected = node.path === selectedPath;
896
+ const badge = node.worstOp ? `<span class="file-op-badge ${OP_BADGE[node.worstOp].cls}" aria-label="${OP_BADGE[node.worstOp].label}">${OP_BADGE[node.worstOp].letter}</span>` : "";
897
+ const chipParts = [];
898
+ if (node.anyPartial) chipParts.push(`<span class="file-flag flag-partial">partial</span>`);
899
+ if (node.anyRedacted) chipParts.push(`<span class="file-flag flag-redacted">redacted</span>`);
900
+ if (node.anyBinary) chipParts.push(`<span class="file-flag flag-binary">binary</span>`);
901
+ return `<a class="file-tree-row file-tree-row-file${isSelected ? " is-selected" : ""}"
902
+ href="#path=${encodeURIComponent(node.path)}"
903
+ data-action="select-file"
904
+ data-path="${esc(node.path)}"
905
+ title="${esc(node.path)}"
906
+ aria-current="${isSelected ? "page" : "false"}"
907
+ style="padding-left: ${depth * 12 + 24}px">
908
+ ${badge}
909
+ <span class="file-tree-name">${esc(node.name)}</span>
910
+ ${chipParts.join(" ")}
911
+ </a>`;
912
+ }
913
+ function renderRightPane(args) {
914
+ const tab = args.tab ?? "final";
915
+ const touches = args.node.changes;
916
+ const totals = touches.reduce(
917
+ (acc, fc) => ({
918
+ added: acc.added + (fc.lines_added ?? 0),
919
+ removed: acc.removed + (fc.lines_removed ?? 0)
920
+ }),
921
+ { added: 0, removed: 0 }
922
+ );
923
+ const op = args.node.worstOp;
924
+ const opLabel = op ? OP_BADGE[op].label : "";
925
+ const touchCount = touches.length;
926
+ const finalActive = tab === "final";
927
+ const historyActive = tab === "history";
928
+ const rawActive = tab === "raw";
929
+ let finalBlobRef;
930
+ let finalIsPartial = false;
931
+ let finalIsRedacted = false;
932
+ let finalSizeBefore;
933
+ let finalSizeAfter;
934
+ for (let i = touches.length - 1; i >= 0; i--) {
935
+ const t = touches[i];
936
+ if (t.after_blob_ref) {
937
+ finalBlobRef = t.after_blob_ref;
938
+ finalIsPartial = t.partial_diff;
939
+ finalIsRedacted = t.redacted;
940
+ finalSizeBefore = t.size_before;
941
+ finalSizeAfter = t.size_after;
942
+ break;
943
+ }
944
+ }
945
+ if (!finalBlobRef) {
946
+ const last = touches[touches.length - 1];
947
+ if (last) {
948
+ finalIsPartial = last.partial_diff;
949
+ finalIsRedacted = last.redacted;
950
+ finalSizeBefore = last.size_before;
951
+ finalSizeAfter = last.size_after;
952
+ }
953
+ }
954
+ const headerHtml = `<div class="file-pane-header">
955
+ <span class="file-pane-path">${esc(args.node.path)}</span>
956
+ <span class="file-pane-stats">
957
+ <span class="files-stat-add">+${totals.added}</span>
958
+ <span class="files-stat-rm">\u2212${totals.removed}</span>
959
+ </span>
960
+ <span class="file-pane-op">${esc(opLabel)} \xB7 ${touchCount} touch${touchCount === 1 ? "" : "es"}</span>
961
+ </div>`;
962
+ const tabStrip = `<div class="tab-bar" role="tablist">
963
+ <button class="tab-btn${finalActive ? " is-active" : ""}"
964
+ role="tab"
965
+ aria-selected="${finalActive}"
966
+ data-action="select-tab"
967
+ data-tab="final"
968
+ data-key="1">Final</button>
969
+ <button class="tab-btn${historyActive ? " is-active" : ""}"
970
+ role="tab"
971
+ aria-selected="${historyActive}"
972
+ data-action="select-tab"
973
+ data-tab="history"
974
+ data-key="2">History <span class="tab-count">${touchCount}</span></button>
975
+ <button class="tab-btn${rawActive ? " is-active" : ""}"
976
+ role="tab"
977
+ aria-selected="${rawActive}"
978
+ data-action="select-tab"
979
+ data-tab="raw"
980
+ data-key="3">Raw <span class="tab-count">${touches.length}</span></button>
981
+ </div>`;
982
+ let finalBody = "";
983
+ if (finalActive) {
984
+ if (finalIsRedacted) {
985
+ finalBody = `<div class="file-banner banner-redacted" role="status">
986
+ \u26D4 Content not captured (file exceeded 50MB cap).
987
+ Original size: ${fmtBytes(finalSizeBefore ?? finalSizeAfter)}.
988
+ <a href="/settings#capture.files.max_skip_bytes">Why? \u2192</a>
989
+ </div>`;
990
+ } else if (!finalBlobRef && op === "delete") {
991
+ finalBody = `<div class="file-banner banner-deleted" role="status">
992
+ \u{1F5D1} File deleted in this run. No final content.
993
+ </div>`;
994
+ } else if (!finalBlobRef) {
995
+ finalBody = `<div class="file-banner banner-missing" role="status">
996
+ \u2139 No final content available for this file.
997
+ </div>`;
998
+ } else if (args.node.anyBinary) {
999
+ finalBody = `<div class="file-binary-placard">
1000
+ <p>Binary file \xB7 ${fmtBytes(finalSizeAfter ?? finalSizeBefore)}</p>
1001
+ <a href="/api/blob/${esc(finalBlobRef)}" class="btn-secondary">Download \u2192</a>
1002
+ </div>`;
1003
+ } else {
1004
+ const partialBanner = finalIsPartial ? `<div class="file-banner banner-partial" role="status">
1005
+ \u26A0 First ${fmtBytes(finalSizeAfter)} of ${fmtBytes(finalSizeBefore ?? finalSizeAfter)} shown
1006
+ \u2014 file exceeded capture policy.
1007
+ <a href="/api/blob/${esc(finalBlobRef)}">Download full blob \u2192</a>
1008
+ </div>` : "";
1009
+ const langQS = args.langHint ? `?lang=${encodeURIComponent(args.langHint)}&path=${encodeURIComponent(args.node.path)}` : `?path=${encodeURIComponent(args.node.path)}`;
1010
+ finalBody = `${partialBanner}
1011
+ <div class="file-final-render"
1012
+ data-blob-ref="${esc(finalBlobRef)}"
1013
+ data-render-url="/api/blob/${esc(finalBlobRef)}/render${langQS}">
1014
+ <iframe class="file-final-frame"
1015
+ src="/api/blob/${esc(finalBlobRef)}/render${langQS}"
1016
+ sandbox="allow-same-origin"
1017
+ loading="lazy"
1018
+ title="Rendered ${esc(args.node.path)}"></iframe>
1019
+ </div>`;
1020
+ }
1021
+ }
1022
+ let historyBody = "";
1023
+ if (historyActive) {
1024
+ if (touches.length === 0) {
1025
+ historyBody = `<p class="file-pane-empty">No touches recorded.</p>`;
1026
+ } else {
1027
+ historyBody = touches.map((t) => renderTouchSummary(t)).join("\n");
1028
+ }
1029
+ }
1030
+ let rawBody = "";
1031
+ if (rawActive) {
1032
+ rawBody = renderRawTab(touches);
1033
+ }
1034
+ return `<section class="file-pane" data-selected-path="${esc(args.node.path)}">
1035
+ ${headerHtml}
1036
+ ${tabStrip}
1037
+ <div class="file-pane-body" role="tabpanel">
1038
+ ${finalBody}${historyBody}${rawBody}
1039
+ </div>
1040
+ </section>`;
1041
+ }
1042
+ function renderTouchSummary(t) {
1043
+ const badge = OP_BADGE[t.op];
1044
+ return `<div class="file-history-touch">
1045
+ <span class="file-op-badge ${badge.cls}" aria-label="${badge.label}">${badge.letter}</span>
1046
+ <span class="file-history-meta">
1047
+ seq <code>${t.sequence}</code> \xB7
1048
+ step <code>${esc(t.step_id.slice(0, 12))}</code> \xB7
1049
+ <span class="files-stat-add">+${t.lines_added ?? 0}</span>
1050
+ <span class="files-stat-rm">\u2212${t.lines_removed ?? 0}</span>
1051
+ ${t.partial_diff ? `<span class="file-flag flag-partial">partial</span>` : ""}
1052
+ ${t.redacted ? `<span class="file-flag flag-redacted">redacted</span>` : ""}
1053
+ </span>
1054
+ ${t.patch_text ? `<pre class="file-history-diff">${esc(t.patch_text)}</pre>` : ""}
1055
+ </div>`;
1056
+ }
1057
+ function renderRawTab(touches) {
1058
+ if (touches.length === 0) {
1059
+ return `<p class="file-pane-empty">No blob refs recorded.</p>`;
1060
+ }
1061
+ const rows = touches.map((t) => {
1062
+ const badge = OP_BADGE[t.op];
1063
+ const beforeCell = t.before_blob_ref ? `<a href="/api/blob/${esc(t.before_blob_ref)}" class="raw-blob-link" title="${esc(t.before_blob_ref)}">${esc(shortHash(t.before_blob_ref))}</a>` : `<span class="raw-blob-none">\u2014</span>`;
1064
+ const afterCell = t.after_blob_ref ? `<a href="/api/blob/${esc(t.after_blob_ref)}" class="raw-blob-link" title="${esc(t.after_blob_ref)}">${esc(shortHash(t.after_blob_ref))}</a>` : `<span class="raw-blob-none">\u2014</span>`;
1065
+ return `<tr>
1066
+ <td>${t.sequence}</td>
1067
+ <td><span class="file-op-badge ${badge.cls}">${badge.letter}</span></td>
1068
+ <td>${beforeCell}</td>
1069
+ <td>${afterCell}</td>
1070
+ <td>${fmtBytes(t.size_before)}</td>
1071
+ <td>${fmtBytes(t.size_after)}</td>
1072
+ <td>${esc(t.patch_format ?? "")}</td>
1073
+ </tr>`;
1074
+ }).join("\n");
1075
+ return `<table class="raw-tab-table">
1076
+ <thead>
1077
+ <tr>
1078
+ <th>seq</th>
1079
+ <th>op</th>
1080
+ <th>before</th>
1081
+ <th>after</th>
1082
+ <th>size before</th>
1083
+ <th>size after</th>
1084
+ <th>format</th>
1085
+ </tr>
1086
+ </thead>
1087
+ <tbody>${rows}</tbody>
1088
+ </table>`;
1089
+ }
1090
+ function shortHash(hash) {
1091
+ if (hash.length <= 12) return hash;
1092
+ return hash.slice(0, 8) + "\u2026" + hash.slice(-4);
1093
+ }
1094
+ function fmtBytes(n) {
1095
+ if (n === void 0 || n === null) return "\u2014";
1096
+ if (n < 1024) return `${n} B`;
1097
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
1098
+ if (n < 1024 * 1024 * 1024) return `${(n / 1024 / 1024).toFixed(1)} MB`;
1099
+ return `${(n / 1024 / 1024 / 1024).toFixed(2)} GB`;
1100
+ }
1101
+ function renderFilesPage(args) {
1102
+ const headerStrip = renderRunHeaderStrip({
1103
+ run: args.run,
1104
+ fileChanges: args.fileChanges,
1105
+ stepCount: args.steps.length
1106
+ });
1107
+ if (args.fileChanges.length === 0 || !args.captureEnabled) {
1108
+ const stubsRedacted = args.fileChanges.filter((fc) => fc.redacted).length;
1109
+ const reason = pickEmptyReason({
1110
+ captureEnabled: args.captureEnabled,
1111
+ totalFileChanges: args.fileChanges.length,
1112
+ redactedStubs: stubsRedacted
1113
+ });
1114
+ return {
1115
+ bodyHtml: `${headerStrip}${renderEmptyState(reason)}`,
1116
+ stylesHtml: FILES_PAGE_STYLES,
1117
+ scriptsHtml: ""
1118
+ };
1119
+ }
1120
+ const tree = buildFileTree(args.fileChanges);
1121
+ const flat = flattenForDefaultSelection(tree);
1122
+ const selected = args.selectedPath && flat.find((n) => n.path === args.selectedPath) ? flat.find((n) => n.path === args.selectedPath) : flat[0];
1123
+ const expanded = new Set(args.expandedPaths ?? []);
1124
+ if (selected) {
1125
+ const segs = selected.path.split("/").filter(Boolean);
1126
+ let acc = "";
1127
+ for (let i = 0; i < segs.length - 1; i++) {
1128
+ acc = acc ? `${acc}/${segs[i]}` : segs[i];
1129
+ expanded.add(acc);
1130
+ }
1131
+ }
1132
+ const treeHtml = `<nav class="file-tree" role="tree" aria-label="Files changed in run">
1133
+ ${tree.map((n) => renderTreeNode(n, selected?.path ?? "", expanded, 0)).join("\n")}
1134
+ </nav>`;
1135
+ const paneHtml = selected ? renderRightPane({
1136
+ runId: args.run.run_id,
1137
+ node: selected,
1138
+ tab: args.tab
1139
+ }) : `<section class="file-pane"><p>Select a file on the left.</p></section>`;
1140
+ const mobileDropdown = `<select class="file-tree-mobile-select"
1141
+ aria-label="Pick a file"
1142
+ data-action="mobile-select-file">
1143
+ ${flat.map(
1144
+ (n) => `<option value="${esc(n.path)}"${n.path === selected?.path ? " selected" : ""}>${esc(n.path)}</option>`
1145
+ ).join("")}
1146
+ </select>`;
1147
+ return {
1148
+ bodyHtml: `${headerStrip}
1149
+ ${mobileDropdown}
1150
+ <div class="files-two-pane">
1151
+ <aside class="files-tree-pane">${treeHtml}</aside>
1152
+ <main class="files-content-pane">${paneHtml}</main>
1153
+ </div>`,
1154
+ stylesHtml: FILES_PAGE_STYLES,
1155
+ scriptsHtml: FILES_PAGE_SCRIPTS
1156
+ };
1157
+ }
1158
+ var FILES_PAGE_STYLES = `<style>
1159
+ .files-header-strip {
1160
+ display: flex; align-items: center; gap: 16px;
1161
+ padding: 14px 20px;
1162
+ border-bottom: 1px solid var(--border);
1163
+ background: var(--surface-1);
1164
+ }
1165
+ .files-back-link {
1166
+ font-size: 12.5px; color: var(--text-secondary);
1167
+ text-decoration: none;
1168
+ }
1169
+ .files-back-link:hover { color: var(--cerulean-400); }
1170
+ .files-empty {
1171
+ padding: 80px 20px; text-align: center;
1172
+ color: var(--text-secondary);
1173
+ }
1174
+ .files-empty-headline {
1175
+ font-size: 15px; color: var(--text-primary); margin: 0 0 8px;
1176
+ }
1177
+ .files-empty-sub {
1178
+ font-size: 13px; color: var(--text-tertiary); margin: 0;
1179
+ }
1180
+ .files-empty code {
1181
+ font-family: var(--font-mono); font-size: 12px;
1182
+ background: var(--surface-2); padding: 1px 6px; border-radius: 3px;
1183
+ }
1184
+ .files-two-pane {
1185
+ display: grid;
1186
+ grid-template-columns: 280px 1fr;
1187
+ gap: 0;
1188
+ min-height: calc(100vh - 120px);
1189
+ }
1190
+ .files-tree-pane {
1191
+ border-right: 1px solid var(--border);
1192
+ background: var(--surface-1);
1193
+ overflow-y: auto;
1194
+ max-height: calc(100vh - 120px);
1195
+ }
1196
+ .files-content-pane {
1197
+ background: var(--surface-0);
1198
+ overflow: hidden;
1199
+ }
1200
+ .file-tree {
1201
+ padding: 4px 0;
1202
+ font-size: 13px;
1203
+ }
1204
+ .file-tree-row {
1205
+ display: flex; align-items: center; gap: 8px;
1206
+ width: 100%; min-height: 28px;
1207
+ padding: 4px 12px 4px 8px;
1208
+ border: none; background: none; color: var(--text-primary);
1209
+ text-align: left; cursor: pointer;
1210
+ font-family: inherit; font-size: 13px;
1211
+ text-decoration: none;
1212
+ border-left: 2px solid transparent;
1213
+ }
1214
+ .file-tree-row:hover { background: var(--surface-2); }
1215
+ .file-tree-row:focus-visible {
1216
+ outline: var(--focus-ring); outline-offset: -2px;
1217
+ }
1218
+ .file-tree-row.is-selected {
1219
+ background: rgba(56,189,248,0.08);
1220
+ border-left-color: var(--cerulean-400);
1221
+ }
1222
+ .file-tree-chevron {
1223
+ display: inline-block;
1224
+ color: var(--text-tertiary);
1225
+ transition: transform 120ms ease;
1226
+ width: 12px; text-align: center;
1227
+ }
1228
+ .file-tree-chevron.is-open { transform: rotate(90deg); }
1229
+ .file-tree-name {
1230
+ flex: 1; min-width: 0;
1231
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
1232
+ }
1233
+ .file-op-badge {
1234
+ display: inline-flex; align-items: center; justify-content: center;
1235
+ width: 20px; height: 20px;
1236
+ font-family: var(--font-mono); font-size: 11px; font-weight: 600;
1237
+ border-radius: 3px;
1238
+ flex-shrink: 0;
1239
+ }
1240
+ .file-tree-mobile-select { display: none; }
1241
+
1242
+ .file-pane {
1243
+ padding: 20px 24px;
1244
+ height: 100%;
1245
+ overflow-y: auto;
1246
+ }
1247
+ .file-pane-header {
1248
+ display: flex; align-items: baseline; gap: 12px;
1249
+ padding-bottom: 12px;
1250
+ border-bottom: 1px solid var(--border);
1251
+ margin-bottom: 12px;
1252
+ }
1253
+ .file-pane-path {
1254
+ font-family: var(--font-mono); font-size: 12.5px;
1255
+ color: var(--text-primary);
1256
+ flex: 1; overflow-wrap: anywhere;
1257
+ }
1258
+ .file-pane-stats { display: inline-flex; gap: 6px; font-size: 12px; }
1259
+ .file-pane-op {
1260
+ font-size: 11.5px; color: var(--text-tertiary);
1261
+ text-transform: uppercase; letter-spacing: 0.04em;
1262
+ }
1263
+ .file-banner {
1264
+ padding: 10px 14px; margin-bottom: 12px;
1265
+ border-radius: 4px; font-size: 12.5px;
1266
+ border-left: 3px solid;
1267
+ }
1268
+ .banner-partial {
1269
+ background: var(--amber-bg); border-left-color: var(--amber-400);
1270
+ color: var(--text-primary);
1271
+ }
1272
+ .banner-redacted {
1273
+ background: rgba(248,113,113,0.08); border-left-color: var(--coral-400);
1274
+ color: var(--text-primary);
1275
+ }
1276
+ .banner-deleted, .banner-missing {
1277
+ background: var(--surface-2); border-left-color: var(--text-tertiary);
1278
+ color: var(--text-secondary);
1279
+ }
1280
+ .file-banner a { color: var(--cerulean-400); }
1281
+ .file-final-render { background: var(--surface-0); border-radius: 4px; }
1282
+ .file-final-frame {
1283
+ width: 100%; min-height: 60vh; border: none;
1284
+ background: var(--surface-0);
1285
+ }
1286
+ .file-binary-placard {
1287
+ padding: 32px 20px; text-align: center;
1288
+ background: var(--surface-2); border-radius: 4px;
1289
+ color: var(--text-secondary);
1290
+ }
1291
+ .btn-secondary {
1292
+ display: inline-block; margin-top: 8px;
1293
+ padding: 6px 14px; border-radius: 4px;
1294
+ background: var(--surface-3); color: var(--text-primary);
1295
+ text-decoration: none; font-size: 12.5px;
1296
+ }
1297
+ .file-history-touch {
1298
+ padding: 10px 0; border-bottom: 1px solid var(--border);
1299
+ }
1300
+ .file-history-touch:last-child { border-bottom: none; }
1301
+ .file-history-meta {
1302
+ font-size: 12px; color: var(--text-tertiary);
1303
+ margin-left: 8px;
1304
+ }
1305
+ .file-history-diff {
1306
+ margin-top: 8px;
1307
+ font-family: var(--font-mono); font-size: 12px;
1308
+ background: var(--surface-1); padding: 10px 12px;
1309
+ border-radius: 4px; overflow-x: auto;
1310
+ }
1311
+ .raw-tab-table {
1312
+ width: 100%; border-collapse: collapse;
1313
+ font-size: 12.5px;
1314
+ }
1315
+ .raw-tab-table th {
1316
+ text-align: left; padding: 8px 12px;
1317
+ border-bottom: 1px solid var(--border);
1318
+ color: var(--text-tertiary);
1319
+ font-weight: 500; text-transform: uppercase;
1320
+ font-size: 10.5px; letter-spacing: 0.04em;
1321
+ }
1322
+ .raw-tab-table td {
1323
+ padding: 8px 12px;
1324
+ border-bottom: 1px solid var(--border);
1325
+ font-family: var(--font-mono);
1326
+ color: var(--text-primary);
1327
+ }
1328
+ .raw-blob-link {
1329
+ color: var(--cerulean-400); text-decoration: none;
1330
+ font-family: var(--font-mono);
1331
+ }
1332
+ .raw-blob-none { color: var(--text-tertiary); }
1333
+ .file-pane-empty {
1334
+ color: var(--text-tertiary); padding: 24px 0;
1335
+ }
1336
+
1337
+ /* D12 \u2014 responsive breakpoints. */
1338
+ @media (max-width: 1100px) {
1339
+ .files-two-pane {
1340
+ grid-template-columns: 1fr;
1341
+ }
1342
+ .files-tree-pane {
1343
+ max-height: 40vh;
1344
+ border-right: none;
1345
+ border-bottom: 1px solid var(--border);
1346
+ }
1347
+ }
1348
+ @media (max-width: 600px) {
1349
+ .files-tree-pane { display: none; }
1350
+ .file-tree-mobile-select {
1351
+ display: block; width: calc(100% - 40px);
1352
+ margin: 12px 20px;
1353
+ padding: 8px 12px;
1354
+ background: var(--surface-2); color: var(--text-primary);
1355
+ border: 1px solid var(--border); border-radius: 4px;
1356
+ font-family: var(--font-mono); font-size: 12.5px;
1357
+ }
1358
+ }
1359
+ </style>`;
1360
+ var FILES_NAV_CLIENT_JS = String.raw`
1361
+ (function() {
1362
+ function parseFragment() {
1363
+ var hash = window.location.hash.replace(/^#/, '');
1364
+ var out = { path: '', open: [] };
1365
+ hash.split('&').forEach(function(part) {
1366
+ var eq = part.indexOf('=');
1367
+ if (eq < 0) return;
1368
+ var k = decodeURIComponent(part.slice(0, eq));
1369
+ var v = decodeURIComponent(part.slice(eq + 1));
1370
+ if (k === 'path') out.path = v;
1371
+ else if (k === 'open') out.open = v ? v.split(',') : [];
1372
+ });
1373
+ return out;
1374
+ }
1375
+ function writeFragment(state) {
1376
+ var parts = [];
1377
+ if (state.path) parts.push('path=' + encodeURIComponent(state.path));
1378
+ if (state.open.length)
1379
+ parts.push('open=' + state.open.map(encodeURIComponent).join(','));
1380
+ window.history.replaceState(null, '', '#' + parts.join('&'));
1381
+ }
1382
+ function getState() {
1383
+ var frag = parseFragment();
1384
+ var selected = document.querySelector('.file-tree-row.is-selected');
1385
+ var openDirs = [];
1386
+ document.querySelectorAll('.file-tree-row-dir[aria-expanded="true"]').forEach(
1387
+ function(el) { openDirs.push(el.dataset.path); }
1388
+ );
1389
+ return {
1390
+ path: selected ? selected.dataset.path : frag.path,
1391
+ open: openDirs.length ? openDirs : frag.open,
1392
+ };
1393
+ }
1394
+ function syncFragmentFromDom() { writeFragment(getState()); }
1395
+
1396
+ function focusableRows() {
1397
+ return Array.prototype.slice.call(
1398
+ document.querySelectorAll('.file-tree-row-file:not([hidden])')
1399
+ );
1400
+ }
1401
+ function currentIndex(rows) {
1402
+ var sel = document.querySelector('.file-tree-row-file.is-selected');
1403
+ if (!sel) return -1;
1404
+ return rows.indexOf(sel);
1405
+ }
1406
+ function selectByIndex(rows, idx) {
1407
+ if (idx < 0 || idx >= rows.length) return;
1408
+ rows.forEach(function(r) {
1409
+ r.classList.remove('is-selected');
1410
+ r.setAttribute('aria-current', 'false');
1411
+ });
1412
+ rows[idx].classList.add('is-selected');
1413
+ rows[idx].setAttribute('aria-current', 'page');
1414
+ rows[idx].focus();
1415
+ // Fragment fetch + swap right pane.
1416
+ var path = rows[idx].dataset.path;
1417
+ loadFragment(path);
1418
+ syncFragmentFromDom();
1419
+ }
1420
+ function loadFragment(path) {
1421
+ var runMatch = window.location.pathname.match(/\\/runs\\/([^/]+)\\/files/);
1422
+ if (!runMatch) return;
1423
+ var runId = runMatch[1];
1424
+ fetch('/runs/' + encodeURIComponent(runId) + '/files/' +
1425
+ encodeURIComponent(path) + window.location.search)
1426
+ .then(function(r) { return r.ok ? r.text() : ''; })
1427
+ .then(function(html) {
1428
+ if (!html) return;
1429
+ var pane = document.querySelector('.files-content-pane');
1430
+ if (pane) pane.innerHTML = html;
1431
+ })
1432
+ .catch(function() {});
1433
+ }
1434
+
1435
+ // Initial state restoration from URL fragment.
1436
+ var initialFrag = parseFragment();
1437
+ if (initialFrag.path) {
1438
+ var initial = document.querySelector(
1439
+ '.file-tree-row-file[data-path="' + cssEsc(initialFrag.path) + '"]'
1440
+ );
1441
+ if (initial) {
1442
+ document.querySelectorAll('.file-tree-row.is-selected').forEach(
1443
+ function(el) { el.classList.remove('is-selected'); }
1444
+ );
1445
+ initial.classList.add('is-selected');
1446
+ initial.setAttribute('aria-current', 'page');
1447
+ }
1448
+ }
1449
+ initialFrag.open.forEach(function(p) {
1450
+ var dir = document.querySelector(
1451
+ '.file-tree-row-dir[data-path="' + cssEsc(p) + '"]'
1452
+ );
1453
+ if (dir) dir.setAttribute('aria-expanded', 'true');
1454
+ });
1455
+
1456
+ function cssEsc(s) {
1457
+ return s.replace(/[\\\\"]/g, function(c) { return '\\\\' + c; });
1458
+ }
1459
+
1460
+ // Click handler — D9 (whole row toggles dir; file row swaps pane).
1461
+ document.addEventListener('click', function(e) {
1462
+ var btn = e.target.closest('[data-action]');
1463
+ if (!btn) return;
1464
+ var action = btn.dataset.action;
1465
+ if (action === 'toggle-dir') {
1466
+ var open = btn.getAttribute('aria-expanded') === 'true';
1467
+ btn.setAttribute('aria-expanded', open ? 'false' : 'true');
1468
+ var chev = btn.querySelector('.file-tree-chevron');
1469
+ if (chev) chev.classList.toggle('is-open');
1470
+ // toggle children visibility.
1471
+ var children = btn.nextElementSibling;
1472
+ if (children) children.style.display = open ? 'none' : '';
1473
+ syncFragmentFromDom();
1474
+ e.preventDefault();
1475
+ } else if (action === 'select-file') {
1476
+ e.preventDefault();
1477
+ var rows = focusableRows();
1478
+ var idx = rows.indexOf(btn);
1479
+ selectByIndex(rows, idx);
1480
+ } else if (action === 'select-tab') {
1481
+ e.preventDefault();
1482
+ selectTab(btn.dataset.tab);
1483
+ } else if (action === 'mobile-select-file') {
1484
+ // Handled by 'change' below.
1485
+ }
1486
+ });
1487
+
1488
+ // Mobile <select> dropdown (D12).
1489
+ document.addEventListener('change', function(e) {
1490
+ var el = e.target;
1491
+ if (el.dataset && el.dataset.action === 'mobile-select-file') {
1492
+ loadFragment(el.value);
1493
+ writeFragment({ path: el.value, open: parseFragment().open });
1494
+ }
1495
+ });
1496
+
1497
+ function selectTab(tab) {
1498
+ document.querySelectorAll('.tab-btn').forEach(function(b) {
1499
+ var isActive = b.dataset.tab === tab;
1500
+ b.classList.toggle('is-active', isActive);
1501
+ b.setAttribute('aria-selected', isActive ? 'true' : 'false');
1502
+ });
1503
+ var sel = document.querySelector('.file-tree-row-file.is-selected');
1504
+ if (!sel) return;
1505
+ var path = sel.dataset.path;
1506
+ var runMatch = window.location.pathname.match(/\\/runs\\/([^/]+)\\/files/);
1507
+ if (!runMatch) return;
1508
+ var runId = runMatch[1];
1509
+ fetch('/runs/' + encodeURIComponent(runId) + '/files/' +
1510
+ encodeURIComponent(path) + '?tab=' + encodeURIComponent(tab))
1511
+ .then(function(r) { return r.ok ? r.text() : ''; })
1512
+ .then(function(html) {
1513
+ if (!html) return;
1514
+ var pane = document.querySelector('.files-content-pane');
1515
+ if (pane) pane.innerHTML = html;
1516
+ })
1517
+ .catch(function() {});
1518
+ }
1519
+
1520
+ // Keyboard nav — D13.
1521
+ document.addEventListener('keydown', function(e) {
1522
+ if (e.target.matches('input, textarea, select')) return;
1523
+ var rows = focusableRows();
1524
+ var idx = currentIndex(rows);
1525
+ if (e.key === 'j' || e.key === 'ArrowDown') {
1526
+ e.preventDefault();
1527
+ selectByIndex(rows, Math.min(rows.length - 1, idx + 1));
1528
+ } else if (e.key === 'k' || e.key === 'ArrowUp') {
1529
+ e.preventDefault();
1530
+ selectByIndex(rows, Math.max(0, idx - 1));
1531
+ } else if (e.key === '1') {
1532
+ selectTab('final');
1533
+ } else if (e.key === '2') {
1534
+ selectTab('history');
1535
+ } else if (e.key === '3') {
1536
+ selectTab('raw');
1537
+ }
1538
+ });
1539
+ })();
1540
+ `;
1541
+ var FILES_PAGE_SCRIPTS = `<script>
1542
+ ${FILES_NAV_CLIENT_JS}
1543
+ </script>`;
1544
+
1545
+ // src/live.ts
1546
+ import { EventEmitter } from "events";
1547
+ import { existsSync } from "fs";
1548
+ import {
1549
+ claudeProjectsRoot,
1550
+ probeFilePath,
1551
+ readState as readProbeState
1552
+ } from "@meterbility/shared";
1553
+ import {
1554
+ ingestSession,
1555
+ discoverSessions,
1556
+ readSession,
1557
+ probeRecords,
1558
+ formatWarning
1559
+ } from "@meterbility/claude-code-adapter";
1560
+ import {
1561
+ getRun as getRun3,
1562
+ listFileChanges,
1563
+ listRuns,
1564
+ listSteps as listSteps4
1565
+ } from "@meterbility/collector";
1566
+
1567
+ // src/live-heuristics.ts
1568
+ function contextUtilization(step) {
1569
+ if (!step) return 0;
1570
+ const window = inferWindowSize(step.model);
1571
+ const total = step.tokens.input + step.tokens.cached_read + step.tokens.cache_creation;
1572
+ if (window === 0) return 0;
1573
+ return Math.min(100, Math.round(total / window * 100));
1574
+ }
1575
+ function inferWindowSize(model) {
1576
+ if (/\[1m\]|-1m\b|1m-context/i.test(model)) return 1e6;
1577
+ if (/opus|sonnet|haiku/i.test(model)) return 2e5;
1578
+ return 128e3;
1579
+ }
1580
+ function classifyRunStatus(run, steps, stallSeconds) {
1581
+ if (run.status === "ok") return "completed";
1582
+ if (run.status === "error") return "errored";
1583
+ if (steps.length === 0) return "progressing";
1584
+ const last = steps[steps.length - 1];
1585
+ const ageS = (Date.now() - new Date(last.timestamp).getTime()) / 1e3;
1586
+ if (last.outcome.status === "pending") {
1587
+ return ageS > stallSeconds ? "stalled" : "awaiting_input";
1588
+ }
1589
+ if (ageS > stallSeconds) return "stalled";
1590
+ if (detectLoop(steps, 4)) return "looping";
1591
+ return "progressing";
1592
+ }
1593
+ function detectLoop(steps, window = 4) {
1594
+ if (steps.length < window) return void 0;
1595
+ const tail = steps.slice(-window);
1596
+ const first = tail[0];
1597
+ if (first.action.kind !== "tool_call" || !first.action.tool_name) {
1598
+ return void 0;
1599
+ }
1600
+ const sig = JSON.stringify(first.action.tool_input ?? null);
1601
+ for (let i = 1; i < tail.length; i++) {
1602
+ const s = tail[i];
1603
+ if (s.action.kind !== "tool_call" || s.action.tool_name !== first.action.tool_name || JSON.stringify(s.action.tool_input ?? null) !== sig) {
1604
+ return void 0;
1605
+ }
1606
+ }
1607
+ return {
1608
+ tool: first.action.tool_name,
1609
+ signature: sig.slice(0, 64),
1610
+ repeats: window
1611
+ };
1612
+ }
1613
+
1614
+ // src/live.ts
1615
+ var DEFAULT_OPTS = {
1616
+ projectsRoot: claudeProjectsRoot(),
1617
+ scanIntervalMs: 1500,
1618
+ stallSeconds: 120,
1619
+ contextThresholds: [50, 70, 90],
1620
+ watchTools: [],
1621
+ loopWindow: 4
1622
+ };
1623
+ function buildFleetEntries(store, opts = {}) {
1624
+ const limit = opts.limit ?? 50;
1625
+ const stallSeconds = opts.stallSeconds ?? 120;
1626
+ const firedAlerts = opts.firedAlerts;
1627
+ const runs = listRuns(store, { limit });
1628
+ return runs.map((run) => {
1629
+ const steps = listSteps4(store, run.run_id);
1630
+ const lastStep = steps[steps.length - 1];
1631
+ const status = classifyRunStatus(run, steps, stallSeconds);
1632
+ const ctxPct = contextUtilization(lastStep);
1633
+ const recentTools = steps.slice(-5).map((s) => s.action.tool_name).filter((x) => !!x);
1634
+ const alerts = firedAlerts ? Array.from(
1635
+ firedAlerts.get(run.run_id)?.values() ?? []
1636
+ ).map((a) => ({ kind: a.kind, message: a.message })) : [];
1637
+ return {
1638
+ run,
1639
+ status,
1640
+ context_pct: ctxPct,
1641
+ recent_tools: recentTools,
1642
+ last_step_at: lastStep?.timestamp,
1643
+ alerts
1644
+ };
1645
+ });
1646
+ }
1647
+ var LiveInspector = class extends EventEmitter {
1648
+ store;
1649
+ opts;
1650
+ knownPaths = /* @__PURE__ */ new Set();
1651
+ // run_id → (dedup key → display info). The key prevents re-fires; the
1652
+ // value is what fleet entries render, so it must be human-readable.
1653
+ firedAlerts = /* @__PURE__ */ new Map();
1654
+ lastSizes = /* @__PURE__ */ new Map();
1655
+ // path → size at last poll
1656
+ lastStepCounts = /* @__PURE__ */ new Map();
1657
+ // run_id → step count
1658
+ lastStatus = /* @__PURE__ */ new Map();
1659
+ // run_id → last seen status
1660
+ probeTracks = /* @__PURE__ */ new Map();
1661
+ // run_id → probe poll state
1662
+ timer;
1663
+ stopped = false;
1664
+ /** Set on the first scan so we can backfill state silently — historical
1665
+ * sessions on disk are not "new runs" the user just kicked off. */
1666
+ booted = false;
1667
+ constructor(store, opts = {}) {
1668
+ super();
1669
+ this.store = store;
1670
+ this.opts = { ...DEFAULT_OPTS, ...opts };
1671
+ }
1672
+ async start() {
1673
+ for (const run of listRuns(this.store, { limit: 1e5 })) {
1674
+ this.lastStepCounts.set(run.run_id, run.step_count);
1675
+ this.lastStatus.set(run.run_id, run.status);
1676
+ }
1677
+ await this.tick({ silent: true });
1678
+ this.booted = true;
1679
+ void this.runShapeProbe().catch((err) => {
1680
+ console.warn("[meter/shape-probe] probe failed (non-fatal):", err);
1681
+ });
1682
+ this.timer = setInterval(() => {
1683
+ void this.tick().catch((err) => {
1684
+ console.error("[meter/live] tick error:", err);
1685
+ });
1686
+ }, this.opts.scanIntervalMs);
1687
+ }
1688
+ /**
1689
+ * Sample the newest few sessions and validate their records against
1690
+ * the shapes `types.ts` claims. One warning per unique drift hash
1691
+ * (so a single schema change doesn't spam thousands of lines).
1692
+ * Disabled when `METERBILITY_DISABLE_SHAPE_PROBE` is set — useful for
1693
+ * tests with intentionally minimal fixtures.
1694
+ */
1695
+ async runShapeProbe() {
1696
+ if (process.env.METERBILITY_DISABLE_SHAPE_PROBE) return;
1697
+ const sessions = await discoverSessions();
1698
+ const sample = sessions.sort((a, b) => b.mtime.getTime() - a.mtime.getTime()).slice(0, 5);
1699
+ if (sample.length === 0) return;
1700
+ const allRecords = [];
1701
+ for (const s of sample) {
1702
+ try {
1703
+ const parsed = await readSession(s.path);
1704
+ for (const p of parsed) allRecords.push(p.record);
1705
+ } catch {
1706
+ }
1707
+ }
1708
+ const warnings = probeRecords(allRecords);
1709
+ for (const w of warnings) {
1710
+ console.warn(formatWarning(w));
1711
+ }
1712
+ }
1713
+ stop() {
1714
+ this.stopped = true;
1715
+ if (this.timer) clearInterval(this.timer);
1716
+ this.timer = void 0;
1717
+ }
1718
+ /**
1719
+ * One scan: discover sessions, re-ingest any whose file has grown,
1720
+ * compute the current fleet snapshot, fire alerts where appropriate.
1721
+ *
1722
+ * `silent: true` runs the same ingest pipeline but suppresses event
1723
+ * emission — used for the first-tick backfill at startup so historical
1724
+ * runs don't masquerade as freshly-created ones.
1725
+ */
1726
+ async tick(opts = {}) {
1727
+ if (this.stopped) return;
1728
+ const silent = opts.silent === true;
1729
+ const sessions = await discoverSessions();
1730
+ const toProcess = /* @__PURE__ */ new Set();
1731
+ for (const s of sessions) {
1732
+ if (!this.knownPaths.has(s.path)) {
1733
+ this.knownPaths.add(s.path);
1734
+ toProcess.add(s.path);
1735
+ }
1736
+ const lastSize = this.lastSizes.get(s.path) ?? 0;
1737
+ if (s.size_bytes > lastSize) {
1738
+ toProcess.add(s.path);
1739
+ this.lastSizes.set(s.path, s.size_bytes);
1740
+ }
1741
+ }
1742
+ for (const path of toProcess) {
1743
+ try {
1744
+ const result = await ingestSession(this.store, path);
1745
+ if (result.status === "empty") continue;
1746
+ const run = getRun3(this.store, result.run_id);
1747
+ if (!run) continue;
1748
+ const newSteps = collectNewSteps(this.store, run, this.lastStepCounts);
1749
+ const wasKnown = this.lastStepCounts.has(run.run_id);
1750
+ const prevStatus = this.lastStatus.get(run.run_id);
1751
+ this.lastStepCounts.set(run.run_id, run.step_count);
1752
+ this.lastStatus.set(run.run_id, run.status);
1753
+ if (!silent) {
1754
+ if (!wasKnown) {
1755
+ this.emit("data", { type: "run:created", run });
1756
+ } else if (newSteps.length > 0) {
1757
+ this.emit("data", {
1758
+ type: "run:updated",
1759
+ run,
1760
+ new_steps: newSteps
1761
+ });
1762
+ }
1763
+ for (const s of newSteps) {
1764
+ this.emitFilesChangedIfAny(run, s);
1765
+ }
1766
+ }
1767
+ await this.maybeAlert(run, newSteps, silent);
1768
+ const isTerminal = run.status === "ok" || run.status === "error";
1769
+ const wasTerminal = prevStatus === "ok" || prevStatus === "error";
1770
+ if (!silent && isTerminal && !wasTerminal) {
1771
+ this.emit("data", {
1772
+ type: "run:completed",
1773
+ run
1774
+ });
1775
+ }
1776
+ } catch (err) {
1777
+ console.error(`[meter/live] ingest failed for ${path}:`, err);
1778
+ }
1779
+ }
1780
+ if (!silent) this.pollProbeStates();
1781
+ this.emit("data", {
1782
+ type: "fleet:snapshot",
1783
+ entries: this.fleetEntries()
1784
+ });
1785
+ }
1786
+ /**
1787
+ * Emit `files:changed` if the given step has any file_change rows.
1788
+ * Pulled into its own method so tests can exercise the dedup-paths
1789
+ * shape without going through a full tick.
1790
+ */
1791
+ emitFilesChangedIfAny(run, step) {
1792
+ const fcs = listFileChanges(this.store, { stepId: step.step_id });
1793
+ if (fcs.length === 0) return;
1794
+ const paths = /* @__PURE__ */ new Set();
1795
+ let partial = false;
1796
+ for (const fc of fcs) {
1797
+ paths.add(fc.path);
1798
+ if (fc.old_path) paths.add(fc.old_path);
1799
+ if (fc.partial_diff) partial = true;
1800
+ }
1801
+ this.emit("data", {
1802
+ type: "files:changed",
1803
+ run_id: run.run_id,
1804
+ step_id: step.step_id,
1805
+ paths: Array.from(paths),
1806
+ partial
1807
+ });
1808
+ }
1809
+ /**
1810
+ * Poll probe state for every run with an active probe file. Two-axis
1811
+ * detection: the FSM `state` drives inject-window accounting; the
1812
+ * `paused_at_ms` / `resumed_at_ms` timestamps drive `run:paused` /
1813
+ * `run:resumed` emission. The timestamp axis matters because a full
1814
+ * pause→ack→resume cycle can complete inside one 1500ms tick — a
1815
+ * pure `state → state` comparison would silently swallow those
1816
+ * events (the bug Codex flagged at this site).
1817
+ *
1818
+ * Runs without a probe file are skipped entirely: probe files only
1819
+ * exist while a run is under operator control (created lazily by
1820
+ * `requestPause` / `setInject`, removed by `clearProbe` on terminal
1821
+ * cleanup), so the file's presence is the cleanest natural gate.
1822
+ * Per-run reads are guarded — a single EACCES or corrupt file on
1823
+ * one probe shouldn't degrade `/api/live` for every other run.
1824
+ */
1825
+ pollProbeStates() {
1826
+ for (const runId of this.lastStepCounts.keys()) {
1827
+ if (!existsSync(probeFilePath(runId))) {
1828
+ this.probeTracks.delete(runId);
1829
+ continue;
1830
+ }
1831
+ let record;
1832
+ try {
1833
+ record = readProbeState(runId);
1834
+ } catch (err) {
1835
+ console.error(`[meter/live] probe read failed for ${runId}:`, err);
1836
+ continue;
1837
+ }
1838
+ let tracked = this.probeTracks.get(runId);
1839
+ if (!tracked) {
1840
+ tracked = {
1841
+ state: record.state,
1842
+ edits: 0,
1843
+ lastInject: null,
1844
+ lastPausedAtMs: null,
1845
+ lastResumedAtMs: null,
1846
+ inPauseWindow: false
1847
+ };
1848
+ this.probeTracks.set(runId, tracked);
1849
+ }
1850
+ const nowInWindow = record.state === "pause_requested" || record.state === "paused";
1851
+ if (nowInWindow && !tracked.inPauseWindow) {
1852
+ tracked.edits = record.inject !== null && record.inject !== "" ? 1 : 0;
1853
+ } else if (nowInWindow && record.inject !== null && record.inject !== "" && record.inject !== tracked.lastInject) {
1854
+ tracked.edits += 1;
1855
+ }
1856
+ tracked.lastInject = record.inject;
1857
+ tracked.inPauseWindow = nowInWindow;
1858
+ const newPause = record.paused_at_ms !== null && record.paused_at_ms !== tracked.lastPausedAtMs;
1859
+ if (newPause) {
1860
+ const steps = listSteps4(this.store, runId);
1861
+ const lastStep = steps[steps.length - 1];
1862
+ this.emit("data", {
1863
+ type: "run:paused",
1864
+ run_id: runId,
1865
+ step_id: lastStep?.step_id ?? null,
1866
+ paused_at: new Date(record.paused_at_ms).toISOString()
1867
+ });
1868
+ tracked.lastPausedAtMs = record.paused_at_ms;
1869
+ }
1870
+ const newResume = record.resumed_at_ms !== null && record.resumed_at_ms !== tracked.lastResumedAtMs;
1871
+ if (newResume) {
1872
+ this.emit("data", {
1873
+ type: "run:resumed",
1874
+ run_id: runId,
1875
+ edits: tracked.edits,
1876
+ resumed_at: new Date(record.resumed_at_ms).toISOString()
1877
+ });
1878
+ tracked.edits = 0;
1879
+ tracked.lastResumedAtMs = record.resumed_at_ms;
1880
+ }
1881
+ tracked.state = record.state;
1882
+ }
1883
+ }
1884
+ /** Compute the current fleet view (active + recently-completed runs). */
1885
+ fleetEntries() {
1886
+ return buildFleetEntries(this.store, {
1887
+ stallSeconds: this.opts.stallSeconds,
1888
+ firedAlerts: this.firedAlerts,
1889
+ limit: 50
1890
+ });
1891
+ }
1892
+ async maybeAlert(run, newSteps, silent = false) {
1893
+ const fired = this.firedAlerts.get(run.run_id) ?? /* @__PURE__ */ new Map();
1894
+ const fireOrSeed = (key, event) => {
1895
+ if (fired.has(key)) return;
1896
+ fired.set(key, { kind: event.kind, message: event.message });
1897
+ if (!silent) this.emit("data", event);
1898
+ };
1899
+ for (const s of newSteps) {
1900
+ if (s.action.kind === "tool_call" && s.action.tool_name && this.opts.watchTools.includes(s.action.tool_name)) {
1901
+ fireOrSeed(`tool:${s.action.tool_name}:${s.step_id}`, {
1902
+ type: "alert",
1903
+ run_id: run.run_id,
1904
+ kind: "tool_called",
1905
+ message: `watched tool ${s.action.tool_name} called at step #${s.sequence}${describeToolInput(s.action.tool_input)}`,
1906
+ meta: { step_id: s.step_id, sequence: s.sequence }
1907
+ });
1908
+ }
1909
+ }
1910
+ for (const s of newSteps) {
1911
+ const pct = contextUtilization(s);
1912
+ for (const t of this.opts.contextThresholds) {
1913
+ if (pct >= t) {
1914
+ fireOrSeed(`ctx:${t}`, {
1915
+ type: "alert",
1916
+ run_id: run.run_id,
1917
+ kind: "context_threshold",
1918
+ message: `context window ${pct}% full \u2014 crossed the ${t}% threshold at step #${s.sequence}`,
1919
+ meta: { sequence: s.sequence, percent: pct }
1920
+ });
1921
+ }
1922
+ }
1923
+ }
1924
+ const allSteps = listSteps4(this.store, run.run_id);
1925
+ const loop = detectLoop(allSteps, this.opts.loopWindow);
1926
+ if (loop) {
1927
+ fireOrSeed(`loop:${loop.tool}:${loop.signature}`, {
1928
+ type: "alert",
1929
+ run_id: run.run_id,
1930
+ kind: "loop",
1931
+ message: `possible loop: last ${loop.repeats} steps all called ${loop.tool} with identical input (${loop.signature.slice(0, 48)}\u2026)`,
1932
+ meta: { window: this.opts.loopWindow }
1933
+ });
1934
+ }
1935
+ if (run.status === "in_progress" && allSteps.length > 0) {
1936
+ const lastTs = new Date(allSteps[allSteps.length - 1].timestamp).getTime();
1937
+ const ageS = (Date.now() - lastTs) / 1e3;
1938
+ const HOUR = 3600;
1939
+ if (ageS > this.opts.stallSeconds && ageS < HOUR) {
1940
+ for (const k of fired.keys()) {
1941
+ if (k.startsWith("stall:")) fired.delete(k);
1942
+ }
1943
+ const lastStep = allSteps[allSteps.length - 1];
1944
+ const doing = lastStep.action.kind === "tool_call" && lastStep.action.tool_name ? `after calling ${lastStep.action.tool_name}` : `after a ${lastStep.action.kind} step`;
1945
+ fireOrSeed(`stall:${Math.floor(ageS / 60)}`, {
1946
+ type: "alert",
1947
+ run_id: run.run_id,
1948
+ kind: "stall",
1949
+ message: `stalled at step #${lastStep.sequence} ${doing} \u2014 no activity for ${formatDuration(ageS)}`
1950
+ });
1951
+ }
1952
+ }
1953
+ if (fired.size > 0) this.firedAlerts.set(run.run_id, fired);
1954
+ }
1955
+ };
1956
+ function collectNewSteps(store, run, lastCounts) {
1957
+ const last = lastCounts.get(run.run_id) ?? 0;
1958
+ const all = listSteps4(store, run.run_id);
1959
+ return all.slice(last);
1960
+ }
1961
+ function formatDuration(seconds) {
1962
+ const s = Math.round(seconds);
1963
+ if (s < 60) return `${s}s`;
1964
+ const m = Math.floor(s / 60);
1965
+ const rem = s % 60;
1966
+ return rem > 0 ? `${m}m ${rem}s` : `${m}m`;
1967
+ }
1968
+ function describeToolInput(input) {
1969
+ if (!input || typeof input !== "object") return "";
1970
+ const obj = input;
1971
+ for (const field of ["command", "file_path", "path", "url", "pattern", "query"]) {
1972
+ const v = obj[field];
1973
+ if (typeof v === "string" && v.length > 0) {
1974
+ const preview = v.length > 60 ? `${v.slice(0, 60)}\u2026` : v;
1975
+ return ` \u2014 ${field}: "${preview}"`;
1976
+ }
1977
+ }
1978
+ return "";
1979
+ }
1980
+ var LiveController = class {
1981
+ store;
1982
+ inspector;
1983
+ subscribers = /* @__PURE__ */ new Set();
1984
+ storeOpts = {};
1985
+ constructor(store) {
1986
+ this.store = store;
1987
+ }
1988
+ /** Spin up an inspector if one isn't already running. Idempotent. */
1989
+ async start(opts) {
1990
+ if (this.inspector) return;
1991
+ if (opts) this.storeOpts = opts;
1992
+ const inspector = new LiveInspector(this.store, this.storeOpts);
1993
+ for (const fn of this.subscribers) inspector.on("data", fn);
1994
+ this.inspector = inspector;
1995
+ await inspector.start();
1996
+ }
1997
+ /** Stop the running inspector if any. Idempotent. */
1998
+ stop() {
1999
+ if (!this.inspector) return;
2000
+ for (const fn of this.subscribers) this.inspector.off("data", fn);
2001
+ this.inspector.stop();
2002
+ this.inspector = void 0;
2003
+ }
2004
+ isLive() {
2005
+ return this.inspector !== void 0;
2006
+ }
2007
+ on(event, fn) {
2008
+ this.subscribers.add(fn);
2009
+ this.inspector?.on(event, fn);
2010
+ }
2011
+ off(event, fn) {
2012
+ this.subscribers.delete(fn);
2013
+ this.inspector?.off(event, fn);
2014
+ }
2015
+ /** Latest fleet snapshot — empty when not running. */
2016
+ fleetEntries() {
2017
+ if (!this.inspector) return [];
2018
+ return this.inspector.fleetEntries();
2019
+ }
2020
+ };
2021
+
2022
+ // src/continuation.ts
2023
+ import { hashJson as hashJson2 } from "@meterbility/shared";
2024
+ import { costCents as costCents2 } from "@meterbility/spec";
2025
+ import {
2026
+ getRun as getRun4,
2027
+ insertStep as insertStep2,
2028
+ listSteps as listSteps5,
2029
+ recordContextSnapshot as recordContextSnapshot2,
2030
+ resolveSnapshotBlobRef as resolveSnapshotBlobRef2,
2031
+ setRunStatus as setRunStatus2,
2032
+ updateRunTotals as updateRunTotals2
2033
+ } from "@meterbility/collector";
2034
+ import { randomUUID as randomUUID2 } from "crypto";
2035
+ async function continueFork(store, forkRunId, opts) {
2036
+ const fork = getRun4(store, forkRunId);
2037
+ if (!fork) throw new Error(`fork run not found: ${forkRunId}`);
2038
+ const max = opts.maxIterations ?? 25;
2039
+ if (opts.mode === "live" && !opts.toolExecutor) {
2040
+ throw new Error("live mode requires a toolExecutor");
2041
+ }
2042
+ if (opts.mode === "simulate" && !opts.originRunId) {
2043
+ throw new Error("simulate mode requires an originRunId");
2044
+ }
2045
+ const simIndex = opts.mode === "simulate" ? buildToolIndex(store, opts.originRunId) : /* @__PURE__ */ new Map();
2046
+ let stepsAdded = 0;
2047
+ let lastStepId = "";
2048
+ for (let iteration = 0; iteration < max; iteration++) {
2049
+ const steps = listSteps5(store, fork.run_id);
2050
+ const prior = steps[steps.length - 1];
2051
+ if (!prior) {
2052
+ throw new Error(`fork run has no steps: ${forkRunId}`);
2053
+ }
2054
+ lastStepId = prior.step_id;
2055
+ const { history, systemPrompt } = await assembleHistory(store, prior);
2056
+ let modelResult;
2057
+ try {
2058
+ modelResult = await opts.modelCaller({
2059
+ history,
2060
+ prior_step: prior,
2061
+ system_prompt: systemPrompt,
2062
+ iteration
2063
+ });
2064
+ } catch (err) {
2065
+ const errorStep = await persistErrorStep(store, fork.run_id, prior, {
2066
+ kind: "model_error",
2067
+ message: err.message
2068
+ });
2069
+ stepsAdded += 1;
2070
+ lastStepId = errorStep.step_id;
2071
+ return {
2072
+ iterations_run: iteration + 1,
2073
+ steps_added: stepsAdded,
2074
+ terminal_reason: "model_error",
2075
+ final_step_id: lastStepId
2076
+ };
2077
+ }
2078
+ const modelStep = await persistModelStep(store, fork.run_id, prior, modelResult);
2079
+ stepsAdded += 1;
2080
+ lastStepId = modelStep.step_id;
2081
+ if (modelResult.action.kind !== "tool_call" || !modelResult.action.tool_name) {
2082
+ setRunStatus2(store, fork.run_id, "ok", (/* @__PURE__ */ new Date()).toISOString());
2083
+ updateRunTotals2(store, fork.run_id);
2084
+ return {
2085
+ iterations_run: iteration + 1,
2086
+ steps_added: stepsAdded,
2087
+ terminal_reason: "model_completed",
2088
+ final_step_id: lastStepId
2089
+ };
2090
+ }
2091
+ const toolCall = {
2092
+ tool_name: modelResult.action.tool_name,
2093
+ tool_use_id: modelResult.action.tool_use_id,
2094
+ tool_input: modelResult.action.tool_input
2095
+ };
2096
+ let toolResult;
2097
+ if (opts.mode === "simulate") {
2098
+ const sigKey = toolSignature(toolCall);
2099
+ const hit = simIndex.get(sigKey);
2100
+ if (!hit) {
2101
+ await tagStep(store, modelStep.step_id, "simulate_miss");
2102
+ setRunStatus2(store, fork.run_id, "in_progress");
2103
+ updateRunTotals2(store, fork.run_id);
2104
+ return {
2105
+ iterations_run: iteration + 1,
2106
+ steps_added: stepsAdded,
2107
+ terminal_reason: "simulate_miss",
2108
+ final_step_id: lastStepId
2109
+ };
2110
+ }
2111
+ toolResult = {
2112
+ output: hit.result,
2113
+ is_error: hit.is_error,
2114
+ summary: hit.summary
2115
+ };
2116
+ } else if (opts.mode === "live" && opts.toolExecutor) {
2117
+ try {
2118
+ toolResult = await opts.toolExecutor(toolCall);
2119
+ } catch (err) {
2120
+ toolResult = {
2121
+ output: { error: err.message },
2122
+ is_error: true,
2123
+ summary: `tool threw: ${err.message}`.slice(0, 200)
2124
+ };
2125
+ }
2126
+ }
2127
+ if (!toolResult) {
2128
+ throw new Error("internal: missing tool result");
2129
+ }
2130
+ const ref = await store.blobs.putJson(toolResult.output);
2131
+ const updatedOutcome = {
2132
+ status: toolResult.is_error ? "error" : "ok",
2133
+ tool_result_ref: ref,
2134
+ is_error: toolResult.is_error ?? false,
2135
+ summary: toolResult.summary
2136
+ };
2137
+ store.db.prepare("UPDATE steps SET outcome_json = ?, status = ? WHERE step_id = ?").run(
2138
+ JSON.stringify(updatedOutcome),
2139
+ toolResult.is_error ? "error" : "ok",
2140
+ modelStep.step_id
2141
+ );
2142
+ if (toolResult.is_error && opts.mode === "live") {
2143
+ setRunStatus2(store, fork.run_id, "error");
2144
+ updateRunTotals2(store, fork.run_id);
2145
+ return {
2146
+ iterations_run: iteration + 1,
2147
+ steps_added: stepsAdded,
2148
+ terminal_reason: "tool_error",
2149
+ final_step_id: lastStepId
2150
+ };
2151
+ }
2152
+ }
2153
+ setRunStatus2(store, fork.run_id, "in_progress");
2154
+ updateRunTotals2(store, fork.run_id);
2155
+ return {
2156
+ iterations_run: max,
2157
+ steps_added: stepsAdded,
2158
+ terminal_reason: "max_iterations",
2159
+ final_step_id: lastStepId
2160
+ };
2161
+ }
2162
+ async function assembleHistory(store, priorStep) {
2163
+ const ref = resolveSnapshotBlobRef2(store, priorStep.context_snapshot_id);
2164
+ const snap = await store.blobs.tryGetString(ref);
2165
+ if (!snap) return { history: [] };
2166
+ let parsed;
2167
+ try {
2168
+ parsed = JSON.parse(snap);
2169
+ } catch {
2170
+ return { history: [] };
2171
+ }
2172
+ const history = [];
2173
+ let systemPrompt;
2174
+ for (const c of parsed.components) {
2175
+ if (c.type === "system_prompt") {
2176
+ systemPrompt = await store.blobs.tryGetString(c.content_ref);
2177
+ } else if (c.type === "conversation_history") {
2178
+ for (const m of c.messages) history.push(m);
2179
+ }
2180
+ }
2181
+ const priorAction = priorStep.action;
2182
+ let assistantText = "";
2183
+ if (priorAction.kind === "message" && priorAction.text) {
2184
+ assistantText = priorAction.text;
2185
+ } else if (priorAction.kind === "tool_call") {
2186
+ assistantText = `[tool_call: ${priorAction.tool_name ?? "?"}]`;
2187
+ }
2188
+ if (assistantText) {
2189
+ const aref = await store.blobs.putString(assistantText);
2190
+ history.push({ role: "assistant", content_ref: aref });
2191
+ }
2192
+ if (priorAction.kind === "tool_call" && priorStep.outcome.tool_result_ref) {
2193
+ history.push({
2194
+ role: "tool",
2195
+ content_ref: priorStep.outcome.tool_result_ref
2196
+ });
2197
+ }
2198
+ return { history, systemPrompt };
2199
+ }
2200
+ async function persistModelStep(store, runId, prior, result) {
2201
+ const { history, systemPrompt } = await assembleHistory(store, prior);
2202
+ const components = [];
2203
+ if (systemPrompt) {
2204
+ components.push({
2205
+ type: "system_prompt",
2206
+ content_ref: await store.blobs.putString(systemPrompt)
2207
+ });
2208
+ }
2209
+ if (history.length > 0) {
2210
+ components.push({ type: "conversation_history", messages: history });
2211
+ }
2212
+ const snapshot = { id: hashJson2(components), components };
2213
+ const blobRef = await store.blobs.putJson(snapshot);
2214
+ recordContextSnapshot2(store, snapshot.id, blobRef, snapshot.components.length);
2215
+ const decisionRef = await store.blobs.putJson(result.decision_content);
2216
+ const { cost_cents, approx } = costCents2(result.model, {
2217
+ input: result.tokens.input,
2218
+ output: result.tokens.output,
2219
+ cached_read: result.tokens.cached_read,
2220
+ cache_creation: result.tokens.cache_creation,
2221
+ cache_creation_1h: result.tokens.cache_creation_1h
2222
+ });
2223
+ const tags = ["continuation"];
2224
+ if (approx) tags.push("cost:approx");
2225
+ const step = {
2226
+ step_id: `stp_${randomUUID2()}`,
2227
+ run_id: runId,
2228
+ parent_step_id: prior.step_id,
2229
+ sequence: prior.sequence + 1,
2230
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2231
+ model: result.model,
2232
+ context_snapshot_id: snapshot.id,
2233
+ decision_ref: decisionRef,
2234
+ action: result.action,
2235
+ outcome: { status: "pending" },
2236
+ tokens: result.tokens,
2237
+ latency_ms: result.latency_ms,
2238
+ cost_cents,
2239
+ tags,
2240
+ status: "in_progress"
2241
+ };
2242
+ insertStep2(store, step);
2243
+ return step;
2244
+ }
2245
+ async function persistErrorStep(store, runId, prior, err) {
2246
+ const components = [];
2247
+ const snapshot = { id: hashJson2(components), components };
2248
+ const blobRef = await store.blobs.putJson(snapshot);
2249
+ recordContextSnapshot2(store, snapshot.id, blobRef, 0);
2250
+ const decisionRef = await store.blobs.putJson({ error: err });
2251
+ const step = {
2252
+ step_id: `stp_${randomUUID2()}`,
2253
+ run_id: runId,
2254
+ parent_step_id: prior.step_id,
2255
+ sequence: prior.sequence + 1,
2256
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2257
+ model: "continuation",
2258
+ context_snapshot_id: snapshot.id,
2259
+ decision_ref: decisionRef,
2260
+ action: { kind: "none" },
2261
+ outcome: {
2262
+ status: "error",
2263
+ is_error: true,
2264
+ summary: err.message.slice(0, 200)
2265
+ },
2266
+ tokens: { input: 0, output: 0, cached_read: 0, cache_creation: 0 },
2267
+ latency_ms: 0,
2268
+ cost_cents: 0,
2269
+ tags: ["continuation", "error", err.kind],
2270
+ status: "error"
2271
+ };
2272
+ insertStep2(store, step);
2273
+ return step;
2274
+ }
2275
+ async function tagStep(store, stepId, tag) {
2276
+ const row = store.db.prepare("SELECT tags FROM steps WHERE step_id = ?").get(stepId);
2277
+ if (!row) return;
2278
+ const tags = JSON.parse(row.tags);
2279
+ if (!tags.includes(tag)) tags.push(tag);
2280
+ store.db.prepare("UPDATE steps SET tags = ? WHERE step_id = ?").run(JSON.stringify(tags), stepId);
2281
+ }
2282
+ function buildToolIndex(store, originRunId) {
2283
+ const out = /* @__PURE__ */ new Map();
2284
+ const steps = listSteps5(store, originRunId);
2285
+ for (const s of steps) {
2286
+ if (s.action.kind !== "tool_call" || !s.action.tool_name) continue;
2287
+ if (!s.outcome.tool_result_ref) continue;
2288
+ const sig = toolSignature({
2289
+ tool_name: s.action.tool_name,
2290
+ tool_input: s.action.tool_input
2291
+ });
2292
+ out.set(sig, {
2293
+ result: { __ref: s.outcome.tool_result_ref },
2294
+ is_error: s.outcome.is_error ?? false,
2295
+ summary: s.outcome.summary
2296
+ });
2297
+ }
2298
+ return out;
2299
+ }
2300
+ function toolSignature(call) {
2301
+ return `${call.tool_name}::${hashJson2(call.tool_input ?? null)}`;
2302
+ }
2303
+ async function resolveSimulatedResult(store, result) {
2304
+ if (result && typeof result === "object" && "__ref" in result && typeof result.__ref === "string") {
2305
+ const ref = result.__ref;
2306
+ const text = await store.blobs.tryGetString(ref);
2307
+ if (!text) return result;
2308
+ try {
2309
+ return JSON.parse(text);
2310
+ } catch {
2311
+ return text;
2312
+ }
2313
+ }
2314
+ return result;
2315
+ }
2316
+
2317
+ // src/slack.ts
2318
+ var SlackNotifier = class {
2319
+ opts;
2320
+ bucket = [];
2321
+ // post timestamps
2322
+ detach;
2323
+ constructor(opts) {
2324
+ if (!opts.webhookUrl || !/^https:\/\/hooks\.slack\.com\//.test(opts.webhookUrl)) {
2325
+ throw new Error("invalid Slack webhook URL");
2326
+ }
2327
+ this.opts = {
2328
+ webhookUrl: opts.webhookUrl,
2329
+ serverUrl: opts.serverUrl ?? "",
2330
+ events: opts.events ?? ["alert"],
2331
+ rateLimitPerMinute: opts.rateLimitPerMinute ?? 30,
2332
+ channel: opts.channel
2333
+ };
2334
+ }
2335
+ attach(live) {
2336
+ if (this.detach) this.detach();
2337
+ const handler = (e) => void this.handleEvent(e);
2338
+ live.on("data", handler);
2339
+ this.detach = () => live.off("data", handler);
2340
+ }
2341
+ detachFrom() {
2342
+ if (this.detach) this.detach();
2343
+ this.detach = void 0;
2344
+ }
2345
+ async handleEvent(e) {
2346
+ if (!this.opts.events.includes(e.type)) return;
2347
+ const payload = this.formatPayload(e);
2348
+ if (!payload) return;
2349
+ if (!this.acquireRateLimitToken()) return;
2350
+ try {
2351
+ const res = await fetch(this.opts.webhookUrl, {
2352
+ method: "POST",
2353
+ headers: { "Content-Type": "application/json" },
2354
+ body: JSON.stringify(payload)
2355
+ });
2356
+ if (!res.ok) {
2357
+ console.warn(
2358
+ `[meter/slack] webhook returned ${res.status}: ${await res.text()}`
2359
+ );
2360
+ }
2361
+ } catch (err) {
2362
+ console.warn(`[meter/slack] post failed:`, err.message);
2363
+ }
2364
+ }
2365
+ /**
2366
+ * Send a one-off test message — used by `meter slack test`.
2367
+ */
2368
+ async sendTest() {
2369
+ const payload = {
2370
+ blocks: [
2371
+ {
2372
+ type: "header",
2373
+ text: { type: "plain_text", text: "Meterbility is connected" }
2374
+ },
2375
+ {
2376
+ type: "section",
2377
+ text: {
2378
+ type: "mrkdwn",
2379
+ text: "If you can read this, your webhook is wired correctly. Alerts will arrive here when an agent run loops, stalls, or crosses a context threshold."
2380
+ }
2381
+ }
2382
+ ]
2383
+ };
2384
+ if (!this.acquireRateLimitToken()) {
2385
+ throw new Error("rate-limited");
2386
+ }
2387
+ const res = await fetch(this.opts.webhookUrl, {
2388
+ method: "POST",
2389
+ headers: { "Content-Type": "application/json" },
2390
+ body: JSON.stringify(payload)
2391
+ });
2392
+ if (!res.ok) throw new Error(`webhook ${res.status}: ${await res.text()}`);
2393
+ }
2394
+ acquireRateLimitToken() {
2395
+ const now = Date.now();
2396
+ this.bucket = this.bucket.filter((t) => now - t < 6e4);
2397
+ if (this.bucket.length >= this.opts.rateLimitPerMinute) return false;
2398
+ this.bucket.push(now);
2399
+ return true;
2400
+ }
2401
+ formatPayload(e) {
2402
+ const link = (runId) => this.opts.serverUrl ? `${this.opts.serverUrl.replace(/\/$/, "")}/runs/${runId}` : `run ${runId.slice(0, 12)}`;
2403
+ if (e.type === "alert") {
2404
+ const color = alertColor(e.kind);
2405
+ return {
2406
+ ...this.opts.channel ? { channel: this.opts.channel } : {},
2407
+ attachments: [
2408
+ {
2409
+ color,
2410
+ blocks: [
2411
+ {
2412
+ type: "section",
2413
+ text: {
2414
+ type: "mrkdwn",
2415
+ text: `*${labelForAlert(e.kind)}* on run \`${e.run_id.slice(0, 12)}\`
2416
+ ${escapeSlack(e.message)}`
2417
+ }
2418
+ },
2419
+ {
2420
+ type: "actions",
2421
+ elements: [
2422
+ {
2423
+ type: "button",
2424
+ text: { type: "plain_text", text: "Open in Meterbility" },
2425
+ url: link(e.run_id)
2426
+ }
2427
+ ]
2428
+ }
2429
+ ]
2430
+ }
2431
+ ]
2432
+ };
2433
+ }
2434
+ if (e.type === "run:created") {
2435
+ return {
2436
+ ...this.opts.channel ? { channel: this.opts.channel } : {},
2437
+ blocks: [
2438
+ {
2439
+ type: "section",
2440
+ text: {
2441
+ type: "mrkdwn",
2442
+ text: `\u25B6\uFE0E New run: *${escapeSlack(e.run.title ?? e.run.run_id)}* (${escapeSlack(e.run.source_runtime)})
2443
+ <${link(e.run.run_id)}|open in Meterbility>`
2444
+ }
2445
+ }
2446
+ ]
2447
+ };
2448
+ }
2449
+ if (e.type === "run:completed") {
2450
+ const icon = e.run.status === "ok" ? "\u2705" : e.run.status === "error" ? "\u274C" : "\u23F9";
2451
+ return {
2452
+ ...this.opts.channel ? { channel: this.opts.channel } : {},
2453
+ blocks: [
2454
+ {
2455
+ type: "section",
2456
+ text: {
2457
+ type: "mrkdwn",
2458
+ text: `${icon} *${escapeSlack(e.run.title ?? e.run.run_id)}* finished \xB7 ${e.run.step_count} steps \xB7 ${formatCost(e.run.cost_cents)}
2459
+ <${link(e.run.run_id)}|open in Meterbility>`
2460
+ }
2461
+ }
2462
+ ]
2463
+ };
2464
+ }
2465
+ return void 0;
2466
+ }
2467
+ };
2468
+ function alertColor(kind) {
2469
+ switch (kind) {
2470
+ case "loop":
2471
+ return "#f85149";
2472
+ case "tool_called":
2473
+ return "#58a6ff";
2474
+ case "context_threshold":
2475
+ return "#d29922";
2476
+ case "stall":
2477
+ return "#8b949e";
2478
+ default:
2479
+ return "#bc8cff";
2480
+ }
2481
+ }
2482
+ function labelForAlert(kind) {
2483
+ switch (kind) {
2484
+ case "loop":
2485
+ return "Loop detected";
2486
+ case "tool_called":
2487
+ return "Watched tool called";
2488
+ case "context_threshold":
2489
+ return "Context utilization";
2490
+ case "stall":
2491
+ return "Run stalled";
2492
+ default:
2493
+ return kind;
2494
+ }
2495
+ }
2496
+ function formatCost(cents) {
2497
+ const dollars = cents / 100;
2498
+ if (dollars === 0) return "$0.00";
2499
+ if (Math.abs(dollars) >= 5e-3) return `$${dollars.toFixed(2)}`;
2500
+ return `$${dollars.toFixed(4)}`;
2501
+ }
2502
+ function escapeSlack(s) {
2503
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2504
+ }
2505
+
2506
+ // src/regression.ts
2507
+ import { randomUUID as randomUUID3 } from "crypto";
2508
+ import { getRun as getRun5, listSteps as listSteps6 } from "@meterbility/collector";
2509
+ function listTests(store) {
2510
+ const rows = store.db.prepare(
2511
+ "SELECT test_id, name, description, assertions_json, canonical_run_id, created_at FROM regression_tests ORDER BY name"
2512
+ ).all();
2513
+ return rows.map((r) => ({
2514
+ test_id: r.test_id,
2515
+ name: r.name,
2516
+ description: r.description ?? void 0,
2517
+ assertions: JSON.parse(r.assertions_json),
2518
+ canonical_run_id: r.canonical_run_id ?? void 0,
2519
+ created_at: r.created_at
2520
+ }));
2521
+ }
2522
+ function getTestByName(store, name) {
2523
+ const r = store.db.prepare(
2524
+ "SELECT test_id, name, description, assertions_json, canonical_run_id, created_at FROM regression_tests WHERE name = ?"
2525
+ ).get(name);
2526
+ if (!r) return void 0;
2527
+ return {
2528
+ test_id: r.test_id,
2529
+ name: r.name,
2530
+ description: r.description ?? void 0,
2531
+ assertions: JSON.parse(r.assertions_json),
2532
+ canonical_run_id: r.canonical_run_id ?? void 0,
2533
+ created_at: r.created_at
2534
+ };
2535
+ }
2536
+ function createTest(store, args) {
2537
+ const test = {
2538
+ test_id: `tst_${randomUUID3()}`,
2539
+ name: args.name,
2540
+ description: args.description,
2541
+ assertions: args.assertions,
2542
+ canonical_run_id: args.canonical_run_id,
2543
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
2544
+ };
2545
+ store.db.prepare(
2546
+ `INSERT INTO regression_tests(test_id, name, description, assertions_json, canonical_run_id, created_at)
2547
+ VALUES (?,?,?,?,?,?)`
2548
+ ).run(
2549
+ test.test_id,
2550
+ test.name,
2551
+ test.description ?? null,
2552
+ JSON.stringify(test.assertions),
2553
+ test.canonical_run_id ?? null,
2554
+ test.created_at
2555
+ );
2556
+ return test;
2557
+ }
2558
+ function deleteTest(store, name) {
2559
+ const res = store.db.prepare("DELETE FROM regression_tests WHERE name = ?").run(name);
2560
+ return res.changes > 0;
2561
+ }
2562
+ function addAssertion(store, testName, assertion) {
2563
+ const t = getTestByName(store, testName);
2564
+ if (!t) throw new Error(`test not found: ${testName}`);
2565
+ t.assertions.push(assertion);
2566
+ store.db.prepare(
2567
+ "UPDATE regression_tests SET assertions_json = ? WHERE test_id = ?"
2568
+ ).run(JSON.stringify(t.assertions), t.test_id);
2569
+ return t;
2570
+ }
2571
+ function deriveAssertionsFromRun(run, steps) {
2572
+ const toolCounts = /* @__PURE__ */ new Map();
2573
+ for (const s of steps) {
2574
+ if (s.action.kind === "tool_call" && s.action.tool_name) {
2575
+ toolCounts.set(
2576
+ s.action.tool_name,
2577
+ (toolCounts.get(s.action.tool_name) ?? 0) + 1
2578
+ );
2579
+ }
2580
+ }
2581
+ const out = [
2582
+ { kind: "final_status", value: run.status, label: "final status" },
2583
+ { kind: "no_error_step", value: 0, label: "no errored step" },
2584
+ {
2585
+ kind: "min_steps",
2586
+ value: Math.max(1, Math.floor(steps.length * 0.5)),
2587
+ label: "min steps (50% of canonical)"
2588
+ },
2589
+ {
2590
+ kind: "max_steps",
2591
+ value: Math.ceil(steps.length * 1.5),
2592
+ label: "max steps (150% of canonical)"
2593
+ },
2594
+ {
2595
+ kind: "max_cost_cents",
2596
+ value: Math.ceil(run.cost_cents * 1.5),
2597
+ label: "max cost (150% of canonical)"
2598
+ }
2599
+ ];
2600
+ for (const [tool] of toolCounts) {
2601
+ out.push({
2602
+ kind: "includes_tool_call",
2603
+ value: tool,
2604
+ label: `must call ${tool}`
2605
+ });
2606
+ }
2607
+ return out;
2608
+ }
2609
+ function runTest(store, test, runId) {
2610
+ const run = getRun5(store, runId);
2611
+ if (!run) throw new Error(`run not found: ${runId}`);
2612
+ const steps = listSteps6(store, run.run_id);
2613
+ const results = test.assertions.map(
2614
+ (a) => evaluateAssertion(a, run, steps)
2615
+ );
2616
+ const passed = results.every((r) => r.passed);
2617
+ const result = {
2618
+ result_id: `res_${randomUUID3()}`,
2619
+ test_id: test.test_id,
2620
+ test_name: test.name,
2621
+ run_id: run.run_id,
2622
+ passed,
2623
+ assertions: results,
2624
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
2625
+ };
2626
+ store.db.prepare(
2627
+ `INSERT INTO regression_results(result_id, test_id, run_id, passed, details_json, created_at)
2628
+ VALUES (?,?,?,?,?,?)`
2629
+ ).run(
2630
+ result.result_id,
2631
+ result.test_id,
2632
+ result.run_id,
2633
+ passed ? 1 : 0,
2634
+ JSON.stringify(results),
2635
+ result.created_at
2636
+ );
2637
+ return result;
2638
+ }
2639
+ function evaluateAssertion(assertion, run, steps) {
2640
+ switch (assertion.kind) {
2641
+ case "includes_tool_call": {
2642
+ const want = String(assertion.value);
2643
+ const found = steps.some(
2644
+ (s) => s.action.kind === "tool_call" && s.action.tool_name === want
2645
+ );
2646
+ return {
2647
+ assertion,
2648
+ passed: found,
2649
+ reason: found ? `${want} was called` : `${want} was never called`
2650
+ };
2651
+ }
2652
+ case "excludes_tool_call": {
2653
+ const want = String(assertion.value);
2654
+ const found = steps.some(
2655
+ (s) => s.action.kind === "tool_call" && s.action.tool_name === want
2656
+ );
2657
+ return {
2658
+ assertion,
2659
+ passed: !found,
2660
+ reason: !found ? `${want} was correctly absent` : `${want} was called (banned)`
2661
+ };
2662
+ }
2663
+ case "tool_call_count": {
2664
+ const want = Number(assertion.value);
2665
+ const count = steps.filter((s) => s.action.kind === "tool_call").length;
2666
+ return {
2667
+ assertion,
2668
+ passed: count === want,
2669
+ reason: `expected ${want} tool calls, got ${count}`
2670
+ };
2671
+ }
2672
+ case "output_contains": {
2673
+ const want = String(assertion.value);
2674
+ const last = steps.findLast?.((s) => s.action.kind === "message") ?? [...steps].reverse().find((s) => s.action.kind === "message");
2675
+ const text = last?.action.text ?? "";
2676
+ const found = text.includes(want);
2677
+ return {
2678
+ assertion,
2679
+ passed: found,
2680
+ reason: found ? `final message contained "${want.slice(0, 40)}"` : `final message did not contain "${want.slice(0, 40)}"`
2681
+ };
2682
+ }
2683
+ case "output_does_not_contain": {
2684
+ const want = String(assertion.value);
2685
+ const last = steps.findLast?.((s) => s.action.kind === "message") ?? [...steps].reverse().find((s) => s.action.kind === "message");
2686
+ const text = last?.action.text ?? "";
2687
+ const found = text.includes(want);
2688
+ return {
2689
+ assertion,
2690
+ passed: !found,
2691
+ reason: !found ? `forbidden phrase absent` : `final message contained forbidden phrase`
2692
+ };
2693
+ }
2694
+ case "min_steps": {
2695
+ const want = Number(assertion.value);
2696
+ return {
2697
+ assertion,
2698
+ passed: steps.length >= want,
2699
+ reason: `expected \u2265${want} steps, got ${steps.length}`
2700
+ };
2701
+ }
2702
+ case "max_steps": {
2703
+ const want = Number(assertion.value);
2704
+ return {
2705
+ assertion,
2706
+ passed: steps.length <= want,
2707
+ reason: `expected \u2264${want} steps, got ${steps.length}`
2708
+ };
2709
+ }
2710
+ case "final_status": {
2711
+ const want = String(assertion.value);
2712
+ return {
2713
+ assertion,
2714
+ passed: run.status === want,
2715
+ reason: `expected status=${want}, got ${run.status}`
2716
+ };
2717
+ }
2718
+ case "max_cost_cents": {
2719
+ const want = Number(assertion.value);
2720
+ return {
2721
+ assertion,
2722
+ passed: run.cost_cents <= want,
2723
+ reason: `expected \u2264$${(want / 100).toFixed(2)}, got $${(run.cost_cents / 100).toFixed(2)}`
2724
+ };
2725
+ }
2726
+ case "no_error_step": {
2727
+ const errored = steps.find((s) => s.status === "error");
2728
+ return {
2729
+ assertion,
2730
+ passed: !errored,
2731
+ reason: errored ? `step #${errored.sequence} errored` : "no errored steps"
2732
+ };
2733
+ }
2734
+ case "step_status_at": {
2735
+ if (assertion.at === void 0) {
2736
+ return {
2737
+ assertion,
2738
+ passed: false,
2739
+ reason: "step_status_at requires `at`"
2740
+ };
2741
+ }
2742
+ const step = steps.find((s) => s.sequence === assertion.at);
2743
+ if (!step) {
2744
+ return {
2745
+ assertion,
2746
+ passed: false,
2747
+ reason: `no step at sequence ${assertion.at}`
2748
+ };
2749
+ }
2750
+ const want = String(assertion.value);
2751
+ return {
2752
+ assertion,
2753
+ passed: step.status === want,
2754
+ reason: `step #${assertion.at}: expected status=${want}, got ${step.status}`
2755
+ };
2756
+ }
2757
+ }
2758
+ }
2759
+ function listResults(store, testId, limit = 50) {
2760
+ const rows = testId ? store.db.prepare(
2761
+ "SELECT * FROM regression_results WHERE test_id = ? ORDER BY created_at DESC LIMIT ?"
2762
+ ).all(testId, limit) : store.db.prepare(
2763
+ "SELECT * FROM regression_results ORDER BY created_at DESC LIMIT ?"
2764
+ ).all(limit);
2765
+ const tests = listTests(store);
2766
+ const nameMap = new Map(tests.map((t) => [t.test_id, t.name]));
2767
+ return rows.map((r) => ({
2768
+ result_id: r.result_id,
2769
+ test_id: r.test_id,
2770
+ test_name: nameMap.get(r.test_id) ?? "(deleted)",
2771
+ run_id: r.run_id,
2772
+ passed: r.passed === 1,
2773
+ assertions: JSON.parse(r.details_json),
2774
+ created_at: r.created_at
2775
+ }));
2776
+ }
2777
+
2778
+ // src/web.ts
2779
+ function buildApp(store, opts = {}) {
2780
+ const app = new Hono();
2781
+ const controller = opts.controller ?? new LiveController(store);
2782
+ app.use("/api/*", async (c, next) => {
2783
+ const expected = getSetting(store, "web.bind_token");
2784
+ if (!expected) return next();
2785
+ const header = c.req.header("authorization") ?? "";
2786
+ const match = /^Bearer\s+(.+)$/i.exec(header);
2787
+ if (!match || match[1] !== expected) {
2788
+ return c.json(
2789
+ { error: "unauthorized: invalid or missing Bearer token" },
2790
+ 401
2791
+ );
2792
+ }
2793
+ return next();
2794
+ });
2795
+ if (opts.live && !opts.controller) {
2796
+ opts.live.on("data", (e) => {
2797
+ for (const fn of controller.subscribers) {
2798
+ fn(e);
2799
+ }
2800
+ });
2801
+ }
2802
+ const shellOpts = () => ({
2803
+ liveMode: controller.isLive() || opts.live !== void 0
2804
+ });
2805
+ app.get("/", (c) => {
2806
+ const isLive = controller.isLive() || opts.live !== void 0;
2807
+ const entries = controller.isLive() ? controller.fleetEntries() : opts.live ? opts.live.fleetEntries() : buildFleetEntries(store, { limit: 50 });
2808
+ return c.html(
2809
+ renderShell(
2810
+ "Meterbility \xB7 Fleet",
2811
+ renderFleet(entries, { liveMode: isLive }),
2812
+ shellOpts()
2813
+ )
2814
+ );
2815
+ });
2816
+ app.get("/runs", (c) => {
2817
+ const status = c.req.query("status");
2818
+ const tool = c.req.query("tool");
2819
+ const project = c.req.query("project")?.toLowerCase();
2820
+ let runs = listRuns3(store, {
2821
+ limit: 500,
2822
+ status: status === "ok" || status === "error" || status === "in_progress" || status === "abandoned" ? status : void 0,
2823
+ containsTool: tool || void 0
2824
+ });
2825
+ if (project) {
2826
+ runs = runs.filter(
2827
+ (r) => (r.cwd ?? "").toLowerCase().includes(project)
2828
+ );
2829
+ }
2830
+ return c.html(
2831
+ renderShell(
2832
+ "Runs",
2833
+ renderRunList(runs, {
2834
+ totalAvailable: listRuns3(store, { limit: 1e3 }).length,
2835
+ filters: { status, tool, project }
2836
+ }),
2837
+ shellOpts()
2838
+ )
2839
+ );
2840
+ });
2841
+ app.get("/api/live", (c) => {
2842
+ c.header("Content-Type", "text/event-stream");
2843
+ c.header("Cache-Control", "no-cache");
2844
+ c.header("Connection", "keep-alive");
2845
+ return stream(c, async (s) => {
2846
+ const send = (e) => {
2847
+ void s.write(`event: ${e.type}
2848
+ data: ${JSON.stringify(e)}
2849
+
2850
+ `);
2851
+ };
2852
+ controller.on("data", send);
2853
+ send({ type: "fleet:snapshot", entries: controller.fleetEntries() });
2854
+ await new Promise((resolve) => {
2855
+ s.onAbort(() => {
2856
+ controller.off("data", send);
2857
+ resolve();
2858
+ });
2859
+ });
2860
+ });
2861
+ });
2862
+ app.get(
2863
+ "/api/live/status",
2864
+ (c) => c.json({ live: controller.isLive() })
2865
+ );
2866
+ app.post("/api/live/start", async (c) => {
2867
+ let liveOpts;
2868
+ try {
2869
+ const body = await c.req.json();
2870
+ if (body) liveOpts = body;
2871
+ } catch {
2872
+ }
2873
+ await controller.start(liveOpts);
2874
+ return c.json({ live: controller.isLive() });
2875
+ });
2876
+ app.post("/api/live/stop", (c) => {
2877
+ controller.stop();
2878
+ return c.json({ live: controller.isLive() });
2879
+ });
2880
+ app.get("/runs/:id", async (c) => {
2881
+ const runId = c.req.param("id");
2882
+ const run = getRun6(store, runId);
2883
+ if (!run) return c.notFound();
2884
+ const steps = listSteps7(store, run.run_id);
2885
+ const annotations = listAnnotations(store, "run", run.run_id);
2886
+ const forks = listForks(store, run.run_id);
2887
+ const stepDecisions = await loadDecisionPreviews(store, steps);
2888
+ const allFcs = listFileChanges2(store, { runId: run.run_id });
2889
+ const fcByStep = /* @__PURE__ */ new Map();
2890
+ for (const fc of allFcs) {
2891
+ const list = fcByStep.get(fc.step_id) ?? [];
2892
+ list.push(fc);
2893
+ fcByStep.set(fc.step_id, list);
2894
+ }
2895
+ const probePanel = run.status === "in_progress" ? renderProbePanel(run.run_id, readProbeState2(run.run_id)) : "";
2896
+ const shell = renderShell(
2897
+ run.title ?? run.run_id,
2898
+ renderRun(run, steps, annotations, forks, stepDecisions, fcByStep, probePanel),
2899
+ shellOpts()
2900
+ );
2901
+ if (process.env.METERBILITY_DEBUG && shell.length > 2e6) {
2902
+ console.warn(
2903
+ `meter: run ${run.run_id.slice(0, 12)} page is ${(shell.length / 1024 / 1024).toFixed(1)}MB \u2014 consider lazy pretty fetch`
2904
+ );
2905
+ }
2906
+ return c.html(shell);
2907
+ });
2908
+ app.get("/runs/:id/files", (c) => {
2909
+ const run = getRun6(store, c.req.param("id"));
2910
+ if (!run) return c.notFound();
2911
+ const fcs = listFileChanges2(store, { runId: run.run_id });
2912
+ const steps = listSteps7(store, run.run_id);
2913
+ const captureEnabled = getSetting(store, "capture.files.enabled") !== "false";
2914
+ const rendered = renderFilesPage({
2915
+ run,
2916
+ fileChanges: fcs,
2917
+ steps,
2918
+ captureEnabled
2919
+ });
2920
+ const shell = renderShell(
2921
+ `${run.title ?? run.run_id} \xB7 files`,
2922
+ `${rendered.stylesHtml}${rendered.bodyHtml}${rendered.scriptsHtml}`,
2923
+ shellOpts()
2924
+ );
2925
+ return c.html(shell);
2926
+ });
2927
+ app.get("/runs/:id/files/:path{.+}", (c) => {
2928
+ const run = getRun6(store, c.req.param("id"));
2929
+ if (!run) return c.notFound();
2930
+ const path = decodeURIComponent(c.req.param("path"));
2931
+ const tabParam = c.req.query("tab");
2932
+ const tab = tabParam === "history" || tabParam === "raw" ? tabParam : "final";
2933
+ const fcs = listFileChanges2(store, { runId: run.run_id });
2934
+ if (fcs.length === 0) return c.notFound();
2935
+ const tree = buildFileTree(fcs);
2936
+ const flat = flattenForDefaultSelection(tree);
2937
+ const node = flat.find((n) => n.path === path);
2938
+ if (!node) return c.notFound();
2939
+ c.header("Content-Type", "text/html; charset=utf-8");
2940
+ return c.body(renderRightPane({ runId: run.run_id, node, tab }));
2941
+ });
2942
+ app.get("/api/runs/:id/files", (c) => {
2943
+ const run = getRun6(store, c.req.param("id"));
2944
+ if (!run) return c.notFound();
2945
+ return c.json(listFileChanges2(store, { runId: run.run_id }));
2946
+ });
2947
+ app.get("/api/runs/:id/files/diff", (c) => {
2948
+ const run = getRun6(store, c.req.param("id"));
2949
+ if (!run) return c.notFound();
2950
+ const path = c.req.query("path");
2951
+ if (!path) return c.json({ error: "missing required ?path=" }, 400);
2952
+ return c.json(listFileChanges2(store, { runId: run.run_id, path }));
2953
+ });
2954
+ app.get("/api/steps/:id/file_changes", (c) => {
2955
+ const step = getStep3(store, c.req.param("id"));
2956
+ if (!step) return c.notFound();
2957
+ return c.json(listFileChanges2(store, { stepId: step.step_id }));
2958
+ });
2959
+ app.get("/api/file_change/:id", (c) => {
2960
+ const fc = getFileChange(store, c.req.param("id"));
2961
+ return fc ? c.json(fc) : c.notFound();
2962
+ });
2963
+ app.get("/api/runs/:id/probe", (c) => {
2964
+ const run = getRun6(store, c.req.param("id"));
2965
+ if (!run) return c.json({ error: "run not found" }, 404);
2966
+ return c.json(readProbeState2(run.run_id));
2967
+ });
2968
+ app.get("/api/runs/:id/probe/panel", (c) => {
2969
+ const run = getRun6(store, c.req.param("id"));
2970
+ if (!run) return c.json({ error: "run not found" }, 404);
2971
+ if (run.status !== "in_progress") return c.json({ error: "run sealed" }, 404);
2972
+ c.header("Content-Type", "text/html; charset=utf-8");
2973
+ return c.body(renderProbePanel(run.run_id, readProbeState2(run.run_id)));
2974
+ });
2975
+ app.post("/api/runs/:id/probe/pause", (c) => {
2976
+ const run = getRun6(store, c.req.param("id"));
2977
+ if (!run) return c.json({ error: "run not found" }, 404);
2978
+ const state = probeRequestPause(run.run_id);
2979
+ recordProbeIntervention(store, run.run_id, "probe_pause", {
2980
+ paused_at: new Date(state.requested_at_ms ?? Date.now()).toISOString()
2981
+ });
2982
+ return c.json(state);
2983
+ });
2984
+ app.post("/api/runs/:id/probe/resume", (c) => {
2985
+ const run = getRun6(store, c.req.param("id"));
2986
+ if (!run) return c.json({ error: "run not found" }, 404);
2987
+ const pre = readProbeState2(run.run_id);
2988
+ const state = probeRequestResume(run.run_id);
2989
+ if (pre.inject !== null) {
2990
+ recordProbeIntervention(store, run.run_id, "probe_edit", {
2991
+ inject_bytes: pre.inject.length,
2992
+ resumed_at: new Date(state.resumed_at_ms ?? Date.now()).toISOString()
2993
+ });
2994
+ }
2995
+ return c.json(state);
2996
+ });
2997
+ app.post("/api/runs/:id/probe/inject", async (c) => {
2998
+ const run = getRun6(store, c.req.param("id"));
2999
+ if (!run) return c.json({ error: "run not found" }, 404);
3000
+ let body;
3001
+ try {
3002
+ body = await c.req.json();
3003
+ } catch {
3004
+ return c.json({ error: "invalid JSON body" }, 400);
3005
+ }
3006
+ if (body.clear) {
3007
+ consumeProbeInject(run.run_id);
3008
+ return c.json(readProbeState2(run.run_id));
3009
+ }
3010
+ if (typeof body.message !== "string" || body.message.length === 0) {
3011
+ return c.json({ error: "message is required (use { clear: true } to discard)" }, 400);
3012
+ }
3013
+ const current = readProbeState2(run.run_id);
3014
+ if (current.inject !== null && !body.force) {
3015
+ return c.json(
3016
+ {
3017
+ error: "a pending inject is already queued (pass { force: true } to overwrite)",
3018
+ current_inject: current.inject
3019
+ },
3020
+ 409
3021
+ );
3022
+ }
3023
+ return c.json(probeSetInject(run.run_id, body.message));
3024
+ });
3025
+ app.post("/api/runs/:id/probe/clear", (c) => {
3026
+ const run = getRun6(store, c.req.param("id"));
3027
+ const target = run?.run_id ?? c.req.param("id");
3028
+ clearProbe(target);
3029
+ return c.json({ cleared: target });
3030
+ });
3031
+ const redirectToCanonical = (newPath) => (c) => {
3032
+ c.header("Deprecation", "true");
3033
+ c.header("Link", `<${newPath}>; rel="successor-version"`);
3034
+ return c.redirect(newPath, 308);
3035
+ };
3036
+ app.get(
3037
+ "/api/probe/:run_id",
3038
+ (c) => redirectToCanonical(`/api/runs/${encodeURIComponent(c.req.param("run_id"))}/probe`)(c)
3039
+ );
3040
+ app.get(
3041
+ "/api/probe/:run_id/panel",
3042
+ (c) => redirectToCanonical(
3043
+ `/api/runs/${encodeURIComponent(c.req.param("run_id"))}/probe/panel`
3044
+ )(c)
3045
+ );
3046
+ app.post(
3047
+ "/api/probe/:run_id/pause",
3048
+ (c) => redirectToCanonical(
3049
+ `/api/runs/${encodeURIComponent(c.req.param("run_id"))}/probe/pause`
3050
+ )(c)
3051
+ );
3052
+ app.post(
3053
+ "/api/probe/:run_id/resume",
3054
+ (c) => redirectToCanonical(
3055
+ `/api/runs/${encodeURIComponent(c.req.param("run_id"))}/probe/resume`
3056
+ )(c)
3057
+ );
3058
+ app.post(
3059
+ "/api/probe/:run_id/inject",
3060
+ (c) => redirectToCanonical(
3061
+ `/api/runs/${encodeURIComponent(c.req.param("run_id"))}/probe/inject`
3062
+ )(c)
3063
+ );
3064
+ app.post(
3065
+ "/api/probe/:run_id/clear",
3066
+ (c) => redirectToCanonical(
3067
+ `/api/runs/${encodeURIComponent(c.req.param("run_id"))}/probe/clear`
3068
+ )(c)
3069
+ );
3070
+ app.get("/api/runs/:id/step-card/:seq", async (c) => {
3071
+ const run = getRun6(store, c.req.param("id"));
3072
+ if (!run) return c.notFound();
3073
+ const seq = Number(c.req.param("seq"));
3074
+ if (!Number.isFinite(seq)) return c.json({ error: "bad seq" }, 400);
3075
+ const step = getStepBySequence2(store, run.run_id, seq);
3076
+ if (!step) return c.notFound();
3077
+ const decisions = await loadDecisionPreviews(store, [step]);
3078
+ const fcs = listFileChanges2(store, { stepId: step.step_id });
3079
+ const fragment = renderStepCardFragment(step, decisions.get(step.step_id) ?? "", fcs);
3080
+ c.header("Content-Type", "text/html; charset=utf-8");
3081
+ return c.body(fragment);
3082
+ });
3083
+ app.get("/diff", (c) => {
3084
+ const a = c.req.query("a");
3085
+ const b = c.req.query("b");
3086
+ const showShared = c.req.query("shared") === "1";
3087
+ if (!a || !b) return c.text("usage: /diff?a=<run-id>&b=<run-id>", 400);
3088
+ const runA = getRun6(store, a);
3089
+ const runB = getRun6(store, b);
3090
+ if (!runA || !runB) return c.notFound();
3091
+ const result = diffRuns(store, runA.run_id, runB.run_id);
3092
+ return c.html(
3093
+ renderShell(
3094
+ "Diff",
3095
+ renderDiff(runA, runB, result, { showShared }),
3096
+ shellOpts()
3097
+ )
3098
+ );
3099
+ });
3100
+ app.get("/api/runs", (c) => c.json(listRuns3(store, { limit: 200 })));
3101
+ app.get("/api/runs/:id", (c) => {
3102
+ const run = getRun6(store, c.req.param("id"));
3103
+ return run ? c.json(run) : c.notFound();
3104
+ });
3105
+ app.get("/api/runs/:id/steps", (c) => {
3106
+ const run = getRun6(store, c.req.param("id"));
3107
+ if (!run) return c.notFound();
3108
+ return c.json(listSteps7(store, run.run_id));
3109
+ });
3110
+ app.get("/api/steps/:id", (c) => {
3111
+ const step = getStep3(store, c.req.param("id"));
3112
+ return step ? c.json(step) : c.notFound();
3113
+ });
3114
+ app.get("/api/blob/:hash", async (c) => {
3115
+ const raw = c.req.param("hash");
3116
+ const ref = resolveSnapshotBlobRef3(store, raw);
3117
+ const text = await store.blobs.tryGetString(ref);
3118
+ if (!text) return c.notFound();
3119
+ c.header("Content-Type", "text/plain; charset=utf-8");
3120
+ return c.body(text);
3121
+ });
3122
+ app.get("/api/blob/:hash/render", async (c) => {
3123
+ const raw = c.req.param("hash");
3124
+ const langOverride = c.req.query("lang");
3125
+ const pathHint = c.req.query("path");
3126
+ const ref = resolveSnapshotBlobRef3(store, raw);
3127
+ const buf = await store.blobs.tryGetBuffer(ref);
3128
+ if (!buf) return c.notFound();
3129
+ const sniff = sniffMimeAndLang(buf, pathHint);
3130
+ if (sniff.mime.startsWith("image/")) {
3131
+ return new Response(
3132
+ new Uint8Array(
3133
+ buf.buffer,
3134
+ buf.byteOffset,
3135
+ buf.byteLength
3136
+ ),
3137
+ {
3138
+ status: 200,
3139
+ headers: {
3140
+ "Content-Type": sniff.mime,
3141
+ "Cache-Control": "public, max-age=31536000, immutable"
3142
+ }
3143
+ }
3144
+ );
3145
+ }
3146
+ if (sniff.binary) {
3147
+ return new Response(
3148
+ new Uint8Array(
3149
+ buf.buffer,
3150
+ buf.byteOffset,
3151
+ buf.byteLength
3152
+ ),
3153
+ {
3154
+ status: 200,
3155
+ headers: {
3156
+ "Content-Type": "application/octet-stream",
3157
+ "Cache-Control": "public, max-age=31536000, immutable"
3158
+ }
3159
+ }
3160
+ );
3161
+ }
3162
+ const effLang = langOverride && langOverride !== "auto" ? langOverride : sniff.lang ?? "plaintext";
3163
+ const cacheKey = renderCacheKey(ref, effLang);
3164
+ const cached = await store.blobs.tryGetString(cacheKey);
3165
+ if (cached !== void 0) {
3166
+ c.header("Content-Type", "text/html; charset=utf-8");
3167
+ c.header("Cache-Control", "public, max-age=31536000, immutable");
3168
+ return c.body(cached);
3169
+ }
3170
+ const html = await renderHighlighted(buf, effLang);
3171
+ try {
3172
+ await store.blobs.putWithKey(html, cacheKey);
3173
+ } catch {
3174
+ }
3175
+ c.header("Content-Type", "text/html; charset=utf-8");
3176
+ c.header("Cache-Control", "public, max-age=31536000, immutable");
3177
+ return c.body(html);
3178
+ });
3179
+ app.get("/contexts/:id", async (c) => {
3180
+ const snapshotId = c.req.param("id");
3181
+ const ref = resolveSnapshotBlobRef3(store, snapshotId);
3182
+ const manifestText = await store.blobs.tryGetString(ref);
3183
+ if (!manifestText) return c.notFound();
3184
+ let manifest;
3185
+ try {
3186
+ manifest = JSON.parse(manifestText);
3187
+ } catch {
3188
+ return c.text("invalid snapshot JSON", 500);
3189
+ }
3190
+ const rendered = await resolveContext(store, manifest);
3191
+ const runQ = c.req.query("run");
3192
+ const stepQ = c.req.query("step");
3193
+ const seqQ = c.req.query("seq");
3194
+ const run = runQ ? getRun6(store, runQ) : void 0;
3195
+ if (run) rendered.runtime = run.source_runtime;
3196
+ const meta = {
3197
+ runId: run?.run_id,
3198
+ stepId: stepQ ?? void 0,
3199
+ sequence: seqQ !== void 0 ? Number(seqQ) : void 0
3200
+ };
3201
+ return c.html(
3202
+ renderShell(
3203
+ `Context \xB7 ${snapshotId.slice(0, 12)}`,
3204
+ renderContext(snapshotId, rendered, meta),
3205
+ shellOpts()
3206
+ )
3207
+ );
3208
+ });
3209
+ app.get("/api/diff", (c) => {
3210
+ const a = c.req.query("a");
3211
+ const b = c.req.query("b");
3212
+ if (!a || !b) return c.json({ error: "missing a/b" }, 400);
3213
+ const ra = getRun6(store, a);
3214
+ const rb = getRun6(store, b);
3215
+ if (!ra || !rb) return c.json({ error: "run not found" }, 404);
3216
+ return c.json(diffRuns(store, ra.run_id, rb.run_id));
3217
+ });
3218
+ app.post("/api/runs/:id/close", async (c) => {
3219
+ const run = getRun6(store, c.req.param("id"));
3220
+ if (!run) return c.json({ error: "run not found" }, 404);
3221
+ let status = "ok";
3222
+ try {
3223
+ const body = await c.req.json();
3224
+ if (body?.status) {
3225
+ if (body.status !== "ok" && body.status !== "error" && body.status !== "abandoned") {
3226
+ return c.json(
3227
+ { error: `invalid status: ${body.status}. allowed: ok | error | abandoned` },
3228
+ 400
3229
+ );
3230
+ }
3231
+ status = body.status;
3232
+ }
3233
+ } catch {
3234
+ }
3235
+ setRunStatus3(store, run.run_id, status, (/* @__PURE__ */ new Date()).toISOString());
3236
+ updateRunTotals3(store, run.run_id);
3237
+ const updated = getRun6(store, run.run_id);
3238
+ return c.json(updated);
3239
+ });
3240
+ app.post("/api/runs/close-stale", async (c) => {
3241
+ let body = {};
3242
+ try {
3243
+ body = await c.req.json() ?? {};
3244
+ } catch {
3245
+ }
3246
+ const olderThanMin = body.older_than_minutes ?? 60;
3247
+ const source = body.source;
3248
+ const status = body.status === "error" || body.status === "abandoned" ? body.status : "ok";
3249
+ const cutoffMs = Date.now() - olderThanMin * 6e4;
3250
+ const all = listRuns3(store, { limit: 1e3 });
3251
+ const targets = all.filter((r) => {
3252
+ if (r.status !== "in_progress") return false;
3253
+ if (source && r.source_runtime !== source) return false;
3254
+ const startedMs = Date.parse(r.started_at);
3255
+ return Number.isFinite(startedMs) && startedMs <= cutoffMs;
3256
+ });
3257
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3258
+ for (const r of targets) {
3259
+ setRunStatus3(store, r.run_id, status, now);
3260
+ updateRunTotals3(store, r.run_id);
3261
+ }
3262
+ return c.json({
3263
+ closed: targets.length,
3264
+ run_ids: targets.map((r) => r.run_id),
3265
+ status
3266
+ });
3267
+ });
3268
+ app.post("/api/annotate", async (c) => {
3269
+ const body = await c.req.json();
3270
+ if (!body.target_kind || !body.target_id) {
3271
+ return c.json({ error: "missing target_kind / target_id" }, 400);
3272
+ }
3273
+ let resolvedId = body.target_id;
3274
+ if (body.target_kind === "run") {
3275
+ const run = getRun6(store, body.target_id);
3276
+ if (!run) return c.json({ error: "run not found" }, 404);
3277
+ resolvedId = run.run_id;
3278
+ } else if (body.target_kind === "step") {
3279
+ const step = getStep3(store, body.target_id);
3280
+ if (!step) return c.json({ error: "step not found" }, 404);
3281
+ resolvedId = step.step_id;
3282
+ }
3283
+ const ann = insertAnnotation2(store, {
3284
+ targetKind: body.target_kind,
3285
+ targetId: resolvedId,
3286
+ author: body.author ?? "anonymous",
3287
+ verdict: body.verdict,
3288
+ note: body.note
3289
+ });
3290
+ return c.json(ann);
3291
+ });
3292
+ app.get("/api/runs/:id/annotations", (c) => {
3293
+ const run = getRun6(store, c.req.param("id"));
3294
+ if (!run) return c.notFound();
3295
+ return c.json(listAnnotations(store, "run", run.run_id));
3296
+ });
3297
+ app.get("/api/steps/:id/annotations", (c) => {
3298
+ const step = getStep3(store, c.req.param("id"));
3299
+ if (!step) return c.notFound();
3300
+ return c.json(listAnnotations(store, "step", step.step_id));
3301
+ });
3302
+ app.post("/api/fork", async (c) => {
3303
+ const body = await c.req.json();
3304
+ if (!body.origin_run_id || body.at === void 0 || !body.edit_type) {
3305
+ return c.json({ error: "missing origin_run_id / at / edit_type" }, 400);
3306
+ }
3307
+ const apiKey = resolveSetting(
3308
+ store,
3309
+ "anthropic.api_key",
3310
+ "ANTHROPIC_API_KEY"
3311
+ );
3312
+ const model = body.model ?? getSetting(store, "fork.default_model") ?? "claude-opus-4-7";
3313
+ const responder = body.fake ? fakeResponder(body.fake) : body.live && apiKey ? anthropicResponder(store, { apiKey, model }) : void 0;
3314
+ try {
3315
+ const result = await forkRun(
3316
+ store,
3317
+ {
3318
+ origin_run_id: body.origin_run_id,
3319
+ at: body.at,
3320
+ edit: {
3321
+ type: body.edit_type,
3322
+ payload: body.edit_payload ?? null
3323
+ }
3324
+ },
3325
+ responder
3326
+ );
3327
+ let continuation;
3328
+ if (body.continue && body.continue !== "none") {
3329
+ if (body.continue !== "simulate" && body.continue !== "live") {
3330
+ return c.json({ error: `invalid continue mode: ${body.continue}` }, 400);
3331
+ }
3332
+ if (body.continue === "live" && !apiKey) {
3333
+ return c.json(
3334
+ { error: "live continuation requires ANTHROPIC_API_KEY" },
3335
+ 400
3336
+ );
3337
+ }
3338
+ const modelCaller = async (args) => {
3339
+ const { default: Anthropic } = await import("@anthropic-ai/sdk");
3340
+ const client = new Anthropic({ apiKey });
3341
+ const messages = [];
3342
+ for (const m of args.history) {
3343
+ if (m.role === "tool") continue;
3344
+ const text2 = await store.blobs.tryGetString(m.content_ref);
3345
+ if (text2) messages.push({ role: m.role, content: text2 });
3346
+ }
3347
+ const t0 = Date.now();
3348
+ const resp = await client.messages.create({
3349
+ model,
3350
+ max_tokens: 4096,
3351
+ system: args.system_prompt,
3352
+ messages: messages.length ? messages : [{ role: "user", content: "(no history)" }]
3353
+ });
3354
+ const t1 = Date.now();
3355
+ const blocks = resp.content ?? [];
3356
+ const toolUse = blocks.find(
3357
+ (b) => b.type === "tool_use"
3358
+ );
3359
+ const text = blocks.filter((b) => b.type === "text").map((b) => b.text).join("\n");
3360
+ const usage = resp.usage;
3361
+ const cc = usage?.cache_creation;
3362
+ return {
3363
+ model: resp.model,
3364
+ decision_content: resp.content,
3365
+ action: toolUse ? {
3366
+ kind: "tool_call",
3367
+ tool_name: toolUse.name,
3368
+ tool_use_id: toolUse.id,
3369
+ tool_input: toolUse.input
3370
+ } : { kind: "message", text },
3371
+ tokens: {
3372
+ input: resp.usage?.input_tokens ?? 0,
3373
+ output: resp.usage?.output_tokens ?? 0,
3374
+ cached_read: usage?.cache_read_input_tokens ?? 0,
3375
+ cache_creation: cc?.ephemeral_5m_input_tokens ?? usage?.cache_creation_input_tokens ?? 0,
3376
+ cache_creation_1h: cc?.ephemeral_1h_input_tokens ?? 0
3377
+ },
3378
+ latency_ms: t1 - t0
3379
+ };
3380
+ };
3381
+ const allowSet = new Set(body.allow_tools ?? []);
3382
+ const toolExecutor = body.continue === "live" ? async (call) => {
3383
+ if (!allowSet.has(call.tool_name)) {
3384
+ return {
3385
+ output: { meter_note: `tool '${call.tool_name}' not allowed` },
3386
+ is_error: false,
3387
+ summary: `skipped: ${call.tool_name}`
3388
+ };
3389
+ }
3390
+ if (call.tool_name === "Bash") {
3391
+ const cmd = call.tool_input?.command;
3392
+ if (!cmd) {
3393
+ return { output: { error: "missing command" }, is_error: true, summary: "missing command" };
3394
+ }
3395
+ if (/\brm\s+-[rRfF]|sudo\b|--no-verify/.test(cmd)) {
3396
+ return { output: { error: "destructive command rejected" }, is_error: true, summary: "rejected" };
3397
+ }
3398
+ const { spawnSync } = await import("child_process");
3399
+ const r = spawnSync("bash", ["-lc", cmd], {
3400
+ encoding: "utf-8",
3401
+ timeout: 3e4
3402
+ });
3403
+ return {
3404
+ output: { stdout: r.stdout?.slice(0, 8e3) ?? "", stderr: r.stderr?.slice(0, 2e3) ?? "", exit_code: r.status },
3405
+ is_error: (r.status ?? 0) !== 0,
3406
+ summary: (r.stdout?.split("\n")[0] ?? "").slice(0, 200)
3407
+ };
3408
+ }
3409
+ return {
3410
+ output: { meter_note: `tool '${call.tool_name}' has no executor; no-op` },
3411
+ is_error: false,
3412
+ summary: `no-op: ${call.tool_name}`
3413
+ };
3414
+ } : (async () => ({ output: {}, is_error: false }));
3415
+ const cont = await continueFork(store, result.fork_run_id, {
3416
+ mode: body.continue,
3417
+ modelCaller,
3418
+ toolExecutor,
3419
+ maxIterations: body.max_iterations ?? 25,
3420
+ originRunId: body.origin_run_id
3421
+ });
3422
+ continuation = {
3423
+ mode: body.continue,
3424
+ iterations: cont.iterations_run,
3425
+ steps_added: cont.steps_added,
3426
+ terminal_reason: cont.terminal_reason
3427
+ };
3428
+ }
3429
+ return c.json({ ...result, continuation });
3430
+ } catch (err) {
3431
+ return c.json({ error: err.message }, 400);
3432
+ }
3433
+ });
3434
+ app.get("/api/tests", (c) => c.json(listTests(store)));
3435
+ app.get("/api/tests/:name", (c) => {
3436
+ const t = getTestByName(store, c.req.param("name"));
3437
+ return t ? c.json(t) : c.notFound();
3438
+ });
3439
+ app.get("/api/tests/:name/results", (c) => {
3440
+ const t = getTestByName(store, c.req.param("name"));
3441
+ if (!t) return c.notFound();
3442
+ return c.json(listResults(store, t.test_id, 50));
3443
+ });
3444
+ app.post("/api/tests", async (c) => {
3445
+ const body = await c.req.json();
3446
+ if (!body.name) return c.json({ error: "missing name" }, 400);
3447
+ let assertions = body.assertions ?? [];
3448
+ let canonicalRunId;
3449
+ if (body.from_run_id) {
3450
+ const run = getRun6(store, body.from_run_id);
3451
+ if (!run) return c.json({ error: "from_run_id not found" }, 404);
3452
+ const steps = listSteps7(store, run.run_id);
3453
+ assertions = deriveAssertionsFromRun(run, steps);
3454
+ canonicalRunId = run.run_id;
3455
+ }
3456
+ try {
3457
+ const t = createTest(store, {
3458
+ name: body.name,
3459
+ description: body.description,
3460
+ assertions,
3461
+ canonical_run_id: canonicalRunId
3462
+ });
3463
+ return c.json(t);
3464
+ } catch (err) {
3465
+ return c.json({ error: err.message }, 400);
3466
+ }
3467
+ });
3468
+ app.put("/api/tests/:name/assertions", async (c) => {
3469
+ const name = c.req.param("name");
3470
+ const body = await c.req.json();
3471
+ const t = getTestByName(store, name);
3472
+ if (!t) return c.notFound();
3473
+ store.db.prepare("UPDATE regression_tests SET assertions_json = ? WHERE test_id = ?").run(JSON.stringify(body.assertions), t.test_id);
3474
+ return c.json(getTestByName(store, name));
3475
+ });
3476
+ app.post("/api/tests/:name/assertions", async (c) => {
3477
+ const name = c.req.param("name");
3478
+ const body = await c.req.json();
3479
+ try {
3480
+ const t = addAssertion(store, name, body.assertion);
3481
+ return c.json(t);
3482
+ } catch (err) {
3483
+ return c.json({ error: err.message }, 400);
3484
+ }
3485
+ });
3486
+ app.delete("/api/tests/:name", (c) => {
3487
+ const ok = deleteTest(store, c.req.param("name"));
3488
+ return ok ? c.json({ ok: true }) : c.notFound();
3489
+ });
3490
+ app.post("/api/tests/:name/run", async (c) => {
3491
+ const name = c.req.param("name");
3492
+ const body = await c.req.json().catch(() => ({}));
3493
+ const t = getTestByName(store, name);
3494
+ if (!t) return c.notFound();
3495
+ let runs = body.run_id ? [getRun6(store, body.run_id)].filter((r) => !!r) : listRuns3(store, { limit: body.limit ?? 50 });
3496
+ const results = runs.map((r) => runTest(store, t, r.run_id));
3497
+ return c.json(results);
3498
+ });
3499
+ app.get("/tests", (c) => {
3500
+ const tests = listTests(store);
3501
+ const recent = listResults(store, void 0, 20);
3502
+ return c.html(renderShell("Tests", renderTests(tests, recent), shellOpts()));
3503
+ });
3504
+ app.get("/settings", async (c) => {
3505
+ const { renderSettings } = await import("./html-QXNPWM2W.js");
3506
+ const slackWebhook = resolveSetting(store, "slack.webhook", "METERBILITY_SLACK_WEBHOOK");
3507
+ const apiKey = resolveSetting(store, "anthropic.api_key", "ANTHROPIC_API_KEY");
3508
+ const pgUrl = resolveSetting(store, "postgres.url", "METERBILITY_DB_URL");
3509
+ return c.html(
3510
+ renderShell(
3511
+ "Settings",
3512
+ renderSettings({
3513
+ slackWebhook,
3514
+ slackWebhookFromEnv: !!process.env.METERBILITY_SLACK_WEBHOOK,
3515
+ apiKey,
3516
+ apiKeyFromEnv: !!process.env.ANTHROPIC_API_KEY,
3517
+ postgresUrl: pgUrl,
3518
+ postgresUrlFromEnv: !!process.env.METERBILITY_DB_URL,
3519
+ watchedTools: getSetting(store, "live.watch_tools") ?? "",
3520
+ stallSeconds: Number(getSetting(store, "live.stall_seconds") ?? 120),
3521
+ defaultModel: getSetting(store, "fork.default_model") ?? "claude-opus-4-7",
3522
+ defaultMaxIterations: Number(
3523
+ getSetting(store, "fork.default_max_iterations") ?? 25
3524
+ )
3525
+ }),
3526
+ shellOpts()
3527
+ )
3528
+ );
3529
+ });
3530
+ app.post("/api/settings", async (c) => {
3531
+ const body = await c.req.json();
3532
+ if (!body.key) return c.json({ error: "missing key" }, 400);
3533
+ if (body.value === void 0 || body.value === "") {
3534
+ deleteSetting(store, body.key);
3535
+ } else {
3536
+ setSetting(store, body.key, body.value);
3537
+ }
3538
+ return c.json({ ok: true });
3539
+ });
3540
+ app.get("/api/doctor", async (c) => {
3541
+ const { existsSync: existsSync2 } = await import("fs");
3542
+ const { stat } = await import("fs/promises");
3543
+ const { claudeHome, claudeProjectsRoot: claudeProjectsRoot2, dbPath, meterHome } = await import("@meterbility/shared");
3544
+ const { discoverSessions: discoverSessions2 } = await import("@meterbility/claude-code-adapter");
3545
+ const checks = [];
3546
+ const node = process.versions.node;
3547
+ const [major, minor] = node.split(".").map(Number);
3548
+ checks.push({
3549
+ name: "Node",
3550
+ status: major > 20 || major === 20 && minor >= 6 || major >= 22 ? "ok" : major >= 20 ? "warn" : "fail",
3551
+ detail: `v${node}`
3552
+ });
3553
+ checks.push({ name: "METERBILITY_HOME", status: "ok", detail: meterHome() });
3554
+ checks.push({
3555
+ name: "CLAUDE_HOME",
3556
+ status: existsSync2(claudeHome()) ? "ok" : "fail",
3557
+ detail: claudeHome()
3558
+ });
3559
+ checks.push({
3560
+ name: "Claude projects dir",
3561
+ status: existsSync2(claudeProjectsRoot2()) ? "ok" : "warn",
3562
+ detail: claudeProjectsRoot2()
3563
+ });
3564
+ try {
3565
+ const sessions = await discoverSessions2();
3566
+ checks.push({
3567
+ name: "Session discovery",
3568
+ status: sessions.length > 0 ? "ok" : "warn",
3569
+ detail: sessions.length === 0 ? "no .jsonl session files found" : `${sessions.length} session(s)`
3570
+ });
3571
+ } catch (err) {
3572
+ checks.push({
3573
+ name: "Session discovery",
3574
+ status: "fail",
3575
+ detail: err.message
3576
+ });
3577
+ }
3578
+ try {
3579
+ const s = await stat(dbPath());
3580
+ checks.push({
3581
+ name: "SQLite store",
3582
+ status: "ok",
3583
+ detail: `${dbPath()} (${s.size} bytes)`
3584
+ });
3585
+ } catch (err) {
3586
+ checks.push({
3587
+ name: "SQLite store",
3588
+ status: "fail",
3589
+ detail: err.message
3590
+ });
3591
+ }
3592
+ return c.json({ checks });
3593
+ });
3594
+ app.post("/api/ingest", async (c) => {
3595
+ const body = await c.req.json();
3596
+ if (!body.runtime) return c.json({ error: "missing runtime" }, 400);
3597
+ try {
3598
+ let runs = 0;
3599
+ let steps = 0;
3600
+ let bytes = 0;
3601
+ let composers = 0;
3602
+ if (body.runtime === "claude-code") {
3603
+ const { discoverSessions: discoverSessions2, ingestSession: ingestSession2 } = await import("@meterbility/claude-code-adapter");
3604
+ let paths = [];
3605
+ if (body.path) paths = [body.path];
3606
+ else {
3607
+ const sessions = await discoverSessions2();
3608
+ paths = sessions.map((s) => s.path);
3609
+ if (body.limit) paths = paths.slice(0, body.limit);
3610
+ }
3611
+ for (const p of paths) {
3612
+ const r = await ingestSession2(store, p);
3613
+ if (r.status === "ok") {
3614
+ runs += 1;
3615
+ steps += r.steps_added;
3616
+ bytes += r.bytes_read;
3617
+ }
3618
+ }
3619
+ } else if (body.runtime === "codex-cli") {
3620
+ const { discoverCodexSessions, ingestCodexSession } = await import("@meterbility/codex-cli-adapter");
3621
+ let paths = [];
3622
+ if (body.path) paths = [body.path];
3623
+ else {
3624
+ const sessions = await discoverCodexSessions();
3625
+ paths = sessions.map((s) => s.path);
3626
+ if (body.limit) paths = paths.slice(0, body.limit);
3627
+ }
3628
+ for (const p of paths) {
3629
+ const r = await ingestCodexSession(store, p);
3630
+ if (r.status === "ok") {
3631
+ runs += 1;
3632
+ steps += r.steps_added;
3633
+ bytes += r.bytes_read;
3634
+ }
3635
+ }
3636
+ } else if (body.runtime === "cursor") {
3637
+ const { ingestCursorGlobal } = await import("@meterbility/cursor-adapter");
3638
+ const r = await ingestCursorGlobal(store, { limit: body.limit });
3639
+ if (r.status === "ok") {
3640
+ composers = r.composers_ingested;
3641
+ steps = r.steps_added;
3642
+ } else {
3643
+ return c.json({ error: r.reason ?? "ingest failed" }, 400);
3644
+ }
3645
+ } else {
3646
+ return c.json({ error: "unknown runtime" }, 400);
3647
+ }
3648
+ return c.json({
3649
+ ok: true,
3650
+ runtime: body.runtime,
3651
+ runs,
3652
+ composers,
3653
+ steps,
3654
+ bytes
3655
+ });
3656
+ } catch (err) {
3657
+ return c.json({ error: err.message }, 500);
3658
+ }
3659
+ });
3660
+ app.post("/api/slack/test", async (c) => {
3661
+ const body = await c.req.json().catch(() => ({}));
3662
+ const url = body.webhook ?? resolveSetting(store, "slack.webhook", "METERBILITY_SLACK_WEBHOOK");
3663
+ if (!url) return c.json({ error: "missing webhook" }, 400);
3664
+ try {
3665
+ const n = new SlackNotifier({ webhookUrl: url });
3666
+ await n.sendTest();
3667
+ return c.json({ ok: true });
3668
+ } catch (err) {
3669
+ return c.json({ error: err.message }, 400);
3670
+ }
3671
+ });
3672
+ app.post("/api/db/postgres-init", async (c) => {
3673
+ const body = await c.req.json().catch(() => ({}));
3674
+ const url = body.url ?? resolveSetting(store, "postgres.url", "METERBILITY_DB_URL");
3675
+ if (!url) return c.json({ error: "missing url" }, 400);
3676
+ try {
3677
+ const { PostgresStore } = await import("@meterbility/store-postgres");
3678
+ const pg = await PostgresStore.open({ url });
3679
+ try {
3680
+ const r = await pg.client.query(
3681
+ "SELECT value FROM meta WHERE key='schema_version'"
3682
+ );
3683
+ return c.json({
3684
+ ok: true,
3685
+ schema_version: r.rows[0]?.value ?? null
3686
+ });
3687
+ } finally {
3688
+ await pg.close();
3689
+ }
3690
+ } catch (err) {
3691
+ return c.json({ error: err.message }, 400);
3692
+ }
3693
+ });
3694
+ app.post("/api/db/postgres-sync", async (c) => {
3695
+ const body = await c.req.json().catch(() => ({}));
3696
+ const url = body.url ?? resolveSetting(store, "postgres.url", "METERBILITY_DB_URL");
3697
+ if (!url) return c.json({ error: "missing url" }, 400);
3698
+ try {
3699
+ const { PostgresStore, syncSqliteToPostgres } = await import("@meterbility/store-postgres");
3700
+ const pg = await PostgresStore.open({ url });
3701
+ try {
3702
+ const r = await syncSqliteToPostgres(store, pg, {
3703
+ limitRuns: body.limit
3704
+ });
3705
+ return c.json({ ok: true, ...r });
3706
+ } finally {
3707
+ await pg.close();
3708
+ }
3709
+ } catch (err) {
3710
+ return c.json({ error: err.message }, 400);
3711
+ }
3712
+ });
3713
+ app.get("/api/runs/:id/export", async (c) => {
3714
+ const run = getRun6(store, c.req.param("id"));
3715
+ if (!run) return c.notFound();
3716
+ const includeBlobs = c.req.query("blobs") !== "0";
3717
+ const includeFileBlobs = c.req.query("file_blobs") === "1";
3718
+ const steps = listSteps7(store, run.run_id);
3719
+ const file_changes = listFileChanges2(store, { runId: run.run_id });
3720
+ const baseline_trees = run.baseline_tree_id ? [getBaselineTree(store, run.baseline_tree_id)].filter(
3721
+ (bt) => !!bt
3722
+ ) : [];
3723
+ const trace = {
3724
+ meter_trace_version: TRACE_FORMAT_VERSION,
3725
+ run,
3726
+ steps,
3727
+ file_changes,
3728
+ baseline_trees
3729
+ };
3730
+ if (includeBlobs) {
3731
+ const blobs = {};
3732
+ const fileBlobRefs = /* @__PURE__ */ new Set();
3733
+ for (const fc of file_changes) {
3734
+ if (fc.before_blob_ref) fileBlobRefs.add(fc.before_blob_ref);
3735
+ if (fc.after_blob_ref) fileBlobRefs.add(fc.after_blob_ref);
3736
+ }
3737
+ const structuralRefs = /* @__PURE__ */ new Set();
3738
+ for (const s of steps) {
3739
+ structuralRefs.add(
3740
+ resolveSnapshotBlobRef3(store, s.context_snapshot_id)
3741
+ );
3742
+ structuralRefs.add(s.decision_ref);
3743
+ if (s.outcome.tool_result_ref) {
3744
+ structuralRefs.add(s.outcome.tool_result_ref);
3745
+ }
3746
+ }
3747
+ for (const bt of baseline_trees) {
3748
+ if (bt.manifest_blob_ref) {
3749
+ structuralRefs.add(bt.manifest_blob_ref);
3750
+ }
3751
+ }
3752
+ const refsToInline = new Set(structuralRefs);
3753
+ if (includeFileBlobs) {
3754
+ for (const r of fileBlobRefs) refsToInline.add(r);
3755
+ }
3756
+ for (const r of refsToInline) {
3757
+ const text = await store.blobs.tryGetString(r);
3758
+ if (text !== void 0) {
3759
+ blobs[r] = Buffer.from(text, "utf-8").toString("base64");
3760
+ }
3761
+ }
3762
+ trace.blobs = blobs;
3763
+ }
3764
+ c.header(
3765
+ "Content-Disposition",
3766
+ `attachment; filename="${run.run_id}.meter.json"`
3767
+ );
3768
+ return c.json(trace);
3769
+ });
3770
+ return app;
3771
+ }
3772
+ async function resolveContext(store, snapshot) {
3773
+ let totalChars = 0;
3774
+ const components = [];
3775
+ const fetchText = async (ref) => {
3776
+ const text = await store.blobs.tryGetString(ref) ?? "(missing blob)";
3777
+ totalChars += text.length;
3778
+ return text;
3779
+ };
3780
+ for (const c of snapshot.components) {
3781
+ if (c.type === "system_prompt") {
3782
+ components.push({
3783
+ type: "system_prompt",
3784
+ ref: c.content_ref,
3785
+ text: await fetchText(c.content_ref)
3786
+ });
3787
+ } else if (c.type === "tool_definitions") {
3788
+ components.push({
3789
+ type: "tool_definitions",
3790
+ ref: c.content_ref,
3791
+ text: await fetchText(c.content_ref)
3792
+ });
3793
+ } else if (c.type === "conversation_history") {
3794
+ const messages = [];
3795
+ for (const m of c.messages ?? []) {
3796
+ messages.push({
3797
+ role: m.role,
3798
+ ref: m.content_ref,
3799
+ text: await fetchText(m.content_ref),
3800
+ step_ref: m.step_ref
3801
+ });
3802
+ }
3803
+ components.push({ type: "conversation_history", messages });
3804
+ } else if (c.type === "retrieved_documents") {
3805
+ const docs = [];
3806
+ for (const d of c.docs ?? []) {
3807
+ docs.push({
3808
+ source: d.source,
3809
+ ref: d.content_ref,
3810
+ text: await fetchText(d.content_ref)
3811
+ });
3812
+ }
3813
+ components.push({ type: "retrieved_documents", docs });
3814
+ } else if (c.type === "compaction_summary") {
3815
+ components.push({
3816
+ type: "compaction_summary",
3817
+ ref: c.content_ref,
3818
+ text: await fetchText(c.content_ref),
3819
+ replaces_steps: c.replaces_steps
3820
+ });
3821
+ }
3822
+ }
3823
+ return { components, totalChars };
3824
+ }
3825
+ async function loadDecisionPreviews(store, steps) {
3826
+ const out = /* @__PURE__ */ new Map();
3827
+ for (const s of steps) {
3828
+ const text = await store.blobs.tryGetString(s.decision_ref);
3829
+ if (text) out.set(s.step_id, text.slice(0, DECISION_PREVIEW_LIMIT));
3830
+ }
3831
+ return out;
3832
+ }
3833
+ function serveApp(store, opts = {}) {
3834
+ const controller = new LiveController(store);
3835
+ if (opts.live) {
3836
+ void controller.start(opts.liveOptions);
3837
+ }
3838
+ const app = buildApp(store, { controller });
3839
+ const port = opts.port ?? 4317;
3840
+ const host = opts.host ?? "127.0.0.1";
3841
+ const server = serve({ fetch: app.fetch, port, hostname: host });
3842
+ return {
3843
+ url: `http://${host}:${port}`,
3844
+ controller,
3845
+ get live() {
3846
+ return controller.inspector;
3847
+ },
3848
+ close: () => {
3849
+ controller.stop();
3850
+ server.close();
3851
+ }
3852
+ };
3853
+ }
3854
+ export {
3855
+ DECISION_PREVIEW_LIMIT,
3856
+ LiveController,
3857
+ LiveInspector,
3858
+ SlackNotifier,
3859
+ addAssertion,
3860
+ anthropicResponder,
3861
+ appendLiveStep,
3862
+ buildApp,
3863
+ buildFleetEntries,
3864
+ classifyRunStatus,
3865
+ contextUtilization,
3866
+ continueFork,
3867
+ createTest,
3868
+ deleteTest,
3869
+ deriveAssertionsFromRun,
3870
+ detectLoop,
3871
+ diffRuns,
3872
+ fakeResponder,
3873
+ forkRun,
3874
+ getTestByName,
3875
+ listResults,
3876
+ listTests,
3877
+ materializePrefix,
3878
+ prettyMultilineString,
3879
+ prettyTab,
3880
+ prettyValue,
3881
+ reformatJsonString,
3882
+ resolveSimulatedResult,
3883
+ rewriteSnapshot,
3884
+ runTest,
3885
+ serveApp
3886
+ };