@open-code-review/cli 2.1.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 (60) hide show
  1. package/dist/dashboard/client/assets/{_basePickBy-B3ALyupE.js → _basePickBy-BBPb8BJA.js} +1 -1
  2. package/dist/dashboard/client/assets/{_baseUniq-b2RALAWc.js → _baseUniq-CFHdos6T.js} +1 -1
  3. package/dist/dashboard/client/assets/{arc-DcSVvhUd.js → arc-BKGGWA2F.js} +1 -1
  4. package/dist/dashboard/client/assets/{architectureDiagram-VXUJARFQ-BNUlmSCS.js → architectureDiagram-VXUJARFQ-B_ovNjX1.js} +1 -1
  5. package/dist/dashboard/client/assets/{blockDiagram-VD42YOAC-BmhiQVwa.js → blockDiagram-VD42YOAC-C2M-avVp.js} +1 -1
  6. package/dist/dashboard/client/assets/{c4Diagram-YG6GDRKO-jyJ3WOv5.js → c4Diagram-YG6GDRKO-BtOBpAzH.js} +1 -1
  7. package/dist/dashboard/client/assets/channel-rgw7C1e7.js +1 -0
  8. package/dist/dashboard/client/assets/{chunk-4BX2VUAB-x1dQU_s3.js → chunk-4BX2VUAB-Cz2EbHPl.js} +1 -1
  9. package/dist/dashboard/client/assets/{chunk-55IACEB6-CwbsE2XQ.js → chunk-55IACEB6-C8xpXw9G.js} +1 -1
  10. package/dist/dashboard/client/assets/{chunk-B4BG7PRW-BaE7c-ti.js → chunk-B4BG7PRW-BSRfOovX.js} +1 -1
  11. package/dist/dashboard/client/assets/{chunk-DI55MBZ5-Bw5PUaMK.js → chunk-DI55MBZ5-CEUbYQWn.js} +1 -1
  12. package/dist/dashboard/client/assets/{chunk-FMBD7UC4-B7cF6P3s.js → chunk-FMBD7UC4-5xWP6GRj.js} +1 -1
  13. package/dist/dashboard/client/assets/{chunk-QN33PNHL-OY4evNHd.js → chunk-QN33PNHL-DfNCVcy8.js} +1 -1
  14. package/dist/dashboard/client/assets/{chunk-QZHKN3VN-BpjQwIWz.js → chunk-QZHKN3VN--OdToKKu.js} +1 -1
  15. package/dist/dashboard/client/assets/{chunk-TZMSLE5B-D8b_Oq9B.js → chunk-TZMSLE5B-B_0K0Qso.js} +1 -1
  16. package/dist/dashboard/client/assets/classDiagram-2ON5EDUG-DTGi7d9X.js +1 -0
  17. package/dist/dashboard/client/assets/classDiagram-v2-WZHVMYZB-DTGi7d9X.js +1 -0
  18. package/dist/dashboard/client/assets/clone-Cz7hswqi.js +1 -0
  19. package/dist/dashboard/client/assets/{cose-bilkent-S5V4N54A-C-sfP8PN.js → cose-bilkent-S5V4N54A-Cc_Dmnxz.js} +1 -1
  20. package/dist/dashboard/client/assets/{dagre-6UL2VRFP-Cqfo0NRg.js → dagre-6UL2VRFP-DaAfvUXU.js} +1 -1
  21. package/dist/dashboard/client/assets/{diagram-PSM6KHXK-BR3ppxqI.js → diagram-PSM6KHXK-7idwN0rC.js} +1 -1
  22. package/dist/dashboard/client/assets/{diagram-QEK2KX5R-Dvcx6x3R.js → diagram-QEK2KX5R-D9j9H13n.js} +1 -1
  23. package/dist/dashboard/client/assets/{diagram-S2PKOQOG-DoyBLnVN.js → diagram-S2PKOQOG-SMF5SB0K.js} +1 -1
  24. package/dist/dashboard/client/assets/{erDiagram-Q2GNP2WA-hy77l1cL.js → erDiagram-Q2GNP2WA-EVJ4Qa2F.js} +1 -1
  25. package/dist/dashboard/client/assets/{flowDiagram-NV44I4VS-Bz0B1rKM.js → flowDiagram-NV44I4VS-tZ7SFE77.js} +1 -1
  26. package/dist/dashboard/client/assets/{ganttDiagram-JELNMOA3-CLgrZPoC.js → ganttDiagram-JELNMOA3-DFSqguY7.js} +1 -1
  27. package/dist/dashboard/client/assets/{gitGraphDiagram-V2S2FVAM-DwJ-1f-v.js → gitGraphDiagram-V2S2FVAM-CqHdP3HE.js} +1 -1
  28. package/dist/dashboard/client/assets/{graph-DDBMM_t2.js → graph-C0XnkNkk.js} +1 -1
  29. package/dist/dashboard/client/assets/{index-Cr9yEo_B.js → index-C3NEq704.js} +133 -138
  30. package/dist/dashboard/client/assets/index-CzxeSSaQ.css +1 -0
  31. package/dist/dashboard/client/assets/{infoDiagram-HS3SLOUP-Bhn1FmAk.js → infoDiagram-HS3SLOUP-DlXZo9U2.js} +1 -1
  32. package/dist/dashboard/client/assets/{journeyDiagram-XKPGCS4Q-CzGbjX1y.js → journeyDiagram-XKPGCS4Q-CgC8_7eN.js} +1 -1
  33. package/dist/dashboard/client/assets/{kanban-definition-3W4ZIXB7-Da77-WYk.js → kanban-definition-3W4ZIXB7-BMAw_jNp.js} +1 -1
  34. package/dist/dashboard/client/assets/{layout-CVwSB-GS.js → layout-XjM3Q-ka.js} +1 -1
  35. package/dist/dashboard/client/assets/{linear-CTRAc5Jn.js → linear-CMUrrr1X.js} +1 -1
  36. package/dist/dashboard/client/assets/{mermaid-renderer-Bjo170ax.js → mermaid-renderer-D2jYNs7K.js} +4 -4
  37. package/dist/dashboard/client/assets/{mindmap-definition-VGOIOE7T-B55C2odl.js → mindmap-definition-VGOIOE7T-CL4hv-vg.js} +1 -1
  38. package/dist/dashboard/client/assets/{pieDiagram-ADFJNKIX-5lrQLrSz.js → pieDiagram-ADFJNKIX-DTqv-1h1.js} +1 -1
  39. package/dist/dashboard/client/assets/{quadrantDiagram-AYHSOK5B-Bg55gC30.js → quadrantDiagram-AYHSOK5B-BpFlSW9N.js} +1 -1
  40. package/dist/dashboard/client/assets/{requirementDiagram-UZGBJVZJ-CyR4YFJY.js → requirementDiagram-UZGBJVZJ-BqYqqXL4.js} +1 -1
  41. package/dist/dashboard/client/assets/{sankeyDiagram-TZEHDZUN-BVWKr9_-.js → sankeyDiagram-TZEHDZUN-kEI9kntR.js} +1 -1
  42. package/dist/dashboard/client/assets/{sequenceDiagram-WL72ISMW-D0AJg_tE.js → sequenceDiagram-WL72ISMW-Cnu_1j-N.js} +1 -1
  43. package/dist/dashboard/client/assets/{stateDiagram-FKZM4ZOC-BuHpTgim.js → stateDiagram-FKZM4ZOC-BoC-rqoG.js} +1 -1
  44. package/dist/dashboard/client/assets/stateDiagram-v2-4FDKWEC3-COR3QD3v.js +1 -0
  45. package/dist/dashboard/client/assets/{timeline-definition-IT6M3QCI-LDhpAmDd.js → timeline-definition-IT6M3QCI-CXMWuzDL.js} +1 -1
  46. package/dist/dashboard/client/assets/{treemap-GDKQZRPO-Dd4gjvUl.js → treemap-GDKQZRPO-o9ZFgpbJ.js} +1 -1
  47. package/dist/dashboard/client/assets/{xychartDiagram-PRI3JC2R-B9RDod39.js → xychartDiagram-PRI3JC2R-CfIuUpeA.js} +1 -1
  48. package/dist/dashboard/client/index.html +2 -2
  49. package/dist/dashboard/server.js +1031 -426
  50. package/dist/index.js +1252 -268
  51. package/dist/lib/db/index.js +485 -24
  52. package/dist/lib/runtime-config.js +29 -13
  53. package/dist/lib/state/index.js +2196 -0
  54. package/package.json +8 -2
  55. package/dist/dashboard/client/assets/channel-D3J8-GF_.js +0 -1
  56. package/dist/dashboard/client/assets/classDiagram-2ON5EDUG-tkFUL-1Y.js +0 -1
  57. package/dist/dashboard/client/assets/classDiagram-v2-WZHVMYZB-tkFUL-1Y.js +0 -1
  58. package/dist/dashboard/client/assets/clone-CkY5ajLr.js +0 -1
  59. package/dist/dashboard/client/assets/index-Z1pPudAt.css +0 -1
  60. package/dist/dashboard/client/assets/stateDiagram-v2-4FDKWEC3-DwAPhteN.js +0 -1
@@ -1,14 +1,14 @@
1
1
  // src/lib/db/index.ts
2
2
  import {
3
- existsSync as existsSync3,
3
+ existsSync as existsSync4,
4
4
  mkdirSync as mkdirSync2,
5
- copyFileSync,
6
- statSync,
5
+ copyFileSync as copyFileSync2,
6
+ statSync as statSync2,
7
7
  mkdtempSync,
8
8
  rmSync
9
9
  } from "node:fs";
10
10
  import { tmpdir } from "node:os";
11
- import { dirname as dirname3, join as join3 } from "node:path";
11
+ import { dirname as dirname4, join as join4 } from "node:path";
12
12
 
13
13
  // src/lib/db/engine.ts
14
14
  import { createRequire } from "node:module";
@@ -669,6 +669,35 @@ var MIGRATIONS = [
669
669
  db.run("DROP INDEX IF EXISTS idx_command_executions_parent;");
670
670
  db.run("ALTER TABLE command_executions DROP COLUMN parent_id;");
671
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
+ }
672
701
  }
673
702
  ];
674
703
  function columnExists(db, table, column) {
@@ -1029,6 +1058,7 @@ var StateError = class extends Error {
1029
1058
  var CANCELLED_EXIT_CODE = -2;
1030
1059
  var ORPHAN_EXIT_CODE = -3;
1031
1060
  var CASCADE_CLOSE_EXIT_CODE = -4;
1061
+ var WATCHDOG_DEADLINE_EXIT_CODE = -5;
1032
1062
 
1033
1063
  // src/lib/db/agent-sessions.ts
1034
1064
  var NOTE_ORPHAN_PREFIX = "orphaned by liveness sweep";
@@ -1399,9 +1429,429 @@ function sweepStaleSessions(db, thresholdSeconds) {
1399
1429
  return { closedSessionIds: rows.map((r) => r.id) };
1400
1430
  }
1401
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
+
1402
1852
  // src/lib/db/command-log.ts
1403
- import { appendFileSync, existsSync as existsSync2, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
1404
- 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";
1405
1855
  import { randomUUID } from "node:crypto";
1406
1856
  var CACHE_DIR = ".cache";
1407
1857
  var FILENAME = "command-history.jsonl";
@@ -1412,16 +1862,16 @@ function generateCommandUid() {
1412
1862
  return randomUUID();
1413
1863
  }
1414
1864
  function cacheDir(ocrDir) {
1415
- return join2(ocrDir, "data", CACHE_DIR);
1865
+ return join3(ocrDir, "data", CACHE_DIR);
1416
1866
  }
1417
1867
  function commandLogPath(ocrDir) {
1418
- return join2(cacheDir(ocrDir), FILENAME);
1868
+ return join3(cacheDir(ocrDir), FILENAME);
1419
1869
  }
1420
1870
  function appendCommandLog(ocrDir, entry) {
1421
1871
  try {
1422
1872
  const filePath = commandLogPath(ocrDir);
1423
- const dir = dirname2(filePath);
1424
- if (!existsSync2(dir)) mkdirSync(dir, { recursive: true });
1873
+ const dir = dirname3(filePath);
1874
+ if (!existsSync3(dir)) mkdirSync(dir, { recursive: true });
1425
1875
  const line = JSON.stringify(entry) + "\n";
1426
1876
  appendFileSync(filePath, line, { encoding: "utf-8" });
1427
1877
  if (approxLineCount >= 0) approxLineCount++;
@@ -1431,7 +1881,7 @@ function appendCommandLog(ocrDir, entry) {
1431
1881
  }
1432
1882
  function readCommandLog(ocrDir) {
1433
1883
  const filePath = commandLogPath(ocrDir);
1434
- if (!existsSync2(filePath)) return [];
1884
+ if (!existsSync3(filePath)) return [];
1435
1885
  const content = readFileSync(filePath, "utf-8");
1436
1886
  const entries = [];
1437
1887
  for (const line of content.split("\n")) {
@@ -1501,11 +1951,11 @@ var V2_SCHEMA_VERSION = 12;
1501
1951
  function maybeSnapshotBeforeUpgrade(db, dbPath, fromVersion) {
1502
1952
  if (fromVersion < 1 || fromVersion >= V2_SCHEMA_VERSION) return null;
1503
1953
  const bakPath = `${dbPath}.bak.v${fromVersion}`;
1504
- if (existsSync3(bakPath)) return bakPath;
1954
+ if (existsSync4(bakPath)) return bakPath;
1505
1955
  try {
1506
- if (!existsSync3(dbPath) || statSync(dbPath).size === 0) return null;
1956
+ if (!existsSync4(dbPath) || statSync2(dbPath).size === 0) return null;
1507
1957
  db.pragma("wal_checkpoint(TRUNCATE)");
1508
- copyFileSync(dbPath, bakPath);
1958
+ copyFileSync2(dbPath, bakPath);
1509
1959
  return bakPath;
1510
1960
  } catch {
1511
1961
  return null;
@@ -1539,8 +1989,8 @@ async function openDatabase(dbPath) {
1539
1989
  if (cached) {
1540
1990
  return cached;
1541
1991
  }
1542
- const dir = dirname3(dbPath);
1543
- if (!existsSync3(dir)) {
1992
+ const dir = dirname4(dbPath);
1993
+ if (!existsSync4(dir)) {
1544
1994
  mkdirSync2(dir, { recursive: true });
1545
1995
  }
1546
1996
  const db = openEngine(dbPath);
@@ -1548,15 +1998,15 @@ async function openDatabase(dbPath) {
1548
1998
  return db;
1549
1999
  }
1550
2000
  async function getDb(ocrDir) {
1551
- const dbPath = join3(ocrDir, "data", "ocr.db");
2001
+ const dbPath = join4(ocrDir, "data", "ocr.db");
1552
2002
  return openDatabase(dbPath);
1553
2003
  }
1554
2004
  async function ensureDatabase(ocrDir) {
1555
- const dataDir = join3(ocrDir, "data");
1556
- if (!existsSync3(dataDir)) {
2005
+ const dataDir = join4(ocrDir, "data");
2006
+ if (!existsSync4(dataDir)) {
1557
2007
  mkdirSync2(dataDir, { recursive: true });
1558
2008
  }
1559
- const dbPath = join3(dataDir, "ocr.db");
2009
+ const dbPath = join4(dataDir, "ocr.db");
1560
2010
  const db = await openDatabase(dbPath);
1561
2011
  let before = 0;
1562
2012
  try {
@@ -1584,7 +2034,7 @@ async function ensureDatabase(ocrDir) {
1584
2034
  return db;
1585
2035
  }
1586
2036
  function walCheckpointTruncate(dbPath) {
1587
- if (!existsSync3(dbPath)) {
2037
+ if (!existsSync4(dbPath)) {
1588
2038
  return "skipped";
1589
2039
  }
1590
2040
  const cached = connections.get(dbPath);
@@ -1626,8 +2076,8 @@ function closeAllDatabases() {
1626
2076
  function probeWrite() {
1627
2077
  let dir;
1628
2078
  try {
1629
- dir = mkdtempSync(join3(tmpdir(), "ocr-probe-"));
1630
- const db = openEngine(join3(dir, "probe.db"));
2079
+ dir = mkdtempSync(join4(tmpdir(), "ocr-probe-"));
2080
+ const db = openEngine(join4(dir, "probe.db"));
1631
2081
  try {
1632
2082
  db.run("CREATE TABLE _probe_write (id INTEGER PRIMARY KEY, v TEXT)");
1633
2083
  db.transaction(() => {
@@ -1666,6 +2116,7 @@ export {
1666
2116
  PID_REUSE_GUARD_MS,
1667
2117
  STATE_EXIT,
1668
2118
  StateError,
2119
+ WATCHDOG_DEADLINE_EXIT_CODE,
1669
2120
  appendCommandLog,
1670
2121
  bindVendorSessionIdOpportunistically,
1671
2122
  bumpAgentSessionHeartbeat,
@@ -1673,10 +2124,12 @@ export {
1673
2124
  cascadeTerminateExecutions,
1674
2125
  closeAllDatabases,
1675
2126
  closeDatabase,
2127
+ collectDbHealth,
1676
2128
  commandLogPath,
1677
2129
  commitReasonClose,
1678
2130
  defaultIsAlive,
1679
2131
  ensureDatabase,
2132
+ fixDb,
1680
2133
  formatUpgradeNotice,
1681
2134
  generateCommandUid,
1682
2135
  getAgentSession,
@@ -1688,6 +2141,7 @@ export {
1688
2141
  getLatestEventId,
1689
2142
  getSchemaVersion,
1690
2143
  getSession,
2144
+ hasInFlightDependents,
1691
2145
  insertAgentSession,
1692
2146
  insertEvent,
1693
2147
  insertSession,
@@ -1697,7 +2151,11 @@ export {
1697
2151
  openDatabase,
1698
2152
  probeEngine,
1699
2153
  probeWrite,
2154
+ pruneBackups,
2155
+ pruneDb,
1700
2156
  readCommandLog,
2157
+ reapOrphanDbFiles,
2158
+ reapStaleExecLogs,
1701
2159
  reconcileLegacyState,
1702
2160
  recordVendorSessionIdForExecution,
1703
2161
  replayCommandLog,
@@ -1707,10 +2165,13 @@ export {
1707
2165
  runMigrations,
1708
2166
  setAgentSessionStatus,
1709
2167
  setAgentSessionVendorId,
2168
+ snapshotDb,
1710
2169
  sqliteUtcMs,
1711
2170
  sweepStaleAgentSessions,
1712
2171
  sweepStaleSessions,
1713
2172
  updateAgentSession,
1714
2173
  updateSession,
1715
- walCheckpointTruncate
2174
+ vacuumDb,
2175
+ walCheckpointTruncate,
2176
+ withForeignKeysDisabled
1716
2177
  };
@@ -2,38 +2,54 @@
2
2
  import { existsSync, readFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  var DEFAULT_AGENT_HEARTBEAT_SECONDS = 60;
5
- function getAgentHeartbeatSeconds(ocrDir) {
5
+ var DEFAULT_WORKFLOW_HARD_DEADLINE_MINUTES = 60;
6
+ function readRuntimePositiveInt(ocrDir, key, defaultValue) {
6
7
  const configPath = join(ocrDir, "config.yaml");
7
- if (!existsSync(configPath)) {
8
- return DEFAULT_AGENT_HEARTBEAT_SECONDS;
9
- }
8
+ if (!existsSync(configPath)) return defaultValue;
10
9
  let content;
11
10
  try {
12
11
  content = readFileSync(configPath, "utf-8");
13
12
  } catch {
14
- return DEFAULT_AGENT_HEARTBEAT_SECONDS;
13
+ return defaultValue;
15
14
  }
16
15
  const blockMatch = content.match(
17
- /^runtime:\s*\n(?:\s+[^\n]*\n)*?\s+agent_heartbeat_seconds:\s*([^\s#\n]+)/m
16
+ new RegExp(
17
+ String.raw`^runtime:\s*\n(?:\s+[^\n]*\n)*?\s+${key}:\s*([^\s#\n]+)`,
18
+ "m"
19
+ )
18
20
  );
19
21
  const inlineMatch = content.match(
20
- /^runtime:\s*\{[^}]*\bagent_heartbeat_seconds:\s*([^\s,}]+)/m
22
+ new RegExp(String.raw`^runtime:\s*\{[^}]*\b${key}:\s*([^\s,}]+)`, "m")
21
23
  );
22
24
  const raw = blockMatch?.[1] ?? inlineMatch?.[1];
23
- if (!raw) {
24
- return DEFAULT_AGENT_HEARTBEAT_SECONDS;
25
- }
25
+ if (!raw) return defaultValue;
26
26
  const parsed = Number(raw);
27
27
  if (!Number.isFinite(parsed) || parsed <= 0 || !Number.isInteger(parsed)) {
28
28
  process.stderr.write(
29
- `[ocr] runtime.agent_heartbeat_seconds is not a positive integer (got "${raw}"); falling back to ${DEFAULT_AGENT_HEARTBEAT_SECONDS}s.
29
+ `[ocr] runtime.${key} is not a positive integer (got "${raw}"); falling back to ${defaultValue}.
30
30
  `
31
31
  );
32
- return DEFAULT_AGENT_HEARTBEAT_SECONDS;
32
+ return defaultValue;
33
33
  }
34
34
  return parsed;
35
35
  }
36
+ function getAgentHeartbeatSeconds(ocrDir) {
37
+ return readRuntimePositiveInt(
38
+ ocrDir,
39
+ "agent_heartbeat_seconds",
40
+ DEFAULT_AGENT_HEARTBEAT_SECONDS
41
+ );
42
+ }
43
+ function getWorkflowHardDeadlineMs(ocrDir) {
44
+ return readRuntimePositiveInt(
45
+ ocrDir,
46
+ "workflow_hard_deadline_minutes",
47
+ DEFAULT_WORKFLOW_HARD_DEADLINE_MINUTES
48
+ ) * 60 * 1e3;
49
+ }
36
50
  export {
37
51
  DEFAULT_AGENT_HEARTBEAT_SECONDS,
38
- getAgentHeartbeatSeconds
52
+ DEFAULT_WORKFLOW_HARD_DEADLINE_MINUTES,
53
+ getAgentHeartbeatSeconds,
54
+ getWorkflowHardDeadlineMs
39
55
  };