@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.
- package/README.md +2 -0
- package/dist/dashboard/client/assets/{_basePickBy-B3ALyupE.js → _basePickBy-BBPb8BJA.js} +1 -1
- package/dist/dashboard/client/assets/{_baseUniq-b2RALAWc.js → _baseUniq-CFHdos6T.js} +1 -1
- package/dist/dashboard/client/assets/{arc-DcSVvhUd.js → arc-BKGGWA2F.js} +1 -1
- package/dist/dashboard/client/assets/{architectureDiagram-VXUJARFQ-BNUlmSCS.js → architectureDiagram-VXUJARFQ-B_ovNjX1.js} +1 -1
- package/dist/dashboard/client/assets/{blockDiagram-VD42YOAC-BmhiQVwa.js → blockDiagram-VD42YOAC-C2M-avVp.js} +1 -1
- package/dist/dashboard/client/assets/{c4Diagram-YG6GDRKO-jyJ3WOv5.js → c4Diagram-YG6GDRKO-BtOBpAzH.js} +1 -1
- package/dist/dashboard/client/assets/channel-rgw7C1e7.js +1 -0
- package/dist/dashboard/client/assets/{chunk-4BX2VUAB-x1dQU_s3.js → chunk-4BX2VUAB-Cz2EbHPl.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-55IACEB6-CwbsE2XQ.js → chunk-55IACEB6-C8xpXw9G.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-B4BG7PRW-BaE7c-ti.js → chunk-B4BG7PRW-BSRfOovX.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-DI55MBZ5-Bw5PUaMK.js → chunk-DI55MBZ5-CEUbYQWn.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-FMBD7UC4-B7cF6P3s.js → chunk-FMBD7UC4-5xWP6GRj.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-QN33PNHL-OY4evNHd.js → chunk-QN33PNHL-DfNCVcy8.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-QZHKN3VN-BpjQwIWz.js → chunk-QZHKN3VN--OdToKKu.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-TZMSLE5B-D8b_Oq9B.js → chunk-TZMSLE5B-B_0K0Qso.js} +1 -1
- package/dist/dashboard/client/assets/classDiagram-2ON5EDUG-DTGi7d9X.js +1 -0
- package/dist/dashboard/client/assets/classDiagram-v2-WZHVMYZB-DTGi7d9X.js +1 -0
- package/dist/dashboard/client/assets/clone-Cz7hswqi.js +1 -0
- package/dist/dashboard/client/assets/{cose-bilkent-S5V4N54A-C-sfP8PN.js → cose-bilkent-S5V4N54A-Cc_Dmnxz.js} +1 -1
- package/dist/dashboard/client/assets/{dagre-6UL2VRFP-Cqfo0NRg.js → dagre-6UL2VRFP-DaAfvUXU.js} +1 -1
- package/dist/dashboard/client/assets/{diagram-PSM6KHXK-BR3ppxqI.js → diagram-PSM6KHXK-7idwN0rC.js} +1 -1
- package/dist/dashboard/client/assets/{diagram-QEK2KX5R-Dvcx6x3R.js → diagram-QEK2KX5R-D9j9H13n.js} +1 -1
- package/dist/dashboard/client/assets/{diagram-S2PKOQOG-DoyBLnVN.js → diagram-S2PKOQOG-SMF5SB0K.js} +1 -1
- package/dist/dashboard/client/assets/{erDiagram-Q2GNP2WA-hy77l1cL.js → erDiagram-Q2GNP2WA-EVJ4Qa2F.js} +1 -1
- package/dist/dashboard/client/assets/{flowDiagram-NV44I4VS-Bz0B1rKM.js → flowDiagram-NV44I4VS-tZ7SFE77.js} +1 -1
- package/dist/dashboard/client/assets/{ganttDiagram-JELNMOA3-CLgrZPoC.js → ganttDiagram-JELNMOA3-DFSqguY7.js} +1 -1
- package/dist/dashboard/client/assets/{gitGraphDiagram-V2S2FVAM-DwJ-1f-v.js → gitGraphDiagram-V2S2FVAM-CqHdP3HE.js} +1 -1
- package/dist/dashboard/client/assets/{graph-DDBMM_t2.js → graph-C0XnkNkk.js} +1 -1
- package/dist/dashboard/client/assets/{index-Cr9yEo_B.js → index-C3NEq704.js} +133 -138
- package/dist/dashboard/client/assets/index-CzxeSSaQ.css +1 -0
- package/dist/dashboard/client/assets/{infoDiagram-HS3SLOUP-Bhn1FmAk.js → infoDiagram-HS3SLOUP-DlXZo9U2.js} +1 -1
- package/dist/dashboard/client/assets/{journeyDiagram-XKPGCS4Q-CzGbjX1y.js → journeyDiagram-XKPGCS4Q-CgC8_7eN.js} +1 -1
- package/dist/dashboard/client/assets/{kanban-definition-3W4ZIXB7-Da77-WYk.js → kanban-definition-3W4ZIXB7-BMAw_jNp.js} +1 -1
- package/dist/dashboard/client/assets/{layout-CVwSB-GS.js → layout-XjM3Q-ka.js} +1 -1
- package/dist/dashboard/client/assets/{linear-CTRAc5Jn.js → linear-CMUrrr1X.js} +1 -1
- package/dist/dashboard/client/assets/{mermaid-renderer-Bjo170ax.js → mermaid-renderer-D2jYNs7K.js} +4 -4
- package/dist/dashboard/client/assets/{mindmap-definition-VGOIOE7T-B55C2odl.js → mindmap-definition-VGOIOE7T-CL4hv-vg.js} +1 -1
- package/dist/dashboard/client/assets/{pieDiagram-ADFJNKIX-5lrQLrSz.js → pieDiagram-ADFJNKIX-DTqv-1h1.js} +1 -1
- package/dist/dashboard/client/assets/{quadrantDiagram-AYHSOK5B-Bg55gC30.js → quadrantDiagram-AYHSOK5B-BpFlSW9N.js} +1 -1
- package/dist/dashboard/client/assets/{requirementDiagram-UZGBJVZJ-CyR4YFJY.js → requirementDiagram-UZGBJVZJ-BqYqqXL4.js} +1 -1
- package/dist/dashboard/client/assets/{sankeyDiagram-TZEHDZUN-BVWKr9_-.js → sankeyDiagram-TZEHDZUN-kEI9kntR.js} +1 -1
- package/dist/dashboard/client/assets/{sequenceDiagram-WL72ISMW-D0AJg_tE.js → sequenceDiagram-WL72ISMW-Cnu_1j-N.js} +1 -1
- package/dist/dashboard/client/assets/{stateDiagram-FKZM4ZOC-BuHpTgim.js → stateDiagram-FKZM4ZOC-BoC-rqoG.js} +1 -1
- package/dist/dashboard/client/assets/stateDiagram-v2-4FDKWEC3-COR3QD3v.js +1 -0
- package/dist/dashboard/client/assets/{timeline-definition-IT6M3QCI-LDhpAmDd.js → timeline-definition-IT6M3QCI-CXMWuzDL.js} +1 -1
- package/dist/dashboard/client/assets/{treemap-GDKQZRPO-Dd4gjvUl.js → treemap-GDKQZRPO-o9ZFgpbJ.js} +1 -1
- package/dist/dashboard/client/assets/{xychartDiagram-PRI3JC2R-B9RDod39.js → xychartDiagram-PRI3JC2R-CfIuUpeA.js} +1 -1
- package/dist/dashboard/client/index.html +2 -2
- package/dist/dashboard/server.js +1175 -450
- package/dist/index.js +1489 -312
- package/dist/lib/db/index.js +666 -48
- package/dist/lib/runtime-config.js +29 -13
- package/dist/lib/state/index.js +2196 -0
- package/package.json +9 -5
- 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-Z1pPudAt.css +0 -1
- package/dist/dashboard/client/assets/stateDiagram-v2-4FDKWEC3-DwAPhteN.js +0 -1
package/dist/lib/db/index.js
CHANGED
|
@@ -1,35 +1,97 @@
|
|
|
1
1
|
// src/lib/db/index.ts
|
|
2
|
-
import {
|
|
3
|
-
|
|
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
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
14
|
-
|
|
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(
|
|
72
|
+
Atomics.wait(SLEEP_BUF, 0, 0, ms);
|
|
18
73
|
}
|
|
19
|
-
var
|
|
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
|
-
|
|
87
|
+
const cols = stmt.columns();
|
|
88
|
+
if (cols.length === 0) {
|
|
27
89
|
stmt.run(...params ?? []);
|
|
28
90
|
return [];
|
|
29
91
|
}
|
|
30
|
-
|
|
31
|
-
const values = stmt.
|
|
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
|
-
|
|
46
|
-
|
|
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
|
|
144
|
+
return this.runOnce(fn);
|
|
49
145
|
} catch (e) {
|
|
50
|
-
if (!isBusyError(e) || attempt
|
|
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
|
-
|
|
171
|
+
this.raw.exec(`PRAGMA ${source}`);
|
|
172
|
+
return void 0;
|
|
57
173
|
}
|
|
58
174
|
close() {
|
|
59
175
|
try {
|
|
60
|
-
this.raw.
|
|
176
|
+
this.raw.exec("PRAGMA wal_checkpoint(TRUNCATE)");
|
|
61
177
|
} catch {
|
|
62
178
|
}
|
|
63
|
-
|
|
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 =
|
|
69
|
-
db.
|
|
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 =
|
|
80
|
-
native.
|
|
81
|
-
native.
|
|
82
|
-
native.
|
|
83
|
-
native.
|
|
84
|
-
return new
|
|
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
|
|
1283
|
-
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";
|
|
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
|
|
1865
|
+
return join3(ocrDir, "data", CACHE_DIR);
|
|
1295
1866
|
}
|
|
1296
1867
|
function commandLogPath(ocrDir) {
|
|
1297
|
-
return
|
|
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 =
|
|
1303
|
-
if (!
|
|
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 (!
|
|
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 (
|
|
1954
|
+
if (existsSync4(bakPath)) return bakPath;
|
|
1384
1955
|
try {
|
|
1385
|
-
if (!
|
|
1956
|
+
if (!existsSync4(dbPath) || statSync2(dbPath).size === 0) return null;
|
|
1386
1957
|
db.pragma("wal_checkpoint(TRUNCATE)");
|
|
1387
|
-
|
|
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 =
|
|
1422
|
-
if (!
|
|
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 =
|
|
2001
|
+
const dbPath = join4(ocrDir, "data", "ocr.db");
|
|
1431
2002
|
return openDatabase(dbPath);
|
|
1432
2003
|
}
|
|
1433
2004
|
async function ensureDatabase(ocrDir) {
|
|
1434
|
-
const dataDir =
|
|
1435
|
-
if (!
|
|
2005
|
+
const dataDir = join4(ocrDir, "data");
|
|
2006
|
+
if (!existsSync4(dataDir)) {
|
|
1436
2007
|
mkdirSync2(dataDir, { recursive: true });
|
|
1437
2008
|
}
|
|
1438
|
-
const dbPath =
|
|
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 (!
|
|
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?.
|
|
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
|
-
|
|
2174
|
+
vacuumDb,
|
|
2175
|
+
walCheckpointTruncate,
|
|
2176
|
+
withForeignKeysDisabled
|
|
1559
2177
|
};
|