@open-code-review/cli 2.2.1 → 2.4.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 +9 -0
- package/dist/dashboard/client/assets/{_basePickBy-BAlGnwHG.js → _basePickBy-CyrHyeyN.js} +1 -1
- package/dist/dashboard/client/assets/{_baseUniq-CoauyOeL.js → _baseUniq-Bg7NJSGS.js} +1 -1
- package/dist/dashboard/client/assets/{arc-DtS0aHfP.js → arc-zDGAKMur.js} +1 -1
- package/dist/dashboard/client/assets/{architectureDiagram-VXUJARFQ-CnWmtRTh.js → architectureDiagram-VXUJARFQ-BxlGxm0Q.js} +1 -1
- package/dist/dashboard/client/assets/{blockDiagram-VD42YOAC-DgPp4oGV.js → blockDiagram-VD42YOAC-BskTNyX5.js} +1 -1
- package/dist/dashboard/client/assets/{c4Diagram-YG6GDRKO--LV4qQaE.js → c4Diagram-YG6GDRKO-Dr9QQ-dn.js} +1 -1
- package/dist/dashboard/client/assets/channel-BUnm_-UQ.js +1 -0
- package/dist/dashboard/client/assets/{chunk-4BX2VUAB-BRglpc7Z.js → chunk-4BX2VUAB-xq9xoCTv.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-55IACEB6-Bgx06_CV.js → chunk-55IACEB6-DYdXYVh5.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-B4BG7PRW-D6HN3Yiy.js → chunk-B4BG7PRW-BGAyFRFS.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-DI55MBZ5-NH9EgN9T.js → chunk-DI55MBZ5-C5ul9stk.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-FMBD7UC4-xriO6WNP.js → chunk-FMBD7UC4-BSaPo2xa.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-QN33PNHL-CV1h6_Zl.js → chunk-QN33PNHL-CyzabUv0.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-QZHKN3VN-CV4VzxNq.js → chunk-QZHKN3VN-CceRbxt_.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-TZMSLE5B-isdklocW.js → chunk-TZMSLE5B-Bjg9IoOQ.js} +1 -1
- package/dist/dashboard/client/assets/classDiagram-2ON5EDUG-D_fkmNvU.js +1 -0
- package/dist/dashboard/client/assets/classDiagram-v2-WZHVMYZB-D_fkmNvU.js +1 -0
- package/dist/dashboard/client/assets/clone-DTyrNOLZ.js +1 -0
- package/dist/dashboard/client/assets/{cose-bilkent-S5V4N54A-CCzlFSJf.js → cose-bilkent-S5V4N54A-DEdXBrCt.js} +1 -1
- package/dist/dashboard/client/assets/{dagre-6UL2VRFP-DVN3PkjZ.js → dagre-6UL2VRFP-DRdIiP58.js} +1 -1
- package/dist/dashboard/client/assets/{diagram-PSM6KHXK-SzJVoSsb.js → diagram-PSM6KHXK-Bo7Q2VlK.js} +1 -1
- package/dist/dashboard/client/assets/{diagram-QEK2KX5R-CgGn7ts-.js → diagram-QEK2KX5R-2Fmc2o5x.js} +1 -1
- package/dist/dashboard/client/assets/{diagram-S2PKOQOG-Bz1ukSx8.js → diagram-S2PKOQOG-5WE8f0p7.js} +1 -1
- package/dist/dashboard/client/assets/{erDiagram-Q2GNP2WA-CpstUTMZ.js → erDiagram-Q2GNP2WA-DD-iXWd_.js} +1 -1
- package/dist/dashboard/client/assets/{flowDiagram-NV44I4VS-aYVydGhp.js → flowDiagram-NV44I4VS-CCWo8Ue9.js} +1 -1
- package/dist/dashboard/client/assets/{ganttDiagram-JELNMOA3-Cb2DUSRk.js → ganttDiagram-JELNMOA3-CNY4d5UK.js} +1 -1
- package/dist/dashboard/client/assets/{gitGraphDiagram-V2S2FVAM-BUOnwA2w.js → gitGraphDiagram-V2S2FVAM-Dq5SBEJJ.js} +1 -1
- package/dist/dashboard/client/assets/{graph-4X5ddhLp.js → graph-BTt9lokK.js} +1 -1
- package/dist/dashboard/client/assets/{index-CKWqYAfu.js → index-B0k81q2b.js} +138 -138
- package/dist/dashboard/client/assets/index-Czwdh6UA.css +1 -0
- package/dist/dashboard/client/assets/{infoDiagram-HS3SLOUP-BlMqcrwm.js → infoDiagram-HS3SLOUP-AnKZja-G.js} +1 -1
- package/dist/dashboard/client/assets/{journeyDiagram-XKPGCS4Q-DF2ew7ju.js → journeyDiagram-XKPGCS4Q-nC-_WjPN.js} +1 -1
- package/dist/dashboard/client/assets/{kanban-definition-3W4ZIXB7-BKQMx0-n.js → kanban-definition-3W4ZIXB7-BEY73sWU.js} +1 -1
- package/dist/dashboard/client/assets/{layout-DNcn2g9w.js → layout-D4DfNpzH.js} +1 -1
- package/dist/dashboard/client/assets/{linear-Bqy9gvqb.js → linear-ZpGvKjeP.js} +1 -1
- package/dist/dashboard/client/assets/{mermaid-renderer-dJ71wgld.js → mermaid-renderer-BCDxmS9g.js} +4 -4
- package/dist/dashboard/client/assets/{mindmap-definition-VGOIOE7T-BARc8sqJ.js → mindmap-definition-VGOIOE7T-MzAaKESA.js} +1 -1
- package/dist/dashboard/client/assets/{pieDiagram-ADFJNKIX-CULlNZTd.js → pieDiagram-ADFJNKIX-B_X1kySF.js} +1 -1
- package/dist/dashboard/client/assets/{quadrantDiagram-AYHSOK5B-BJEZPVe9.js → quadrantDiagram-AYHSOK5B-CMoIEMLN.js} +1 -1
- package/dist/dashboard/client/assets/{requirementDiagram-UZGBJVZJ-BhMsmUIs.js → requirementDiagram-UZGBJVZJ-v4CRsn1w.js} +1 -1
- package/dist/dashboard/client/assets/{sankeyDiagram-TZEHDZUN-BYbNgogG.js → sankeyDiagram-TZEHDZUN-CPcyN8Jj.js} +1 -1
- package/dist/dashboard/client/assets/{sequenceDiagram-WL72ISMW-MoM_NwWk.js → sequenceDiagram-WL72ISMW-CTg0Vx1H.js} +1 -1
- package/dist/dashboard/client/assets/{stateDiagram-FKZM4ZOC-ditrlbM3.js → stateDiagram-FKZM4ZOC-BMWBN6Nq.js} +1 -1
- package/dist/dashboard/client/assets/stateDiagram-v2-4FDKWEC3-C9Jk1xd0.js +1 -0
- package/dist/dashboard/client/assets/{timeline-definition-IT6M3QCI-DOAJyjuz.js → timeline-definition-IT6M3QCI-B8xFcSGb.js} +1 -1
- package/dist/dashboard/client/assets/{treemap-GDKQZRPO-BBJkjnJl.js → treemap-GDKQZRPO-HQQuGl9w.js} +1 -1
- package/dist/dashboard/client/assets/{xychartDiagram-PRI3JC2R-CPW4s5vm.js → xychartDiagram-PRI3JC2R-Drz0SW3I.js} +1 -1
- package/dist/dashboard/client/index.html +2 -2
- package/dist/dashboard/server.js +926 -461
- package/dist/index.js +1344 -323
- package/package.json +5 -38
- package/dist/dashboard/client/assets/channel-BU2129fl.js +0 -1
- package/dist/dashboard/client/assets/classDiagram-2ON5EDUG-CVftFGiR.js +0 -1
- package/dist/dashboard/client/assets/classDiagram-v2-WZHVMYZB-CVftFGiR.js +0 -1
- package/dist/dashboard/client/assets/clone-DC6LEEC5.js +0 -1
- package/dist/dashboard/client/assets/index-CzxeSSaQ.css +0 -1
- package/dist/dashboard/client/assets/stateDiagram-v2-4FDKWEC3-SqoG2LCn.js +0 -1
- package/dist/lib/db/index.js +0 -2177
- package/dist/lib/models.js +0 -160
- package/dist/lib/runtime-config.js +0 -55
- package/dist/lib/state/index.js +0 -2196
- package/dist/lib/team-config.js +0 -175
- package/dist/lib/vendor-resume.js +0 -31
package/dist/lib/db/index.js
DELETED
|
@@ -1,2177 +0,0 @@
|
|
|
1
|
-
// src/lib/db/index.ts
|
|
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";
|
|
12
|
-
|
|
13
|
-
// src/lib/db/engine.ts
|
|
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;
|
|
37
|
-
var BUSY_RETRY_ATTEMPTS = 5;
|
|
38
|
-
var BUSY_RETRY_BACKOFF_MS = 50;
|
|
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
|
-
}
|
|
63
|
-
}
|
|
64
|
-
return new _DatabaseSyncCtor(path);
|
|
65
|
-
}
|
|
66
|
-
function isBusyError(e) {
|
|
67
|
-
const errcode = e?.errcode;
|
|
68
|
-
return errcode === SQLITE_BUSY || errcode === SQLITE_BUSY_SNAPSHOT;
|
|
69
|
-
}
|
|
70
|
-
var SLEEP_BUF = new Int32Array(new SharedArrayBuffer(4));
|
|
71
|
-
function sleepSync(ms) {
|
|
72
|
-
Atomics.wait(SLEEP_BUF, 0, 0, ms);
|
|
73
|
-
}
|
|
74
|
-
var NodeSqliteAdapter = class {
|
|
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;
|
|
82
|
-
constructor(db) {
|
|
83
|
-
this.raw = db;
|
|
84
|
-
}
|
|
85
|
-
exec(sql, params) {
|
|
86
|
-
const stmt = this.raw.prepare(sql);
|
|
87
|
-
const cols = stmt.columns();
|
|
88
|
-
if (cols.length === 0) {
|
|
89
|
-
stmt.run(...params ?? []);
|
|
90
|
-
return [];
|
|
91
|
-
}
|
|
92
|
-
stmt.setReturnArrays(true);
|
|
93
|
-
const values = stmt.all(...params ?? []);
|
|
94
|
-
return values.length > 0 ? [{ columns: cols.map((c) => c.name), values }] : [];
|
|
95
|
-
}
|
|
96
|
-
run(sql, params) {
|
|
97
|
-
if (params !== void 0) {
|
|
98
|
-
this.raw.prepare(sql).run(...params);
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
this.raw.exec(sql);
|
|
102
|
-
}
|
|
103
|
-
prepare(sql) {
|
|
104
|
-
return this.raw.prepare(sql);
|
|
105
|
-
}
|
|
106
|
-
transaction(fn) {
|
|
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++) {
|
|
143
|
-
try {
|
|
144
|
-
return this.runOnce(fn);
|
|
145
|
-
} catch (e) {
|
|
146
|
-
if (!isBusyError(e) || attempt === BUSY_RETRY_ATTEMPTS - 1) throw e;
|
|
147
|
-
sleepSync(BUSY_RETRY_BACKOFF_MS);
|
|
148
|
-
}
|
|
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
|
-
}
|
|
169
|
-
}
|
|
170
|
-
pragma(source) {
|
|
171
|
-
this.raw.exec(`PRAGMA ${source}`);
|
|
172
|
-
return void 0;
|
|
173
|
-
}
|
|
174
|
-
close() {
|
|
175
|
-
try {
|
|
176
|
-
this.raw.exec("PRAGMA wal_checkpoint(TRUNCATE)");
|
|
177
|
-
} catch {
|
|
178
|
-
}
|
|
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
|
-
}
|
|
185
|
-
}
|
|
186
|
-
};
|
|
187
|
-
function probeEngine() {
|
|
188
|
-
try {
|
|
189
|
-
const db = newDatabase(":memory:");
|
|
190
|
-
db.exec("PRAGMA journal_mode = WAL");
|
|
191
|
-
db.exec("CREATE TABLE _probe(x); INSERT INTO _probe VALUES (1);");
|
|
192
|
-
const row = db.prepare("SELECT sqlite_version() AS v").get();
|
|
193
|
-
db.close();
|
|
194
|
-
return { ok: true, version: row.v };
|
|
195
|
-
} catch (e) {
|
|
196
|
-
return { ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
function openEngine(dbPath) {
|
|
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);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// src/lib/db/migrations.ts
|
|
209
|
-
var MIGRATIONS = [
|
|
210
|
-
{
|
|
211
|
-
version: 1,
|
|
212
|
-
description: "Initial schema \u2014 sessions, events, artifacts, user state",
|
|
213
|
-
sql: `
|
|
214
|
-
-- Layer 1: Workflow State
|
|
215
|
-
|
|
216
|
-
CREATE TABLE sessions (
|
|
217
|
-
id TEXT PRIMARY KEY,
|
|
218
|
-
branch TEXT NOT NULL,
|
|
219
|
-
status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'closed')),
|
|
220
|
-
workflow_type TEXT NOT NULL CHECK(workflow_type IN ('review', 'map')),
|
|
221
|
-
current_phase TEXT NOT NULL DEFAULT 'context',
|
|
222
|
-
phase_number INTEGER NOT NULL DEFAULT 1,
|
|
223
|
-
current_round INTEGER NOT NULL DEFAULT 1,
|
|
224
|
-
current_map_run INTEGER NOT NULL DEFAULT 1,
|
|
225
|
-
started_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
226
|
-
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
227
|
-
session_dir TEXT NOT NULL
|
|
228
|
-
);
|
|
229
|
-
|
|
230
|
-
CREATE TABLE orchestration_events (
|
|
231
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
232
|
-
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
233
|
-
event_type TEXT NOT NULL,
|
|
234
|
-
phase TEXT,
|
|
235
|
-
phase_number INTEGER,
|
|
236
|
-
round INTEGER,
|
|
237
|
-
metadata TEXT,
|
|
238
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
239
|
-
);
|
|
240
|
-
CREATE INDEX idx_events_session ON orchestration_events(session_id);
|
|
241
|
-
CREATE INDEX idx_events_type ON orchestration_events(event_type);
|
|
242
|
-
|
|
243
|
-
-- Layer 2: Artifacts
|
|
244
|
-
|
|
245
|
-
CREATE TABLE review_rounds (
|
|
246
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
247
|
-
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
248
|
-
round_number INTEGER NOT NULL,
|
|
249
|
-
verdict TEXT,
|
|
250
|
-
blocker_count INTEGER DEFAULT 0,
|
|
251
|
-
suggestion_count INTEGER DEFAULT 0,
|
|
252
|
-
should_fix_count INTEGER DEFAULT 0,
|
|
253
|
-
final_md_path TEXT,
|
|
254
|
-
parsed_at TEXT,
|
|
255
|
-
UNIQUE(session_id, round_number)
|
|
256
|
-
);
|
|
257
|
-
|
|
258
|
-
CREATE TABLE reviewer_outputs (
|
|
259
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
260
|
-
round_id INTEGER NOT NULL REFERENCES review_rounds(id) ON DELETE CASCADE,
|
|
261
|
-
reviewer_type TEXT NOT NULL,
|
|
262
|
-
instance_number INTEGER NOT NULL DEFAULT 1,
|
|
263
|
-
file_path TEXT NOT NULL,
|
|
264
|
-
finding_count INTEGER DEFAULT 0,
|
|
265
|
-
parsed_at TEXT,
|
|
266
|
-
UNIQUE(round_id, reviewer_type, instance_number)
|
|
267
|
-
);
|
|
268
|
-
|
|
269
|
-
CREATE TABLE review_findings (
|
|
270
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
271
|
-
reviewer_output_id INTEGER NOT NULL REFERENCES reviewer_outputs(id) ON DELETE CASCADE,
|
|
272
|
-
title TEXT NOT NULL,
|
|
273
|
-
severity TEXT NOT NULL CHECK(severity IN ('critical', 'high', 'medium', 'low', 'info')),
|
|
274
|
-
file_path TEXT,
|
|
275
|
-
line_start INTEGER,
|
|
276
|
-
line_end INTEGER,
|
|
277
|
-
summary TEXT,
|
|
278
|
-
is_blocker INTEGER NOT NULL DEFAULT 0,
|
|
279
|
-
parsed_at TEXT
|
|
280
|
-
);
|
|
281
|
-
CREATE INDEX idx_findings_severity ON review_findings(severity);
|
|
282
|
-
|
|
283
|
-
CREATE TABLE markdown_artifacts (
|
|
284
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
285
|
-
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
286
|
-
artifact_type TEXT NOT NULL,
|
|
287
|
-
round_number INTEGER,
|
|
288
|
-
file_path TEXT NOT NULL,
|
|
289
|
-
content TEXT NOT NULL,
|
|
290
|
-
parsed_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
291
|
-
UNIQUE(session_id, artifact_type, round_number, file_path)
|
|
292
|
-
);
|
|
293
|
-
|
|
294
|
-
CREATE TABLE map_runs (
|
|
295
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
296
|
-
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
297
|
-
run_number INTEGER NOT NULL,
|
|
298
|
-
file_count INTEGER DEFAULT 0,
|
|
299
|
-
map_md_path TEXT,
|
|
300
|
-
parsed_at TEXT,
|
|
301
|
-
UNIQUE(session_id, run_number)
|
|
302
|
-
);
|
|
303
|
-
|
|
304
|
-
CREATE TABLE map_sections (
|
|
305
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
306
|
-
map_run_id INTEGER NOT NULL REFERENCES map_runs(id) ON DELETE CASCADE,
|
|
307
|
-
section_number INTEGER NOT NULL,
|
|
308
|
-
title TEXT NOT NULL,
|
|
309
|
-
description TEXT,
|
|
310
|
-
file_count INTEGER DEFAULT 0,
|
|
311
|
-
display_order INTEGER NOT NULL DEFAULT 0,
|
|
312
|
-
UNIQUE(map_run_id, section_number)
|
|
313
|
-
);
|
|
314
|
-
|
|
315
|
-
CREATE TABLE map_files (
|
|
316
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
317
|
-
section_id INTEGER NOT NULL REFERENCES map_sections(id) ON DELETE CASCADE,
|
|
318
|
-
file_path TEXT NOT NULL,
|
|
319
|
-
role TEXT,
|
|
320
|
-
lines_added INTEGER DEFAULT 0,
|
|
321
|
-
lines_deleted INTEGER DEFAULT 0,
|
|
322
|
-
display_order INTEGER NOT NULL DEFAULT 0,
|
|
323
|
-
UNIQUE(section_id, file_path)
|
|
324
|
-
);
|
|
325
|
-
|
|
326
|
-
-- Layer 3: User Interaction
|
|
327
|
-
|
|
328
|
-
CREATE TABLE user_file_progress (
|
|
329
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
330
|
-
map_file_id INTEGER NOT NULL REFERENCES map_files(id) ON DELETE CASCADE,
|
|
331
|
-
is_reviewed INTEGER NOT NULL DEFAULT 0,
|
|
332
|
-
reviewed_at TEXT,
|
|
333
|
-
UNIQUE(map_file_id)
|
|
334
|
-
);
|
|
335
|
-
|
|
336
|
-
CREATE TABLE user_finding_progress (
|
|
337
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
338
|
-
finding_id INTEGER NOT NULL REFERENCES review_findings(id) ON DELETE CASCADE,
|
|
339
|
-
status TEXT NOT NULL DEFAULT 'unread' CHECK(status IN ('unread', 'read', 'acknowledged', 'fixed', 'wont_fix')),
|
|
340
|
-
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
341
|
-
UNIQUE(finding_id)
|
|
342
|
-
);
|
|
343
|
-
|
|
344
|
-
CREATE TABLE user_notes (
|
|
345
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
346
|
-
target_type TEXT NOT NULL CHECK(target_type IN ('session', 'round', 'finding', 'run', 'section', 'file')),
|
|
347
|
-
target_id TEXT NOT NULL,
|
|
348
|
-
content TEXT NOT NULL,
|
|
349
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
350
|
-
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
351
|
-
);
|
|
352
|
-
|
|
353
|
-
CREATE TABLE command_executions (
|
|
354
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
355
|
-
command TEXT NOT NULL,
|
|
356
|
-
args TEXT,
|
|
357
|
-
exit_code INTEGER,
|
|
358
|
-
started_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
359
|
-
finished_at TEXT,
|
|
360
|
-
output TEXT
|
|
361
|
-
);
|
|
362
|
-
`
|
|
363
|
-
},
|
|
364
|
-
{
|
|
365
|
-
version: 2,
|
|
366
|
-
description: "Add chat conversations, messages, and round progress tables",
|
|
367
|
-
sql: `
|
|
368
|
-
CREATE TABLE IF NOT EXISTS chat_conversations (
|
|
369
|
-
id TEXT PRIMARY KEY,
|
|
370
|
-
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
371
|
-
target_type TEXT NOT NULL CHECK(target_type IN ('map_run', 'review_round')),
|
|
372
|
-
target_id INTEGER NOT NULL,
|
|
373
|
-
claude_session_id TEXT,
|
|
374
|
-
status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'expired')),
|
|
375
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
376
|
-
last_active_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
377
|
-
);
|
|
378
|
-
|
|
379
|
-
CREATE TABLE IF NOT EXISTS chat_messages (
|
|
380
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
381
|
-
conversation_id TEXT NOT NULL REFERENCES chat_conversations(id) ON DELETE CASCADE,
|
|
382
|
-
role TEXT NOT NULL CHECK(role IN ('user', 'assistant')),
|
|
383
|
-
content TEXT NOT NULL,
|
|
384
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
385
|
-
);
|
|
386
|
-
|
|
387
|
-
CREATE TABLE IF NOT EXISTS user_round_progress (
|
|
388
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
389
|
-
round_id INTEGER NOT NULL REFERENCES review_rounds(id) ON DELETE CASCADE,
|
|
390
|
-
status TEXT NOT NULL DEFAULT 'needs_review'
|
|
391
|
-
CHECK(status IN ('needs_review', 'in_progress', 'changes_made', 'acknowledged', 'dismissed')),
|
|
392
|
-
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
393
|
-
UNIQUE(round_id)
|
|
394
|
-
);
|
|
395
|
-
`
|
|
396
|
-
},
|
|
397
|
-
{
|
|
398
|
-
version: 3,
|
|
399
|
-
description: "Add PID tracking to command_executions for orphan process cleanup",
|
|
400
|
-
sql: `
|
|
401
|
-
ALTER TABLE command_executions ADD COLUMN pid INTEGER;
|
|
402
|
-
`
|
|
403
|
-
},
|
|
404
|
-
{
|
|
405
|
-
version: 4,
|
|
406
|
-
description: "Add is_detached flag to command_executions for process group kill strategy",
|
|
407
|
-
sql: `
|
|
408
|
-
ALTER TABLE command_executions ADD COLUMN is_detached INTEGER NOT NULL DEFAULT 0;
|
|
409
|
-
`
|
|
410
|
-
},
|
|
411
|
-
{
|
|
412
|
-
version: 5,
|
|
413
|
-
description: "Change orchestration_events FK to RESTRICT to protect audit trail",
|
|
414
|
-
sql: `
|
|
415
|
-
CREATE TABLE orchestration_events_new (
|
|
416
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
417
|
-
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE RESTRICT,
|
|
418
|
-
event_type TEXT NOT NULL,
|
|
419
|
-
phase TEXT,
|
|
420
|
-
phase_number INTEGER,
|
|
421
|
-
round INTEGER,
|
|
422
|
-
metadata TEXT,
|
|
423
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
424
|
-
);
|
|
425
|
-
INSERT INTO orchestration_events_new SELECT * FROM orchestration_events;
|
|
426
|
-
DROP TABLE orchestration_events;
|
|
427
|
-
ALTER TABLE orchestration_events_new RENAME TO orchestration_events;
|
|
428
|
-
CREATE INDEX idx_events_session ON orchestration_events(session_id);
|
|
429
|
-
CREATE INDEX idx_events_type ON orchestration_events(event_type);
|
|
430
|
-
`
|
|
431
|
-
},
|
|
432
|
-
{
|
|
433
|
-
version: 6,
|
|
434
|
-
description: "Add orchestrator-first columns to review_rounds for round-meta.json support",
|
|
435
|
-
sql: `
|
|
436
|
-
ALTER TABLE review_rounds ADD COLUMN source TEXT DEFAULT NULL;
|
|
437
|
-
ALTER TABLE review_rounds ADD COLUMN reviewer_count INTEGER DEFAULT 0;
|
|
438
|
-
ALTER TABLE review_rounds ADD COLUMN total_finding_count INTEGER DEFAULT 0;
|
|
439
|
-
`
|
|
440
|
-
},
|
|
441
|
-
{
|
|
442
|
-
version: 7,
|
|
443
|
-
description: "Add category column to review_findings for blocker/should_fix/suggestion classification",
|
|
444
|
-
sql: `
|
|
445
|
-
ALTER TABLE review_findings ADD COLUMN category TEXT DEFAULT NULL;
|
|
446
|
-
`
|
|
447
|
-
},
|
|
448
|
-
{
|
|
449
|
-
version: 8,
|
|
450
|
-
description: "Add orchestrator-first columns to map_runs for map-meta.json support",
|
|
451
|
-
sql: `
|
|
452
|
-
ALTER TABLE map_runs ADD COLUMN source TEXT DEFAULT NULL;
|
|
453
|
-
ALTER TABLE map_runs ADD COLUMN section_count INTEGER DEFAULT 0;
|
|
454
|
-
`
|
|
455
|
-
},
|
|
456
|
-
{
|
|
457
|
-
version: 9,
|
|
458
|
-
description: "Add uid column to command_executions for JSONL-backed recovery",
|
|
459
|
-
sql: `
|
|
460
|
-
ALTER TABLE command_executions ADD COLUMN uid TEXT;
|
|
461
|
-
CREATE UNIQUE INDEX idx_command_executions_uid ON command_executions(uid);
|
|
462
|
-
`
|
|
463
|
-
},
|
|
464
|
-
{
|
|
465
|
-
version: 10,
|
|
466
|
-
description: "Add agent_sessions journal for per-instance lifecycle tracking",
|
|
467
|
-
sql: `
|
|
468
|
-
CREATE TABLE agent_sessions (
|
|
469
|
-
id TEXT PRIMARY KEY,
|
|
470
|
-
workflow_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE RESTRICT,
|
|
471
|
-
vendor TEXT NOT NULL,
|
|
472
|
-
vendor_session_id TEXT,
|
|
473
|
-
persona TEXT,
|
|
474
|
-
instance_index INTEGER,
|
|
475
|
-
name TEXT,
|
|
476
|
-
resolved_model TEXT,
|
|
477
|
-
phase TEXT,
|
|
478
|
-
status TEXT NOT NULL CHECK(status IN ('spawning', 'running', 'done', 'crashed', 'cancelled', 'orphaned')),
|
|
479
|
-
pid INTEGER,
|
|
480
|
-
started_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
481
|
-
last_heartbeat_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
482
|
-
ended_at TEXT,
|
|
483
|
-
exit_code INTEGER,
|
|
484
|
-
notes TEXT
|
|
485
|
-
);
|
|
486
|
-
CREATE INDEX idx_agent_sessions_workflow ON agent_sessions(workflow_id);
|
|
487
|
-
CREATE INDEX idx_agent_sessions_status_heartbeat ON agent_sessions(status, last_heartbeat_at);
|
|
488
|
-
`
|
|
489
|
-
},
|
|
490
|
-
{
|
|
491
|
-
version: 11,
|
|
492
|
-
description: "Unify agent_sessions into command_executions \u2014 every spawned process is one execution row",
|
|
493
|
-
sql: `
|
|
494
|
-
-- Extend command_executions with the journaling fields previously on agent_sessions.
|
|
495
|
-
-- A NULL workflow_id is allowed because some commands (e.g. sync-reviewers,
|
|
496
|
-
-- create-reviewer) don't tie to a review workflow. Existing rows get NULL by default.
|
|
497
|
-
ALTER TABLE command_executions ADD COLUMN workflow_id TEXT REFERENCES sessions(id) ON DELETE RESTRICT;
|
|
498
|
-
-- parent_id = the dashboard-spawn that's the "Tech Lead" parent of an AI-spawned
|
|
499
|
-
-- session-instance row. NULL for top-level dashboard spawns.
|
|
500
|
-
ALTER TABLE command_executions ADD COLUMN parent_id INTEGER REFERENCES command_executions(id);
|
|
501
|
-
-- Vendor metadata (claude | opencode | gemini | \u2026). NULL for non-AI commands.
|
|
502
|
-
ALTER TABLE command_executions ADD COLUMN vendor TEXT;
|
|
503
|
-
-- The underlying CLI's own session id, captured from stream events.
|
|
504
|
-
-- Used for resume / handoff. Hidden from users (ocr exposes its own id only).
|
|
505
|
-
ALTER TABLE command_executions ADD COLUMN vendor_session_id TEXT;
|
|
506
|
-
-- Persona/instance metadata for AI sub-agents (set when the AI calls
|
|
507
|
-
-- ocr session start-instance). NULL for the parent dashboard spawn.
|
|
508
|
-
ALTER TABLE command_executions ADD COLUMN persona TEXT;
|
|
509
|
-
ALTER TABLE command_executions ADD COLUMN instance_index INTEGER;
|
|
510
|
-
ALTER TABLE command_executions ADD COLUMN name TEXT;
|
|
511
|
-
-- Resolved model string passed to --model post-alias-expansion.
|
|
512
|
-
ALTER TABLE command_executions ADD COLUMN resolved_model TEXT;
|
|
513
|
-
-- Liveness heartbeat. Bumped on every state event the AI emits.
|
|
514
|
-
-- Stale rows past the threshold are reclassified to orphaned (exit_code=-3).
|
|
515
|
-
ALTER TABLE command_executions ADD COLUMN last_heartbeat_at TEXT;
|
|
516
|
-
-- Free-form annotations (sweep notes, host-CLI capability warnings, etc).
|
|
517
|
-
ALTER TABLE command_executions ADD COLUMN notes TEXT;
|
|
518
|
-
CREATE INDEX idx_command_executions_workflow ON command_executions(workflow_id);
|
|
519
|
-
CREATE INDEX idx_command_executions_parent ON command_executions(parent_id);
|
|
520
|
-
CREATE INDEX idx_command_executions_heartbeat ON command_executions(last_heartbeat_at);
|
|
521
|
-
|
|
522
|
-
-- The agent_sessions table is retired. Phase 1 was a parallel journal that
|
|
523
|
-
-- this migration consolidates. We drop the table outright \u2014 the only existing
|
|
524
|
-
-- consumers are the cli helpers and tests, which are updated alongside this
|
|
525
|
-
-- migration. No production deployments have agent_sessions data worth migrating.
|
|
526
|
-
DROP INDEX IF EXISTS idx_agent_sessions_workflow;
|
|
527
|
-
DROP INDEX IF EXISTS idx_agent_sessions_status_heartbeat;
|
|
528
|
-
DROP TABLE IF EXISTS agent_sessions;
|
|
529
|
-
`
|
|
530
|
-
},
|
|
531
|
-
{
|
|
532
|
-
version: 12,
|
|
533
|
-
description: "Event-sourced lifecycle hardening: event_type taxonomy guard, sweep indexes, session_completeness view",
|
|
534
|
-
sql: `
|
|
535
|
-
-- \u2500\u2500 Indexes for the now-periodic stale-session sweep + round derivation \u2500\u2500
|
|
536
|
-
-- The sweep filters sessions by status and rolls up MAX(created_at) per
|
|
537
|
-
-- session over the event log; deriveNextRound does MAX(round). Index both.
|
|
538
|
-
CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
|
|
539
|
-
CREATE INDEX IF NOT EXISTS idx_events_session_created
|
|
540
|
-
ON orchestration_events(session_id, created_at);
|
|
541
|
-
|
|
542
|
-
-- \u2500\u2500 Event-type taxonomy guard \u2500\u2500
|
|
543
|
-
-- orchestration_events.event_type is the spine of all lifecycle
|
|
544
|
-
-- derivation. A typo (e.g. 'round_complete' vs 'round_completed') would
|
|
545
|
-
-- silently break deriveNextRound and the completeness view. SQLite cannot
|
|
546
|
-
-- add a CHECK to an existing column without a table rebuild, so enforce
|
|
547
|
-
-- the closed vocabulary with a BEFORE INSERT trigger instead.
|
|
548
|
-
CREATE TRIGGER IF NOT EXISTS trg_events_known_type
|
|
549
|
-
BEFORE INSERT ON orchestration_events
|
|
550
|
-
WHEN NEW.event_type NOT IN (
|
|
551
|
-
'session_created', 'session_resumed', 'round_started', 'phase_transition',
|
|
552
|
-
'round_completed', 'map_completed', 'session_closed', 'session_aborted',
|
|
553
|
-
'session_auto_closed_stale', 'session_synced', 'session_legacy_import'
|
|
554
|
-
)
|
|
555
|
-
BEGIN
|
|
556
|
-
SELECT RAISE(ABORT, 'unknown orchestration_events.event_type');
|
|
557
|
-
END;
|
|
558
|
-
|
|
559
|
-
-- \u2500\u2500 Close-guard (DB backstop for the completion invariant) \u2500\u2500
|
|
560
|
-
-- A session cannot transition active \u2192 closed unless its current
|
|
561
|
-
-- round/run has a terminal artifact event, OR an explicit reason event
|
|
562
|
-
-- (abort / auto-close-stale / sync / legacy-import) is present. Only a
|
|
563
|
-
-- *silent* premature close is banned \u2014 every legitimate non-artifact
|
|
564
|
-
-- close carries a reason event and passes. App-level guards in
|
|
565
|
-
-- stateClose/finish are the primary check; this makes the illegal state
|
|
566
|
-
-- unrepresentable even via raw SQL.
|
|
567
|
-
--
|
|
568
|
-
-- DEFENCE-IN-DEPTH NOTE (intentional, documented gap): the reason-event
|
|
569
|
-
-- branch below (event_type IN (...)) is NOT round-scoped \u2014 a reason event
|
|
570
|
-
-- recorded for an earlier round would also satisfy a later close. The
|
|
571
|
-
-- app-level guards ARE round-scoped (hasCompletionInvariant checks the
|
|
572
|
-
-- current round/run), so the precise check lives in the application; this
|
|
573
|
-
-- trigger is a coarse backstop against a *silent* premature close via raw
|
|
574
|
-
-- SQL. Tightening it to be round-scoped would require a new migration
|
|
575
|
-
-- (this v12 trigger is append-only and already shipped); the residual
|
|
576
|
-
-- risk is a non-artifact close carrying a stale reason event, which is
|
|
577
|
-
-- still an explicit, audited terminal \u2014 not the failure mode this guards.
|
|
578
|
-
CREATE TRIGGER IF NOT EXISTS trg_sessions_close_guard
|
|
579
|
-
BEFORE UPDATE OF status ON sessions
|
|
580
|
-
WHEN NEW.status = 'closed' AND OLD.status <> 'closed'
|
|
581
|
-
AND NOT EXISTS (
|
|
582
|
-
SELECT 1 FROM orchestration_events e
|
|
583
|
-
WHERE e.session_id = NEW.id
|
|
584
|
-
AND (
|
|
585
|
-
(NEW.workflow_type = 'review' AND e.event_type = 'round_completed' AND e.round = NEW.current_round)
|
|
586
|
-
OR (NEW.workflow_type = 'map' AND e.event_type = 'map_completed' AND e.round = NEW.current_map_run)
|
|
587
|
-
OR e.event_type IN ('session_aborted','session_auto_closed_stale','session_synced','session_legacy_import')
|
|
588
|
-
)
|
|
589
|
-
)
|
|
590
|
-
BEGIN
|
|
591
|
-
SELECT RAISE(ABORT, 'cannot close session without a completed round/run or an explicit reason event');
|
|
592
|
-
END;
|
|
593
|
-
|
|
594
|
-
-- \u2500\u2500 session_completeness view \u2500\u2500
|
|
595
|
-
-- The published contract for "is this session actually complete, and if
|
|
596
|
-
-- not, what's missing". Completion is DERIVED from the event log, never a
|
|
597
|
-
-- mutable flag: a session is complete iff it is closed AND a terminal
|
|
598
|
-
-- artifact event exists for its current round/run. The dashboard's
|
|
599
|
-
-- outcome derivation and the agent 'status' command read this view, so
|
|
600
|
-
-- they cannot disagree.
|
|
601
|
-
--
|
|
602
|
-
-- completeness_state is an INTENTIONAL HYBRID: it combines the mutable
|
|
603
|
-
-- status column (marked_closed) with append-only event evidence (the
|
|
604
|
-
-- terminal artifact event). This is sound precisely because the
|
|
605
|
-
-- close-guard trigger above makes the status column trustworthy \u2014 a row
|
|
606
|
-
-- can only reach status='closed' with a completed round/run or an
|
|
607
|
-
-- explicit reason event \u2014 so reading the column is not a regression to
|
|
608
|
-
-- the old "mutable flag that could lie" model.
|
|
609
|
-
--
|
|
610
|
-
-- completeness_state:
|
|
611
|
-
-- 'complete' \u2014 closed + terminal artifact for current round/run
|
|
612
|
-
-- 'closed_without_artifact' \u2014 closed but no terminal artifact (the
|
|
613
|
-
-- "completed too soon" condition)
|
|
614
|
-
-- 'in_flight' \u2014 open with a dependent process still running
|
|
615
|
-
-- 'open_no_artifact' \u2014 open, no in-flight dependents
|
|
616
|
-
CREATE VIEW IF NOT EXISTS session_completeness AS
|
|
617
|
-
SELECT
|
|
618
|
-
s.id AS session_id,
|
|
619
|
-
s.workflow_type AS workflow_type,
|
|
620
|
-
s.status AS status,
|
|
621
|
-
s.current_round AS current_round,
|
|
622
|
-
s.current_map_run AS current_map_run,
|
|
623
|
-
CASE WHEN EXISTS (
|
|
624
|
-
SELECT 1 FROM orchestration_events e
|
|
625
|
-
WHERE e.session_id = s.id
|
|
626
|
-
AND (
|
|
627
|
-
(s.workflow_type = 'review' AND e.event_type = 'round_completed' AND e.round = s.current_round)
|
|
628
|
-
OR (s.workflow_type = 'map' AND e.event_type = 'map_completed' AND e.round = s.current_map_run)
|
|
629
|
-
)
|
|
630
|
-
) THEN 1 ELSE 0 END AS has_terminal_artifact,
|
|
631
|
-
CASE WHEN s.status = 'closed' THEN 1 ELSE 0 END AS marked_closed,
|
|
632
|
-
CASE WHEN NOT EXISTS (
|
|
633
|
-
SELECT 1 FROM command_executions ce
|
|
634
|
-
WHERE ce.workflow_id = s.id AND ce.finished_at IS NULL
|
|
635
|
-
) THEN 1 ELSE 0 END AS dependents_settled,
|
|
636
|
-
CASE
|
|
637
|
-
WHEN s.status = 'closed' AND EXISTS (
|
|
638
|
-
SELECT 1 FROM orchestration_events e
|
|
639
|
-
WHERE e.session_id = s.id
|
|
640
|
-
AND (
|
|
641
|
-
(s.workflow_type = 'review' AND e.event_type = 'round_completed' AND e.round = s.current_round)
|
|
642
|
-
OR (s.workflow_type = 'map' AND e.event_type = 'map_completed' AND e.round = s.current_map_run)
|
|
643
|
-
)
|
|
644
|
-
) THEN 'complete'
|
|
645
|
-
WHEN s.status = 'closed' THEN 'closed_without_artifact'
|
|
646
|
-
WHEN EXISTS (
|
|
647
|
-
SELECT 1 FROM command_executions ce
|
|
648
|
-
WHERE ce.workflow_id = s.id AND ce.finished_at IS NULL
|
|
649
|
-
) THEN 'in_flight'
|
|
650
|
-
ELSE 'open_no_artifact'
|
|
651
|
-
END AS completeness_state
|
|
652
|
-
FROM sessions s;
|
|
653
|
-
`
|
|
654
|
-
},
|
|
655
|
-
{
|
|
656
|
-
version: 13,
|
|
657
|
-
description: "Retire dead parent_id column on command_executions (never written; row kind is derived from command)",
|
|
658
|
-
// parent_id was reserved for an AI-instance → dashboard-spawn lineage link
|
|
659
|
-
// that was never wired (no writer, no reader). A process's KIND (supervisor
|
|
660
|
-
// / reviewer-instance / utility) is derived from columns that are always
|
|
661
|
-
// present (command + last_heartbeat_at), so the dead lineage column and its
|
|
662
|
-
// all-NULL index are removed. Re-add a wired parent_id alongside a real
|
|
663
|
-
// consumer (e.g. a parent→child tree view) if lineage is ever needed.
|
|
664
|
-
//
|
|
665
|
-
// Imperative + guarded so the DROP COLUMN (which SQLite can't express as
|
|
666
|
-
// IF EXISTS) is idempotent under re-application.
|
|
667
|
-
run: (db) => {
|
|
668
|
-
if (!columnExists(db, "command_executions", "parent_id")) return;
|
|
669
|
-
db.run("DROP INDEX IF EXISTS idx_command_executions_parent;");
|
|
670
|
-
db.run("ALTER TABLE command_executions DROP COLUMN parent_id;");
|
|
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
|
-
}
|
|
701
|
-
}
|
|
702
|
-
];
|
|
703
|
-
function columnExists(db, table, column) {
|
|
704
|
-
const result = db.exec(`PRAGMA table_info(${table})`);
|
|
705
|
-
const first = result[0];
|
|
706
|
-
if (!first) return false;
|
|
707
|
-
const nameIdx = first.columns.indexOf("name");
|
|
708
|
-
return first.values.some((row) => row[nameIdx] === column);
|
|
709
|
-
}
|
|
710
|
-
function ensureSchemaVersionTable(db) {
|
|
711
|
-
db.run(`
|
|
712
|
-
CREATE TABLE IF NOT EXISTS schema_version (
|
|
713
|
-
version INTEGER PRIMARY KEY,
|
|
714
|
-
applied_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
715
|
-
description TEXT NOT NULL
|
|
716
|
-
);
|
|
717
|
-
`);
|
|
718
|
-
}
|
|
719
|
-
function getSchemaVersion(db) {
|
|
720
|
-
ensureSchemaVersionTable(db);
|
|
721
|
-
return getCurrentVersion(db);
|
|
722
|
-
}
|
|
723
|
-
function getCurrentVersion(db) {
|
|
724
|
-
const result = db.exec(
|
|
725
|
-
"SELECT MAX(version) as v FROM schema_version"
|
|
726
|
-
);
|
|
727
|
-
if (result.length === 0 || result[0]?.values.length === 0) {
|
|
728
|
-
return 0;
|
|
729
|
-
}
|
|
730
|
-
const val = result[0]?.values[0]?.[0];
|
|
731
|
-
return typeof val === "number" ? val : 0;
|
|
732
|
-
}
|
|
733
|
-
function runMigrations(db) {
|
|
734
|
-
ensureSchemaVersionTable(db);
|
|
735
|
-
const currentVersion = getCurrentVersion(db);
|
|
736
|
-
for (const migration of MIGRATIONS) {
|
|
737
|
-
if (migration.version <= currentVersion) {
|
|
738
|
-
continue;
|
|
739
|
-
}
|
|
740
|
-
db.run("BEGIN IMMEDIATE;");
|
|
741
|
-
try {
|
|
742
|
-
if (migration.sql) db.run(migration.sql);
|
|
743
|
-
migration.run?.(db);
|
|
744
|
-
db.run(
|
|
745
|
-
"INSERT INTO schema_version (version, description) VALUES (?, ?);",
|
|
746
|
-
[migration.version, migration.description]
|
|
747
|
-
);
|
|
748
|
-
db.run("COMMIT;");
|
|
749
|
-
} catch (error) {
|
|
750
|
-
db.run("ROLLBACK;");
|
|
751
|
-
throw error;
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
// src/lib/db/reconcile.ts
|
|
757
|
-
import { existsSync } from "node:fs";
|
|
758
|
-
import { isAbsolute, join, dirname } from "node:path";
|
|
759
|
-
|
|
760
|
-
// src/lib/db/result-mapper.ts
|
|
761
|
-
function resultToRows(result) {
|
|
762
|
-
if (result.length === 0 || !result[0]) {
|
|
763
|
-
return [];
|
|
764
|
-
}
|
|
765
|
-
const { columns, values } = result[0];
|
|
766
|
-
return values.map((row) => {
|
|
767
|
-
const obj = {};
|
|
768
|
-
for (let i = 0; i < columns.length; i++) {
|
|
769
|
-
obj[columns[i]] = row[i];
|
|
770
|
-
}
|
|
771
|
-
return obj;
|
|
772
|
-
});
|
|
773
|
-
}
|
|
774
|
-
function resultToRow(result) {
|
|
775
|
-
const rows = resultToRows(result);
|
|
776
|
-
return rows[0];
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
// src/lib/db/queries.ts
|
|
780
|
-
function insertSession(db, params) {
|
|
781
|
-
const {
|
|
782
|
-
id,
|
|
783
|
-
branch,
|
|
784
|
-
workflow_type,
|
|
785
|
-
current_phase = "context",
|
|
786
|
-
phase_number = 1,
|
|
787
|
-
current_round = 1,
|
|
788
|
-
current_map_run = 1,
|
|
789
|
-
session_dir
|
|
790
|
-
} = params;
|
|
791
|
-
db.run(
|
|
792
|
-
`INSERT INTO sessions (id, branch, workflow_type, current_phase, phase_number, current_round, current_map_run, session_dir)
|
|
793
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
794
|
-
[id, branch, workflow_type, current_phase, phase_number, current_round, current_map_run, session_dir]
|
|
795
|
-
);
|
|
796
|
-
}
|
|
797
|
-
function updateSession(db, id, params) {
|
|
798
|
-
const setClauses = [];
|
|
799
|
-
const values = [];
|
|
800
|
-
if (params.status !== void 0) {
|
|
801
|
-
setClauses.push("status = ?");
|
|
802
|
-
values.push(params.status);
|
|
803
|
-
}
|
|
804
|
-
if (params.current_phase !== void 0) {
|
|
805
|
-
setClauses.push("current_phase = ?");
|
|
806
|
-
values.push(params.current_phase);
|
|
807
|
-
}
|
|
808
|
-
if (params.phase_number !== void 0) {
|
|
809
|
-
setClauses.push("phase_number = ?");
|
|
810
|
-
values.push(params.phase_number);
|
|
811
|
-
}
|
|
812
|
-
if (params.current_round !== void 0) {
|
|
813
|
-
setClauses.push("current_round = ?");
|
|
814
|
-
values.push(params.current_round);
|
|
815
|
-
}
|
|
816
|
-
if (params.current_map_run !== void 0) {
|
|
817
|
-
setClauses.push("current_map_run = ?");
|
|
818
|
-
values.push(params.current_map_run);
|
|
819
|
-
}
|
|
820
|
-
if (setClauses.length === 0) {
|
|
821
|
-
return;
|
|
822
|
-
}
|
|
823
|
-
setClauses.push("updated_at = datetime('now')");
|
|
824
|
-
values.push(id);
|
|
825
|
-
db.run(
|
|
826
|
-
`UPDATE sessions SET ${setClauses.join(", ")} WHERE id = ?`,
|
|
827
|
-
values
|
|
828
|
-
);
|
|
829
|
-
}
|
|
830
|
-
function getSession(db, id) {
|
|
831
|
-
return resultToRow(
|
|
832
|
-
db.exec("SELECT * FROM sessions WHERE id = ?", [id])
|
|
833
|
-
);
|
|
834
|
-
}
|
|
835
|
-
function getLatestActiveSession(db) {
|
|
836
|
-
return resultToRow(
|
|
837
|
-
db.exec(
|
|
838
|
-
"SELECT * FROM sessions WHERE status = 'active' ORDER BY started_at DESC LIMIT 1"
|
|
839
|
-
)
|
|
840
|
-
);
|
|
841
|
-
}
|
|
842
|
-
function getAllSessions(db) {
|
|
843
|
-
return resultToRows(
|
|
844
|
-
db.exec("SELECT * FROM sessions ORDER BY started_at DESC")
|
|
845
|
-
);
|
|
846
|
-
}
|
|
847
|
-
function insertEvent(db, params) {
|
|
848
|
-
const {
|
|
849
|
-
session_id,
|
|
850
|
-
event_type,
|
|
851
|
-
phase,
|
|
852
|
-
phase_number,
|
|
853
|
-
round,
|
|
854
|
-
metadata
|
|
855
|
-
} = params;
|
|
856
|
-
db.run(
|
|
857
|
-
`INSERT INTO orchestration_events (session_id, event_type, phase, phase_number, round, metadata)
|
|
858
|
-
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
859
|
-
[
|
|
860
|
-
session_id,
|
|
861
|
-
event_type,
|
|
862
|
-
phase ?? null,
|
|
863
|
-
phase_number ?? null,
|
|
864
|
-
round ?? null,
|
|
865
|
-
metadata ?? null
|
|
866
|
-
]
|
|
867
|
-
);
|
|
868
|
-
}
|
|
869
|
-
function getEventsForSession(db, sessionId) {
|
|
870
|
-
return resultToRows(
|
|
871
|
-
db.exec(
|
|
872
|
-
"SELECT * FROM orchestration_events WHERE session_id = ? ORDER BY id ASC",
|
|
873
|
-
[sessionId]
|
|
874
|
-
)
|
|
875
|
-
);
|
|
876
|
-
}
|
|
877
|
-
function getLatestEventId(db) {
|
|
878
|
-
const result = db.exec(
|
|
879
|
-
"SELECT MAX(id) FROM orchestration_events"
|
|
880
|
-
);
|
|
881
|
-
if (result.length === 0 || result[0]?.values.length === 0) {
|
|
882
|
-
return 0;
|
|
883
|
-
}
|
|
884
|
-
const val = result[0]?.values[0]?.[0];
|
|
885
|
-
return typeof val === "number" ? val : 0;
|
|
886
|
-
}
|
|
887
|
-
function commitReasonClose(db, sessionId, reasonEvent, projectionUpdates) {
|
|
888
|
-
db.transaction(() => {
|
|
889
|
-
insertEvent(db, { session_id: sessionId, ...reasonEvent });
|
|
890
|
-
updateSession(db, sessionId, projectionUpdates);
|
|
891
|
-
});
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
// src/lib/db/reconcile.ts
|
|
895
|
-
var DEFAULT_STALE_THRESHOLD_SECONDS = 7 * 24 * 60 * 60;
|
|
896
|
-
function hasTerminalArtifactEvent(db, sessionId, workflowType, currentRound, currentMapRun) {
|
|
897
|
-
const eventType = workflowType === "map" ? "map_completed" : "round_completed";
|
|
898
|
-
const round = workflowType === "map" ? currentMapRun : currentRound;
|
|
899
|
-
const r = db.exec(
|
|
900
|
-
`SELECT 1 FROM orchestration_events
|
|
901
|
-
WHERE session_id = ? AND event_type = ? AND round = ? LIMIT 1`,
|
|
902
|
-
[sessionId, eventType, round]
|
|
903
|
-
);
|
|
904
|
-
return (r[0]?.values.length ?? 0) > 0;
|
|
905
|
-
}
|
|
906
|
-
function hasReasonEvent(db, sessionId) {
|
|
907
|
-
const r = db.exec(
|
|
908
|
-
`SELECT 1 FROM orchestration_events
|
|
909
|
-
WHERE session_id = ?
|
|
910
|
-
AND event_type IN ('session_aborted','session_auto_closed_stale','session_synced','session_legacy_import')
|
|
911
|
-
LIMIT 1`,
|
|
912
|
-
[sessionId]
|
|
913
|
-
);
|
|
914
|
-
return (r[0]?.values.length ?? 0) > 0;
|
|
915
|
-
}
|
|
916
|
-
function lastEventAgeSeconds(db, sessionId) {
|
|
917
|
-
const r = db.exec(
|
|
918
|
-
`SELECT (julianday('now') - julianday(MAX(created_at))) * 86400
|
|
919
|
-
FROM orchestration_events WHERE session_id = ?`,
|
|
920
|
-
[sessionId]
|
|
921
|
-
);
|
|
922
|
-
const v = r[0]?.values[0]?.[0];
|
|
923
|
-
return typeof v === "number" ? v : null;
|
|
924
|
-
}
|
|
925
|
-
function hasInFlightDependents(db, sessionId) {
|
|
926
|
-
const r = db.exec(
|
|
927
|
-
`SELECT 1 FROM command_executions
|
|
928
|
-
WHERE workflow_id = ? AND finished_at IS NULL LIMIT 1`,
|
|
929
|
-
[sessionId]
|
|
930
|
-
);
|
|
931
|
-
return (r[0]?.values.length ?? 0) > 0;
|
|
932
|
-
}
|
|
933
|
-
function resolveSessionDir(ocrDir, sessionDir) {
|
|
934
|
-
if (!sessionDir) return null;
|
|
935
|
-
if (isAbsolute(sessionDir)) return sessionDir;
|
|
936
|
-
return join(dirname(ocrDir), sessionDir);
|
|
937
|
-
}
|
|
938
|
-
function reconcileLegacyState(db, ocrDir, opts = {}) {
|
|
939
|
-
const dryRun = opts.dryRun ?? false;
|
|
940
|
-
const threshold = opts.staleThresholdSeconds ?? DEFAULT_STALE_THRESHOLD_SECONDS;
|
|
941
|
-
const actions = [];
|
|
942
|
-
for (const s of getAllSessions(db)) {
|
|
943
|
-
const dir = resolveSessionDir(ocrDir, s.session_dir);
|
|
944
|
-
if (s.status === "closed") {
|
|
945
|
-
if (hasTerminalArtifactEvent(db, s.id, s.workflow_type, s.current_round, s.current_map_run) || hasReasonEvent(db, s.id)) {
|
|
946
|
-
continue;
|
|
947
|
-
}
|
|
948
|
-
const reviewFinal = s.workflow_type === "review" && dir ? existsSync(join(dir, "rounds", `round-${s.current_round}`, "final.md")) : false;
|
|
949
|
-
const mapFinal = s.workflow_type === "map" && dir ? existsSync(join(dir, "map", "runs", `run-${s.current_map_run}`, "map.md")) : false;
|
|
950
|
-
if (reviewFinal) {
|
|
951
|
-
actions.push({
|
|
952
|
-
sessionId: s.id,
|
|
953
|
-
kind: "synthesize-round-completed",
|
|
954
|
-
detail: `final.md present for round ${s.current_round}; synthesizing round_completed`
|
|
955
|
-
});
|
|
956
|
-
if (!dryRun) {
|
|
957
|
-
insertEvent(db, {
|
|
958
|
-
session_id: s.id,
|
|
959
|
-
event_type: "round_completed",
|
|
960
|
-
phase: "synthesis",
|
|
961
|
-
phase_number: 7,
|
|
962
|
-
round: s.current_round,
|
|
963
|
-
metadata: JSON.stringify({ source: "reconciled", synthesized_from: "final.md" })
|
|
964
|
-
});
|
|
965
|
-
}
|
|
966
|
-
} else if (mapFinal) {
|
|
967
|
-
actions.push({
|
|
968
|
-
sessionId: s.id,
|
|
969
|
-
kind: "synthesize-map-completed",
|
|
970
|
-
detail: `map.md present for run ${s.current_map_run}; synthesizing map_completed`
|
|
971
|
-
});
|
|
972
|
-
if (!dryRun) {
|
|
973
|
-
insertEvent(db, {
|
|
974
|
-
session_id: s.id,
|
|
975
|
-
event_type: "map_completed",
|
|
976
|
-
phase: "synthesis",
|
|
977
|
-
phase_number: 5,
|
|
978
|
-
round: s.current_map_run,
|
|
979
|
-
metadata: JSON.stringify({ source: "reconciled", synthesized_from: "map.md" })
|
|
980
|
-
});
|
|
981
|
-
}
|
|
982
|
-
} else {
|
|
983
|
-
actions.push({
|
|
984
|
-
sessionId: s.id,
|
|
985
|
-
kind: "grandfather",
|
|
986
|
-
detail: "no provable artifact; recording session_legacy_import"
|
|
987
|
-
});
|
|
988
|
-
if (!dryRun) {
|
|
989
|
-
insertEvent(db, {
|
|
990
|
-
session_id: s.id,
|
|
991
|
-
event_type: "session_legacy_import",
|
|
992
|
-
phase: "complete",
|
|
993
|
-
metadata: JSON.stringify({ source: "reconciled" })
|
|
994
|
-
});
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
continue;
|
|
998
|
-
}
|
|
999
|
-
const age = lastEventAgeSeconds(db, s.id);
|
|
1000
|
-
const stale = (age === null || age > threshold) && !hasInFlightDependents(db, s.id);
|
|
1001
|
-
if (stale) {
|
|
1002
|
-
actions.push({
|
|
1003
|
-
sessionId: s.id,
|
|
1004
|
-
kind: "stale-close",
|
|
1005
|
-
detail: age === null ? "active with no events and no in-flight dependents" : `active, last event ${Math.round(age / 86400)}d ago, no in-flight dependents`
|
|
1006
|
-
});
|
|
1007
|
-
if (!dryRun) {
|
|
1008
|
-
commitReasonClose(
|
|
1009
|
-
db,
|
|
1010
|
-
s.id,
|
|
1011
|
-
{
|
|
1012
|
-
event_type: "session_auto_closed_stale",
|
|
1013
|
-
phase: "complete",
|
|
1014
|
-
metadata: JSON.stringify({ source: "reconciled", threshold_seconds: threshold })
|
|
1015
|
-
},
|
|
1016
|
-
{ status: "closed", current_phase: "complete" }
|
|
1017
|
-
);
|
|
1018
|
-
}
|
|
1019
|
-
}
|
|
1020
|
-
}
|
|
1021
|
-
return { dryRun, actions };
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
// src/lib/db/liveness.ts
|
|
1025
|
-
var PID_REUSE_GUARD_MS = 24 * 60 * 60 * 1e3;
|
|
1026
|
-
function defaultIsAlive(pid) {
|
|
1027
|
-
try {
|
|
1028
|
-
process.kill(pid, 0);
|
|
1029
|
-
return true;
|
|
1030
|
-
} catch (err) {
|
|
1031
|
-
return !(err instanceof Error && "code" in err && err.code === "ESRCH");
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
function sqliteUtcMs(ts) {
|
|
1035
|
-
const sqliteShape = ts.includes(" ");
|
|
1036
|
-
return new Date(sqliteShape ? ts.replace(" ", "T") + "Z" : ts).getTime();
|
|
1037
|
-
}
|
|
1038
|
-
|
|
1039
|
-
// src/lib/state/exit-codes.ts
|
|
1040
|
-
var STATE_EXIT = {
|
|
1041
|
-
OK: 0,
|
|
1042
|
-
USAGE: 2,
|
|
1043
|
-
AMBIGUOUS: 3,
|
|
1044
|
-
NOT_FOUND: 4,
|
|
1045
|
-
ILLEGAL_TRANSITION: 5,
|
|
1046
|
-
INVARIANT_UNMET: 6,
|
|
1047
|
-
SCHEMA_INVALID: 7,
|
|
1048
|
-
/** Database was locked past the bounded retry budget (SQLITE_BUSY). */
|
|
1049
|
-
BUSY: 8
|
|
1050
|
-
};
|
|
1051
|
-
var StateError = class extends Error {
|
|
1052
|
-
constructor(code, message) {
|
|
1053
|
-
super(message);
|
|
1054
|
-
this.code = code;
|
|
1055
|
-
this.name = "StateError";
|
|
1056
|
-
}
|
|
1057
|
-
};
|
|
1058
|
-
var CANCELLED_EXIT_CODE = -2;
|
|
1059
|
-
var ORPHAN_EXIT_CODE = -3;
|
|
1060
|
-
var CASCADE_CLOSE_EXIT_CODE = -4;
|
|
1061
|
-
var WATCHDOG_DEADLINE_EXIT_CODE = -5;
|
|
1062
|
-
|
|
1063
|
-
// src/lib/db/agent-sessions.ts
|
|
1064
|
-
var NOTE_ORPHAN_PREFIX = "orphaned by liveness sweep";
|
|
1065
|
-
var INSTANCE_COMMAND = "session-instance";
|
|
1066
|
-
function cascadeTerminateExecutions(db, workflowId, exitCode, note) {
|
|
1067
|
-
db.run(
|
|
1068
|
-
`UPDATE command_executions
|
|
1069
|
-
SET finished_at = datetime('now'),
|
|
1070
|
-
exit_code = ?,
|
|
1071
|
-
pid = NULL,
|
|
1072
|
-
notes = COALESCE(notes || char(10), '') || ?
|
|
1073
|
-
WHERE workflow_id = ?
|
|
1074
|
-
AND finished_at IS NULL`,
|
|
1075
|
-
[exitCode, note, workflowId]
|
|
1076
|
-
);
|
|
1077
|
-
}
|
|
1078
|
-
function rowToAgentSession(row) {
|
|
1079
|
-
return {
|
|
1080
|
-
// The OCR-owned id is the `uid` column. Fall back to the integer
|
|
1081
|
-
// primary key for legacy command_executions rows without a uid.
|
|
1082
|
-
id: row.uid ?? String(row.id),
|
|
1083
|
-
workflow_id: row.workflow_id ?? "",
|
|
1084
|
-
vendor: row.vendor ?? "",
|
|
1085
|
-
vendor_session_id: row.vendor_session_id,
|
|
1086
|
-
persona: row.persona,
|
|
1087
|
-
instance_index: row.instance_index,
|
|
1088
|
-
name: row.name,
|
|
1089
|
-
resolved_model: row.resolved_model,
|
|
1090
|
-
phase: null,
|
|
1091
|
-
status: deriveStatus(row),
|
|
1092
|
-
kind: rowKind(row),
|
|
1093
|
-
pid: row.pid,
|
|
1094
|
-
started_at: row.started_at,
|
|
1095
|
-
last_heartbeat_at: row.last_heartbeat_at ?? row.started_at,
|
|
1096
|
-
ended_at: row.finished_at,
|
|
1097
|
-
exit_code: row.exit_code,
|
|
1098
|
-
notes: row.notes
|
|
1099
|
-
};
|
|
1100
|
-
}
|
|
1101
|
-
function deriveStatus(row) {
|
|
1102
|
-
if (row.finished_at === null) {
|
|
1103
|
-
return "running";
|
|
1104
|
-
}
|
|
1105
|
-
if (row.exit_code === ORPHAN_EXIT_CODE) return "orphaned";
|
|
1106
|
-
if (row.exit_code === CANCELLED_EXIT_CODE || row.exit_code === CASCADE_CLOSE_EXIT_CODE) {
|
|
1107
|
-
return "cancelled";
|
|
1108
|
-
}
|
|
1109
|
-
if (row.exit_code === 0) return "done";
|
|
1110
|
-
return "crashed";
|
|
1111
|
-
}
|
|
1112
|
-
function insertAgentSession(db, params) {
|
|
1113
|
-
const {
|
|
1114
|
-
id,
|
|
1115
|
-
workflow_id,
|
|
1116
|
-
vendor,
|
|
1117
|
-
persona = null,
|
|
1118
|
-
instance_index = null,
|
|
1119
|
-
name = null,
|
|
1120
|
-
resolved_model = null,
|
|
1121
|
-
pid = null,
|
|
1122
|
-
notes = null
|
|
1123
|
-
} = params;
|
|
1124
|
-
const command = persona && instance_index !== null ? `${INSTANCE_COMMAND}:${persona}-${instance_index}` : INSTANCE_COMMAND;
|
|
1125
|
-
db.run(
|
|
1126
|
-
`INSERT INTO command_executions
|
|
1127
|
-
(uid, command, args, workflow_id, vendor, persona, instance_index, name,
|
|
1128
|
-
resolved_model, pid, notes, last_heartbeat_at)
|
|
1129
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))`,
|
|
1130
|
-
[
|
|
1131
|
-
id,
|
|
1132
|
-
command,
|
|
1133
|
-
null,
|
|
1134
|
-
workflow_id,
|
|
1135
|
-
vendor,
|
|
1136
|
-
persona,
|
|
1137
|
-
instance_index,
|
|
1138
|
-
name,
|
|
1139
|
-
resolved_model,
|
|
1140
|
-
pid,
|
|
1141
|
-
notes
|
|
1142
|
-
]
|
|
1143
|
-
);
|
|
1144
|
-
}
|
|
1145
|
-
function getAgentSession(db, id) {
|
|
1146
|
-
const row = resultToRow(
|
|
1147
|
-
db.exec(
|
|
1148
|
-
`SELECT * FROM command_executions WHERE uid = ? AND last_heartbeat_at IS NOT NULL`,
|
|
1149
|
-
[id]
|
|
1150
|
-
)
|
|
1151
|
-
);
|
|
1152
|
-
return row ? rowToAgentSession(row) : void 0;
|
|
1153
|
-
}
|
|
1154
|
-
function listAgentSessionsForWorkflow(db, workflowId) {
|
|
1155
|
-
const rows = resultToRows(
|
|
1156
|
-
db.exec(
|
|
1157
|
-
`SELECT * FROM command_executions
|
|
1158
|
-
WHERE workflow_id = ? AND last_heartbeat_at IS NOT NULL
|
|
1159
|
-
ORDER BY started_at ASC, id ASC`,
|
|
1160
|
-
[workflowId]
|
|
1161
|
-
)
|
|
1162
|
-
);
|
|
1163
|
-
return rows.map(rowToAgentSession);
|
|
1164
|
-
}
|
|
1165
|
-
function getLatestAgentSessionWithVendorId(db, workflowId) {
|
|
1166
|
-
const row = resultToRow(
|
|
1167
|
-
db.exec(
|
|
1168
|
-
`SELECT * FROM command_executions
|
|
1169
|
-
WHERE workflow_id = ? AND vendor_session_id IS NOT NULL
|
|
1170
|
-
ORDER BY started_at DESC, id DESC
|
|
1171
|
-
LIMIT 1`,
|
|
1172
|
-
[workflowId]
|
|
1173
|
-
)
|
|
1174
|
-
);
|
|
1175
|
-
return row ? rowToAgentSession(row) : void 0;
|
|
1176
|
-
}
|
|
1177
|
-
function bumpAgentSessionHeartbeat(db, id) {
|
|
1178
|
-
db.run(
|
|
1179
|
-
`UPDATE command_executions
|
|
1180
|
-
SET last_heartbeat_at = datetime('now')
|
|
1181
|
-
WHERE uid = ?`,
|
|
1182
|
-
[id]
|
|
1183
|
-
);
|
|
1184
|
-
}
|
|
1185
|
-
function setAgentSessionVendorId(db, id, vendorSessionId) {
|
|
1186
|
-
const existing = getAgentSession(db, id);
|
|
1187
|
-
if (!existing) {
|
|
1188
|
-
throw new Error(`Agent session not found: ${id}`);
|
|
1189
|
-
}
|
|
1190
|
-
if (existing.vendor_session_id && existing.vendor_session_id !== vendorSessionId) {
|
|
1191
|
-
throw new Error(
|
|
1192
|
-
`Agent session ${id} already bound to vendor session ${existing.vendor_session_id}; refusing to rebind to ${vendorSessionId}`
|
|
1193
|
-
);
|
|
1194
|
-
}
|
|
1195
|
-
db.run(
|
|
1196
|
-
`UPDATE command_executions
|
|
1197
|
-
SET vendor_session_id = ?,
|
|
1198
|
-
last_heartbeat_at = datetime('now')
|
|
1199
|
-
WHERE uid = ?`,
|
|
1200
|
-
[vendorSessionId, id]
|
|
1201
|
-
);
|
|
1202
|
-
}
|
|
1203
|
-
function bindVendorSessionIdOpportunistically(db, vendorSessionId) {
|
|
1204
|
-
const alreadyBound = resultToRow(
|
|
1205
|
-
db.exec(
|
|
1206
|
-
`SELECT c.uid FROM command_executions c
|
|
1207
|
-
INNER JOIN sessions s ON s.id = c.workflow_id
|
|
1208
|
-
WHERE c.vendor_session_id = ?
|
|
1209
|
-
LIMIT 1`,
|
|
1210
|
-
[vendorSessionId]
|
|
1211
|
-
)
|
|
1212
|
-
);
|
|
1213
|
-
if (alreadyBound?.uid) return alreadyBound.uid;
|
|
1214
|
-
const candidate = resultToRow(
|
|
1215
|
-
db.exec(
|
|
1216
|
-
`SELECT c.uid, c.id FROM command_executions c
|
|
1217
|
-
INNER JOIN sessions s ON s.id = c.workflow_id
|
|
1218
|
-
WHERE c.finished_at IS NULL
|
|
1219
|
-
AND c.vendor_session_id IS NULL
|
|
1220
|
-
AND c.last_heartbeat_at IS NOT NULL
|
|
1221
|
-
AND s.status = 'active'
|
|
1222
|
-
ORDER BY c.started_at DESC, c.id DESC
|
|
1223
|
-
LIMIT 1`
|
|
1224
|
-
)
|
|
1225
|
-
);
|
|
1226
|
-
if (!candidate) return null;
|
|
1227
|
-
db.run(
|
|
1228
|
-
`UPDATE command_executions
|
|
1229
|
-
SET vendor_session_id = ?,
|
|
1230
|
-
last_heartbeat_at = datetime('now')
|
|
1231
|
-
WHERE id = ?`,
|
|
1232
|
-
[vendorSessionId, candidate.id]
|
|
1233
|
-
);
|
|
1234
|
-
return candidate.uid ?? String(candidate.id);
|
|
1235
|
-
}
|
|
1236
|
-
function recordVendorSessionIdForExecution(db, executionId, vendorSessionId) {
|
|
1237
|
-
db.run(
|
|
1238
|
-
`UPDATE command_executions
|
|
1239
|
-
SET vendor_session_id = COALESCE(vendor_session_id, ?),
|
|
1240
|
-
last_heartbeat_at = datetime('now')
|
|
1241
|
-
WHERE id = ?`,
|
|
1242
|
-
[vendorSessionId, executionId]
|
|
1243
|
-
);
|
|
1244
|
-
}
|
|
1245
|
-
function linkDashboardInvocationToWorkflow(db, dashboardUid, workflowId) {
|
|
1246
|
-
db.run(
|
|
1247
|
-
`UPDATE command_executions
|
|
1248
|
-
SET workflow_id = COALESCE(workflow_id, ?),
|
|
1249
|
-
last_heartbeat_at = COALESCE(last_heartbeat_at, datetime('now'))
|
|
1250
|
-
WHERE uid = ?`,
|
|
1251
|
-
[workflowId, dashboardUid]
|
|
1252
|
-
);
|
|
1253
|
-
}
|
|
1254
|
-
function setAgentSessionStatus(db, id, status, options = {}) {
|
|
1255
|
-
const { exitCode, note, setEndedAt } = options;
|
|
1256
|
-
const isTerminal = status === "done" || status === "crashed" || status === "cancelled" || status === "orphaned";
|
|
1257
|
-
const stampEnded = setEndedAt ?? isTerminal;
|
|
1258
|
-
let resolvedExit;
|
|
1259
|
-
if (exitCode !== void 0) {
|
|
1260
|
-
resolvedExit = exitCode;
|
|
1261
|
-
} else if (status === "done") {
|
|
1262
|
-
resolvedExit = 0;
|
|
1263
|
-
} else if (status === "cancelled") {
|
|
1264
|
-
resolvedExit = CANCELLED_EXIT_CODE;
|
|
1265
|
-
} else if (status === "orphaned") {
|
|
1266
|
-
resolvedExit = ORPHAN_EXIT_CODE;
|
|
1267
|
-
} else if (status === "crashed") {
|
|
1268
|
-
resolvedExit = 1;
|
|
1269
|
-
} else {
|
|
1270
|
-
resolvedExit = null;
|
|
1271
|
-
}
|
|
1272
|
-
const finishedClause = stampEnded ? ", finished_at = datetime('now')" : "";
|
|
1273
|
-
if (note !== void 0) {
|
|
1274
|
-
db.run(
|
|
1275
|
-
`UPDATE command_executions
|
|
1276
|
-
SET exit_code = ?,
|
|
1277
|
-
notes = COALESCE(notes || char(10), '') || ?
|
|
1278
|
-
${finishedClause}
|
|
1279
|
-
WHERE uid = ?`,
|
|
1280
|
-
[resolvedExit, note, id]
|
|
1281
|
-
);
|
|
1282
|
-
} else {
|
|
1283
|
-
db.run(
|
|
1284
|
-
`UPDATE command_executions
|
|
1285
|
-
SET exit_code = ?
|
|
1286
|
-
${finishedClause}
|
|
1287
|
-
WHERE uid = ?`,
|
|
1288
|
-
[resolvedExit, id]
|
|
1289
|
-
);
|
|
1290
|
-
}
|
|
1291
|
-
}
|
|
1292
|
-
function updateAgentSession(db, id, params) {
|
|
1293
|
-
const setClauses = [];
|
|
1294
|
-
const values = [];
|
|
1295
|
-
if (params.vendor_session_id !== void 0) {
|
|
1296
|
-
setClauses.push("vendor_session_id = ?");
|
|
1297
|
-
values.push(params.vendor_session_id);
|
|
1298
|
-
}
|
|
1299
|
-
if (params.status !== void 0) {
|
|
1300
|
-
setAgentSessionStatus(db, id, params.status, {
|
|
1301
|
-
exitCode: params.exit_code ?? void 0,
|
|
1302
|
-
note: params.notes ?? void 0
|
|
1303
|
-
});
|
|
1304
|
-
return;
|
|
1305
|
-
}
|
|
1306
|
-
if (params.pid !== void 0) {
|
|
1307
|
-
setClauses.push("pid = ?");
|
|
1308
|
-
values.push(params.pid);
|
|
1309
|
-
}
|
|
1310
|
-
if (params.ended_at !== void 0) {
|
|
1311
|
-
setClauses.push("finished_at = ?");
|
|
1312
|
-
values.push(params.ended_at);
|
|
1313
|
-
}
|
|
1314
|
-
if (params.exit_code !== void 0) {
|
|
1315
|
-
setClauses.push("exit_code = ?");
|
|
1316
|
-
values.push(params.exit_code);
|
|
1317
|
-
}
|
|
1318
|
-
if (params.notes !== void 0) {
|
|
1319
|
-
setClauses.push("notes = ?");
|
|
1320
|
-
values.push(params.notes);
|
|
1321
|
-
}
|
|
1322
|
-
if (setClauses.length === 0) return;
|
|
1323
|
-
values.push(id);
|
|
1324
|
-
db.run(
|
|
1325
|
-
`UPDATE command_executions SET ${setClauses.join(", ")} WHERE uid = ?`,
|
|
1326
|
-
values
|
|
1327
|
-
);
|
|
1328
|
-
}
|
|
1329
|
-
function sweepStaleAgentSessions(db, thresholdSeconds, isAlive = defaultIsAlive) {
|
|
1330
|
-
const candidates = resultToRows(
|
|
1331
|
-
db.exec(
|
|
1332
|
-
`SELECT uid, id, pid, started_at, workflow_id, command, last_heartbeat_at
|
|
1333
|
-
FROM command_executions
|
|
1334
|
-
WHERE finished_at IS NULL
|
|
1335
|
-
AND pid IS NOT NULL
|
|
1336
|
-
AND last_heartbeat_at IS NOT NULL
|
|
1337
|
-
AND (julianday('now') - julianday(last_heartbeat_at)) * 86400 > ?`,
|
|
1338
|
-
[thresholdSeconds]
|
|
1339
|
-
)
|
|
1340
|
-
);
|
|
1341
|
-
if (candidates.length === 0) {
|
|
1342
|
-
return { orphanedIds: [], cascadedWorkflowIds: [] };
|
|
1343
|
-
}
|
|
1344
|
-
const reuseCutoffMs = Date.now() - PID_REUSE_GUARD_MS;
|
|
1345
|
-
const dead = candidates.filter((row) => {
|
|
1346
|
-
if (row.pid === null) return false;
|
|
1347
|
-
if (sqliteUtcMs(row.started_at) < reuseCutoffMs) return false;
|
|
1348
|
-
return !isAlive(row.pid);
|
|
1349
|
-
});
|
|
1350
|
-
if (dead.length === 0) {
|
|
1351
|
-
return { orphanedIds: [], cascadedWorkflowIds: [] };
|
|
1352
|
-
}
|
|
1353
|
-
const note = `${NOTE_ORPHAN_PREFIX} (threshold ${thresholdSeconds}s)`;
|
|
1354
|
-
const placeholders = dead.map(() => "?").join(", ");
|
|
1355
|
-
const cascadedWorkflowIds = [];
|
|
1356
|
-
db.transaction(() => {
|
|
1357
|
-
db.run(
|
|
1358
|
-
`UPDATE command_executions
|
|
1359
|
-
SET finished_at = datetime('now'),
|
|
1360
|
-
exit_code = ?,
|
|
1361
|
-
pid = NULL,
|
|
1362
|
-
notes = COALESCE(notes || char(10), '') || ?
|
|
1363
|
-
WHERE id IN (${placeholders})
|
|
1364
|
-
AND finished_at IS NULL`,
|
|
1365
|
-
[ORPHAN_EXIT_CODE, note, ...dead.map((r) => r.id)]
|
|
1366
|
-
);
|
|
1367
|
-
for (const row of dead) {
|
|
1368
|
-
if (row.workflow_id && rowKind(row) === "supervisor") {
|
|
1369
|
-
cascadeTerminateExecutions(
|
|
1370
|
-
db,
|
|
1371
|
-
row.workflow_id,
|
|
1372
|
-
CASCADE_CLOSE_EXIT_CODE,
|
|
1373
|
-
"cascade-closed: workflow process orphaned by liveness sweep"
|
|
1374
|
-
);
|
|
1375
|
-
cascadedWorkflowIds.push(row.workflow_id);
|
|
1376
|
-
}
|
|
1377
|
-
}
|
|
1378
|
-
});
|
|
1379
|
-
return {
|
|
1380
|
-
orphanedIds: dead.map((r) => r.uid ?? String(r.id)),
|
|
1381
|
-
cascadedWorkflowIds
|
|
1382
|
-
};
|
|
1383
|
-
}
|
|
1384
|
-
function rowKind(row) {
|
|
1385
|
-
if (row.command === INSTANCE_COMMAND || row.command.startsWith(`${INSTANCE_COMMAND}:`)) {
|
|
1386
|
-
return "instance";
|
|
1387
|
-
}
|
|
1388
|
-
return row.last_heartbeat_at == null ? "utility" : "supervisor";
|
|
1389
|
-
}
|
|
1390
|
-
function sweepStaleSessions(db, thresholdSeconds) {
|
|
1391
|
-
const sql = `
|
|
1392
|
-
SELECT s.id
|
|
1393
|
-
FROM sessions s
|
|
1394
|
-
LEFT JOIN (
|
|
1395
|
-
SELECT session_id, MAX(created_at) AS last_event_at
|
|
1396
|
-
FROM orchestration_events
|
|
1397
|
-
GROUP BY session_id
|
|
1398
|
-
) e ON e.session_id = s.id
|
|
1399
|
-
WHERE s.status = 'active'
|
|
1400
|
-
AND (
|
|
1401
|
-
e.last_event_at IS NULL
|
|
1402
|
-
OR (julianday('now') - julianday(e.last_event_at)) * 86400 > ?
|
|
1403
|
-
)
|
|
1404
|
-
AND NOT EXISTS (
|
|
1405
|
-
SELECT 1 FROM command_executions ce
|
|
1406
|
-
WHERE ce.workflow_id = s.id
|
|
1407
|
-
AND ce.finished_at IS NULL
|
|
1408
|
-
)
|
|
1409
|
-
`;
|
|
1410
|
-
const rows = resultToRows(db.exec(sql, [thresholdSeconds]));
|
|
1411
|
-
if (rows.length === 0) {
|
|
1412
|
-
return { closedSessionIds: [] };
|
|
1413
|
-
}
|
|
1414
|
-
for (const row of rows) {
|
|
1415
|
-
commitReasonClose(
|
|
1416
|
-
db,
|
|
1417
|
-
row.id,
|
|
1418
|
-
{
|
|
1419
|
-
event_type: "session_auto_closed_stale",
|
|
1420
|
-
phase: "complete",
|
|
1421
|
-
metadata: JSON.stringify({
|
|
1422
|
-
reason: "no events past threshold; no in-flight dependents",
|
|
1423
|
-
threshold_seconds: thresholdSeconds
|
|
1424
|
-
})
|
|
1425
|
-
},
|
|
1426
|
-
{ status: "closed", current_phase: "complete" }
|
|
1427
|
-
);
|
|
1428
|
-
}
|
|
1429
|
-
return { closedSessionIds: rows.map((r) => r.id) };
|
|
1430
|
-
}
|
|
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
|
-
|
|
1852
|
-
// src/lib/db/command-log.ts
|
|
1853
|
-
import { appendFileSync, existsSync as existsSync3, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
1854
|
-
import { dirname as dirname3, join as join3 } from "node:path";
|
|
1855
|
-
import { randomUUID } from "node:crypto";
|
|
1856
|
-
var CACHE_DIR = ".cache";
|
|
1857
|
-
var FILENAME = "command-history.jsonl";
|
|
1858
|
-
var MAX_LINES = 5e3;
|
|
1859
|
-
var KEEP_LINES = 4e3;
|
|
1860
|
-
var approxLineCount = -1;
|
|
1861
|
-
function generateCommandUid() {
|
|
1862
|
-
return randomUUID();
|
|
1863
|
-
}
|
|
1864
|
-
function cacheDir(ocrDir) {
|
|
1865
|
-
return join3(ocrDir, "data", CACHE_DIR);
|
|
1866
|
-
}
|
|
1867
|
-
function commandLogPath(ocrDir) {
|
|
1868
|
-
return join3(cacheDir(ocrDir), FILENAME);
|
|
1869
|
-
}
|
|
1870
|
-
function appendCommandLog(ocrDir, entry) {
|
|
1871
|
-
try {
|
|
1872
|
-
const filePath = commandLogPath(ocrDir);
|
|
1873
|
-
const dir = dirname3(filePath);
|
|
1874
|
-
if (!existsSync3(dir)) mkdirSync(dir, { recursive: true });
|
|
1875
|
-
const line = JSON.stringify(entry) + "\n";
|
|
1876
|
-
appendFileSync(filePath, line, { encoding: "utf-8" });
|
|
1877
|
-
if (approxLineCount >= 0) approxLineCount++;
|
|
1878
|
-
rotateIfNeeded(filePath);
|
|
1879
|
-
} catch {
|
|
1880
|
-
}
|
|
1881
|
-
}
|
|
1882
|
-
function readCommandLog(ocrDir) {
|
|
1883
|
-
const filePath = commandLogPath(ocrDir);
|
|
1884
|
-
if (!existsSync3(filePath)) return [];
|
|
1885
|
-
const content = readFileSync(filePath, "utf-8");
|
|
1886
|
-
const entries = [];
|
|
1887
|
-
for (const line of content.split("\n")) {
|
|
1888
|
-
if (!line.trim()) continue;
|
|
1889
|
-
try {
|
|
1890
|
-
entries.push(JSON.parse(line));
|
|
1891
|
-
} catch {
|
|
1892
|
-
}
|
|
1893
|
-
}
|
|
1894
|
-
return entries;
|
|
1895
|
-
}
|
|
1896
|
-
function replayCommandLog(db, ocrDir) {
|
|
1897
|
-
const entries = readCommandLog(ocrDir);
|
|
1898
|
-
if (entries.length === 0) return 0;
|
|
1899
|
-
const latest = /* @__PURE__ */ new Map();
|
|
1900
|
-
for (const entry of entries) {
|
|
1901
|
-
if (!entry.uid || !entry.command || !entry.started_at) continue;
|
|
1902
|
-
const existing = latest.get(entry.uid);
|
|
1903
|
-
if (!existing || entry.event !== "start") {
|
|
1904
|
-
latest.set(entry.uid, entry);
|
|
1905
|
-
}
|
|
1906
|
-
}
|
|
1907
|
-
let imported = 0;
|
|
1908
|
-
for (const entry of latest.values()) {
|
|
1909
|
-
if (entry.event === "start" && !entry.finished_at) continue;
|
|
1910
|
-
const existing = db.exec(
|
|
1911
|
-
"SELECT COUNT(*) as c FROM command_executions WHERE uid = ?",
|
|
1912
|
-
[entry.uid]
|
|
1913
|
-
);
|
|
1914
|
-
if ((existing[0]?.values[0]?.[0] ?? 0) > 0) continue;
|
|
1915
|
-
db.run(
|
|
1916
|
-
`INSERT INTO command_executions
|
|
1917
|
-
(uid, command, args, exit_code, started_at, finished_at, pid, is_detached)
|
|
1918
|
-
VALUES (?, ?, ?, ?, ?, ?, NULL, ?)`,
|
|
1919
|
-
[
|
|
1920
|
-
entry.uid,
|
|
1921
|
-
entry.command,
|
|
1922
|
-
entry.args,
|
|
1923
|
-
entry.exit_code,
|
|
1924
|
-
entry.started_at,
|
|
1925
|
-
entry.finished_at,
|
|
1926
|
-
entry.is_detached
|
|
1927
|
-
]
|
|
1928
|
-
);
|
|
1929
|
-
imported++;
|
|
1930
|
-
}
|
|
1931
|
-
return imported;
|
|
1932
|
-
}
|
|
1933
|
-
function rotateIfNeeded(filePath) {
|
|
1934
|
-
try {
|
|
1935
|
-
if (approxLineCount >= 0 && approxLineCount <= MAX_LINES) return;
|
|
1936
|
-
const content = readFileSync(filePath, "utf-8");
|
|
1937
|
-
const lines = content.split("\n").filter((l) => l.trim());
|
|
1938
|
-
approxLineCount = lines.length;
|
|
1939
|
-
if (approxLineCount <= MAX_LINES) return;
|
|
1940
|
-
const kept = lines.slice(lines.length - KEEP_LINES);
|
|
1941
|
-
const tmpPath = `${filePath}.${process.pid}.tmp`;
|
|
1942
|
-
writeFileSync(tmpPath, kept.join("\n") + "\n", { encoding: "utf-8" });
|
|
1943
|
-
renameSync(tmpPath, filePath);
|
|
1944
|
-
approxLineCount = KEEP_LINES;
|
|
1945
|
-
} catch {
|
|
1946
|
-
}
|
|
1947
|
-
}
|
|
1948
|
-
|
|
1949
|
-
// src/lib/db/index.ts
|
|
1950
|
-
var V2_SCHEMA_VERSION = 12;
|
|
1951
|
-
function maybeSnapshotBeforeUpgrade(db, dbPath, fromVersion) {
|
|
1952
|
-
if (fromVersion < 1 || fromVersion >= V2_SCHEMA_VERSION) return null;
|
|
1953
|
-
const bakPath = `${dbPath}.bak.v${fromVersion}`;
|
|
1954
|
-
if (existsSync4(bakPath)) return bakPath;
|
|
1955
|
-
try {
|
|
1956
|
-
if (!existsSync4(dbPath) || statSync2(dbPath).size === 0) return null;
|
|
1957
|
-
db.pragma("wal_checkpoint(TRUNCATE)");
|
|
1958
|
-
copyFileSync2(dbPath, bakPath);
|
|
1959
|
-
return bakPath;
|
|
1960
|
-
} catch {
|
|
1961
|
-
return null;
|
|
1962
|
-
}
|
|
1963
|
-
}
|
|
1964
|
-
function formatUpgradeNotice(bakPath, reconcile) {
|
|
1965
|
-
const lines = [
|
|
1966
|
-
"Storage upgraded to v2.0 \u2014 durable SQLite engine (WAL), event-sourced lifecycle."
|
|
1967
|
-
];
|
|
1968
|
-
if (bakPath) {
|
|
1969
|
-
lines.push(` A backup of your previous database was saved to: ${bakPath}`);
|
|
1970
|
-
}
|
|
1971
|
-
const repairs = (reconcile?.actions ?? []).filter((a) => a.kind !== "ok");
|
|
1972
|
-
if (repairs.length > 0) {
|
|
1973
|
-
const n = (kind) => repairs.filter((a) => a.kind === kind).length;
|
|
1974
|
-
const parts = [];
|
|
1975
|
-
const finalized = n("synthesize-round-completed") + n("synthesize-map-completed");
|
|
1976
|
-
if (finalized > 0) parts.push(`${finalized} finalized from artifacts`);
|
|
1977
|
-
if (n("grandfather") > 0) parts.push(`${n("grandfather")} grandfathered`);
|
|
1978
|
-
if (n("stale-close") > 0) parts.push(`${n("stale-close")} stale closed`);
|
|
1979
|
-
lines.push(
|
|
1980
|
-
` Reconciled ${repairs.length} legacy session(s): ${parts.join(", ")}.`
|
|
1981
|
-
);
|
|
1982
|
-
}
|
|
1983
|
-
lines.push(" Run `ocr doctor` to verify the storage engine.");
|
|
1984
|
-
return lines.map((l) => `[ocr] ${l}`).join("\n");
|
|
1985
|
-
}
|
|
1986
|
-
var connections = /* @__PURE__ */ new Map();
|
|
1987
|
-
async function openDatabase(dbPath) {
|
|
1988
|
-
const cached = connections.get(dbPath);
|
|
1989
|
-
if (cached) {
|
|
1990
|
-
return cached;
|
|
1991
|
-
}
|
|
1992
|
-
const dir = dirname4(dbPath);
|
|
1993
|
-
if (!existsSync4(dir)) {
|
|
1994
|
-
mkdirSync2(dir, { recursive: true });
|
|
1995
|
-
}
|
|
1996
|
-
const db = openEngine(dbPath);
|
|
1997
|
-
connections.set(dbPath, db);
|
|
1998
|
-
return db;
|
|
1999
|
-
}
|
|
2000
|
-
async function getDb(ocrDir) {
|
|
2001
|
-
const dbPath = join4(ocrDir, "data", "ocr.db");
|
|
2002
|
-
return openDatabase(dbPath);
|
|
2003
|
-
}
|
|
2004
|
-
async function ensureDatabase(ocrDir) {
|
|
2005
|
-
const dataDir = join4(ocrDir, "data");
|
|
2006
|
-
if (!existsSync4(dataDir)) {
|
|
2007
|
-
mkdirSync2(dataDir, { recursive: true });
|
|
2008
|
-
}
|
|
2009
|
-
const dbPath = join4(dataDir, "ocr.db");
|
|
2010
|
-
const db = await openDatabase(dbPath);
|
|
2011
|
-
let before = 0;
|
|
2012
|
-
try {
|
|
2013
|
-
before = getSchemaVersion(db);
|
|
2014
|
-
} catch {
|
|
2015
|
-
before = 0;
|
|
2016
|
-
}
|
|
2017
|
-
const isLegacyUpgrade = before >= 1 && before < V2_SCHEMA_VERSION;
|
|
2018
|
-
const bakPath = maybeSnapshotBeforeUpgrade(db, dbPath, before);
|
|
2019
|
-
runMigrations(db);
|
|
2020
|
-
let reconcile;
|
|
2021
|
-
if (before < V2_SCHEMA_VERSION) {
|
|
2022
|
-
try {
|
|
2023
|
-
reconcile = reconcileLegacyState(db, ocrDir);
|
|
2024
|
-
} catch (err) {
|
|
2025
|
-
console.error(
|
|
2026
|
-
`[ocr] legacy reconciliation skipped: ${err instanceof Error ? err.message : String(err)}`
|
|
2027
|
-
);
|
|
2028
|
-
}
|
|
2029
|
-
}
|
|
2030
|
-
if (isLegacyUpgrade) {
|
|
2031
|
-
const notice = formatUpgradeNotice(bakPath, reconcile);
|
|
2032
|
-
if (notice) console.error(notice);
|
|
2033
|
-
}
|
|
2034
|
-
return db;
|
|
2035
|
-
}
|
|
2036
|
-
function walCheckpointTruncate(dbPath) {
|
|
2037
|
-
if (!existsSync4(dbPath)) {
|
|
2038
|
-
return "skipped";
|
|
2039
|
-
}
|
|
2040
|
-
const cached = connections.get(dbPath);
|
|
2041
|
-
if (cached) {
|
|
2042
|
-
try {
|
|
2043
|
-
cached.pragma("wal_checkpoint(TRUNCATE)");
|
|
2044
|
-
return "checkpointed";
|
|
2045
|
-
} catch {
|
|
2046
|
-
return "failed";
|
|
2047
|
-
}
|
|
2048
|
-
}
|
|
2049
|
-
let transient;
|
|
2050
|
-
try {
|
|
2051
|
-
transient = openEngine(dbPath);
|
|
2052
|
-
transient.pragma("wal_checkpoint(TRUNCATE)");
|
|
2053
|
-
return "checkpointed";
|
|
2054
|
-
} catch {
|
|
2055
|
-
return "failed";
|
|
2056
|
-
} finally {
|
|
2057
|
-
try {
|
|
2058
|
-
transient?.close();
|
|
2059
|
-
} catch {
|
|
2060
|
-
}
|
|
2061
|
-
}
|
|
2062
|
-
}
|
|
2063
|
-
function closeDatabase(dbPath) {
|
|
2064
|
-
const db = connections.get(dbPath);
|
|
2065
|
-
if (db) {
|
|
2066
|
-
db.close();
|
|
2067
|
-
connections.delete(dbPath);
|
|
2068
|
-
}
|
|
2069
|
-
}
|
|
2070
|
-
function closeAllDatabases() {
|
|
2071
|
-
for (const [path, db] of connections) {
|
|
2072
|
-
db.close();
|
|
2073
|
-
connections.delete(path);
|
|
2074
|
-
}
|
|
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
|
-
}
|
|
2111
|
-
export {
|
|
2112
|
-
CANCELLED_EXIT_CODE,
|
|
2113
|
-
CASCADE_CLOSE_EXIT_CODE,
|
|
2114
|
-
MIGRATIONS,
|
|
2115
|
-
ORPHAN_EXIT_CODE,
|
|
2116
|
-
PID_REUSE_GUARD_MS,
|
|
2117
|
-
STATE_EXIT,
|
|
2118
|
-
StateError,
|
|
2119
|
-
WATCHDOG_DEADLINE_EXIT_CODE,
|
|
2120
|
-
appendCommandLog,
|
|
2121
|
-
bindVendorSessionIdOpportunistically,
|
|
2122
|
-
bumpAgentSessionHeartbeat,
|
|
2123
|
-
cacheDir,
|
|
2124
|
-
cascadeTerminateExecutions,
|
|
2125
|
-
closeAllDatabases,
|
|
2126
|
-
closeDatabase,
|
|
2127
|
-
collectDbHealth,
|
|
2128
|
-
commandLogPath,
|
|
2129
|
-
commitReasonClose,
|
|
2130
|
-
defaultIsAlive,
|
|
2131
|
-
ensureDatabase,
|
|
2132
|
-
fixDb,
|
|
2133
|
-
formatUpgradeNotice,
|
|
2134
|
-
generateCommandUid,
|
|
2135
|
-
getAgentSession,
|
|
2136
|
-
getAllSessions,
|
|
2137
|
-
getDb,
|
|
2138
|
-
getEventsForSession,
|
|
2139
|
-
getLatestActiveSession,
|
|
2140
|
-
getLatestAgentSessionWithVendorId,
|
|
2141
|
-
getLatestEventId,
|
|
2142
|
-
getSchemaVersion,
|
|
2143
|
-
getSession,
|
|
2144
|
-
hasInFlightDependents,
|
|
2145
|
-
insertAgentSession,
|
|
2146
|
-
insertEvent,
|
|
2147
|
-
insertSession,
|
|
2148
|
-
isBusyError,
|
|
2149
|
-
linkDashboardInvocationToWorkflow,
|
|
2150
|
-
listAgentSessionsForWorkflow,
|
|
2151
|
-
openDatabase,
|
|
2152
|
-
probeEngine,
|
|
2153
|
-
probeWrite,
|
|
2154
|
-
pruneBackups,
|
|
2155
|
-
pruneDb,
|
|
2156
|
-
readCommandLog,
|
|
2157
|
-
reapOrphanDbFiles,
|
|
2158
|
-
reapStaleExecLogs,
|
|
2159
|
-
reconcileLegacyState,
|
|
2160
|
-
recordVendorSessionIdForExecution,
|
|
2161
|
-
replayCommandLog,
|
|
2162
|
-
resultToRow,
|
|
2163
|
-
resultToRows,
|
|
2164
|
-
rowKind,
|
|
2165
|
-
runMigrations,
|
|
2166
|
-
setAgentSessionStatus,
|
|
2167
|
-
setAgentSessionVendorId,
|
|
2168
|
-
snapshotDb,
|
|
2169
|
-
sqliteUtcMs,
|
|
2170
|
-
sweepStaleAgentSessions,
|
|
2171
|
-
sweepStaleSessions,
|
|
2172
|
-
updateAgentSession,
|
|
2173
|
-
updateSession,
|
|
2174
|
-
vacuumDb,
|
|
2175
|
-
walCheckpointTruncate,
|
|
2176
|
-
withForeignKeysDisabled
|
|
2177
|
-
};
|