@open-code-review/cli 2.1.0 → 2.2.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/dashboard/client/assets/{_basePickBy-B3ALyupE.js → _basePickBy-BAlGnwHG.js} +1 -1
- package/dist/dashboard/client/assets/{_baseUniq-b2RALAWc.js → _baseUniq-CoauyOeL.js} +1 -1
- package/dist/dashboard/client/assets/{arc-DcSVvhUd.js → arc-DtS0aHfP.js} +1 -1
- package/dist/dashboard/client/assets/{architectureDiagram-VXUJARFQ-BNUlmSCS.js → architectureDiagram-VXUJARFQ-CnWmtRTh.js} +1 -1
- package/dist/dashboard/client/assets/{blockDiagram-VD42YOAC-BmhiQVwa.js → blockDiagram-VD42YOAC-DgPp4oGV.js} +1 -1
- package/dist/dashboard/client/assets/{c4Diagram-YG6GDRKO-jyJ3WOv5.js → c4Diagram-YG6GDRKO--LV4qQaE.js} +1 -1
- package/dist/dashboard/client/assets/channel-BU2129fl.js +1 -0
- package/dist/dashboard/client/assets/{chunk-4BX2VUAB-x1dQU_s3.js → chunk-4BX2VUAB-BRglpc7Z.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-55IACEB6-CwbsE2XQ.js → chunk-55IACEB6-Bgx06_CV.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-B4BG7PRW-BaE7c-ti.js → chunk-B4BG7PRW-D6HN3Yiy.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-DI55MBZ5-Bw5PUaMK.js → chunk-DI55MBZ5-NH9EgN9T.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-FMBD7UC4-B7cF6P3s.js → chunk-FMBD7UC4-xriO6WNP.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-QN33PNHL-OY4evNHd.js → chunk-QN33PNHL-CV1h6_Zl.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-QZHKN3VN-BpjQwIWz.js → chunk-QZHKN3VN-CV4VzxNq.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-TZMSLE5B-D8b_Oq9B.js → chunk-TZMSLE5B-isdklocW.js} +1 -1
- package/dist/dashboard/client/assets/classDiagram-2ON5EDUG-CVftFGiR.js +1 -0
- package/dist/dashboard/client/assets/classDiagram-v2-WZHVMYZB-CVftFGiR.js +1 -0
- package/dist/dashboard/client/assets/clone-DC6LEEC5.js +1 -0
- package/dist/dashboard/client/assets/{cose-bilkent-S5V4N54A-C-sfP8PN.js → cose-bilkent-S5V4N54A-CCzlFSJf.js} +1 -1
- package/dist/dashboard/client/assets/{dagre-6UL2VRFP-Cqfo0NRg.js → dagre-6UL2VRFP-DVN3PkjZ.js} +1 -1
- package/dist/dashboard/client/assets/{diagram-PSM6KHXK-BR3ppxqI.js → diagram-PSM6KHXK-SzJVoSsb.js} +1 -1
- package/dist/dashboard/client/assets/{diagram-QEK2KX5R-Dvcx6x3R.js → diagram-QEK2KX5R-CgGn7ts-.js} +1 -1
- package/dist/dashboard/client/assets/{diagram-S2PKOQOG-DoyBLnVN.js → diagram-S2PKOQOG-Bz1ukSx8.js} +1 -1
- package/dist/dashboard/client/assets/{erDiagram-Q2GNP2WA-hy77l1cL.js → erDiagram-Q2GNP2WA-CpstUTMZ.js} +1 -1
- package/dist/dashboard/client/assets/{flowDiagram-NV44I4VS-Bz0B1rKM.js → flowDiagram-NV44I4VS-aYVydGhp.js} +1 -1
- package/dist/dashboard/client/assets/{ganttDiagram-JELNMOA3-CLgrZPoC.js → ganttDiagram-JELNMOA3-Cb2DUSRk.js} +1 -1
- package/dist/dashboard/client/assets/{gitGraphDiagram-V2S2FVAM-DwJ-1f-v.js → gitGraphDiagram-V2S2FVAM-BUOnwA2w.js} +1 -1
- package/dist/dashboard/client/assets/{graph-DDBMM_t2.js → graph-4X5ddhLp.js} +1 -1
- package/dist/dashboard/client/assets/index-CKWqYAfu.js +581 -0
- package/dist/dashboard/client/assets/index-CzxeSSaQ.css +1 -0
- package/dist/dashboard/client/assets/{infoDiagram-HS3SLOUP-Bhn1FmAk.js → infoDiagram-HS3SLOUP-BlMqcrwm.js} +1 -1
- package/dist/dashboard/client/assets/{journeyDiagram-XKPGCS4Q-CzGbjX1y.js → journeyDiagram-XKPGCS4Q-DF2ew7ju.js} +1 -1
- package/dist/dashboard/client/assets/{kanban-definition-3W4ZIXB7-Da77-WYk.js → kanban-definition-3W4ZIXB7-BKQMx0-n.js} +1 -1
- package/dist/dashboard/client/assets/{layout-CVwSB-GS.js → layout-DNcn2g9w.js} +1 -1
- package/dist/dashboard/client/assets/{linear-CTRAc5Jn.js → linear-Bqy9gvqb.js} +1 -1
- package/dist/dashboard/client/assets/{mermaid-renderer-Bjo170ax.js → mermaid-renderer-dJ71wgld.js} +4 -4
- package/dist/dashboard/client/assets/{mindmap-definition-VGOIOE7T-B55C2odl.js → mindmap-definition-VGOIOE7T-BARc8sqJ.js} +1 -1
- package/dist/dashboard/client/assets/{pieDiagram-ADFJNKIX-5lrQLrSz.js → pieDiagram-ADFJNKIX-CULlNZTd.js} +1 -1
- package/dist/dashboard/client/assets/{quadrantDiagram-AYHSOK5B-Bg55gC30.js → quadrantDiagram-AYHSOK5B-BJEZPVe9.js} +1 -1
- package/dist/dashboard/client/assets/{requirementDiagram-UZGBJVZJ-CyR4YFJY.js → requirementDiagram-UZGBJVZJ-BhMsmUIs.js} +1 -1
- package/dist/dashboard/client/assets/{sankeyDiagram-TZEHDZUN-BVWKr9_-.js → sankeyDiagram-TZEHDZUN-BYbNgogG.js} +1 -1
- package/dist/dashboard/client/assets/{sequenceDiagram-WL72ISMW-D0AJg_tE.js → sequenceDiagram-WL72ISMW-MoM_NwWk.js} +1 -1
- package/dist/dashboard/client/assets/{stateDiagram-FKZM4ZOC-BuHpTgim.js → stateDiagram-FKZM4ZOC-ditrlbM3.js} +1 -1
- package/dist/dashboard/client/assets/stateDiagram-v2-4FDKWEC3-SqoG2LCn.js +1 -0
- package/dist/dashboard/client/assets/{timeline-definition-IT6M3QCI-LDhpAmDd.js → timeline-definition-IT6M3QCI-DOAJyjuz.js} +1 -1
- package/dist/dashboard/client/assets/{treemap-GDKQZRPO-Dd4gjvUl.js → treemap-GDKQZRPO-BBJkjnJl.js} +1 -1
- package/dist/dashboard/client/assets/{xychartDiagram-PRI3JC2R-B9RDod39.js → xychartDiagram-PRI3JC2R-CPW4s5vm.js} +1 -1
- package/dist/dashboard/client/index.html +2 -2
- package/dist/dashboard/server.js +1188 -579
- package/dist/index.js +1395 -335
- package/dist/lib/db/index.js +485 -24
- package/dist/lib/models.js +125 -50
- package/dist/lib/runtime-config.js +29 -13
- package/dist/lib/state/index.js +2196 -0
- package/package.json +8 -2
- package/dist/dashboard/client/assets/channel-D3J8-GF_.js +0 -1
- package/dist/dashboard/client/assets/classDiagram-2ON5EDUG-tkFUL-1Y.js +0 -1
- package/dist/dashboard/client/assets/classDiagram-v2-WZHVMYZB-tkFUL-1Y.js +0 -1
- package/dist/dashboard/client/assets/clone-CkY5ajLr.js +0 -1
- package/dist/dashboard/client/assets/index-Cr9yEo_B.js +0 -576
- package/dist/dashboard/client/assets/index-Z1pPudAt.css +0 -1
- package/dist/dashboard/client/assets/stateDiagram-v2-4FDKWEC3-DwAPhteN.js +0 -1
package/dist/lib/db/index.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
// src/lib/db/index.ts
|
|
2
2
|
import {
|
|
3
|
-
existsSync as
|
|
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
|
|
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
|
|
1404
|
-
import { dirname as
|
|
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
|
|
1865
|
+
return join3(ocrDir, "data", CACHE_DIR);
|
|
1416
1866
|
}
|
|
1417
1867
|
function commandLogPath(ocrDir) {
|
|
1418
|
-
return
|
|
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 =
|
|
1424
|
-
if (!
|
|
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 (!
|
|
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 (
|
|
1954
|
+
if (existsSync4(bakPath)) return bakPath;
|
|
1505
1955
|
try {
|
|
1506
|
-
if (!
|
|
1956
|
+
if (!existsSync4(dbPath) || statSync2(dbPath).size === 0) return null;
|
|
1507
1957
|
db.pragma("wal_checkpoint(TRUNCATE)");
|
|
1508
|
-
|
|
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 =
|
|
1543
|
-
if (!
|
|
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 =
|
|
2001
|
+
const dbPath = join4(ocrDir, "data", "ocr.db");
|
|
1552
2002
|
return openDatabase(dbPath);
|
|
1553
2003
|
}
|
|
1554
2004
|
async function ensureDatabase(ocrDir) {
|
|
1555
|
-
const dataDir =
|
|
1556
|
-
if (!
|
|
2005
|
+
const dataDir = join4(ocrDir, "data");
|
|
2006
|
+
if (!existsSync4(dataDir)) {
|
|
1557
2007
|
mkdirSync2(dataDir, { recursive: true });
|
|
1558
2008
|
}
|
|
1559
|
-
const dbPath =
|
|
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 (!
|
|
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(
|
|
1630
|
-
const db = openEngine(
|
|
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
|
-
|
|
2174
|
+
vacuumDb,
|
|
2175
|
+
walCheckpointTruncate,
|
|
2176
|
+
withForeignKeysDisabled
|
|
1716
2177
|
};
|