@open-code-review/cli 2.0.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +2 -0
  2. package/dist/dashboard/client/assets/{_basePickBy-B3ALyupE.js → _basePickBy-BBPb8BJA.js} +1 -1
  3. package/dist/dashboard/client/assets/{_baseUniq-b2RALAWc.js → _baseUniq-CFHdos6T.js} +1 -1
  4. package/dist/dashboard/client/assets/{arc-DcSVvhUd.js → arc-BKGGWA2F.js} +1 -1
  5. package/dist/dashboard/client/assets/{architectureDiagram-VXUJARFQ-BNUlmSCS.js → architectureDiagram-VXUJARFQ-B_ovNjX1.js} +1 -1
  6. package/dist/dashboard/client/assets/{blockDiagram-VD42YOAC-BmhiQVwa.js → blockDiagram-VD42YOAC-C2M-avVp.js} +1 -1
  7. package/dist/dashboard/client/assets/{c4Diagram-YG6GDRKO-jyJ3WOv5.js → c4Diagram-YG6GDRKO-BtOBpAzH.js} +1 -1
  8. package/dist/dashboard/client/assets/channel-rgw7C1e7.js +1 -0
  9. package/dist/dashboard/client/assets/{chunk-4BX2VUAB-x1dQU_s3.js → chunk-4BX2VUAB-Cz2EbHPl.js} +1 -1
  10. package/dist/dashboard/client/assets/{chunk-55IACEB6-CwbsE2XQ.js → chunk-55IACEB6-C8xpXw9G.js} +1 -1
  11. package/dist/dashboard/client/assets/{chunk-B4BG7PRW-BaE7c-ti.js → chunk-B4BG7PRW-BSRfOovX.js} +1 -1
  12. package/dist/dashboard/client/assets/{chunk-DI55MBZ5-Bw5PUaMK.js → chunk-DI55MBZ5-CEUbYQWn.js} +1 -1
  13. package/dist/dashboard/client/assets/{chunk-FMBD7UC4-B7cF6P3s.js → chunk-FMBD7UC4-5xWP6GRj.js} +1 -1
  14. package/dist/dashboard/client/assets/{chunk-QN33PNHL-OY4evNHd.js → chunk-QN33PNHL-DfNCVcy8.js} +1 -1
  15. package/dist/dashboard/client/assets/{chunk-QZHKN3VN-BpjQwIWz.js → chunk-QZHKN3VN--OdToKKu.js} +1 -1
  16. package/dist/dashboard/client/assets/{chunk-TZMSLE5B-D8b_Oq9B.js → chunk-TZMSLE5B-B_0K0Qso.js} +1 -1
  17. package/dist/dashboard/client/assets/classDiagram-2ON5EDUG-DTGi7d9X.js +1 -0
  18. package/dist/dashboard/client/assets/classDiagram-v2-WZHVMYZB-DTGi7d9X.js +1 -0
  19. package/dist/dashboard/client/assets/clone-Cz7hswqi.js +1 -0
  20. package/dist/dashboard/client/assets/{cose-bilkent-S5V4N54A-C-sfP8PN.js → cose-bilkent-S5V4N54A-Cc_Dmnxz.js} +1 -1
  21. package/dist/dashboard/client/assets/{dagre-6UL2VRFP-Cqfo0NRg.js → dagre-6UL2VRFP-DaAfvUXU.js} +1 -1
  22. package/dist/dashboard/client/assets/{diagram-PSM6KHXK-BR3ppxqI.js → diagram-PSM6KHXK-7idwN0rC.js} +1 -1
  23. package/dist/dashboard/client/assets/{diagram-QEK2KX5R-Dvcx6x3R.js → diagram-QEK2KX5R-D9j9H13n.js} +1 -1
  24. package/dist/dashboard/client/assets/{diagram-S2PKOQOG-DoyBLnVN.js → diagram-S2PKOQOG-SMF5SB0K.js} +1 -1
  25. package/dist/dashboard/client/assets/{erDiagram-Q2GNP2WA-hy77l1cL.js → erDiagram-Q2GNP2WA-EVJ4Qa2F.js} +1 -1
  26. package/dist/dashboard/client/assets/{flowDiagram-NV44I4VS-Bz0B1rKM.js → flowDiagram-NV44I4VS-tZ7SFE77.js} +1 -1
  27. package/dist/dashboard/client/assets/{ganttDiagram-JELNMOA3-CLgrZPoC.js → ganttDiagram-JELNMOA3-DFSqguY7.js} +1 -1
  28. package/dist/dashboard/client/assets/{gitGraphDiagram-V2S2FVAM-DwJ-1f-v.js → gitGraphDiagram-V2S2FVAM-CqHdP3HE.js} +1 -1
  29. package/dist/dashboard/client/assets/{graph-DDBMM_t2.js → graph-C0XnkNkk.js} +1 -1
  30. package/dist/dashboard/client/assets/{index-Cr9yEo_B.js → index-C3NEq704.js} +133 -138
  31. package/dist/dashboard/client/assets/index-CzxeSSaQ.css +1 -0
  32. package/dist/dashboard/client/assets/{infoDiagram-HS3SLOUP-Bhn1FmAk.js → infoDiagram-HS3SLOUP-DlXZo9U2.js} +1 -1
  33. package/dist/dashboard/client/assets/{journeyDiagram-XKPGCS4Q-CzGbjX1y.js → journeyDiagram-XKPGCS4Q-CgC8_7eN.js} +1 -1
  34. package/dist/dashboard/client/assets/{kanban-definition-3W4ZIXB7-Da77-WYk.js → kanban-definition-3W4ZIXB7-BMAw_jNp.js} +1 -1
  35. package/dist/dashboard/client/assets/{layout-CVwSB-GS.js → layout-XjM3Q-ka.js} +1 -1
  36. package/dist/dashboard/client/assets/{linear-CTRAc5Jn.js → linear-CMUrrr1X.js} +1 -1
  37. package/dist/dashboard/client/assets/{mermaid-renderer-Bjo170ax.js → mermaid-renderer-D2jYNs7K.js} +4 -4
  38. package/dist/dashboard/client/assets/{mindmap-definition-VGOIOE7T-B55C2odl.js → mindmap-definition-VGOIOE7T-CL4hv-vg.js} +1 -1
  39. package/dist/dashboard/client/assets/{pieDiagram-ADFJNKIX-5lrQLrSz.js → pieDiagram-ADFJNKIX-DTqv-1h1.js} +1 -1
  40. package/dist/dashboard/client/assets/{quadrantDiagram-AYHSOK5B-Bg55gC30.js → quadrantDiagram-AYHSOK5B-BpFlSW9N.js} +1 -1
  41. package/dist/dashboard/client/assets/{requirementDiagram-UZGBJVZJ-CyR4YFJY.js → requirementDiagram-UZGBJVZJ-BqYqqXL4.js} +1 -1
  42. package/dist/dashboard/client/assets/{sankeyDiagram-TZEHDZUN-BVWKr9_-.js → sankeyDiagram-TZEHDZUN-kEI9kntR.js} +1 -1
  43. package/dist/dashboard/client/assets/{sequenceDiagram-WL72ISMW-D0AJg_tE.js → sequenceDiagram-WL72ISMW-Cnu_1j-N.js} +1 -1
  44. package/dist/dashboard/client/assets/{stateDiagram-FKZM4ZOC-BuHpTgim.js → stateDiagram-FKZM4ZOC-BoC-rqoG.js} +1 -1
  45. package/dist/dashboard/client/assets/stateDiagram-v2-4FDKWEC3-COR3QD3v.js +1 -0
  46. package/dist/dashboard/client/assets/{timeline-definition-IT6M3QCI-LDhpAmDd.js → timeline-definition-IT6M3QCI-CXMWuzDL.js} +1 -1
  47. package/dist/dashboard/client/assets/{treemap-GDKQZRPO-Dd4gjvUl.js → treemap-GDKQZRPO-o9ZFgpbJ.js} +1 -1
  48. package/dist/dashboard/client/assets/{xychartDiagram-PRI3JC2R-B9RDod39.js → xychartDiagram-PRI3JC2R-CfIuUpeA.js} +1 -1
  49. package/dist/dashboard/client/index.html +2 -2
  50. package/dist/dashboard/server.js +1175 -450
  51. package/dist/index.js +1489 -312
  52. package/dist/lib/db/index.js +666 -48
  53. package/dist/lib/runtime-config.js +29 -13
  54. package/dist/lib/state/index.js +2196 -0
  55. package/package.json +9 -5
  56. package/dist/dashboard/client/assets/channel-D3J8-GF_.js +0 -1
  57. package/dist/dashboard/client/assets/classDiagram-2ON5EDUG-tkFUL-1Y.js +0 -1
  58. package/dist/dashboard/client/assets/classDiagram-v2-WZHVMYZB-tkFUL-1Y.js +0 -1
  59. package/dist/dashboard/client/assets/clone-CkY5ajLr.js +0 -1
  60. package/dist/dashboard/client/assets/index-Z1pPudAt.css +0 -1
  61. package/dist/dashboard/client/assets/stateDiagram-v2-4FDKWEC3-DwAPhteN.js +0 -1
@@ -1,35 +1,97 @@
1
1
  // src/lib/db/index.ts
2
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, copyFileSync, statSync } from "node:fs";
3
- import { dirname as dirname3, join as join3 } from "node:path";
2
+ import {
3
+ existsSync as existsSync4,
4
+ mkdirSync as mkdirSync2,
5
+ copyFileSync as copyFileSync2,
6
+ statSync as statSync2,
7
+ mkdtempSync,
8
+ rmSync
9
+ } from "node:fs";
10
+ import { tmpdir } from "node:os";
11
+ import { dirname as dirname4, join as join4 } from "node:path";
4
12
 
5
13
  // src/lib/db/engine.ts
6
- import BetterSqlite3 from "better-sqlite3";
14
+ import { createRequire } from "node:module";
15
+
16
+ // src/lib/runtime-checks.ts
17
+ var NODE_FLOOR = { major: 22, minor: 5 };
18
+ function isSupportedNode(version) {
19
+ const [major = 0, minor = 0] = version.split(".").map((n) => Number.parseInt(n, 10) || 0);
20
+ return major > NODE_FLOOR.major || major === NODE_FLOOR.major && minor >= NODE_FLOOR.minor;
21
+ }
22
+ function nodeVersionGuardMessage(version) {
23
+ return `
24
+ Open Code Review requires Node.js >= ${NODE_FLOOR.major}.${NODE_FLOOR.minor} (it uses Node's built-in SQLite, \`node:sqlite\`).
25
+ You have Node ${version}. Upgrade Node (e.g. \`nvm install 22 && nvm use 22\`) and re-run.
26
+
27
+ `;
28
+ }
29
+ function isSuppressibleSqliteWarning(warning) {
30
+ const message = typeof warning === "string" ? warning : warning?.message;
31
+ return typeof message === "string" && message.includes("SQLite is an experimental feature");
32
+ }
33
+
34
+ // src/lib/db/engine.ts
35
+ var SQLITE_BUSY = 5;
36
+ var SQLITE_BUSY_SNAPSHOT = 261;
7
37
  var BUSY_RETRY_ATTEMPTS = 5;
8
38
  var BUSY_RETRY_BACKOFF_MS = 50;
9
- function isBusyError(e) {
10
- if (e instanceof BetterSqlite3.SqliteError) {
11
- return e.code === "SQLITE_BUSY" || e.code === "SQLITE_BUSY_SNAPSHOT";
39
+ var savepointName = (depth) => `ocr_sp_${depth}`;
40
+ var nodeRequire = createRequire(import.meta.url);
41
+ var _preconditionsApplied = false;
42
+ function applyEnginePreconditions() {
43
+ if (_preconditionsApplied) return;
44
+ _preconditionsApplied = true;
45
+ const originalEmitWarning = process.emitWarning.bind(process);
46
+ process.emitWarning = (warning, ...args) => {
47
+ if (isSuppressibleSqliteWarning(warning)) return;
48
+ originalEmitWarning(warning, ...args);
49
+ };
50
+ }
51
+ var _DatabaseSyncCtor;
52
+ function newDatabase(path) {
53
+ if (!_DatabaseSyncCtor) {
54
+ applyEnginePreconditions();
55
+ try {
56
+ _DatabaseSyncCtor = nodeRequire("node:sqlite").DatabaseSync;
57
+ } catch (e) {
58
+ if (!isSupportedNode(process.versions.node)) {
59
+ throw new Error(nodeVersionGuardMessage(process.versions.node).trim());
60
+ }
61
+ throw e;
62
+ }
12
63
  }
13
- const code = e?.code;
14
- return code === "SQLITE_BUSY" || code === "SQLITE_BUSY_SNAPSHOT";
64
+ return new _DatabaseSyncCtor(path);
65
+ }
66
+ function isBusyError(e) {
67
+ const errcode = e?.errcode;
68
+ return errcode === SQLITE_BUSY || errcode === SQLITE_BUSY_SNAPSHOT;
15
69
  }
70
+ var SLEEP_BUF = new Int32Array(new SharedArrayBuffer(4));
16
71
  function sleepSync(ms) {
17
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
72
+ Atomics.wait(SLEEP_BUF, 0, 0, ms);
18
73
  }
19
- var BetterSqliteAdapter = class {
74
+ var NodeSqliteAdapter = class {
20
75
  raw;
76
+ /**
77
+ * Transaction nesting depth. `node:sqlite` has no transaction helper, so we
78
+ * drive `BEGIN IMMEDIATE` ourselves and use SAVEPOINTs for nested calls
79
+ * (better-sqlite3 did this automatically). 0 = no transaction open.
80
+ */
81
+ txnDepth = 0;
21
82
  constructor(db) {
22
83
  this.raw = db;
23
84
  }
24
85
  exec(sql, params) {
25
86
  const stmt = this.raw.prepare(sql);
26
- if (!stmt.reader) {
87
+ const cols = stmt.columns();
88
+ if (cols.length === 0) {
27
89
  stmt.run(...params ?? []);
28
90
  return [];
29
91
  }
30
- const columns = stmt.columns().map((c) => c.name);
31
- const values = stmt.raw().all(...params ?? []);
32
- return values.length > 0 ? [{ columns, values }] : [];
92
+ stmt.setReturnArrays(true);
93
+ const values = stmt.all(...params ?? []);
94
+ return values.length > 0 ? [{ columns: cols.map((c) => c.name), values }] : [];
33
95
  }
34
96
  run(sql, params) {
35
97
  if (params !== void 0) {
@@ -42,31 +104,90 @@ var BetterSqliteAdapter = class {
42
104
  return this.raw.prepare(sql);
43
105
  }
44
106
  transaction(fn) {
45
- const tx = this.raw.transaction(fn);
46
- for (let attempt = 0; ; attempt++) {
107
+ return this.txnDepth > 0 ? this.runNested(fn) : this.runOuter(fn);
108
+ }
109
+ /**
110
+ * Nested call: a SAVEPOINT within the outer transaction's write lock. No
111
+ * busy-retry — the outer transaction already holds the lock. The savepoint
112
+ * lets the inner block roll back independently while the outer continues.
113
+ */
114
+ runNested(fn) {
115
+ const name = savepointName(this.txnDepth);
116
+ this.raw.exec(`SAVEPOINT ${name}`);
117
+ this.txnDepth++;
118
+ try {
119
+ const result = fn();
120
+ this.raw.exec(`RELEASE ${name}`);
121
+ return result;
122
+ } catch (e) {
123
+ try {
124
+ this.raw.exec(`ROLLBACK TO ${name}`);
125
+ this.raw.exec(`RELEASE ${name}`);
126
+ } catch {
127
+ }
128
+ throw e;
129
+ } finally {
130
+ this.txnDepth--;
131
+ }
132
+ }
133
+ /**
134
+ * Outer transaction: `BEGIN IMMEDIATE` acquires the write lock up front so
135
+ * cross-process writers serialize cleanly under WAL instead of failing late
136
+ * on upgrade. `busy_timeout` covers most contention; a bounded synchronous
137
+ * retry absorbs the residual SQLITE_BUSY (another connection holds the lock
138
+ * past the timeout, or BUSY_SNAPSHOT). Non-busy errors and the final attempt
139
+ * re-throw so genuine failures propagate.
140
+ */
141
+ runOuter(fn) {
142
+ for (let attempt = 0; attempt < BUSY_RETRY_ATTEMPTS; attempt++) {
47
143
  try {
48
- return tx.immediate();
144
+ return this.runOnce(fn);
49
145
  } catch (e) {
50
- if (!isBusyError(e) || attempt >= BUSY_RETRY_ATTEMPTS - 1) throw e;
146
+ if (!isBusyError(e) || attempt === BUSY_RETRY_ATTEMPTS - 1) throw e;
51
147
  sleepSync(BUSY_RETRY_BACKOFF_MS);
52
148
  }
53
149
  }
150
+ throw new Error("transaction retry budget exhausted");
151
+ }
152
+ /** One `BEGIN IMMEDIATE` / `COMMIT` / `ROLLBACK` lifecycle. */
153
+ runOnce(fn) {
154
+ this.raw.exec("BEGIN IMMEDIATE");
155
+ this.txnDepth = 1;
156
+ try {
157
+ const result = fn();
158
+ this.raw.exec("COMMIT");
159
+ return result;
160
+ } catch (e) {
161
+ try {
162
+ this.raw.exec("ROLLBACK");
163
+ } catch {
164
+ }
165
+ throw e;
166
+ } finally {
167
+ this.txnDepth = 0;
168
+ }
54
169
  }
55
170
  pragma(source) {
56
- return this.raw.pragma(source);
171
+ this.raw.exec(`PRAGMA ${source}`);
172
+ return void 0;
57
173
  }
58
174
  close() {
59
175
  try {
60
- this.raw.pragma("wal_checkpoint(TRUNCATE)");
176
+ this.raw.exec("PRAGMA wal_checkpoint(TRUNCATE)");
61
177
  } catch {
62
178
  }
63
- this.raw.close();
179
+ try {
180
+ this.raw.close();
181
+ } catch (e) {
182
+ const message = e?.message ?? "";
183
+ if (!/database is not open/i.test(message)) throw e;
184
+ }
64
185
  }
65
186
  };
66
187
  function probeEngine() {
67
188
  try {
68
- const db = new BetterSqlite3(":memory:");
69
- db.pragma("journal_mode = WAL");
189
+ const db = newDatabase(":memory:");
190
+ db.exec("PRAGMA journal_mode = WAL");
70
191
  db.exec("CREATE TABLE _probe(x); INSERT INTO _probe VALUES (1);");
71
192
  const row = db.prepare("SELECT sqlite_version() AS v").get();
72
193
  db.close();
@@ -76,12 +197,12 @@ function probeEngine() {
76
197
  }
77
198
  }
78
199
  function openEngine(dbPath) {
79
- const native = new BetterSqlite3(dbPath);
80
- native.pragma("journal_mode = WAL");
81
- native.pragma("foreign_keys = ON");
82
- native.pragma("busy_timeout = 5000");
83
- native.pragma("synchronous = NORMAL");
84
- return new BetterSqliteAdapter(native);
200
+ const native = newDatabase(dbPath);
201
+ native.exec("PRAGMA journal_mode = WAL");
202
+ native.exec("PRAGMA foreign_keys = ON");
203
+ native.exec("PRAGMA busy_timeout = 5000");
204
+ native.exec("PRAGMA synchronous = NORMAL");
205
+ return new NodeSqliteAdapter(native);
85
206
  }
86
207
 
87
208
  // src/lib/db/migrations.ts
@@ -548,6 +669,35 @@ var MIGRATIONS = [
548
669
  db.run("DROP INDEX IF EXISTS idx_command_executions_parent;");
549
670
  db.run("ALTER TABLE command_executions DROP COLUMN parent_id;");
550
671
  }
672
+ },
673
+ {
674
+ version: 14,
675
+ description: "Self-heal markdown_artifacts duplication: collapse NULL-round duplicate rows and add a NULL-safe unique index so the dedup bug cannot recur",
676
+ // The table's `UNIQUE(session_id, artifact_type, round_number, file_path)`
677
+ // never deduped session-level artifacts because SQLite treats NULL ≠ NULL,
678
+ // and the writer used `INSERT OR REPLACE` — so every re-parse of a
679
+ // NULL-round artifact (context.md, map.md, …) appended a duplicate (one
680
+ // context.md reached 775 identical rows, ~177 MB). The writer is now an
681
+ // explicit UPDATE-or-INSERT; this migration heals existing DBs and adds a
682
+ // NULL-collapsing unique index as a DB-level backstop.
683
+ //
684
+ // Orphan-row sweep (FK-dangling children from the pre-FK-enforcement era)
685
+ // is intentionally NOT done here — it needs `PRAGMA foreign_keys = OFF`,
686
+ // which is a no-op inside the migration transaction. `ocr db doctor --fix`
687
+ // performs it outside a transaction.
688
+ run: (db) => {
689
+ db.run(`
690
+ DELETE FROM markdown_artifacts
691
+ WHERE rowid NOT IN (
692
+ SELECT MAX(rowid) FROM markdown_artifacts
693
+ GROUP BY session_id, artifact_type, IFNULL(round_number, -1), file_path
694
+ )
695
+ `);
696
+ db.run(`
697
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_markdown_artifacts_logical
698
+ ON markdown_artifacts(session_id, artifact_type, IFNULL(round_number, -1), file_path)
699
+ `);
700
+ }
551
701
  }
552
702
  ];
553
703
  function columnExists(db, table, column) {
@@ -908,6 +1058,7 @@ var StateError = class extends Error {
908
1058
  var CANCELLED_EXIT_CODE = -2;
909
1059
  var ORPHAN_EXIT_CODE = -3;
910
1060
  var CASCADE_CLOSE_EXIT_CODE = -4;
1061
+ var WATCHDOG_DEADLINE_EXIT_CODE = -5;
911
1062
 
912
1063
  // src/lib/db/agent-sessions.ts
913
1064
  var NOTE_ORPHAN_PREFIX = "orphaned by liveness sweep";
@@ -1278,9 +1429,429 @@ function sweepStaleSessions(db, thresholdSeconds) {
1278
1429
  return { closedSessionIds: rows.map((r) => r.id) };
1279
1430
  }
1280
1431
 
1432
+ // src/lib/db/maintenance.ts
1433
+ import {
1434
+ existsSync as existsSync2,
1435
+ readdirSync,
1436
+ statSync,
1437
+ unlinkSync,
1438
+ copyFileSync
1439
+ } from "node:fs";
1440
+ import { dirname as dirname2, join as join2, basename } from "node:path";
1441
+
1442
+ // ../shared/platform/src/index.ts
1443
+ import {
1444
+ execFile,
1445
+ execFileSync,
1446
+ spawn
1447
+ } from "node:child_process";
1448
+ import { promisify } from "node:util";
1449
+ var execFilePromise = promisify(execFile);
1450
+ var isWindows = process.platform === "win32";
1451
+ function isProcessAlive(pid) {
1452
+ try {
1453
+ process.kill(pid, 0);
1454
+ return true;
1455
+ } catch (err) {
1456
+ return !(err instanceof Error && "code" in err && err.code === "ESRCH");
1457
+ }
1458
+ }
1459
+
1460
+ // src/lib/db/maintenance.ts
1461
+ var PROTECTED_TABLES = /* @__PURE__ */ new Set([
1462
+ "sessions",
1463
+ "orchestration_events",
1464
+ "agent_sessions",
1465
+ "command_executions",
1466
+ "schema_version"
1467
+ ]);
1468
+ var ORPHAN_SWEEPS = [
1469
+ // session-rooted parents first
1470
+ {
1471
+ table: "review_rounds",
1472
+ sql: "DELETE FROM review_rounds WHERE session_id NOT IN (SELECT id FROM sessions)"
1473
+ },
1474
+ {
1475
+ table: "map_runs",
1476
+ sql: "DELETE FROM map_runs WHERE session_id NOT IN (SELECT id FROM sessions)"
1477
+ },
1478
+ {
1479
+ table: "markdown_artifacts",
1480
+ sql: "DELETE FROM markdown_artifacts WHERE session_id NOT IN (SELECT id FROM sessions)"
1481
+ },
1482
+ {
1483
+ table: "chat_conversations",
1484
+ sql: "DELETE FROM chat_conversations WHERE session_id NOT IN (SELECT id FROM sessions)"
1485
+ },
1486
+ // second level (pick up parents deleted above)
1487
+ {
1488
+ table: "reviewer_outputs",
1489
+ sql: "DELETE FROM reviewer_outputs WHERE round_id NOT IN (SELECT id FROM review_rounds)"
1490
+ },
1491
+ {
1492
+ table: "map_sections",
1493
+ sql: "DELETE FROM map_sections WHERE map_run_id NOT IN (SELECT id FROM map_runs)"
1494
+ },
1495
+ {
1496
+ table: "chat_messages",
1497
+ sql: "DELETE FROM chat_messages WHERE conversation_id NOT IN (SELECT id FROM chat_conversations)"
1498
+ },
1499
+ {
1500
+ table: "user_round_progress",
1501
+ sql: "DELETE FROM user_round_progress WHERE round_id NOT IN (SELECT id FROM review_rounds)"
1502
+ },
1503
+ // third level
1504
+ {
1505
+ table: "review_findings",
1506
+ sql: "DELETE FROM review_findings WHERE reviewer_output_id NOT IN (SELECT id FROM reviewer_outputs)"
1507
+ },
1508
+ {
1509
+ table: "map_files",
1510
+ sql: "DELETE FROM map_files WHERE section_id NOT IN (SELECT id FROM map_sections)"
1511
+ },
1512
+ // leaves
1513
+ {
1514
+ table: "user_finding_progress",
1515
+ sql: "DELETE FROM user_finding_progress WHERE finding_id NOT IN (SELECT id FROM review_findings)"
1516
+ },
1517
+ {
1518
+ table: "user_file_progress",
1519
+ sql: "DELETE FROM user_file_progress WHERE map_file_id NOT IN (SELECT id FROM map_files)"
1520
+ }
1521
+ ];
1522
+ var MARKDOWN_DEDUP_SQL = `
1523
+ DELETE FROM markdown_artifacts
1524
+ WHERE rowid NOT IN (
1525
+ SELECT MAX(rowid) FROM markdown_artifacts
1526
+ GROUP BY session_id, artifact_type, IFNULL(round_number, -1), file_path
1527
+ )`;
1528
+ var ONE_HOUR_MS = 60 * 60 * 1e3;
1529
+ function withForeignKeysDisabled(db, fn) {
1530
+ db.pragma("foreign_keys = OFF");
1531
+ try {
1532
+ return fn();
1533
+ } finally {
1534
+ db.pragma("foreign_keys = ON");
1535
+ }
1536
+ }
1537
+ function scalarInt(db, sql) {
1538
+ const r = db.exec(sql);
1539
+ const v = r[0]?.values[0]?.[0];
1540
+ return typeof v === "number" ? v : Number(v ?? 0);
1541
+ }
1542
+ function foreignKeyViolationGroups(db) {
1543
+ const r = db.exec("PRAGMA foreign_key_check");
1544
+ const rows = r[0]?.values ?? [];
1545
+ const counts = /* @__PURE__ */ new Map();
1546
+ for (const row of rows) {
1547
+ const table = String(row[0]);
1548
+ counts.set(table, (counts.get(table) ?? 0) + 1);
1549
+ }
1550
+ return [...counts.entries()].map(([table, count]) => ({ table, count })).sort((a, b) => b.count - a.count);
1551
+ }
1552
+ function scanOrphanTempFiles(dataDir) {
1553
+ let entries;
1554
+ try {
1555
+ entries = readdirSync(dataDir);
1556
+ } catch {
1557
+ return [];
1558
+ }
1559
+ const out = [];
1560
+ for (const name of entries) {
1561
+ const m = name.match(/^ocr\.db\.(\d+)\.tmp$/);
1562
+ if (!m) continue;
1563
+ const pid = Number(m[1]);
1564
+ let ageMs = 0;
1565
+ try {
1566
+ ageMs = Date.now() - statSync(join2(dataDir, name)).mtimeMs;
1567
+ } catch {
1568
+ continue;
1569
+ }
1570
+ const alive = isProcessAlive(pid);
1571
+ out.push({
1572
+ name,
1573
+ pid,
1574
+ ageMs,
1575
+ // Reapable only when the writer PID is dead AND the file is old enough
1576
+ // that no live mid-write could plausibly own it.
1577
+ reapable: !alive && ageMs > ONE_HOUR_MS
1578
+ });
1579
+ }
1580
+ return out;
1581
+ }
1582
+ function scanBackupFiles(dataDir, dbBase) {
1583
+ let entries;
1584
+ try {
1585
+ entries = readdirSync(dataDir);
1586
+ } catch {
1587
+ return [];
1588
+ }
1589
+ const out = [];
1590
+ for (const name of entries) {
1591
+ if (!name.startsWith(`${dbBase}.bak`)) continue;
1592
+ try {
1593
+ out.push({ name, sizeBytes: statSync(join2(dataDir, name)).size });
1594
+ } catch {
1595
+ }
1596
+ }
1597
+ return out.sort((a, b) => b.sizeBytes - a.sizeBytes);
1598
+ }
1599
+ function collectDbHealth(db, dbPath) {
1600
+ const dataDir = dirname2(dbPath);
1601
+ const dbBase = basename(dbPath);
1602
+ const pageSize = scalarInt(db, "PRAGMA page_size");
1603
+ const pageCount = scalarInt(db, "PRAGMA page_count");
1604
+ const freelistCount = scalarInt(db, "PRAGMA freelist_count");
1605
+ const integ = db.exec("PRAGMA integrity_check");
1606
+ const integRows = (integ[0]?.values ?? []).map((v) => String(v[0]));
1607
+ const integrityOk = integRows.length === 1 && integRows[0] === "ok";
1608
+ const allGroups = foreignKeyViolationGroups(db);
1609
+ const fkViolations = allGroups.filter((g) => !PROTECTED_TABLES.has(g.table));
1610
+ const protectedFkViolations = allGroups.filter(
1611
+ (g) => PROTECTED_TABLES.has(g.table)
1612
+ );
1613
+ const fileSizeBytes = existsSync2(dbPath) ? statSync(dbPath).size : 0;
1614
+ return {
1615
+ dbPath,
1616
+ fileSizeBytes,
1617
+ pageSize,
1618
+ pageCount,
1619
+ freelistCount,
1620
+ reclaimableBytes: freelistCount * pageSize,
1621
+ integrityOk,
1622
+ integrityErrors: integrityOk ? [] : integRows,
1623
+ fkViolations,
1624
+ protectedFkViolations,
1625
+ totalFkViolations: allGroups.reduce((n, g) => n + g.count, 0),
1626
+ markdownDuplicateRows: scalarInt(
1627
+ db,
1628
+ `SELECT COALESCE(SUM(cnt - 1), 0) FROM (
1629
+ SELECT COUNT(*) AS cnt FROM markdown_artifacts
1630
+ GROUP BY session_id, artifact_type, IFNULL(round_number, -1), file_path
1631
+ HAVING cnt > 1)`
1632
+ ),
1633
+ orphanTempFiles: scanOrphanTempFiles(dataDir),
1634
+ backupFiles: scanBackupFiles(dataDir, dbBase),
1635
+ eventCount: scalarInt(db, "SELECT COUNT(*) FROM orchestration_events"),
1636
+ sessionCount: scalarInt(db, "SELECT COUNT(*) FROM sessions")
1637
+ };
1638
+ }
1639
+ function snapshotDb(db, dbPath, label = "doctor") {
1640
+ try {
1641
+ if (!existsSync2(dbPath) || statSync(dbPath).size === 0) return null;
1642
+ db.pragma("wal_checkpoint(TRUNCATE)");
1643
+ const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
1644
+ const bakPath = `${dbPath}.bak.${label}.${ts}`;
1645
+ copyFileSync(dbPath, bakPath);
1646
+ return bakPath;
1647
+ } catch {
1648
+ return null;
1649
+ }
1650
+ }
1651
+ function reapOrphanDbFiles(dataDir) {
1652
+ const reaped = [];
1653
+ for (const f of scanOrphanTempFiles(dataDir)) {
1654
+ if (!f.reapable) continue;
1655
+ try {
1656
+ unlinkSync(join2(dataDir, f.name));
1657
+ reaped.push(f.name);
1658
+ } catch {
1659
+ }
1660
+ }
1661
+ return reaped;
1662
+ }
1663
+ var SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1e3;
1664
+ function reapStaleExecLogs(execLogsDir, maxAgeMs = SEVEN_DAYS_MS) {
1665
+ let entries;
1666
+ try {
1667
+ entries = readdirSync(execLogsDir);
1668
+ } catch {
1669
+ return [];
1670
+ }
1671
+ const cutoff = Date.now() - maxAgeMs;
1672
+ const reaped = [];
1673
+ for (const name of entries) {
1674
+ if (!name.endsWith(".log")) continue;
1675
+ const full = join2(execLogsDir, name);
1676
+ try {
1677
+ if (statSync(full).mtimeMs > cutoff) continue;
1678
+ unlinkSync(full);
1679
+ reaped.push(name);
1680
+ } catch {
1681
+ }
1682
+ }
1683
+ return reaped;
1684
+ }
1685
+ function pruneBackups(dataDir, dbPath, opts = {}) {
1686
+ const keep = opts.keep ?? 1;
1687
+ if (!Number.isInteger(keep) || keep < 0) {
1688
+ throw new Error(
1689
+ `pruneBackups: keep must be a non-negative integer (got ${String(keep)})`
1690
+ );
1691
+ }
1692
+ const dryRun = opts.dryRun ?? false;
1693
+ const dbBase = basename(dbPath);
1694
+ const withMtime = [];
1695
+ for (const file of scanBackupFiles(dataDir, dbBase)) {
1696
+ try {
1697
+ withMtime.push({ file, mtimeMs: statSync(join2(dataDir, file.name)).mtimeMs });
1698
+ } catch {
1699
+ }
1700
+ }
1701
+ withMtime.sort((a, b) => b.mtimeMs - a.mtimeMs);
1702
+ const kept = withMtime.slice(0, keep).map((x) => x.file);
1703
+ const toDelete = withMtime.slice(keep).map((x) => x.file);
1704
+ const deleted = [];
1705
+ if (!dryRun) {
1706
+ for (const b of toDelete) {
1707
+ try {
1708
+ unlinkSync(join2(dataDir, b.name));
1709
+ deleted.push(b);
1710
+ } catch {
1711
+ }
1712
+ }
1713
+ }
1714
+ const reported = dryRun ? toDelete : deleted;
1715
+ return {
1716
+ dryRun,
1717
+ deleted: reported,
1718
+ kept,
1719
+ reclaimedBytes: reported.reduce((n, b) => n + b.sizeBytes, 0)
1720
+ };
1721
+ }
1722
+ function fixDb(db, dbPath, opts = {}) {
1723
+ const dataDir = dirname2(dbPath);
1724
+ const sizeBeforeBytes = existsSync2(dbPath) ? statSync(dbPath).size : 0;
1725
+ const snapshotPath = opts.snapshot === false ? null : snapshotDb(db, dbPath, "doctor");
1726
+ const fkOrphansDeleted = [];
1727
+ withForeignKeysDisabled(db, () => {
1728
+ db.transaction(() => {
1729
+ for (const sweep of ORPHAN_SWEEPS) {
1730
+ const info = db.prepare(sweep.sql).run();
1731
+ const count = Number(info.changes);
1732
+ if (count > 0) fkOrphansDeleted.push({ table: sweep.table, count });
1733
+ }
1734
+ });
1735
+ });
1736
+ let markdownDupsDeleted = 0;
1737
+ db.transaction(() => {
1738
+ const info = db.prepare(MARKDOWN_DEDUP_SQL).run();
1739
+ markdownDupsDeleted = Number(info.changes);
1740
+ });
1741
+ const tempsReaped = opts.reapTemps === false ? [] : reapOrphanDbFiles(dataDir);
1742
+ let vacuumed = false;
1743
+ if (opts.vacuum !== false) {
1744
+ try {
1745
+ db.pragma("wal_checkpoint(TRUNCATE)");
1746
+ db.run("VACUUM");
1747
+ vacuumed = true;
1748
+ } catch {
1749
+ vacuumed = false;
1750
+ }
1751
+ }
1752
+ const post = collectDbHealth(db, dbPath);
1753
+ return {
1754
+ snapshotPath,
1755
+ fkOrphansDeleted,
1756
+ totalFkOrphansDeleted: fkOrphansDeleted.reduce((n, g) => n + g.count, 0),
1757
+ protectedViolationsRemaining: post.protectedFkViolations,
1758
+ markdownDupsDeleted,
1759
+ tempsReaped,
1760
+ vacuumed,
1761
+ sizeBeforeBytes,
1762
+ sizeAfterBytes: post.fileSizeBytes,
1763
+ integrityOkAfter: post.integrityOk,
1764
+ fkViolationsAfter: post.totalFkViolations
1765
+ };
1766
+ }
1767
+ function vacuumDb(db, dbPath, opts = {}) {
1768
+ const sizeBeforeBytes = existsSync2(dbPath) ? statSync(dbPath).size : 0;
1769
+ const snapshotPath = opts.snapshot === false ? null : snapshotDb(db, dbPath, "vacuum");
1770
+ db.pragma("wal_checkpoint(TRUNCATE)");
1771
+ db.run("VACUUM");
1772
+ db.pragma("wal_checkpoint(TRUNCATE)");
1773
+ const sizeAfterBytes = existsSync2(dbPath) ? statSync(dbPath).size : 0;
1774
+ return {
1775
+ snapshotPath,
1776
+ sizeBeforeBytes,
1777
+ sizeAfterBytes,
1778
+ reclaimedBytes: Math.max(0, sizeBeforeBytes - sizeAfterBytes)
1779
+ };
1780
+ }
1781
+ function countSessionArtifacts(db, sessionId) {
1782
+ const r = db.exec(
1783
+ `SELECT
1784
+ (SELECT COUNT(*) FROM markdown_artifacts WHERE session_id = ?) +
1785
+ (SELECT COUNT(*) FROM review_rounds WHERE session_id = ?) +
1786
+ (SELECT COUNT(*) FROM reviewer_outputs ro JOIN review_rounds rr ON ro.round_id = rr.id WHERE rr.session_id = ?) +
1787
+ (SELECT COUNT(*) FROM review_findings rf JOIN reviewer_outputs ro ON rf.reviewer_output_id = ro.id JOIN review_rounds rr ON ro.round_id = rr.id WHERE rr.session_id = ?) +
1788
+ (SELECT COUNT(*) FROM map_runs WHERE session_id = ?) +
1789
+ (SELECT COUNT(*) FROM chat_conversations WHERE session_id = ?)`,
1790
+ Array(6).fill(sessionId)
1791
+ );
1792
+ const v = r[0]?.values[0]?.[0];
1793
+ return typeof v === "number" ? v : Number(v ?? 0);
1794
+ }
1795
+ function pruneDb(db, dbPath, opts = {}) {
1796
+ const dryRun = opts.dryRun ?? false;
1797
+ const hasBound = opts.olderThanDays !== void 0 || opts.keepSessions !== void 0;
1798
+ if (!hasBound) {
1799
+ return { dryRun, snapshotPath: null, prunedSessions: [], totalArtifactRows: 0 };
1800
+ }
1801
+ const rows = db.exec(
1802
+ `SELECT s.id,
1803
+ (SELECT (julianday('now') - julianday(MAX(e.created_at))) * 86400
1804
+ FROM orchestration_events e WHERE e.session_id = s.id) AS quiet_seconds
1805
+ FROM sessions s
1806
+ WHERE s.status = 'closed'
1807
+ ORDER BY quiet_seconds ASC`
1808
+ );
1809
+ const closed = (rows[0]?.values ?? []).map((v) => ({
1810
+ id: String(v[0]),
1811
+ quietSeconds: typeof v[1] === "number" ? v[1] : Number(v[1] ?? 0)
1812
+ }));
1813
+ const keepN = opts.keepSessions ?? 0;
1814
+ const olderThanSeconds = opts.olderThanDays !== void 0 ? opts.olderThanDays * 86400 : null;
1815
+ const targets = closed.filter((s, idx) => {
1816
+ if (idx < keepN) return false;
1817
+ if (olderThanSeconds !== null && s.quietSeconds < olderThanSeconds)
1818
+ return false;
1819
+ return true;
1820
+ });
1821
+ const prunedSessions = [];
1822
+ for (const t of targets) {
1823
+ const artifactRows = countSessionArtifacts(db, t.id);
1824
+ if (artifactRows === 0) continue;
1825
+ prunedSessions.push({ sessionId: t.id, artifactRows });
1826
+ }
1827
+ if (dryRun || prunedSessions.length === 0) {
1828
+ return {
1829
+ dryRun,
1830
+ snapshotPath: null,
1831
+ prunedSessions,
1832
+ totalArtifactRows: prunedSessions.reduce((n, p) => n + p.artifactRows, 0)
1833
+ };
1834
+ }
1835
+ const snapshotPath = snapshotDb(db, dbPath, "prune");
1836
+ db.transaction(() => {
1837
+ for (const p of prunedSessions) {
1838
+ db.run("DELETE FROM review_rounds WHERE session_id = ?", [p.sessionId]);
1839
+ db.run("DELETE FROM map_runs WHERE session_id = ?", [p.sessionId]);
1840
+ db.run("DELETE FROM markdown_artifacts WHERE session_id = ?", [p.sessionId]);
1841
+ db.run("DELETE FROM chat_conversations WHERE session_id = ?", [p.sessionId]);
1842
+ }
1843
+ });
1844
+ return {
1845
+ dryRun,
1846
+ snapshotPath,
1847
+ prunedSessions,
1848
+ totalArtifactRows: prunedSessions.reduce((n, p) => n + p.artifactRows, 0)
1849
+ };
1850
+ }
1851
+
1281
1852
  // src/lib/db/command-log.ts
1282
- import { appendFileSync, existsSync as existsSync2, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
1283
- import { dirname as dirname2, join as join2 } from "node:path";
1853
+ import { appendFileSync, existsSync as existsSync3, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
1854
+ import { dirname as dirname3, join as join3 } from "node:path";
1284
1855
  import { randomUUID } from "node:crypto";
1285
1856
  var CACHE_DIR = ".cache";
1286
1857
  var FILENAME = "command-history.jsonl";
@@ -1291,16 +1862,16 @@ function generateCommandUid() {
1291
1862
  return randomUUID();
1292
1863
  }
1293
1864
  function cacheDir(ocrDir) {
1294
- return join2(ocrDir, "data", CACHE_DIR);
1865
+ return join3(ocrDir, "data", CACHE_DIR);
1295
1866
  }
1296
1867
  function commandLogPath(ocrDir) {
1297
- return join2(cacheDir(ocrDir), FILENAME);
1868
+ return join3(cacheDir(ocrDir), FILENAME);
1298
1869
  }
1299
1870
  function appendCommandLog(ocrDir, entry) {
1300
1871
  try {
1301
1872
  const filePath = commandLogPath(ocrDir);
1302
- const dir = dirname2(filePath);
1303
- if (!existsSync2(dir)) mkdirSync(dir, { recursive: true });
1873
+ const dir = dirname3(filePath);
1874
+ if (!existsSync3(dir)) mkdirSync(dir, { recursive: true });
1304
1875
  const line = JSON.stringify(entry) + "\n";
1305
1876
  appendFileSync(filePath, line, { encoding: "utf-8" });
1306
1877
  if (approxLineCount >= 0) approxLineCount++;
@@ -1310,7 +1881,7 @@ function appendCommandLog(ocrDir, entry) {
1310
1881
  }
1311
1882
  function readCommandLog(ocrDir) {
1312
1883
  const filePath = commandLogPath(ocrDir);
1313
- if (!existsSync2(filePath)) return [];
1884
+ if (!existsSync3(filePath)) return [];
1314
1885
  const content = readFileSync(filePath, "utf-8");
1315
1886
  const entries = [];
1316
1887
  for (const line of content.split("\n")) {
@@ -1380,11 +1951,11 @@ var V2_SCHEMA_VERSION = 12;
1380
1951
  function maybeSnapshotBeforeUpgrade(db, dbPath, fromVersion) {
1381
1952
  if (fromVersion < 1 || fromVersion >= V2_SCHEMA_VERSION) return null;
1382
1953
  const bakPath = `${dbPath}.bak.v${fromVersion}`;
1383
- if (existsSync3(bakPath)) return bakPath;
1954
+ if (existsSync4(bakPath)) return bakPath;
1384
1955
  try {
1385
- if (!existsSync3(dbPath) || statSync(dbPath).size === 0) return null;
1956
+ if (!existsSync4(dbPath) || statSync2(dbPath).size === 0) return null;
1386
1957
  db.pragma("wal_checkpoint(TRUNCATE)");
1387
- copyFileSync(dbPath, bakPath);
1958
+ copyFileSync2(dbPath, bakPath);
1388
1959
  return bakPath;
1389
1960
  } catch {
1390
1961
  return null;
@@ -1418,8 +1989,8 @@ async function openDatabase(dbPath) {
1418
1989
  if (cached) {
1419
1990
  return cached;
1420
1991
  }
1421
- const dir = dirname3(dbPath);
1422
- if (!existsSync3(dir)) {
1992
+ const dir = dirname4(dbPath);
1993
+ if (!existsSync4(dir)) {
1423
1994
  mkdirSync2(dir, { recursive: true });
1424
1995
  }
1425
1996
  const db = openEngine(dbPath);
@@ -1427,15 +1998,15 @@ async function openDatabase(dbPath) {
1427
1998
  return db;
1428
1999
  }
1429
2000
  async function getDb(ocrDir) {
1430
- const dbPath = join3(ocrDir, "data", "ocr.db");
2001
+ const dbPath = join4(ocrDir, "data", "ocr.db");
1431
2002
  return openDatabase(dbPath);
1432
2003
  }
1433
2004
  async function ensureDatabase(ocrDir) {
1434
- const dataDir = join3(ocrDir, "data");
1435
- if (!existsSync3(dataDir)) {
2005
+ const dataDir = join4(ocrDir, "data");
2006
+ if (!existsSync4(dataDir)) {
1436
2007
  mkdirSync2(dataDir, { recursive: true });
1437
2008
  }
1438
- const dbPath = join3(dataDir, "ocr.db");
2009
+ const dbPath = join4(dataDir, "ocr.db");
1439
2010
  const db = await openDatabase(dbPath);
1440
2011
  let before = 0;
1441
2012
  try {
@@ -1463,7 +2034,7 @@ async function ensureDatabase(ocrDir) {
1463
2034
  return db;
1464
2035
  }
1465
2036
  function walCheckpointTruncate(dbPath) {
1466
- if (!existsSync3(dbPath)) {
2037
+ if (!existsSync4(dbPath)) {
1467
2038
  return "skipped";
1468
2039
  }
1469
2040
  const cached = connections.get(dbPath);
@@ -1484,7 +2055,7 @@ function walCheckpointTruncate(dbPath) {
1484
2055
  return "failed";
1485
2056
  } finally {
1486
2057
  try {
1487
- transient?.raw.close();
2058
+ transient?.close();
1488
2059
  } catch {
1489
2060
  }
1490
2061
  }
@@ -1502,6 +2073,41 @@ function closeAllDatabases() {
1502
2073
  connections.delete(path);
1503
2074
  }
1504
2075
  }
2076
+ function probeWrite() {
2077
+ let dir;
2078
+ try {
2079
+ dir = mkdtempSync(join4(tmpdir(), "ocr-probe-"));
2080
+ const db = openEngine(join4(dir, "probe.db"));
2081
+ try {
2082
+ db.run("CREATE TABLE _probe_write (id INTEGER PRIMARY KEY, v TEXT)");
2083
+ db.transaction(() => {
2084
+ db.run("INSERT INTO _probe_write (v) VALUES (?)", ["written-in-txn"]);
2085
+ });
2086
+ const value = db.exec("SELECT v FROM _probe_write")[0]?.values[0]?.[0];
2087
+ if (value !== "written-in-txn") {
2088
+ return { ok: false, error: `unexpected probe value: ${String(value)}` };
2089
+ }
2090
+ return { ok: true };
2091
+ } finally {
2092
+ db.close();
2093
+ }
2094
+ } catch (e) {
2095
+ return { ok: false, error: e instanceof Error ? e.message : String(e) };
2096
+ } finally {
2097
+ if (dir) rmDirBestEffort(dir);
2098
+ }
2099
+ }
2100
+ function rmDirBestEffort(dir) {
2101
+ for (let attempt = 0; attempt < 3; attempt++) {
2102
+ try {
2103
+ rmSync(dir, { recursive: true, force: true });
2104
+ return;
2105
+ } catch {
2106
+ if (attempt === 2) return;
2107
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
2108
+ }
2109
+ }
2110
+ }
1505
2111
  export {
1506
2112
  CANCELLED_EXIT_CODE,
1507
2113
  CASCADE_CLOSE_EXIT_CODE,
@@ -1510,6 +2116,7 @@ export {
1510
2116
  PID_REUSE_GUARD_MS,
1511
2117
  STATE_EXIT,
1512
2118
  StateError,
2119
+ WATCHDOG_DEADLINE_EXIT_CODE,
1513
2120
  appendCommandLog,
1514
2121
  bindVendorSessionIdOpportunistically,
1515
2122
  bumpAgentSessionHeartbeat,
@@ -1517,10 +2124,12 @@ export {
1517
2124
  cascadeTerminateExecutions,
1518
2125
  closeAllDatabases,
1519
2126
  closeDatabase,
2127
+ collectDbHealth,
1520
2128
  commandLogPath,
1521
2129
  commitReasonClose,
1522
2130
  defaultIsAlive,
1523
2131
  ensureDatabase,
2132
+ fixDb,
1524
2133
  formatUpgradeNotice,
1525
2134
  generateCommandUid,
1526
2135
  getAgentSession,
@@ -1532,6 +2141,7 @@ export {
1532
2141
  getLatestEventId,
1533
2142
  getSchemaVersion,
1534
2143
  getSession,
2144
+ hasInFlightDependents,
1535
2145
  insertAgentSession,
1536
2146
  insertEvent,
1537
2147
  insertSession,
@@ -1540,7 +2150,12 @@ export {
1540
2150
  listAgentSessionsForWorkflow,
1541
2151
  openDatabase,
1542
2152
  probeEngine,
2153
+ probeWrite,
2154
+ pruneBackups,
2155
+ pruneDb,
1543
2156
  readCommandLog,
2157
+ reapOrphanDbFiles,
2158
+ reapStaleExecLogs,
1544
2159
  reconcileLegacyState,
1545
2160
  recordVendorSessionIdForExecution,
1546
2161
  replayCommandLog,
@@ -1550,10 +2165,13 @@ export {
1550
2165
  runMigrations,
1551
2166
  setAgentSessionStatus,
1552
2167
  setAgentSessionVendorId,
2168
+ snapshotDb,
1553
2169
  sqliteUtcMs,
1554
2170
  sweepStaleAgentSessions,
1555
2171
  sweepStaleSessions,
1556
2172
  updateAgentSession,
1557
2173
  updateSession,
1558
- walCheckpointTruncate
2174
+ vacuumDb,
2175
+ walCheckpointTruncate,
2176
+ withForeignKeysDisabled
1559
2177
  };