@open-code-review/cli 1.11.0 → 2.1.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 +10 -4
- package/dist/dashboard/client/assets/{_basePickBy-D8RU9s_y.js → _basePickBy-B3ALyupE.js} +1 -1
- package/dist/dashboard/client/assets/{_baseUniq-CjVeYx1J.js → _baseUniq-b2RALAWc.js} +1 -1
- package/dist/dashboard/client/assets/{arc-DsFstmf9.js → arc-DcSVvhUd.js} +1 -1
- package/dist/dashboard/client/assets/{architectureDiagram-VXUJARFQ-iNJB-g1N.js → architectureDiagram-VXUJARFQ-BNUlmSCS.js} +1 -1
- package/dist/dashboard/client/assets/{blockDiagram-VD42YOAC-Zp2Aw0zR.js → blockDiagram-VD42YOAC-BmhiQVwa.js} +1 -1
- package/dist/dashboard/client/assets/{c4Diagram-YG6GDRKO-BGppUmwT.js → c4Diagram-YG6GDRKO-jyJ3WOv5.js} +1 -1
- package/dist/dashboard/client/assets/channel-D3J8-GF_.js +1 -0
- package/dist/dashboard/client/assets/{chunk-4BX2VUAB-CZcRxeE4.js → chunk-4BX2VUAB-x1dQU_s3.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-55IACEB6-CVdL59yY.js → chunk-55IACEB6-CwbsE2XQ.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-B4BG7PRW-CFPp6g6e.js → chunk-B4BG7PRW-BaE7c-ti.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-DI55MBZ5-DH9BzE6I.js → chunk-DI55MBZ5-Bw5PUaMK.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-FMBD7UC4-DZ2DTwqS.js → chunk-FMBD7UC4-B7cF6P3s.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-QN33PNHL-DODPm0CR.js → chunk-QN33PNHL-OY4evNHd.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-QZHKN3VN-CNI_LxUf.js → chunk-QZHKN3VN-BpjQwIWz.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-TZMSLE5B-sxZQF02c.js → chunk-TZMSLE5B-D8b_Oq9B.js} +1 -1
- package/dist/dashboard/client/assets/classDiagram-2ON5EDUG-tkFUL-1Y.js +1 -0
- package/dist/dashboard/client/assets/classDiagram-v2-WZHVMYZB-tkFUL-1Y.js +1 -0
- package/dist/dashboard/client/assets/clone-CkY5ajLr.js +1 -0
- package/dist/dashboard/client/assets/{cose-bilkent-S5V4N54A-BHa2lABH.js → cose-bilkent-S5V4N54A-C-sfP8PN.js} +1 -1
- package/dist/dashboard/client/assets/{dagre-6UL2VRFP-CvCLBtkz.js → dagre-6UL2VRFP-Cqfo0NRg.js} +1 -1
- package/dist/dashboard/client/assets/{diagram-PSM6KHXK-Cklwd4YA.js → diagram-PSM6KHXK-BR3ppxqI.js} +1 -1
- package/dist/dashboard/client/assets/{diagram-QEK2KX5R-3bDERTbp.js → diagram-QEK2KX5R-Dvcx6x3R.js} +1 -1
- package/dist/dashboard/client/assets/{diagram-S2PKOQOG-DbiWlPc6.js → diagram-S2PKOQOG-DoyBLnVN.js} +1 -1
- package/dist/dashboard/client/assets/{erDiagram-Q2GNP2WA-BQa_VNbt.js → erDiagram-Q2GNP2WA-hy77l1cL.js} +1 -1
- package/dist/dashboard/client/assets/{flowDiagram-NV44I4VS-BDaJyl9N.js → flowDiagram-NV44I4VS-Bz0B1rKM.js} +1 -1
- package/dist/dashboard/client/assets/{ganttDiagram-JELNMOA3-DsTnleSr.js → ganttDiagram-JELNMOA3-CLgrZPoC.js} +1 -1
- package/dist/dashboard/client/assets/{gitGraphDiagram-V2S2FVAM-BRuBadgn.js → gitGraphDiagram-V2S2FVAM-DwJ-1f-v.js} +1 -1
- package/dist/dashboard/client/assets/{graph-CYYqXm9c.js → graph-DDBMM_t2.js} +1 -1
- package/dist/dashboard/client/assets/{index-eZMoytob.js → index-Cr9yEo_B.js} +123 -123
- package/dist/dashboard/client/assets/{infoDiagram-HS3SLOUP-CHnA8k7H.js → infoDiagram-HS3SLOUP-Bhn1FmAk.js} +1 -1
- package/dist/dashboard/client/assets/{journeyDiagram-XKPGCS4Q-CAXR1-Ju.js → journeyDiagram-XKPGCS4Q-CzGbjX1y.js} +1 -1
- package/dist/dashboard/client/assets/{kanban-definition-3W4ZIXB7-Clf3HfHz.js → kanban-definition-3W4ZIXB7-Da77-WYk.js} +1 -1
- package/dist/dashboard/client/assets/{layout-DQPaNqnO.js → layout-CVwSB-GS.js} +1 -1
- package/dist/dashboard/client/assets/{linear-qUnNXvWB.js → linear-CTRAc5Jn.js} +1 -1
- package/dist/dashboard/client/assets/{mermaid-renderer-C7Se8vjl.js → mermaid-renderer-Bjo170ax.js} +4 -4
- package/dist/dashboard/client/assets/{mindmap-definition-VGOIOE7T-DBIdG0OR.js → mindmap-definition-VGOIOE7T-B55C2odl.js} +1 -1
- package/dist/dashboard/client/assets/{pieDiagram-ADFJNKIX-DXAIiG6W.js → pieDiagram-ADFJNKIX-5lrQLrSz.js} +1 -1
- package/dist/dashboard/client/assets/{quadrantDiagram-AYHSOK5B-D4yAxif0.js → quadrantDiagram-AYHSOK5B-Bg55gC30.js} +1 -1
- package/dist/dashboard/client/assets/{requirementDiagram-UZGBJVZJ-D27ME1VO.js → requirementDiagram-UZGBJVZJ-CyR4YFJY.js} +1 -1
- package/dist/dashboard/client/assets/{sankeyDiagram-TZEHDZUN-BeEaA_QM.js → sankeyDiagram-TZEHDZUN-BVWKr9_-.js} +1 -1
- package/dist/dashboard/client/assets/{sequenceDiagram-WL72ISMW-GTI12qU0.js → sequenceDiagram-WL72ISMW-D0AJg_tE.js} +1 -1
- package/dist/dashboard/client/assets/{stateDiagram-FKZM4ZOC-ClSoeZM0.js → stateDiagram-FKZM4ZOC-BuHpTgim.js} +1 -1
- package/dist/dashboard/client/assets/stateDiagram-v2-4FDKWEC3-DwAPhteN.js +1 -0
- package/dist/dashboard/client/assets/{timeline-definition-IT6M3QCI-cj5d_Kyh.js → timeline-definition-IT6M3QCI-LDhpAmDd.js} +1 -1
- package/dist/dashboard/client/assets/{treemap-GDKQZRPO-BrRT1igb.js → treemap-GDKQZRPO-Dd4gjvUl.js} +1 -1
- package/dist/dashboard/client/assets/{xychartDiagram-PRI3JC2R-DlzGitHh.js → xychartDiagram-PRI3JC2R-B9RDod39.js} +1 -1
- package/dist/dashboard/client/index.html +1 -1
- package/dist/dashboard/server.js +1232 -656
- package/dist/index.js +1905 -711
- package/dist/lib/db/index.js +794 -100
- package/package.json +3 -5
- package/dist/dashboard/client/assets/channel-C8plpfdz.js +0 -1
- package/dist/dashboard/client/assets/classDiagram-2ON5EDUG-Dqn6u1oQ.js +0 -1
- package/dist/dashboard/client/assets/classDiagram-v2-WZHVMYZB-Dqn6u1oQ.js +0 -1
- package/dist/dashboard/client/assets/clone-BQ8hOLqM.js +0 -1
- package/dist/dashboard/client/assets/stateDiagram-v2-4FDKWEC3-Bim3s-dq.js +0 -1
package/dist/dashboard/server.js
CHANGED
|
@@ -18413,7 +18413,7 @@ var require_view = __commonJS({
|
|
|
18413
18413
|
var dirname14 = path2.dirname;
|
|
18414
18414
|
var basename4 = path2.basename;
|
|
18415
18415
|
var extname = path2.extname;
|
|
18416
|
-
var
|
|
18416
|
+
var join19 = path2.join;
|
|
18417
18417
|
var resolve3 = path2.resolve;
|
|
18418
18418
|
module.exports = View;
|
|
18419
18419
|
function View(name, options) {
|
|
@@ -18461,12 +18461,12 @@ var require_view = __commonJS({
|
|
|
18461
18461
|
};
|
|
18462
18462
|
View.prototype.resolve = function resolve4(dir, file) {
|
|
18463
18463
|
var ext = this.ext;
|
|
18464
|
-
var path3 =
|
|
18464
|
+
var path3 = join19(dir, file);
|
|
18465
18465
|
var stat = tryStat(path3);
|
|
18466
18466
|
if (stat && stat.isFile()) {
|
|
18467
18467
|
return path3;
|
|
18468
18468
|
}
|
|
18469
|
-
path3 =
|
|
18469
|
+
path3 = join19(dir, basename4(file, ext), "index" + ext);
|
|
18470
18470
|
stat = tryStat(path3);
|
|
18471
18471
|
if (stat && stat.isFile()) {
|
|
18472
18472
|
return path3;
|
|
@@ -19099,7 +19099,7 @@ var require_send = __commonJS({
|
|
|
19099
19099
|
var Stream = __require("stream");
|
|
19100
19100
|
var util = __require("util");
|
|
19101
19101
|
var extname = path2.extname;
|
|
19102
|
-
var
|
|
19102
|
+
var join19 = path2.join;
|
|
19103
19103
|
var normalize = path2.normalize;
|
|
19104
19104
|
var resolve3 = path2.resolve;
|
|
19105
19105
|
var sep = path2.sep;
|
|
@@ -19318,7 +19318,7 @@ var require_send = __commonJS({
|
|
|
19318
19318
|
return res;
|
|
19319
19319
|
}
|
|
19320
19320
|
parts = path3.split(sep);
|
|
19321
|
-
path3 = normalize(
|
|
19321
|
+
path3 = normalize(join19(root, path3));
|
|
19322
19322
|
} else {
|
|
19323
19323
|
if (UP_PATH_REGEXP.test(path3)) {
|
|
19324
19324
|
debug('malicious path "%s"', path3);
|
|
@@ -19453,7 +19453,7 @@ var require_send = __commonJS({
|
|
|
19453
19453
|
if (err) return self.onStatError(err);
|
|
19454
19454
|
return self.error(404);
|
|
19455
19455
|
}
|
|
19456
|
-
var p =
|
|
19456
|
+
var p = join19(path3, self._index[i]);
|
|
19457
19457
|
debug('stat "%s"', p);
|
|
19458
19458
|
fs6.stat(p, function(err2, stat) {
|
|
19459
19459
|
if (err2) return next(err2);
|
|
@@ -21876,7 +21876,7 @@ var require_response = __commonJS({
|
|
|
21876
21876
|
var encodeUrl = require_encodeurl();
|
|
21877
21877
|
var escapeHtml = require_escape_html();
|
|
21878
21878
|
var http = __require("http");
|
|
21879
|
-
var
|
|
21879
|
+
var isAbsolute3 = require_utils2().isAbsolute;
|
|
21880
21880
|
var onFinished = require_on_finished();
|
|
21881
21881
|
var path2 = __require("path");
|
|
21882
21882
|
var statuses = require_statuses();
|
|
@@ -22082,7 +22082,7 @@ var require_response = __commonJS({
|
|
|
22082
22082
|
done = options;
|
|
22083
22083
|
opts = {};
|
|
22084
22084
|
}
|
|
22085
|
-
if (!opts.root && !
|
|
22085
|
+
if (!opts.root && !isAbsolute3(path3)) {
|
|
22086
22086
|
throw new TypeError("path must be absolute or specify root to res.sendFile");
|
|
22087
22087
|
}
|
|
22088
22088
|
var pathname = encodeURI(path3);
|
|
@@ -30483,8 +30483,8 @@ var init_open = __esm({
|
|
|
30483
30483
|
// src/server/index.ts
|
|
30484
30484
|
var import_express15 = __toESM(require_express2(), 1);
|
|
30485
30485
|
import { createServer } from "node:http";
|
|
30486
|
-
import { existsSync as existsSync15, readFileSync as
|
|
30487
|
-
import { join as
|
|
30486
|
+
import { existsSync as existsSync15, readFileSync as readFileSync12, writeFileSync as writeFileSync5, unlinkSync as unlinkSync3, mkdirSync as mkdirSync6 } from "node:fs";
|
|
30487
|
+
import { join as join18, dirname as dirname13, resolve as resolve2 } from "node:path";
|
|
30488
30488
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
30489
30489
|
import { randomBytes } from "node:crypto";
|
|
30490
30490
|
import { Server as SocketIOServer } from "socket.io";
|
|
@@ -30509,17 +30509,199 @@ function resolveOcrDir(startDir) {
|
|
|
30509
30509
|
}
|
|
30510
30510
|
}
|
|
30511
30511
|
|
|
30512
|
-
// src/server/db.ts
|
|
30513
|
-
import { existsSync as existsSync4, readFileSync as readFileSync3, renameSync as renameSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "node:fs";
|
|
30514
|
-
import { join as join4, dirname as dirname4 } from "node:path";
|
|
30515
|
-
import initSqlJs2 from "sql.js";
|
|
30516
|
-
|
|
30517
30512
|
// ../cli/src/lib/db/index.ts
|
|
30518
|
-
import {
|
|
30519
|
-
|
|
30513
|
+
import {
|
|
30514
|
+
existsSync as existsSync4,
|
|
30515
|
+
mkdirSync as mkdirSync2,
|
|
30516
|
+
copyFileSync,
|
|
30517
|
+
statSync,
|
|
30518
|
+
mkdtempSync,
|
|
30519
|
+
rmSync
|
|
30520
|
+
} from "node:fs";
|
|
30521
|
+
import { dirname as dirname4, join as join4 } from "node:path";
|
|
30522
|
+
|
|
30523
|
+
// ../cli/src/lib/db/engine.ts
|
|
30520
30524
|
import { createRequire } from "node:module";
|
|
30521
|
-
|
|
30522
|
-
|
|
30525
|
+
|
|
30526
|
+
// ../cli/src/lib/runtime-checks.ts
|
|
30527
|
+
var NODE_FLOOR = { major: 22, minor: 5 };
|
|
30528
|
+
function isSupportedNode(version) {
|
|
30529
|
+
const [major = 0, minor = 0] = version.split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
30530
|
+
return major > NODE_FLOOR.major || major === NODE_FLOOR.major && minor >= NODE_FLOOR.minor;
|
|
30531
|
+
}
|
|
30532
|
+
function nodeVersionGuardMessage(version) {
|
|
30533
|
+
return `
|
|
30534
|
+
Open Code Review requires Node.js >= ${NODE_FLOOR.major}.${NODE_FLOOR.minor} (it uses Node's built-in SQLite, \`node:sqlite\`).
|
|
30535
|
+
You have Node ${version}. Upgrade Node (e.g. \`nvm install 22 && nvm use 22\`) and re-run.
|
|
30536
|
+
|
|
30537
|
+
`;
|
|
30538
|
+
}
|
|
30539
|
+
function isSuppressibleSqliteWarning(warning) {
|
|
30540
|
+
const message = typeof warning === "string" ? warning : warning?.message;
|
|
30541
|
+
return typeof message === "string" && message.includes("SQLite is an experimental feature");
|
|
30542
|
+
}
|
|
30543
|
+
|
|
30544
|
+
// ../cli/src/lib/db/engine.ts
|
|
30545
|
+
var SQLITE_BUSY = 5;
|
|
30546
|
+
var SQLITE_BUSY_SNAPSHOT = 261;
|
|
30547
|
+
var BUSY_RETRY_ATTEMPTS = 5;
|
|
30548
|
+
var BUSY_RETRY_BACKOFF_MS = 50;
|
|
30549
|
+
var savepointName = (depth) => `ocr_sp_${depth}`;
|
|
30550
|
+
var nodeRequire = createRequire(import.meta.url);
|
|
30551
|
+
var _preconditionsApplied = false;
|
|
30552
|
+
function applyEnginePreconditions() {
|
|
30553
|
+
if (_preconditionsApplied) return;
|
|
30554
|
+
_preconditionsApplied = true;
|
|
30555
|
+
const originalEmitWarning = process.emitWarning.bind(process);
|
|
30556
|
+
process.emitWarning = (warning, ...args) => {
|
|
30557
|
+
if (isSuppressibleSqliteWarning(warning)) return;
|
|
30558
|
+
originalEmitWarning(warning, ...args);
|
|
30559
|
+
};
|
|
30560
|
+
}
|
|
30561
|
+
var _DatabaseSyncCtor;
|
|
30562
|
+
function newDatabase(path2) {
|
|
30563
|
+
if (!_DatabaseSyncCtor) {
|
|
30564
|
+
applyEnginePreconditions();
|
|
30565
|
+
try {
|
|
30566
|
+
_DatabaseSyncCtor = nodeRequire("node:sqlite").DatabaseSync;
|
|
30567
|
+
} catch (e) {
|
|
30568
|
+
if (!isSupportedNode(process.versions.node)) {
|
|
30569
|
+
throw new Error(nodeVersionGuardMessage(process.versions.node).trim());
|
|
30570
|
+
}
|
|
30571
|
+
throw e;
|
|
30572
|
+
}
|
|
30573
|
+
}
|
|
30574
|
+
return new _DatabaseSyncCtor(path2);
|
|
30575
|
+
}
|
|
30576
|
+
function isBusyError(e) {
|
|
30577
|
+
const errcode = e?.errcode;
|
|
30578
|
+
return errcode === SQLITE_BUSY || errcode === SQLITE_BUSY_SNAPSHOT;
|
|
30579
|
+
}
|
|
30580
|
+
var SLEEP_BUF = new Int32Array(new SharedArrayBuffer(4));
|
|
30581
|
+
function sleepSync(ms) {
|
|
30582
|
+
Atomics.wait(SLEEP_BUF, 0, 0, ms);
|
|
30583
|
+
}
|
|
30584
|
+
var NodeSqliteAdapter = class {
|
|
30585
|
+
raw;
|
|
30586
|
+
/**
|
|
30587
|
+
* Transaction nesting depth. `node:sqlite` has no transaction helper, so we
|
|
30588
|
+
* drive `BEGIN IMMEDIATE` ourselves and use SAVEPOINTs for nested calls
|
|
30589
|
+
* (better-sqlite3 did this automatically). 0 = no transaction open.
|
|
30590
|
+
*/
|
|
30591
|
+
txnDepth = 0;
|
|
30592
|
+
constructor(db) {
|
|
30593
|
+
this.raw = db;
|
|
30594
|
+
}
|
|
30595
|
+
exec(sql, params) {
|
|
30596
|
+
const stmt = this.raw.prepare(sql);
|
|
30597
|
+
const cols = stmt.columns();
|
|
30598
|
+
if (cols.length === 0) {
|
|
30599
|
+
stmt.run(...params ?? []);
|
|
30600
|
+
return [];
|
|
30601
|
+
}
|
|
30602
|
+
stmt.setReturnArrays(true);
|
|
30603
|
+
const values = stmt.all(...params ?? []);
|
|
30604
|
+
return values.length > 0 ? [{ columns: cols.map((c) => c.name), values }] : [];
|
|
30605
|
+
}
|
|
30606
|
+
run(sql, params) {
|
|
30607
|
+
if (params !== void 0) {
|
|
30608
|
+
this.raw.prepare(sql).run(...params);
|
|
30609
|
+
return;
|
|
30610
|
+
}
|
|
30611
|
+
this.raw.exec(sql);
|
|
30612
|
+
}
|
|
30613
|
+
prepare(sql) {
|
|
30614
|
+
return this.raw.prepare(sql);
|
|
30615
|
+
}
|
|
30616
|
+
transaction(fn) {
|
|
30617
|
+
return this.txnDepth > 0 ? this.runNested(fn) : this.runOuter(fn);
|
|
30618
|
+
}
|
|
30619
|
+
/**
|
|
30620
|
+
* Nested call: a SAVEPOINT within the outer transaction's write lock. No
|
|
30621
|
+
* busy-retry — the outer transaction already holds the lock. The savepoint
|
|
30622
|
+
* lets the inner block roll back independently while the outer continues.
|
|
30623
|
+
*/
|
|
30624
|
+
runNested(fn) {
|
|
30625
|
+
const name = savepointName(this.txnDepth);
|
|
30626
|
+
this.raw.exec(`SAVEPOINT ${name}`);
|
|
30627
|
+
this.txnDepth++;
|
|
30628
|
+
try {
|
|
30629
|
+
const result = fn();
|
|
30630
|
+
this.raw.exec(`RELEASE ${name}`);
|
|
30631
|
+
return result;
|
|
30632
|
+
} catch (e) {
|
|
30633
|
+
try {
|
|
30634
|
+
this.raw.exec(`ROLLBACK TO ${name}`);
|
|
30635
|
+
this.raw.exec(`RELEASE ${name}`);
|
|
30636
|
+
} catch {
|
|
30637
|
+
}
|
|
30638
|
+
throw e;
|
|
30639
|
+
} finally {
|
|
30640
|
+
this.txnDepth--;
|
|
30641
|
+
}
|
|
30642
|
+
}
|
|
30643
|
+
/**
|
|
30644
|
+
* Outer transaction: `BEGIN IMMEDIATE` acquires the write lock up front so
|
|
30645
|
+
* cross-process writers serialize cleanly under WAL instead of failing late
|
|
30646
|
+
* on upgrade. `busy_timeout` covers most contention; a bounded synchronous
|
|
30647
|
+
* retry absorbs the residual SQLITE_BUSY (another connection holds the lock
|
|
30648
|
+
* past the timeout, or BUSY_SNAPSHOT). Non-busy errors and the final attempt
|
|
30649
|
+
* re-throw so genuine failures propagate.
|
|
30650
|
+
*/
|
|
30651
|
+
runOuter(fn) {
|
|
30652
|
+
for (let attempt = 0; attempt < BUSY_RETRY_ATTEMPTS; attempt++) {
|
|
30653
|
+
try {
|
|
30654
|
+
return this.runOnce(fn);
|
|
30655
|
+
} catch (e) {
|
|
30656
|
+
if (!isBusyError(e) || attempt === BUSY_RETRY_ATTEMPTS - 1) throw e;
|
|
30657
|
+
sleepSync(BUSY_RETRY_BACKOFF_MS);
|
|
30658
|
+
}
|
|
30659
|
+
}
|
|
30660
|
+
throw new Error("transaction retry budget exhausted");
|
|
30661
|
+
}
|
|
30662
|
+
/** One `BEGIN IMMEDIATE` / `COMMIT` / `ROLLBACK` lifecycle. */
|
|
30663
|
+
runOnce(fn) {
|
|
30664
|
+
this.raw.exec("BEGIN IMMEDIATE");
|
|
30665
|
+
this.txnDepth = 1;
|
|
30666
|
+
try {
|
|
30667
|
+
const result = fn();
|
|
30668
|
+
this.raw.exec("COMMIT");
|
|
30669
|
+
return result;
|
|
30670
|
+
} catch (e) {
|
|
30671
|
+
try {
|
|
30672
|
+
this.raw.exec("ROLLBACK");
|
|
30673
|
+
} catch {
|
|
30674
|
+
}
|
|
30675
|
+
throw e;
|
|
30676
|
+
} finally {
|
|
30677
|
+
this.txnDepth = 0;
|
|
30678
|
+
}
|
|
30679
|
+
}
|
|
30680
|
+
pragma(source) {
|
|
30681
|
+
this.raw.exec(`PRAGMA ${source}`);
|
|
30682
|
+
return void 0;
|
|
30683
|
+
}
|
|
30684
|
+
close() {
|
|
30685
|
+
try {
|
|
30686
|
+
this.raw.exec("PRAGMA wal_checkpoint(TRUNCATE)");
|
|
30687
|
+
} catch {
|
|
30688
|
+
}
|
|
30689
|
+
try {
|
|
30690
|
+
this.raw.close();
|
|
30691
|
+
} catch (e) {
|
|
30692
|
+
const message = e?.message ?? "";
|
|
30693
|
+
if (!/database is not open/i.test(message)) throw e;
|
|
30694
|
+
}
|
|
30695
|
+
}
|
|
30696
|
+
};
|
|
30697
|
+
function openEngine(dbPath) {
|
|
30698
|
+
const native = newDatabase(dbPath);
|
|
30699
|
+
native.exec("PRAGMA journal_mode = WAL");
|
|
30700
|
+
native.exec("PRAGMA foreign_keys = ON");
|
|
30701
|
+
native.exec("PRAGMA busy_timeout = 5000");
|
|
30702
|
+
native.exec("PRAGMA synchronous = NORMAL");
|
|
30703
|
+
return new NodeSqliteAdapter(native);
|
|
30704
|
+
}
|
|
30523
30705
|
|
|
30524
30706
|
// ../cli/src/lib/db/migrations.ts
|
|
30525
30707
|
var MIGRATIONS = [
|
|
@@ -30843,8 +31025,157 @@ var MIGRATIONS = [
|
|
|
30843
31025
|
DROP INDEX IF EXISTS idx_agent_sessions_status_heartbeat;
|
|
30844
31026
|
DROP TABLE IF EXISTS agent_sessions;
|
|
30845
31027
|
`
|
|
31028
|
+
},
|
|
31029
|
+
{
|
|
31030
|
+
version: 12,
|
|
31031
|
+
description: "Event-sourced lifecycle hardening: event_type taxonomy guard, sweep indexes, session_completeness view",
|
|
31032
|
+
sql: `
|
|
31033
|
+
-- \u2500\u2500 Indexes for the now-periodic stale-session sweep + round derivation \u2500\u2500
|
|
31034
|
+
-- The sweep filters sessions by status and rolls up MAX(created_at) per
|
|
31035
|
+
-- session over the event log; deriveNextRound does MAX(round). Index both.
|
|
31036
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
|
|
31037
|
+
CREATE INDEX IF NOT EXISTS idx_events_session_created
|
|
31038
|
+
ON orchestration_events(session_id, created_at);
|
|
31039
|
+
|
|
31040
|
+
-- \u2500\u2500 Event-type taxonomy guard \u2500\u2500
|
|
31041
|
+
-- orchestration_events.event_type is the spine of all lifecycle
|
|
31042
|
+
-- derivation. A typo (e.g. 'round_complete' vs 'round_completed') would
|
|
31043
|
+
-- silently break deriveNextRound and the completeness view. SQLite cannot
|
|
31044
|
+
-- add a CHECK to an existing column without a table rebuild, so enforce
|
|
31045
|
+
-- the closed vocabulary with a BEFORE INSERT trigger instead.
|
|
31046
|
+
CREATE TRIGGER IF NOT EXISTS trg_events_known_type
|
|
31047
|
+
BEFORE INSERT ON orchestration_events
|
|
31048
|
+
WHEN NEW.event_type NOT IN (
|
|
31049
|
+
'session_created', 'session_resumed', 'round_started', 'phase_transition',
|
|
31050
|
+
'round_completed', 'map_completed', 'session_closed', 'session_aborted',
|
|
31051
|
+
'session_auto_closed_stale', 'session_synced', 'session_legacy_import'
|
|
31052
|
+
)
|
|
31053
|
+
BEGIN
|
|
31054
|
+
SELECT RAISE(ABORT, 'unknown orchestration_events.event_type');
|
|
31055
|
+
END;
|
|
31056
|
+
|
|
31057
|
+
-- \u2500\u2500 Close-guard (DB backstop for the completion invariant) \u2500\u2500
|
|
31058
|
+
-- A session cannot transition active \u2192 closed unless its current
|
|
31059
|
+
-- round/run has a terminal artifact event, OR an explicit reason event
|
|
31060
|
+
-- (abort / auto-close-stale / sync / legacy-import) is present. Only a
|
|
31061
|
+
-- *silent* premature close is banned \u2014 every legitimate non-artifact
|
|
31062
|
+
-- close carries a reason event and passes. App-level guards in
|
|
31063
|
+
-- stateClose/finish are the primary check; this makes the illegal state
|
|
31064
|
+
-- unrepresentable even via raw SQL.
|
|
31065
|
+
--
|
|
31066
|
+
-- DEFENCE-IN-DEPTH NOTE (intentional, documented gap): the reason-event
|
|
31067
|
+
-- branch below (event_type IN (...)) is NOT round-scoped \u2014 a reason event
|
|
31068
|
+
-- recorded for an earlier round would also satisfy a later close. The
|
|
31069
|
+
-- app-level guards ARE round-scoped (hasCompletionInvariant checks the
|
|
31070
|
+
-- current round/run), so the precise check lives in the application; this
|
|
31071
|
+
-- trigger is a coarse backstop against a *silent* premature close via raw
|
|
31072
|
+
-- SQL. Tightening it to be round-scoped would require a new migration
|
|
31073
|
+
-- (this v12 trigger is append-only and already shipped); the residual
|
|
31074
|
+
-- risk is a non-artifact close carrying a stale reason event, which is
|
|
31075
|
+
-- still an explicit, audited terminal \u2014 not the failure mode this guards.
|
|
31076
|
+
CREATE TRIGGER IF NOT EXISTS trg_sessions_close_guard
|
|
31077
|
+
BEFORE UPDATE OF status ON sessions
|
|
31078
|
+
WHEN NEW.status = 'closed' AND OLD.status <> 'closed'
|
|
31079
|
+
AND NOT EXISTS (
|
|
31080
|
+
SELECT 1 FROM orchestration_events e
|
|
31081
|
+
WHERE e.session_id = NEW.id
|
|
31082
|
+
AND (
|
|
31083
|
+
(NEW.workflow_type = 'review' AND e.event_type = 'round_completed' AND e.round = NEW.current_round)
|
|
31084
|
+
OR (NEW.workflow_type = 'map' AND e.event_type = 'map_completed' AND e.round = NEW.current_map_run)
|
|
31085
|
+
OR e.event_type IN ('session_aborted','session_auto_closed_stale','session_synced','session_legacy_import')
|
|
31086
|
+
)
|
|
31087
|
+
)
|
|
31088
|
+
BEGIN
|
|
31089
|
+
SELECT RAISE(ABORT, 'cannot close session without a completed round/run or an explicit reason event');
|
|
31090
|
+
END;
|
|
31091
|
+
|
|
31092
|
+
-- \u2500\u2500 session_completeness view \u2500\u2500
|
|
31093
|
+
-- The published contract for "is this session actually complete, and if
|
|
31094
|
+
-- not, what's missing". Completion is DERIVED from the event log, never a
|
|
31095
|
+
-- mutable flag: a session is complete iff it is closed AND a terminal
|
|
31096
|
+
-- artifact event exists for its current round/run. The dashboard's
|
|
31097
|
+
-- outcome derivation and the agent 'status' command read this view, so
|
|
31098
|
+
-- they cannot disagree.
|
|
31099
|
+
--
|
|
31100
|
+
-- completeness_state is an INTENTIONAL HYBRID: it combines the mutable
|
|
31101
|
+
-- status column (marked_closed) with append-only event evidence (the
|
|
31102
|
+
-- terminal artifact event). This is sound precisely because the
|
|
31103
|
+
-- close-guard trigger above makes the status column trustworthy \u2014 a row
|
|
31104
|
+
-- can only reach status='closed' with a completed round/run or an
|
|
31105
|
+
-- explicit reason event \u2014 so reading the column is not a regression to
|
|
31106
|
+
-- the old "mutable flag that could lie" model.
|
|
31107
|
+
--
|
|
31108
|
+
-- completeness_state:
|
|
31109
|
+
-- 'complete' \u2014 closed + terminal artifact for current round/run
|
|
31110
|
+
-- 'closed_without_artifact' \u2014 closed but no terminal artifact (the
|
|
31111
|
+
-- "completed too soon" condition)
|
|
31112
|
+
-- 'in_flight' \u2014 open with a dependent process still running
|
|
31113
|
+
-- 'open_no_artifact' \u2014 open, no in-flight dependents
|
|
31114
|
+
CREATE VIEW IF NOT EXISTS session_completeness AS
|
|
31115
|
+
SELECT
|
|
31116
|
+
s.id AS session_id,
|
|
31117
|
+
s.workflow_type AS workflow_type,
|
|
31118
|
+
s.status AS status,
|
|
31119
|
+
s.current_round AS current_round,
|
|
31120
|
+
s.current_map_run AS current_map_run,
|
|
31121
|
+
CASE WHEN EXISTS (
|
|
31122
|
+
SELECT 1 FROM orchestration_events e
|
|
31123
|
+
WHERE e.session_id = s.id
|
|
31124
|
+
AND (
|
|
31125
|
+
(s.workflow_type = 'review' AND e.event_type = 'round_completed' AND e.round = s.current_round)
|
|
31126
|
+
OR (s.workflow_type = 'map' AND e.event_type = 'map_completed' AND e.round = s.current_map_run)
|
|
31127
|
+
)
|
|
31128
|
+
) THEN 1 ELSE 0 END AS has_terminal_artifact,
|
|
31129
|
+
CASE WHEN s.status = 'closed' THEN 1 ELSE 0 END AS marked_closed,
|
|
31130
|
+
CASE WHEN NOT EXISTS (
|
|
31131
|
+
SELECT 1 FROM command_executions ce
|
|
31132
|
+
WHERE ce.workflow_id = s.id AND ce.finished_at IS NULL
|
|
31133
|
+
) THEN 1 ELSE 0 END AS dependents_settled,
|
|
31134
|
+
CASE
|
|
31135
|
+
WHEN s.status = 'closed' AND EXISTS (
|
|
31136
|
+
SELECT 1 FROM orchestration_events e
|
|
31137
|
+
WHERE e.session_id = s.id
|
|
31138
|
+
AND (
|
|
31139
|
+
(s.workflow_type = 'review' AND e.event_type = 'round_completed' AND e.round = s.current_round)
|
|
31140
|
+
OR (s.workflow_type = 'map' AND e.event_type = 'map_completed' AND e.round = s.current_map_run)
|
|
31141
|
+
)
|
|
31142
|
+
) THEN 'complete'
|
|
31143
|
+
WHEN s.status = 'closed' THEN 'closed_without_artifact'
|
|
31144
|
+
WHEN EXISTS (
|
|
31145
|
+
SELECT 1 FROM command_executions ce
|
|
31146
|
+
WHERE ce.workflow_id = s.id AND ce.finished_at IS NULL
|
|
31147
|
+
) THEN 'in_flight'
|
|
31148
|
+
ELSE 'open_no_artifact'
|
|
31149
|
+
END AS completeness_state
|
|
31150
|
+
FROM sessions s;
|
|
31151
|
+
`
|
|
31152
|
+
},
|
|
31153
|
+
{
|
|
31154
|
+
version: 13,
|
|
31155
|
+
description: "Retire dead parent_id column on command_executions (never written; row kind is derived from command)",
|
|
31156
|
+
// parent_id was reserved for an AI-instance → dashboard-spawn lineage link
|
|
31157
|
+
// that was never wired (no writer, no reader). A process's KIND (supervisor
|
|
31158
|
+
// / reviewer-instance / utility) is derived from columns that are always
|
|
31159
|
+
// present (command + last_heartbeat_at), so the dead lineage column and its
|
|
31160
|
+
// all-NULL index are removed. Re-add a wired parent_id alongside a real
|
|
31161
|
+
// consumer (e.g. a parent→child tree view) if lineage is ever needed.
|
|
31162
|
+
//
|
|
31163
|
+
// Imperative + guarded so the DROP COLUMN (which SQLite can't express as
|
|
31164
|
+
// IF EXISTS) is idempotent under re-application.
|
|
31165
|
+
run: (db) => {
|
|
31166
|
+
if (!columnExists(db, "command_executions", "parent_id")) return;
|
|
31167
|
+
db.run("DROP INDEX IF EXISTS idx_command_executions_parent;");
|
|
31168
|
+
db.run("ALTER TABLE command_executions DROP COLUMN parent_id;");
|
|
31169
|
+
}
|
|
30846
31170
|
}
|
|
30847
31171
|
];
|
|
31172
|
+
function columnExists(db, table, column) {
|
|
31173
|
+
const result = db.exec(`PRAGMA table_info(${table})`);
|
|
31174
|
+
const first = result[0];
|
|
31175
|
+
if (!first) return false;
|
|
31176
|
+
const nameIdx = first.columns.indexOf("name");
|
|
31177
|
+
return first.values.some((row) => row[nameIdx] === column);
|
|
31178
|
+
}
|
|
30848
31179
|
function ensureSchemaVersionTable(db) {
|
|
30849
31180
|
db.run(`
|
|
30850
31181
|
CREATE TABLE IF NOT EXISTS schema_version (
|
|
@@ -30854,6 +31185,10 @@ function ensureSchemaVersionTable(db) {
|
|
|
30854
31185
|
);
|
|
30855
31186
|
`);
|
|
30856
31187
|
}
|
|
31188
|
+
function getSchemaVersion(db) {
|
|
31189
|
+
ensureSchemaVersionTable(db);
|
|
31190
|
+
return getCurrentVersion(db);
|
|
31191
|
+
}
|
|
30857
31192
|
function getCurrentVersion(db) {
|
|
30858
31193
|
const result = db.exec(
|
|
30859
31194
|
"SELECT MAX(version) as v FROM schema_version"
|
|
@@ -30871,9 +31206,10 @@ function runMigrations(db) {
|
|
|
30871
31206
|
if (migration.version <= currentVersion) {
|
|
30872
31207
|
continue;
|
|
30873
31208
|
}
|
|
30874
|
-
db.run("BEGIN
|
|
31209
|
+
db.run("BEGIN IMMEDIATE;");
|
|
30875
31210
|
try {
|
|
30876
|
-
db.run(migration.sql);
|
|
31211
|
+
if (migration.sql) db.run(migration.sql);
|
|
31212
|
+
migration.run?.(db);
|
|
30877
31213
|
db.run(
|
|
30878
31214
|
"INSERT INTO schema_version (version, description) VALUES (?, ?);",
|
|
30879
31215
|
[migration.version, migration.description]
|
|
@@ -30886,6 +31222,10 @@ function runMigrations(db) {
|
|
|
30886
31222
|
}
|
|
30887
31223
|
}
|
|
30888
31224
|
|
|
31225
|
+
// ../cli/src/lib/db/reconcile.ts
|
|
31226
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
31227
|
+
import { isAbsolute, join as join2, dirname as dirname2 } from "node:path";
|
|
31228
|
+
|
|
30889
31229
|
// ../cli/src/lib/db/result-mapper.ts
|
|
30890
31230
|
function resultToRows(result) {
|
|
30891
31231
|
if (result.length === 0 || !result[0]) {
|
|
@@ -30906,16 +31246,260 @@ function resultToRow(result) {
|
|
|
30906
31246
|
}
|
|
30907
31247
|
|
|
30908
31248
|
// ../cli/src/lib/db/queries.ts
|
|
31249
|
+
function insertSession(db, params) {
|
|
31250
|
+
const {
|
|
31251
|
+
id,
|
|
31252
|
+
branch,
|
|
31253
|
+
workflow_type,
|
|
31254
|
+
current_phase = "context",
|
|
31255
|
+
phase_number = 1,
|
|
31256
|
+
current_round = 1,
|
|
31257
|
+
current_map_run = 1,
|
|
31258
|
+
session_dir
|
|
31259
|
+
} = params;
|
|
31260
|
+
db.run(
|
|
31261
|
+
`INSERT INTO sessions (id, branch, workflow_type, current_phase, phase_number, current_round, current_map_run, session_dir)
|
|
31262
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
31263
|
+
[id, branch, workflow_type, current_phase, phase_number, current_round, current_map_run, session_dir]
|
|
31264
|
+
);
|
|
31265
|
+
}
|
|
31266
|
+
function updateSession(db, id, params) {
|
|
31267
|
+
const setClauses = [];
|
|
31268
|
+
const values = [];
|
|
31269
|
+
if (params.status !== void 0) {
|
|
31270
|
+
setClauses.push("status = ?");
|
|
31271
|
+
values.push(params.status);
|
|
31272
|
+
}
|
|
31273
|
+
if (params.current_phase !== void 0) {
|
|
31274
|
+
setClauses.push("current_phase = ?");
|
|
31275
|
+
values.push(params.current_phase);
|
|
31276
|
+
}
|
|
31277
|
+
if (params.phase_number !== void 0) {
|
|
31278
|
+
setClauses.push("phase_number = ?");
|
|
31279
|
+
values.push(params.phase_number);
|
|
31280
|
+
}
|
|
31281
|
+
if (params.current_round !== void 0) {
|
|
31282
|
+
setClauses.push("current_round = ?");
|
|
31283
|
+
values.push(params.current_round);
|
|
31284
|
+
}
|
|
31285
|
+
if (params.current_map_run !== void 0) {
|
|
31286
|
+
setClauses.push("current_map_run = ?");
|
|
31287
|
+
values.push(params.current_map_run);
|
|
31288
|
+
}
|
|
31289
|
+
if (setClauses.length === 0) {
|
|
31290
|
+
return;
|
|
31291
|
+
}
|
|
31292
|
+
setClauses.push("updated_at = datetime('now')");
|
|
31293
|
+
values.push(id);
|
|
31294
|
+
db.run(
|
|
31295
|
+
`UPDATE sessions SET ${setClauses.join(", ")} WHERE id = ?`,
|
|
31296
|
+
values
|
|
31297
|
+
);
|
|
31298
|
+
}
|
|
30909
31299
|
function getSession(db, id) {
|
|
30910
31300
|
return resultToRow(
|
|
30911
31301
|
db.exec("SELECT * FROM sessions WHERE id = ?", [id])
|
|
30912
31302
|
);
|
|
30913
31303
|
}
|
|
31304
|
+
function getAllSessions(db) {
|
|
31305
|
+
return resultToRows(
|
|
31306
|
+
db.exec("SELECT * FROM sessions ORDER BY started_at DESC")
|
|
31307
|
+
);
|
|
31308
|
+
}
|
|
31309
|
+
function insertEvent(db, params) {
|
|
31310
|
+
const {
|
|
31311
|
+
session_id,
|
|
31312
|
+
event_type,
|
|
31313
|
+
phase,
|
|
31314
|
+
phase_number,
|
|
31315
|
+
round,
|
|
31316
|
+
metadata
|
|
31317
|
+
} = params;
|
|
31318
|
+
db.run(
|
|
31319
|
+
`INSERT INTO orchestration_events (session_id, event_type, phase, phase_number, round, metadata)
|
|
31320
|
+
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
31321
|
+
[
|
|
31322
|
+
session_id,
|
|
31323
|
+
event_type,
|
|
31324
|
+
phase ?? null,
|
|
31325
|
+
phase_number ?? null,
|
|
31326
|
+
round ?? null,
|
|
31327
|
+
metadata ?? null
|
|
31328
|
+
]
|
|
31329
|
+
);
|
|
31330
|
+
}
|
|
31331
|
+
function commitReasonClose(db, sessionId, reasonEvent, projectionUpdates) {
|
|
31332
|
+
db.transaction(() => {
|
|
31333
|
+
insertEvent(db, { session_id: sessionId, ...reasonEvent });
|
|
31334
|
+
updateSession(db, sessionId, projectionUpdates);
|
|
31335
|
+
});
|
|
31336
|
+
}
|
|
30914
31337
|
|
|
30915
|
-
// ../cli/src/lib/db/
|
|
30916
|
-
var
|
|
31338
|
+
// ../cli/src/lib/db/reconcile.ts
|
|
31339
|
+
var DEFAULT_STALE_THRESHOLD_SECONDS = 7 * 24 * 60 * 60;
|
|
31340
|
+
function hasTerminalArtifactEvent(db, sessionId, workflowType, currentRound, currentMapRun) {
|
|
31341
|
+
const eventType = workflowType === "map" ? "map_completed" : "round_completed";
|
|
31342
|
+
const round = workflowType === "map" ? currentMapRun : currentRound;
|
|
31343
|
+
const r = db.exec(
|
|
31344
|
+
`SELECT 1 FROM orchestration_events
|
|
31345
|
+
WHERE session_id = ? AND event_type = ? AND round = ? LIMIT 1`,
|
|
31346
|
+
[sessionId, eventType, round]
|
|
31347
|
+
);
|
|
31348
|
+
return (r[0]?.values.length ?? 0) > 0;
|
|
31349
|
+
}
|
|
31350
|
+
function hasReasonEvent(db, sessionId) {
|
|
31351
|
+
const r = db.exec(
|
|
31352
|
+
`SELECT 1 FROM orchestration_events
|
|
31353
|
+
WHERE session_id = ?
|
|
31354
|
+
AND event_type IN ('session_aborted','session_auto_closed_stale','session_synced','session_legacy_import')
|
|
31355
|
+
LIMIT 1`,
|
|
31356
|
+
[sessionId]
|
|
31357
|
+
);
|
|
31358
|
+
return (r[0]?.values.length ?? 0) > 0;
|
|
31359
|
+
}
|
|
31360
|
+
function lastEventAgeSeconds(db, sessionId) {
|
|
31361
|
+
const r = db.exec(
|
|
31362
|
+
`SELECT (julianday('now') - julianday(MAX(created_at))) * 86400
|
|
31363
|
+
FROM orchestration_events WHERE session_id = ?`,
|
|
31364
|
+
[sessionId]
|
|
31365
|
+
);
|
|
31366
|
+
const v = r[0]?.values[0]?.[0];
|
|
31367
|
+
return typeof v === "number" ? v : null;
|
|
31368
|
+
}
|
|
31369
|
+
function hasInFlightDependents(db, sessionId) {
|
|
31370
|
+
const r = db.exec(
|
|
31371
|
+
`SELECT 1 FROM command_executions
|
|
31372
|
+
WHERE workflow_id = ? AND finished_at IS NULL LIMIT 1`,
|
|
31373
|
+
[sessionId]
|
|
31374
|
+
);
|
|
31375
|
+
return (r[0]?.values.length ?? 0) > 0;
|
|
31376
|
+
}
|
|
31377
|
+
function resolveSessionDir(ocrDir, sessionDir) {
|
|
31378
|
+
if (!sessionDir) return null;
|
|
31379
|
+
if (isAbsolute(sessionDir)) return sessionDir;
|
|
31380
|
+
return join2(dirname2(ocrDir), sessionDir);
|
|
31381
|
+
}
|
|
31382
|
+
function reconcileLegacyState(db, ocrDir, opts = {}) {
|
|
31383
|
+
const dryRun = opts.dryRun ?? false;
|
|
31384
|
+
const threshold = opts.staleThresholdSeconds ?? DEFAULT_STALE_THRESHOLD_SECONDS;
|
|
31385
|
+
const actions = [];
|
|
31386
|
+
for (const s of getAllSessions(db)) {
|
|
31387
|
+
const dir = resolveSessionDir(ocrDir, s.session_dir);
|
|
31388
|
+
if (s.status === "closed") {
|
|
31389
|
+
if (hasTerminalArtifactEvent(db, s.id, s.workflow_type, s.current_round, s.current_map_run) || hasReasonEvent(db, s.id)) {
|
|
31390
|
+
continue;
|
|
31391
|
+
}
|
|
31392
|
+
const reviewFinal = s.workflow_type === "review" && dir ? existsSync2(join2(dir, "rounds", `round-${s.current_round}`, "final.md")) : false;
|
|
31393
|
+
const mapFinal = s.workflow_type === "map" && dir ? existsSync2(join2(dir, "map", "runs", `run-${s.current_map_run}`, "map.md")) : false;
|
|
31394
|
+
if (reviewFinal) {
|
|
31395
|
+
actions.push({
|
|
31396
|
+
sessionId: s.id,
|
|
31397
|
+
kind: "synthesize-round-completed",
|
|
31398
|
+
detail: `final.md present for round ${s.current_round}; synthesizing round_completed`
|
|
31399
|
+
});
|
|
31400
|
+
if (!dryRun) {
|
|
31401
|
+
insertEvent(db, {
|
|
31402
|
+
session_id: s.id,
|
|
31403
|
+
event_type: "round_completed",
|
|
31404
|
+
phase: "synthesis",
|
|
31405
|
+
phase_number: 7,
|
|
31406
|
+
round: s.current_round,
|
|
31407
|
+
metadata: JSON.stringify({ source: "reconciled", synthesized_from: "final.md" })
|
|
31408
|
+
});
|
|
31409
|
+
}
|
|
31410
|
+
} else if (mapFinal) {
|
|
31411
|
+
actions.push({
|
|
31412
|
+
sessionId: s.id,
|
|
31413
|
+
kind: "synthesize-map-completed",
|
|
31414
|
+
detail: `map.md present for run ${s.current_map_run}; synthesizing map_completed`
|
|
31415
|
+
});
|
|
31416
|
+
if (!dryRun) {
|
|
31417
|
+
insertEvent(db, {
|
|
31418
|
+
session_id: s.id,
|
|
31419
|
+
event_type: "map_completed",
|
|
31420
|
+
phase: "synthesis",
|
|
31421
|
+
phase_number: 5,
|
|
31422
|
+
round: s.current_map_run,
|
|
31423
|
+
metadata: JSON.stringify({ source: "reconciled", synthesized_from: "map.md" })
|
|
31424
|
+
});
|
|
31425
|
+
}
|
|
31426
|
+
} else {
|
|
31427
|
+
actions.push({
|
|
31428
|
+
sessionId: s.id,
|
|
31429
|
+
kind: "grandfather",
|
|
31430
|
+
detail: "no provable artifact; recording session_legacy_import"
|
|
31431
|
+
});
|
|
31432
|
+
if (!dryRun) {
|
|
31433
|
+
insertEvent(db, {
|
|
31434
|
+
session_id: s.id,
|
|
31435
|
+
event_type: "session_legacy_import",
|
|
31436
|
+
phase: "complete",
|
|
31437
|
+
metadata: JSON.stringify({ source: "reconciled" })
|
|
31438
|
+
});
|
|
31439
|
+
}
|
|
31440
|
+
}
|
|
31441
|
+
continue;
|
|
31442
|
+
}
|
|
31443
|
+
const age = lastEventAgeSeconds(db, s.id);
|
|
31444
|
+
const stale = (age === null || age > threshold) && !hasInFlightDependents(db, s.id);
|
|
31445
|
+
if (stale) {
|
|
31446
|
+
actions.push({
|
|
31447
|
+
sessionId: s.id,
|
|
31448
|
+
kind: "stale-close",
|
|
31449
|
+
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`
|
|
31450
|
+
});
|
|
31451
|
+
if (!dryRun) {
|
|
31452
|
+
commitReasonClose(
|
|
31453
|
+
db,
|
|
31454
|
+
s.id,
|
|
31455
|
+
{
|
|
31456
|
+
event_type: "session_auto_closed_stale",
|
|
31457
|
+
phase: "complete",
|
|
31458
|
+
metadata: JSON.stringify({ source: "reconciled", threshold_seconds: threshold })
|
|
31459
|
+
},
|
|
31460
|
+
{ status: "closed", current_phase: "complete" }
|
|
31461
|
+
);
|
|
31462
|
+
}
|
|
31463
|
+
}
|
|
31464
|
+
}
|
|
31465
|
+
return { dryRun, actions };
|
|
31466
|
+
}
|
|
31467
|
+
|
|
31468
|
+
// ../cli/src/lib/db/liveness.ts
|
|
31469
|
+
var PID_REUSE_GUARD_MS = 24 * 60 * 60 * 1e3;
|
|
31470
|
+
function defaultIsAlive(pid) {
|
|
31471
|
+
try {
|
|
31472
|
+
process.kill(pid, 0);
|
|
31473
|
+
return true;
|
|
31474
|
+
} catch (err) {
|
|
31475
|
+
return !(err instanceof Error && "code" in err && err.code === "ESRCH");
|
|
31476
|
+
}
|
|
31477
|
+
}
|
|
31478
|
+
function sqliteUtcMs(ts) {
|
|
31479
|
+
const sqliteShape = ts.includes(" ");
|
|
31480
|
+
return new Date(sqliteShape ? ts.replace(" ", "T") + "Z" : ts).getTime();
|
|
31481
|
+
}
|
|
31482
|
+
|
|
31483
|
+
// ../cli/src/lib/state/exit-codes.ts
|
|
30917
31484
|
var CANCELLED_EXIT_CODE = -2;
|
|
31485
|
+
var ORPHAN_EXIT_CODE = -3;
|
|
31486
|
+
var CASCADE_CLOSE_EXIT_CODE = -4;
|
|
31487
|
+
|
|
31488
|
+
// ../cli/src/lib/db/agent-sessions.ts
|
|
30918
31489
|
var NOTE_ORPHAN_PREFIX = "orphaned by liveness sweep";
|
|
31490
|
+
var INSTANCE_COMMAND = "session-instance";
|
|
31491
|
+
function cascadeTerminateExecutions(db, workflowId, exitCode, note) {
|
|
31492
|
+
db.run(
|
|
31493
|
+
`UPDATE command_executions
|
|
31494
|
+
SET finished_at = datetime('now'),
|
|
31495
|
+
exit_code = ?,
|
|
31496
|
+
pid = NULL,
|
|
31497
|
+
notes = COALESCE(notes || char(10), '') || ?
|
|
31498
|
+
WHERE workflow_id = ?
|
|
31499
|
+
AND finished_at IS NULL`,
|
|
31500
|
+
[exitCode, note, workflowId]
|
|
31501
|
+
);
|
|
31502
|
+
}
|
|
30919
31503
|
function rowToAgentSession(row) {
|
|
30920
31504
|
return {
|
|
30921
31505
|
// The OCR-owned id is the `uid` column. Fall back to the integer
|
|
@@ -30930,6 +31514,7 @@ function rowToAgentSession(row) {
|
|
|
30930
31514
|
resolved_model: row.resolved_model,
|
|
30931
31515
|
phase: null,
|
|
30932
31516
|
status: deriveStatus(row),
|
|
31517
|
+
kind: rowKind(row),
|
|
30933
31518
|
pid: row.pid,
|
|
30934
31519
|
started_at: row.started_at,
|
|
30935
31520
|
last_heartbeat_at: row.last_heartbeat_at ?? row.started_at,
|
|
@@ -30943,7 +31528,9 @@ function deriveStatus(row) {
|
|
|
30943
31528
|
return "running";
|
|
30944
31529
|
}
|
|
30945
31530
|
if (row.exit_code === ORPHAN_EXIT_CODE) return "orphaned";
|
|
30946
|
-
if (row.exit_code === CANCELLED_EXIT_CODE)
|
|
31531
|
+
if (row.exit_code === CANCELLED_EXIT_CODE || row.exit_code === CASCADE_CLOSE_EXIT_CODE) {
|
|
31532
|
+
return "cancelled";
|
|
31533
|
+
}
|
|
30947
31534
|
if (row.exit_code === 0) return "done";
|
|
30948
31535
|
return "crashed";
|
|
30949
31536
|
}
|
|
@@ -30988,38 +31575,112 @@ function linkDashboardInvocationToWorkflow(db, dashboardUid, workflowId) {
|
|
|
30988
31575
|
[workflowId, dashboardUid]
|
|
30989
31576
|
);
|
|
30990
31577
|
}
|
|
30991
|
-
function sweepStaleAgentSessions(db, thresholdSeconds) {
|
|
30992
|
-
const
|
|
30993
|
-
|
|
30994
|
-
|
|
30995
|
-
|
|
30996
|
-
|
|
30997
|
-
|
|
30998
|
-
|
|
30999
|
-
|
|
31578
|
+
function sweepStaleAgentSessions(db, thresholdSeconds, isAlive = defaultIsAlive) {
|
|
31579
|
+
const candidates = resultToRows(
|
|
31580
|
+
db.exec(
|
|
31581
|
+
`SELECT uid, id, pid, started_at, workflow_id, command, last_heartbeat_at
|
|
31582
|
+
FROM command_executions
|
|
31583
|
+
WHERE finished_at IS NULL
|
|
31584
|
+
AND pid IS NOT NULL
|
|
31585
|
+
AND last_heartbeat_at IS NOT NULL
|
|
31586
|
+
AND (julianday('now') - julianday(last_heartbeat_at)) * 86400 > ?`,
|
|
31587
|
+
[thresholdSeconds]
|
|
31588
|
+
)
|
|
31000
31589
|
);
|
|
31001
|
-
if (
|
|
31002
|
-
return { orphanedIds: [] };
|
|
31590
|
+
if (candidates.length === 0) {
|
|
31591
|
+
return { orphanedIds: [], cascadedWorkflowIds: [] };
|
|
31592
|
+
}
|
|
31593
|
+
const reuseCutoffMs = Date.now() - PID_REUSE_GUARD_MS;
|
|
31594
|
+
const dead = candidates.filter((row) => {
|
|
31595
|
+
if (row.pid === null) return false;
|
|
31596
|
+
if (sqliteUtcMs(row.started_at) < reuseCutoffMs) return false;
|
|
31597
|
+
return !isAlive(row.pid);
|
|
31598
|
+
});
|
|
31599
|
+
if (dead.length === 0) {
|
|
31600
|
+
return { orphanedIds: [], cascadedWorkflowIds: [] };
|
|
31003
31601
|
}
|
|
31004
31602
|
const note = `${NOTE_ORPHAN_PREFIX} (threshold ${thresholdSeconds}s)`;
|
|
31005
|
-
|
|
31006
|
-
|
|
31007
|
-
|
|
31008
|
-
|
|
31009
|
-
|
|
31010
|
-
|
|
31011
|
-
|
|
31012
|
-
|
|
31013
|
-
|
|
31014
|
-
|
|
31603
|
+
const placeholders = dead.map(() => "?").join(", ");
|
|
31604
|
+
const cascadedWorkflowIds = [];
|
|
31605
|
+
db.transaction(() => {
|
|
31606
|
+
db.run(
|
|
31607
|
+
`UPDATE command_executions
|
|
31608
|
+
SET finished_at = datetime('now'),
|
|
31609
|
+
exit_code = ?,
|
|
31610
|
+
pid = NULL,
|
|
31611
|
+
notes = COALESCE(notes || char(10), '') || ?
|
|
31612
|
+
WHERE id IN (${placeholders})
|
|
31613
|
+
AND finished_at IS NULL`,
|
|
31614
|
+
[ORPHAN_EXIT_CODE, note, ...dead.map((r) => r.id)]
|
|
31615
|
+
);
|
|
31616
|
+
for (const row of dead) {
|
|
31617
|
+
if (row.workflow_id && rowKind(row) === "supervisor") {
|
|
31618
|
+
cascadeTerminateExecutions(
|
|
31619
|
+
db,
|
|
31620
|
+
row.workflow_id,
|
|
31621
|
+
CASCADE_CLOSE_EXIT_CODE,
|
|
31622
|
+
"cascade-closed: workflow process orphaned by liveness sweep"
|
|
31623
|
+
);
|
|
31624
|
+
cascadedWorkflowIds.push(row.workflow_id);
|
|
31625
|
+
}
|
|
31626
|
+
}
|
|
31627
|
+
});
|
|
31015
31628
|
return {
|
|
31016
|
-
orphanedIds:
|
|
31629
|
+
orphanedIds: dead.map((r) => r.uid ?? String(r.id)),
|
|
31630
|
+
cascadedWorkflowIds
|
|
31017
31631
|
};
|
|
31018
31632
|
}
|
|
31633
|
+
function rowKind(row) {
|
|
31634
|
+
if (row.command === INSTANCE_COMMAND || row.command.startsWith(`${INSTANCE_COMMAND}:`)) {
|
|
31635
|
+
return "instance";
|
|
31636
|
+
}
|
|
31637
|
+
return row.last_heartbeat_at == null ? "utility" : "supervisor";
|
|
31638
|
+
}
|
|
31639
|
+
function sweepStaleSessions(db, thresholdSeconds) {
|
|
31640
|
+
const sql = `
|
|
31641
|
+
SELECT s.id
|
|
31642
|
+
FROM sessions s
|
|
31643
|
+
LEFT JOIN (
|
|
31644
|
+
SELECT session_id, MAX(created_at) AS last_event_at
|
|
31645
|
+
FROM orchestration_events
|
|
31646
|
+
GROUP BY session_id
|
|
31647
|
+
) e ON e.session_id = s.id
|
|
31648
|
+
WHERE s.status = 'active'
|
|
31649
|
+
AND (
|
|
31650
|
+
e.last_event_at IS NULL
|
|
31651
|
+
OR (julianday('now') - julianday(e.last_event_at)) * 86400 > ?
|
|
31652
|
+
)
|
|
31653
|
+
AND NOT EXISTS (
|
|
31654
|
+
SELECT 1 FROM command_executions ce
|
|
31655
|
+
WHERE ce.workflow_id = s.id
|
|
31656
|
+
AND ce.finished_at IS NULL
|
|
31657
|
+
)
|
|
31658
|
+
`;
|
|
31659
|
+
const rows = resultToRows(db.exec(sql, [thresholdSeconds]));
|
|
31660
|
+
if (rows.length === 0) {
|
|
31661
|
+
return { closedSessionIds: [] };
|
|
31662
|
+
}
|
|
31663
|
+
for (const row of rows) {
|
|
31664
|
+
commitReasonClose(
|
|
31665
|
+
db,
|
|
31666
|
+
row.id,
|
|
31667
|
+
{
|
|
31668
|
+
event_type: "session_auto_closed_stale",
|
|
31669
|
+
phase: "complete",
|
|
31670
|
+
metadata: JSON.stringify({
|
|
31671
|
+
reason: "no events past threshold; no in-flight dependents",
|
|
31672
|
+
threshold_seconds: thresholdSeconds
|
|
31673
|
+
})
|
|
31674
|
+
},
|
|
31675
|
+
{ status: "closed", current_phase: "complete" }
|
|
31676
|
+
);
|
|
31677
|
+
}
|
|
31678
|
+
return { closedSessionIds: rows.map((r) => r.id) };
|
|
31679
|
+
}
|
|
31019
31680
|
|
|
31020
31681
|
// ../cli/src/lib/db/command-log.ts
|
|
31021
|
-
import { appendFileSync, existsSync as
|
|
31022
|
-
import { dirname as
|
|
31682
|
+
import { appendFileSync, existsSync as existsSync3, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
31683
|
+
import { dirname as dirname3, join as join3 } from "node:path";
|
|
31023
31684
|
import { randomUUID } from "node:crypto";
|
|
31024
31685
|
var CACHE_DIR = ".cache";
|
|
31025
31686
|
var FILENAME = "command-history.jsonl";
|
|
@@ -31030,16 +31691,16 @@ function generateCommandUid() {
|
|
|
31030
31691
|
return randomUUID();
|
|
31031
31692
|
}
|
|
31032
31693
|
function cacheDir(ocrDir) {
|
|
31033
|
-
return
|
|
31694
|
+
return join3(ocrDir, "data", CACHE_DIR);
|
|
31034
31695
|
}
|
|
31035
31696
|
function commandLogPath(ocrDir) {
|
|
31036
|
-
return
|
|
31697
|
+
return join3(cacheDir(ocrDir), FILENAME);
|
|
31037
31698
|
}
|
|
31038
31699
|
function appendCommandLog(ocrDir, entry) {
|
|
31039
31700
|
try {
|
|
31040
31701
|
const filePath = commandLogPath(ocrDir);
|
|
31041
|
-
const dir =
|
|
31042
|
-
if (!
|
|
31702
|
+
const dir = dirname3(filePath);
|
|
31703
|
+
if (!existsSync3(dir)) mkdirSync(dir, { recursive: true });
|
|
31043
31704
|
const line = JSON.stringify(entry) + "\n";
|
|
31044
31705
|
appendFileSync(filePath, line, { encoding: "utf-8" });
|
|
31045
31706
|
if (approxLineCount >= 0) approxLineCount++;
|
|
@@ -31049,7 +31710,7 @@ function appendCommandLog(ocrDir, entry) {
|
|
|
31049
31710
|
}
|
|
31050
31711
|
function readCommandLog(ocrDir) {
|
|
31051
31712
|
const filePath = commandLogPath(ocrDir);
|
|
31052
|
-
if (!
|
|
31713
|
+
if (!existsSync3(filePath)) return [];
|
|
31053
31714
|
const content = readFileSync(filePath, "utf-8");
|
|
31054
31715
|
const entries = [];
|
|
31055
31716
|
for (const line of content.split("\n")) {
|
|
@@ -31115,99 +31776,137 @@ function rotateIfNeeded(filePath) {
|
|
|
31115
31776
|
}
|
|
31116
31777
|
|
|
31117
31778
|
// ../cli/src/lib/db/index.ts
|
|
31118
|
-
|
|
31119
|
-
|
|
31120
|
-
|
|
31121
|
-
|
|
31779
|
+
var V2_SCHEMA_VERSION = 12;
|
|
31780
|
+
function maybeSnapshotBeforeUpgrade(db, dbPath, fromVersion) {
|
|
31781
|
+
if (fromVersion < 1 || fromVersion >= V2_SCHEMA_VERSION) return null;
|
|
31782
|
+
const bakPath = `${dbPath}.bak.v${fromVersion}`;
|
|
31783
|
+
if (existsSync4(bakPath)) return bakPath;
|
|
31784
|
+
try {
|
|
31785
|
+
if (!existsSync4(dbPath) || statSync(dbPath).size === 0) return null;
|
|
31786
|
+
db.pragma("wal_checkpoint(TRUNCATE)");
|
|
31787
|
+
copyFileSync(dbPath, bakPath);
|
|
31788
|
+
return bakPath;
|
|
31789
|
+
} catch {
|
|
31790
|
+
return null;
|
|
31791
|
+
}
|
|
31122
31792
|
}
|
|
31123
|
-
function
|
|
31124
|
-
|
|
31125
|
-
|
|
31126
|
-
|
|
31793
|
+
function formatUpgradeNotice(bakPath, reconcile) {
|
|
31794
|
+
const lines = [
|
|
31795
|
+
"Storage upgraded to v2.0 \u2014 durable SQLite engine (WAL), event-sourced lifecycle."
|
|
31796
|
+
];
|
|
31797
|
+
if (bakPath) {
|
|
31798
|
+
lines.push(` A backup of your previous database was saved to: ${bakPath}`);
|
|
31799
|
+
}
|
|
31800
|
+
const repairs = (reconcile?.actions ?? []).filter((a) => a.kind !== "ok");
|
|
31801
|
+
if (repairs.length > 0) {
|
|
31802
|
+
const n = (kind) => repairs.filter((a) => a.kind === kind).length;
|
|
31803
|
+
const parts = [];
|
|
31804
|
+
const finalized = n("synthesize-round-completed") + n("synthesize-map-completed");
|
|
31805
|
+
if (finalized > 0) parts.push(`${finalized} finalized from artifacts`);
|
|
31806
|
+
if (n("grandfather") > 0) parts.push(`${n("grandfather")} grandfathered`);
|
|
31807
|
+
if (n("stale-close") > 0) parts.push(`${n("stale-close")} stale closed`);
|
|
31808
|
+
lines.push(
|
|
31809
|
+
` Reconciled ${repairs.length} legacy session(s): ${parts.join(", ")}.`
|
|
31810
|
+
);
|
|
31811
|
+
}
|
|
31812
|
+
lines.push(" Run `ocr doctor` to verify the storage engine.");
|
|
31813
|
+
return lines.map((l) => `[ocr] ${l}`).join("\n");
|
|
31127
31814
|
}
|
|
31128
|
-
|
|
31129
|
-
|
|
31130
|
-
|
|
31815
|
+
var connections = /* @__PURE__ */ new Map();
|
|
31816
|
+
async function openDatabase(dbPath) {
|
|
31817
|
+
const cached = connections.get(dbPath);
|
|
31818
|
+
if (cached) {
|
|
31819
|
+
return cached;
|
|
31820
|
+
}
|
|
31821
|
+
const dir = dirname4(dbPath);
|
|
31822
|
+
if (!existsSync4(dir)) {
|
|
31823
|
+
mkdirSync2(dir, { recursive: true });
|
|
31131
31824
|
}
|
|
31825
|
+
const db = openEngine(dbPath);
|
|
31826
|
+
connections.set(dbPath, db);
|
|
31827
|
+
return db;
|
|
31828
|
+
}
|
|
31829
|
+
async function ensureDatabase(ocrDir) {
|
|
31830
|
+
const dataDir = join4(ocrDir, "data");
|
|
31831
|
+
if (!existsSync4(dataDir)) {
|
|
31832
|
+
mkdirSync2(dataDir, { recursive: true });
|
|
31833
|
+
}
|
|
31834
|
+
const dbPath = join4(dataDir, "ocr.db");
|
|
31835
|
+
const db = await openDatabase(dbPath);
|
|
31836
|
+
let before = 0;
|
|
31132
31837
|
try {
|
|
31133
|
-
|
|
31134
|
-
stdio: "ignore",
|
|
31135
|
-
timeout: 2e3
|
|
31136
|
-
});
|
|
31137
|
-
if (probe.status !== 0) {
|
|
31138
|
-
return "skipped";
|
|
31139
|
-
}
|
|
31838
|
+
before = getSchemaVersion(db);
|
|
31140
31839
|
} catch {
|
|
31840
|
+
before = 0;
|
|
31841
|
+
}
|
|
31842
|
+
const isLegacyUpgrade = before >= 1 && before < V2_SCHEMA_VERSION;
|
|
31843
|
+
const bakPath = maybeSnapshotBeforeUpgrade(db, dbPath, before);
|
|
31844
|
+
runMigrations(db);
|
|
31845
|
+
let reconcile;
|
|
31846
|
+
if (before < V2_SCHEMA_VERSION) {
|
|
31847
|
+
try {
|
|
31848
|
+
reconcile = reconcileLegacyState(db, ocrDir);
|
|
31849
|
+
} catch (err) {
|
|
31850
|
+
console.error(
|
|
31851
|
+
`[ocr] legacy reconciliation skipped: ${err instanceof Error ? err.message : String(err)}`
|
|
31852
|
+
);
|
|
31853
|
+
}
|
|
31854
|
+
}
|
|
31855
|
+
if (isLegacyUpgrade) {
|
|
31856
|
+
const notice = formatUpgradeNotice(bakPath, reconcile);
|
|
31857
|
+
if (notice) console.error(notice);
|
|
31858
|
+
}
|
|
31859
|
+
return db;
|
|
31860
|
+
}
|
|
31861
|
+
function walCheckpointTruncate(dbPath) {
|
|
31862
|
+
if (!existsSync4(dbPath)) {
|
|
31141
31863
|
return "skipped";
|
|
31142
31864
|
}
|
|
31865
|
+
const cached = connections.get(dbPath);
|
|
31866
|
+
if (cached) {
|
|
31867
|
+
try {
|
|
31868
|
+
cached.pragma("wal_checkpoint(TRUNCATE)");
|
|
31869
|
+
return "checkpointed";
|
|
31870
|
+
} catch {
|
|
31871
|
+
return "failed";
|
|
31872
|
+
}
|
|
31873
|
+
}
|
|
31874
|
+
let transient;
|
|
31143
31875
|
try {
|
|
31144
|
-
|
|
31145
|
-
|
|
31146
|
-
|
|
31147
|
-
{
|
|
31148
|
-
stdio: "ignore",
|
|
31149
|
-
timeout: 5e3
|
|
31150
|
-
}
|
|
31151
|
-
);
|
|
31152
|
-
return result.status === 0 ? "checkpointed" : "failed";
|
|
31876
|
+
transient = openEngine(dbPath);
|
|
31877
|
+
transient.pragma("wal_checkpoint(TRUNCATE)");
|
|
31878
|
+
return "checkpointed";
|
|
31153
31879
|
} catch {
|
|
31154
31880
|
return "failed";
|
|
31881
|
+
} finally {
|
|
31882
|
+
try {
|
|
31883
|
+
transient?.close();
|
|
31884
|
+
} catch {
|
|
31885
|
+
}
|
|
31886
|
+
}
|
|
31887
|
+
}
|
|
31888
|
+
function closeDatabase(dbPath) {
|
|
31889
|
+
const db = connections.get(dbPath);
|
|
31890
|
+
if (db) {
|
|
31891
|
+
db.close();
|
|
31892
|
+
connections.delete(dbPath);
|
|
31155
31893
|
}
|
|
31156
31894
|
}
|
|
31157
31895
|
|
|
31158
31896
|
// src/server/db.ts
|
|
31897
|
+
import { join as join5 } from "node:path";
|
|
31159
31898
|
var cachedDb = null;
|
|
31160
31899
|
var cachedDbPath = null;
|
|
31161
|
-
var preSaveHook = null;
|
|
31162
|
-
var postSaveHook = null;
|
|
31163
|
-
function registerSaveHooks(preSave, postSave) {
|
|
31164
|
-
preSaveHook = preSave;
|
|
31165
|
-
postSaveHook = postSave;
|
|
31166
|
-
}
|
|
31167
31900
|
async function openDb(ocrDir) {
|
|
31168
|
-
const dbPath =
|
|
31169
|
-
|
|
31170
|
-
return cachedDb;
|
|
31171
|
-
}
|
|
31172
|
-
const wasmBuffer = readFileSync3(locateWasm());
|
|
31173
|
-
const wasmBinary = wasmBuffer.buffer.slice(
|
|
31174
|
-
wasmBuffer.byteOffset,
|
|
31175
|
-
wasmBuffer.byteOffset + wasmBuffer.byteLength
|
|
31176
|
-
);
|
|
31177
|
-
const SQL = await initSqlJs2({ wasmBinary });
|
|
31178
|
-
let db;
|
|
31179
|
-
if (existsSync4(dbPath)) {
|
|
31180
|
-
const fileBuffer = readFileSync3(dbPath);
|
|
31181
|
-
db = new SQL.Database(fileBuffer);
|
|
31182
|
-
} else {
|
|
31183
|
-
const dataDir = dirname4(dbPath);
|
|
31184
|
-
if (!existsSync4(dataDir)) {
|
|
31185
|
-
mkdirSync3(dataDir, { recursive: true });
|
|
31186
|
-
}
|
|
31187
|
-
db = new SQL.Database();
|
|
31188
|
-
}
|
|
31189
|
-
applyPragmas(db);
|
|
31190
|
-
runMigrations(db);
|
|
31901
|
+
const dbPath = join5(ocrDir, "data", "ocr.db");
|
|
31902
|
+
const db = await ensureDatabase(ocrDir);
|
|
31191
31903
|
cachedDb = db;
|
|
31192
31904
|
cachedDbPath = dbPath;
|
|
31193
31905
|
return db;
|
|
31194
31906
|
}
|
|
31195
|
-
function saveDb(db, ocrDir) {
|
|
31196
|
-
preSaveHook?.();
|
|
31197
|
-
const dbPath = join4(ocrDir, "data", "ocr.db");
|
|
31198
|
-
const data = db.export();
|
|
31199
|
-
const dir = dirname4(dbPath);
|
|
31200
|
-
if (!existsSync4(dir)) {
|
|
31201
|
-
mkdirSync3(dir, { recursive: true });
|
|
31202
|
-
}
|
|
31203
|
-
const tmpPath = `${dbPath}.${process.pid}.tmp`;
|
|
31204
|
-
writeFileSync3(tmpPath, Buffer.from(data));
|
|
31205
|
-
renameSync3(tmpPath, dbPath);
|
|
31206
|
-
postSaveHook?.();
|
|
31207
|
-
}
|
|
31208
31907
|
function closeDb() {
|
|
31209
|
-
if (
|
|
31210
|
-
|
|
31908
|
+
if (cachedDbPath) {
|
|
31909
|
+
closeDatabase(cachedDbPath);
|
|
31211
31910
|
cachedDb = null;
|
|
31212
31911
|
cachedDbPath = null;
|
|
31213
31912
|
}
|
|
@@ -31431,7 +32130,11 @@ function deleteNote(db, noteId) {
|
|
|
31431
32130
|
function getCommandHistory(db, limit = 50) {
|
|
31432
32131
|
return resultToRows(
|
|
31433
32132
|
db.exec(
|
|
31434
|
-
|
|
32133
|
+
`SELECT ce.*, sc.completeness_state AS workflow_completeness
|
|
32134
|
+
FROM command_executions ce
|
|
32135
|
+
LEFT JOIN session_completeness sc ON sc.session_id = ce.workflow_id
|
|
32136
|
+
ORDER BY ce.started_at DESC
|
|
32137
|
+
LIMIT ?`,
|
|
31435
32138
|
[limit]
|
|
31436
32139
|
)
|
|
31437
32140
|
);
|
|
@@ -32120,34 +32823,7 @@ var VALID_ROUND_STATUSES = /* @__PURE__ */ new Set([
|
|
|
32120
32823
|
"acknowledged",
|
|
32121
32824
|
"dismissed"
|
|
32122
32825
|
]);
|
|
32123
|
-
|
|
32124
|
-
var pendingDb = null;
|
|
32125
|
-
var pendingOcrDir = null;
|
|
32126
|
-
function debouncedSave(db, ocrDir) {
|
|
32127
|
-
pendingDb = db;
|
|
32128
|
-
pendingOcrDir = ocrDir;
|
|
32129
|
-
if (saveTimer) clearTimeout(saveTimer);
|
|
32130
|
-
saveTimer = setTimeout(() => {
|
|
32131
|
-
if (pendingDb && pendingOcrDir) {
|
|
32132
|
-
saveDb(pendingDb, pendingOcrDir);
|
|
32133
|
-
}
|
|
32134
|
-
saveTimer = null;
|
|
32135
|
-
pendingDb = null;
|
|
32136
|
-
pendingOcrDir = null;
|
|
32137
|
-
}, 500);
|
|
32138
|
-
}
|
|
32139
|
-
function flushSave() {
|
|
32140
|
-
if (saveTimer) {
|
|
32141
|
-
clearTimeout(saveTimer);
|
|
32142
|
-
saveTimer = null;
|
|
32143
|
-
}
|
|
32144
|
-
if (pendingDb && pendingOcrDir) {
|
|
32145
|
-
saveDb(pendingDb, pendingOcrDir);
|
|
32146
|
-
pendingDb = null;
|
|
32147
|
-
pendingOcrDir = null;
|
|
32148
|
-
}
|
|
32149
|
-
}
|
|
32150
|
-
function createProgressRouter(db, ocrDir) {
|
|
32826
|
+
function createProgressRouter(db) {
|
|
32151
32827
|
const router = (0, import_express5.Router)();
|
|
32152
32828
|
router.patch("/map-files/:id/progress", (req, res) => {
|
|
32153
32829
|
try {
|
|
@@ -32167,7 +32843,6 @@ function createProgressRouter(db, ocrDir) {
|
|
|
32167
32843
|
return;
|
|
32168
32844
|
}
|
|
32169
32845
|
upsertFileProgress(db, fileId, isReviewed);
|
|
32170
|
-
debouncedSave(db, ocrDir);
|
|
32171
32846
|
const progress = getFileProgress(db, fileId);
|
|
32172
32847
|
res.json(progress);
|
|
32173
32848
|
} catch (err) {
|
|
@@ -32183,7 +32858,6 @@ function createProgressRouter(db, ocrDir) {
|
|
|
32183
32858
|
return;
|
|
32184
32859
|
}
|
|
32185
32860
|
deleteFileProgress(db, fileId);
|
|
32186
|
-
debouncedSave(db, ocrDir);
|
|
32187
32861
|
res.status(200).json({ deleted: true });
|
|
32188
32862
|
} catch (err) {
|
|
32189
32863
|
console.error("Failed to clear file progress:", err);
|
|
@@ -32211,7 +32885,6 @@ function createProgressRouter(db, ocrDir) {
|
|
|
32211
32885
|
return;
|
|
32212
32886
|
}
|
|
32213
32887
|
upsertFindingProgress(db, findingId, status);
|
|
32214
|
-
debouncedSave(db, ocrDir);
|
|
32215
32888
|
const progress = getFindingProgress(db, findingId);
|
|
32216
32889
|
res.json(progress);
|
|
32217
32890
|
} catch (err) {
|
|
@@ -32227,7 +32900,6 @@ function createProgressRouter(db, ocrDir) {
|
|
|
32227
32900
|
return;
|
|
32228
32901
|
}
|
|
32229
32902
|
deleteFindingProgress(db, findingId);
|
|
32230
|
-
debouncedSave(db, ocrDir);
|
|
32231
32903
|
res.status(200).json({ deleted: true });
|
|
32232
32904
|
} catch (err) {
|
|
32233
32905
|
console.error("Failed to clear finding progress:", err);
|
|
@@ -32255,7 +32927,6 @@ function createProgressRouter(db, ocrDir) {
|
|
|
32255
32927
|
return;
|
|
32256
32928
|
}
|
|
32257
32929
|
upsertRoundProgress(db, roundId, status);
|
|
32258
|
-
debouncedSave(db, ocrDir);
|
|
32259
32930
|
const progress = getRoundProgress(db, roundId);
|
|
32260
32931
|
res.json(progress);
|
|
32261
32932
|
} catch (err) {
|
|
@@ -32271,7 +32942,6 @@ function createProgressRouter(db, ocrDir) {
|
|
|
32271
32942
|
return;
|
|
32272
32943
|
}
|
|
32273
32944
|
deleteRoundProgress(db, roundId);
|
|
32274
|
-
debouncedSave(db, ocrDir);
|
|
32275
32945
|
res.status(200).json({ deleted: true });
|
|
32276
32946
|
} catch (err) {
|
|
32277
32947
|
console.error("Failed to clear round progress:", err);
|
|
@@ -32291,7 +32961,7 @@ var VALID_TARGET_TYPES = /* @__PURE__ */ new Set([
|
|
|
32291
32961
|
"section",
|
|
32292
32962
|
"file"
|
|
32293
32963
|
]);
|
|
32294
|
-
function createNotesRouter(db
|
|
32964
|
+
function createNotesRouter(db) {
|
|
32295
32965
|
const router = (0, import_express6.Router)();
|
|
32296
32966
|
router.get("/", (req, res) => {
|
|
32297
32967
|
try {
|
|
@@ -32330,7 +33000,6 @@ function createNotesRouter(db, ocrDir) {
|
|
|
32330
33000
|
return;
|
|
32331
33001
|
}
|
|
32332
33002
|
const noteId = insertNote(db, target_type, target_id, content);
|
|
32333
|
-
saveDb(db, ocrDir);
|
|
32334
33003
|
const note = getNote(db, noteId);
|
|
32335
33004
|
res.status(201).json(note);
|
|
32336
33005
|
} catch (err) {
|
|
@@ -32356,7 +33025,6 @@ function createNotesRouter(db, ocrDir) {
|
|
|
32356
33025
|
return;
|
|
32357
33026
|
}
|
|
32358
33027
|
updateNote(db, noteId, content);
|
|
32359
|
-
saveDb(db, ocrDir);
|
|
32360
33028
|
const note = getNote(db, noteId);
|
|
32361
33029
|
res.json(note);
|
|
32362
33030
|
} catch (err) {
|
|
@@ -32377,7 +33045,6 @@ function createNotesRouter(db, ocrDir) {
|
|
|
32377
33045
|
return;
|
|
32378
33046
|
}
|
|
32379
33047
|
deleteNote(db, noteId);
|
|
32380
|
-
saveDb(db, ocrDir);
|
|
32381
33048
|
res.status(200).json({ deleted: true });
|
|
32382
33049
|
} catch (err) {
|
|
32383
33050
|
console.error("Failed to delete note:", err);
|
|
@@ -32442,12 +33109,44 @@ function spawnBinary(binary, args, opts) {
|
|
|
32442
33109
|
}
|
|
32443
33110
|
|
|
32444
33111
|
// src/server/socket/command-runner.ts
|
|
32445
|
-
import { readFileSync as
|
|
32446
|
-
import { dirname as dirname6, join as
|
|
33112
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync2, unlinkSync, mkdirSync as mkdirSync4, existsSync as existsSync7 } from "node:fs";
|
|
33113
|
+
import { dirname as dirname6, join as join10 } from "node:path";
|
|
33114
|
+
|
|
33115
|
+
// src/server/services/command-outcome.ts
|
|
33116
|
+
function deriveCommandOutcome(exitCode, completeness) {
|
|
33117
|
+
if (exitCode === null) return null;
|
|
33118
|
+
if (exitCode === CANCELLED_EXIT_CODE || exitCode === CASCADE_CLOSE_EXIT_CODE) {
|
|
33119
|
+
return "cancelled";
|
|
33120
|
+
}
|
|
33121
|
+
if (exitCode !== 0) return "failed";
|
|
33122
|
+
if (completeness === null || completeness === "complete") return "success";
|
|
33123
|
+
return "incomplete";
|
|
33124
|
+
}
|
|
33125
|
+
function deriveCancellationReason(exitCode) {
|
|
33126
|
+
if (exitCode === CANCELLED_EXIT_CODE) return "user";
|
|
33127
|
+
if (exitCode === CASCADE_CLOSE_EXIT_CODE) return "cascade";
|
|
33128
|
+
return null;
|
|
33129
|
+
}
|
|
33130
|
+
function getWorkflowCompletenessForExecution(db, executionId) {
|
|
33131
|
+
const result = db.exec(
|
|
33132
|
+
`SELECT sc.completeness_state
|
|
33133
|
+
FROM command_executions ce
|
|
33134
|
+
LEFT JOIN session_completeness sc ON sc.session_id = ce.workflow_id
|
|
33135
|
+
WHERE ce.id = ?`,
|
|
33136
|
+
[executionId]
|
|
33137
|
+
);
|
|
33138
|
+
const row = result[0]?.values[0];
|
|
33139
|
+
if (!row) return null;
|
|
33140
|
+
const state = row[0];
|
|
33141
|
+
if (state === "complete" || state === "closed_without_artifact" || state === "in_flight" || state === "open_no_artifact") {
|
|
33142
|
+
return state;
|
|
33143
|
+
}
|
|
33144
|
+
return null;
|
|
33145
|
+
}
|
|
32447
33146
|
|
|
32448
33147
|
// src/server/services/ai-cli/index.ts
|
|
32449
|
-
import { readFileSync as
|
|
32450
|
-
import { join as
|
|
33148
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
33149
|
+
import { join as join8 } from "node:path";
|
|
32451
33150
|
|
|
32452
33151
|
// src/server/socket/env.ts
|
|
32453
33152
|
var ENV_ALLOWLIST = [
|
|
@@ -32937,19 +33636,19 @@ function extractToolOutput(part) {
|
|
|
32937
33636
|
import {
|
|
32938
33637
|
createWriteStream,
|
|
32939
33638
|
existsSync as existsSync5,
|
|
32940
|
-
mkdirSync as
|
|
32941
|
-
readFileSync as
|
|
33639
|
+
mkdirSync as mkdirSync3,
|
|
33640
|
+
readFileSync as readFileSync2
|
|
32942
33641
|
} from "node:fs";
|
|
32943
|
-
import { join as
|
|
33642
|
+
import { join as join6 } from "node:path";
|
|
32944
33643
|
function eventsDir(ocrDir) {
|
|
32945
|
-
const dir =
|
|
33644
|
+
const dir = join6(ocrDir, "data", "events");
|
|
32946
33645
|
if (!existsSync5(dir)) {
|
|
32947
|
-
|
|
33646
|
+
mkdirSync3(dir, { recursive: true });
|
|
32948
33647
|
}
|
|
32949
33648
|
return dir;
|
|
32950
33649
|
}
|
|
32951
33650
|
function eventJournalPath(ocrDir, executionId) {
|
|
32952
|
-
return
|
|
33651
|
+
return join6(eventsDir(ocrDir), `${executionId}.jsonl`);
|
|
32953
33652
|
}
|
|
32954
33653
|
var EventJournalAppender = class {
|
|
32955
33654
|
stream;
|
|
@@ -32989,7 +33688,7 @@ function readEventJournal(ocrDir, executionId) {
|
|
|
32989
33688
|
if (!existsSync5(path2)) return [];
|
|
32990
33689
|
let raw;
|
|
32991
33690
|
try {
|
|
32992
|
-
raw =
|
|
33691
|
+
raw = readFileSync2(path2, "utf-8");
|
|
32993
33692
|
} catch {
|
|
32994
33693
|
return [];
|
|
32995
33694
|
}
|
|
@@ -33008,7 +33707,7 @@ function readEventJournal(ocrDir, executionId) {
|
|
|
33008
33707
|
|
|
33009
33708
|
// src/server/services/ai-cli/helpers.ts
|
|
33010
33709
|
import { tmpdir } from "node:os";
|
|
33011
|
-
import { join as
|
|
33710
|
+
import { join as join7 } from "node:path";
|
|
33012
33711
|
function formatToolDetail(tool, input) {
|
|
33013
33712
|
switch (tool) {
|
|
33014
33713
|
case "Read":
|
|
@@ -33032,13 +33731,13 @@ function formatToolDetail(tool, input) {
|
|
|
33032
33731
|
return `Using ${tool}`;
|
|
33033
33732
|
}
|
|
33034
33733
|
}
|
|
33035
|
-
var TEMP_BASE =
|
|
33734
|
+
var TEMP_BASE = join7(tmpdir(), "ocr-ai-prompts");
|
|
33036
33735
|
|
|
33037
33736
|
// src/server/services/ai-cli/index.ts
|
|
33038
33737
|
function readAiCliPreference(ocrDir) {
|
|
33039
33738
|
try {
|
|
33040
|
-
const configPath =
|
|
33041
|
-
const content =
|
|
33739
|
+
const configPath = join8(ocrDir, "config.yaml");
|
|
33740
|
+
const content = readFileSync3(configPath, "utf-8");
|
|
33042
33741
|
const match = content.match(/^\s*ai_cli:\s*(\S+)/m);
|
|
33043
33742
|
const value = match?.[1] ?? "auto";
|
|
33044
33743
|
if (value === "claude" || value === "opencode" || value === "off") return value;
|
|
@@ -33148,19 +33847,19 @@ var AiCliService = class {
|
|
|
33148
33847
|
|
|
33149
33848
|
// src/server/socket/cli-resolver.ts
|
|
33150
33849
|
import { existsSync as existsSync6 } from "node:fs";
|
|
33151
|
-
import { dirname as dirname5, join as
|
|
33850
|
+
import { dirname as dirname5, join as join9 } from "node:path";
|
|
33152
33851
|
import { fileURLToPath } from "node:url";
|
|
33153
33852
|
var __dirname = dirname5(fileURLToPath(import.meta.url));
|
|
33154
33853
|
function resolveLocalCli() {
|
|
33155
|
-
const parentDir =
|
|
33156
|
-
const bundledCli =
|
|
33157
|
-
if (existsSync6(bundledCli) && existsSync6(
|
|
33854
|
+
const parentDir = join9(__dirname, "..");
|
|
33855
|
+
const bundledCli = join9(parentDir, "index.js");
|
|
33856
|
+
if (existsSync6(bundledCli) && existsSync6(join9(parentDir, "dashboard", "server.js"))) {
|
|
33158
33857
|
return bundledCli;
|
|
33159
33858
|
}
|
|
33160
33859
|
let dir = __dirname;
|
|
33161
33860
|
for (let i = 0; i < 8; i++) {
|
|
33162
|
-
if (existsSync6(
|
|
33163
|
-
const candidate =
|
|
33861
|
+
if (existsSync6(join9(dir, "nx.json"))) {
|
|
33862
|
+
const candidate = join9(dir, "packages", "cli", "dist", "index.js");
|
|
33164
33863
|
if (existsSync6(candidate)) return candidate;
|
|
33165
33864
|
break;
|
|
33166
33865
|
}
|
|
@@ -33286,8 +33985,8 @@ function buildPrompt(opts) {
|
|
|
33286
33985
|
"",
|
|
33287
33986
|
"Examples:",
|
|
33288
33987
|
`- Instead of \`ocr state show\`, run: \`node ${localCli} state show\``,
|
|
33289
|
-
`- Instead of \`ocr state
|
|
33290
|
-
`- Instead of \`ocr state
|
|
33988
|
+
`- Instead of \`ocr state begin ...\`, run: \`node ${localCli} state begin ...\``,
|
|
33989
|
+
`- Instead of \`ocr state advance ...\`, run: \`node ${localCli} state advance ...\``,
|
|
33291
33990
|
"",
|
|
33292
33991
|
"This applies to every `ocr` invocation. Do NOT use bare `ocr` commands."
|
|
33293
33992
|
);
|
|
@@ -33297,7 +33996,7 @@ function buildPrompt(opts) {
|
|
|
33297
33996
|
"",
|
|
33298
33997
|
"## Dashboard Linkage (REQUIRED for terminal handoff)",
|
|
33299
33998
|
"",
|
|
33300
|
-
'You are running inside the OCR dashboard. To enable the "Pick up in terminal" affordance for this review, your first `ocr state
|
|
33999
|
+
'You are running inside the OCR dashboard. To enable the "Pick up in terminal" affordance for this review, your first `ocr state begin` invocation MUST include this flag:',
|
|
33301
34000
|
"",
|
|
33302
34001
|
"```",
|
|
33303
34002
|
`--dashboard-uid ${executionUid}`,
|
|
@@ -33306,7 +34005,7 @@ function buildPrompt(opts) {
|
|
|
33306
34005
|
"Full example:",
|
|
33307
34006
|
"",
|
|
33308
34007
|
"```",
|
|
33309
|
-
`node ${localCli} state
|
|
34008
|
+
`node ${localCli} state begin --session-id <id> --branch <branch> --workflow-type review --dashboard-uid ${executionUid}`,
|
|
33310
34009
|
"```",
|
|
33311
34010
|
"",
|
|
33312
34011
|
"Without this flag the dashboard cannot link your review session to its execution row, and the resume command will not be available."
|
|
@@ -33353,17 +34052,17 @@ function extractPerInstanceModels(subArgs) {
|
|
|
33353
34052
|
var MAX_CONCURRENT = 3;
|
|
33354
34053
|
var activeCommands = /* @__PURE__ */ new Map();
|
|
33355
34054
|
function spawnMarkerPath(ocrDir) {
|
|
33356
|
-
return
|
|
34055
|
+
return join10(ocrDir, "data", "dashboard-active-spawn.json");
|
|
33357
34056
|
}
|
|
33358
34057
|
function writeSpawnMarker(ocrDir, executionUid, pid) {
|
|
33359
|
-
const dataDir =
|
|
33360
|
-
if (!existsSync7(dataDir))
|
|
34058
|
+
const dataDir = join10(ocrDir, "data");
|
|
34059
|
+
if (!existsSync7(dataDir)) mkdirSync4(dataDir, { recursive: true });
|
|
33361
34060
|
const payload = JSON.stringify({
|
|
33362
34061
|
execution_uid: executionUid,
|
|
33363
34062
|
pid,
|
|
33364
34063
|
started_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
33365
34064
|
});
|
|
33366
|
-
|
|
34065
|
+
writeFileSync2(spawnMarkerPath(ocrDir), payload, { mode: 384 });
|
|
33367
34066
|
}
|
|
33368
34067
|
function clearSpawnMarker(ocrDir) {
|
|
33369
34068
|
try {
|
|
@@ -33571,10 +34270,10 @@ function spawnAiCommand(io2, _socket, db, ocrDir, executionId, baseCommand, subA
|
|
|
33571
34270
|
io2.emit("command:output", { execution_id: executionId, content: warning });
|
|
33572
34271
|
}
|
|
33573
34272
|
}
|
|
33574
|
-
const commandMdPath =
|
|
34273
|
+
const commandMdPath = join10(ocrDir, "commands", `${baseCommand}.md`);
|
|
33575
34274
|
let commandContent;
|
|
33576
34275
|
try {
|
|
33577
|
-
commandContent =
|
|
34276
|
+
commandContent = readFileSync4(commandMdPath, "utf-8");
|
|
33578
34277
|
} catch {
|
|
33579
34278
|
const content = `Error: Could not read command file at ${commandMdPath}
|
|
33580
34279
|
`;
|
|
@@ -33805,7 +34504,9 @@ function finishExecution(io2, db, ocrDir, executionId, code, output) {
|
|
|
33805
34504
|
WHERE id = ?`,
|
|
33806
34505
|
[code, finishedAt, output, executionId]
|
|
33807
34506
|
);
|
|
33808
|
-
|
|
34507
|
+
const completeness = getWorkflowCompletenessForExecution(db, executionId);
|
|
34508
|
+
const outcome = deriveCommandOutcome(code, completeness);
|
|
34509
|
+
const cancellationReason = deriveCancellationReason(code);
|
|
33809
34510
|
if (entry?.uid) {
|
|
33810
34511
|
appendCommandLog(ocrDir, {
|
|
33811
34512
|
v: 1,
|
|
@@ -33824,7 +34525,9 @@ function finishExecution(io2, db, ocrDir, executionId, code, output) {
|
|
|
33824
34525
|
io2.emit("command:finished", {
|
|
33825
34526
|
execution_id: executionId,
|
|
33826
34527
|
exitCode: code,
|
|
33827
|
-
finished_at: finishedAt
|
|
34528
|
+
finished_at: finishedAt,
|
|
34529
|
+
outcome,
|
|
34530
|
+
cancellation_reason: cancellationReason
|
|
33828
34531
|
});
|
|
33829
34532
|
activeCommands.delete(executionId);
|
|
33830
34533
|
}
|
|
@@ -33874,10 +34577,21 @@ function createCommandsRouter(db, ocrDir) {
|
|
|
33874
34577
|
router.get("/history", (req, res) => {
|
|
33875
34578
|
try {
|
|
33876
34579
|
const limit = parseInt(req.query["limit"], 10) || 50;
|
|
33877
|
-
const history = getCommandHistory(db, limit).map((row) =>
|
|
33878
|
-
...row
|
|
33879
|
-
|
|
33880
|
-
|
|
34580
|
+
const history = getCommandHistory(db, limit).map((row) => {
|
|
34581
|
+
const { workflow_completeness, ...persisted } = row;
|
|
34582
|
+
return {
|
|
34583
|
+
...persisted,
|
|
34584
|
+
duration_ms: row.finished_at && row.started_at ? new Date(row.finished_at).getTime() - new Date(row.started_at).getTime() : null,
|
|
34585
|
+
// Derived from (exit_code, event-sourced workflow completeness) —
|
|
34586
|
+
// single source of truth shared with the live `command:finished`
|
|
34587
|
+
// socket event.
|
|
34588
|
+
outcome: deriveCommandOutcome(row.exit_code, workflow_completeness),
|
|
34589
|
+
// Orthogonal discriminator within the 'cancelled' bucket so the
|
|
34590
|
+
// client distinguishes a user cancel from a cascade close without
|
|
34591
|
+
// reaching past `outcome` to match a magic exit-code number.
|
|
34592
|
+
cancellation_reason: deriveCancellationReason(row.exit_code)
|
|
34593
|
+
};
|
|
34594
|
+
});
|
|
33881
34595
|
res.json(history);
|
|
33882
34596
|
} catch (err) {
|
|
33883
34597
|
console.error("Failed to fetch command history:", err);
|
|
@@ -33903,8 +34617,8 @@ function createCommandsRouter(db, ocrDir) {
|
|
|
33903
34617
|
|
|
33904
34618
|
// src/server/routes/config.ts
|
|
33905
34619
|
var import_express9 = __toESM(require_express2(), 1);
|
|
33906
|
-
import { readFileSync as
|
|
33907
|
-
import { join as
|
|
34620
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
|
|
34621
|
+
import { join as join11, dirname as dirname7, basename } from "node:path";
|
|
33908
34622
|
var VALID_IDES = ["vscode", "cursor", "windsurf", "jetbrains", "sublime"];
|
|
33909
34623
|
function detectIde() {
|
|
33910
34624
|
const termProgram = process.env.TERM_PROGRAM?.toLowerCase() ?? "";
|
|
@@ -33921,8 +34635,8 @@ function detectIde() {
|
|
|
33921
34635
|
}
|
|
33922
34636
|
function readIdeFromConfig(ocrDir) {
|
|
33923
34637
|
try {
|
|
33924
|
-
const configPath =
|
|
33925
|
-
const content =
|
|
34638
|
+
const configPath = join11(ocrDir, "config.yaml");
|
|
34639
|
+
const content = readFileSync5(configPath, "utf-8");
|
|
33926
34640
|
const match = content.match(/^\s*ide:\s*(\S+)/m);
|
|
33927
34641
|
return match?.[1] ?? "auto";
|
|
33928
34642
|
} catch {
|
|
@@ -33968,8 +34682,8 @@ function createConfigRouter(ocrDir, aiCliService) {
|
|
|
33968
34682
|
return;
|
|
33969
34683
|
}
|
|
33970
34684
|
try {
|
|
33971
|
-
const configPath =
|
|
33972
|
-
let content =
|
|
34685
|
+
const configPath = join11(ocrDir, "config.yaml");
|
|
34686
|
+
let content = readFileSync5(configPath, "utf-8");
|
|
33973
34687
|
if (content.match(/^\s*ide:\s*\S+/m)) {
|
|
33974
34688
|
content = content.replace(/^(\s*ide:\s*)\S+/m, `$1${ide}`);
|
|
33975
34689
|
} else if (content.includes("dashboard:")) {
|
|
@@ -33981,7 +34695,7 @@ dashboard:
|
|
|
33981
34695
|
ide: ${ide}
|
|
33982
34696
|
`;
|
|
33983
34697
|
}
|
|
33984
|
-
|
|
34698
|
+
writeFileSync3(configPath, content);
|
|
33985
34699
|
res.json({ ide });
|
|
33986
34700
|
} catch (err) {
|
|
33987
34701
|
console.error("Failed to update config:", err);
|
|
@@ -33993,7 +34707,7 @@ dashboard:
|
|
|
33993
34707
|
|
|
33994
34708
|
// src/server/routes/chat.ts
|
|
33995
34709
|
var import_express10 = __toESM(require_express2(), 1);
|
|
33996
|
-
function createChatRouter(db
|
|
34710
|
+
function createChatRouter(db) {
|
|
33997
34711
|
const router = (0, import_express10.Router)();
|
|
33998
34712
|
router.get("/:id/chat", (req, res) => {
|
|
33999
34713
|
try {
|
|
@@ -34046,7 +34760,6 @@ function createChatRouter(db, ocrDir) {
|
|
|
34046
34760
|
return;
|
|
34047
34761
|
}
|
|
34048
34762
|
deleteConversation(db, conversationId);
|
|
34049
|
-
saveDb(db, ocrDir);
|
|
34050
34763
|
res.status(200).json({ deleted: true });
|
|
34051
34764
|
} catch (err) {
|
|
34052
34765
|
console.error("Failed to delete conversation:", err);
|
|
@@ -34058,15 +34771,15 @@ function createChatRouter(db, ocrDir) {
|
|
|
34058
34771
|
|
|
34059
34772
|
// src/server/routes/reviewers.ts
|
|
34060
34773
|
var import_express11 = __toESM(require_express2(), 1);
|
|
34061
|
-
import { readFileSync as
|
|
34062
|
-
import { join as
|
|
34774
|
+
import { readFileSync as readFileSync6, existsSync as existsSync8, watch } from "node:fs";
|
|
34775
|
+
import { join as join12 } from "node:path";
|
|
34063
34776
|
function readReviewersMeta(ocrDir) {
|
|
34064
|
-
const metaPath =
|
|
34777
|
+
const metaPath = join12(ocrDir, "reviewers-meta.json");
|
|
34065
34778
|
if (!existsSync8(metaPath)) {
|
|
34066
34779
|
return { reviewers: [], defaults: [] };
|
|
34067
34780
|
}
|
|
34068
34781
|
try {
|
|
34069
|
-
const raw =
|
|
34782
|
+
const raw = readFileSync6(metaPath, "utf-8");
|
|
34070
34783
|
const meta = JSON.parse(raw);
|
|
34071
34784
|
const reviewers = meta.reviewers ?? [];
|
|
34072
34785
|
const defaults = reviewers.filter((r) => r.is_default).map((r) => r.id);
|
|
@@ -34087,13 +34800,13 @@ function createReviewersRouter(ocrDir) {
|
|
|
34087
34800
|
res.status(400).json({ error: "Invalid reviewer ID" });
|
|
34088
34801
|
return;
|
|
34089
34802
|
}
|
|
34090
|
-
const filePath =
|
|
34803
|
+
const filePath = join12(ocrDir, "skills", "references", "reviewers", `${id}.md`);
|
|
34091
34804
|
if (!existsSync8(filePath)) {
|
|
34092
34805
|
res.status(404).json({ error: "Reviewer not found", id });
|
|
34093
34806
|
return;
|
|
34094
34807
|
}
|
|
34095
34808
|
try {
|
|
34096
|
-
const content =
|
|
34809
|
+
const content = readFileSync6(filePath, "utf-8");
|
|
34097
34810
|
res.json({ id, content });
|
|
34098
34811
|
} catch {
|
|
34099
34812
|
res.status(500).json({ error: "Failed to read reviewer file", id });
|
|
@@ -34102,7 +34815,7 @@ function createReviewersRouter(ocrDir) {
|
|
|
34102
34815
|
return router;
|
|
34103
34816
|
}
|
|
34104
34817
|
function watchReviewersMeta(ocrDir, io2) {
|
|
34105
|
-
const metaPath =
|
|
34818
|
+
const metaPath = join12(ocrDir, "reviewers-meta.json");
|
|
34106
34819
|
let watcher = null;
|
|
34107
34820
|
let debounce;
|
|
34108
34821
|
try {
|
|
@@ -34178,18 +34891,18 @@ function createHandoffRouter(sessionCapture, ocrDir, syncFromDisk = () => {
|
|
|
34178
34891
|
|
|
34179
34892
|
// src/server/routes/team.ts
|
|
34180
34893
|
var import_express14 = __toESM(require_express2(), 1);
|
|
34181
|
-
import { spawnSync
|
|
34894
|
+
import { spawnSync } from "node:child_process";
|
|
34182
34895
|
|
|
34183
34896
|
// ../cli/src/lib/team-config.ts
|
|
34184
34897
|
var import_yaml = __toESM(require_dist(), 1);
|
|
34185
|
-
import { existsSync as existsSync9, readFileSync as
|
|
34186
|
-
import { join as
|
|
34898
|
+
import { existsSync as existsSync9, readFileSync as readFileSync7 } from "node:fs";
|
|
34899
|
+
import { join as join13 } from "node:path";
|
|
34187
34900
|
function loadTeamConfig(ocrDir) {
|
|
34188
|
-
const configPath =
|
|
34901
|
+
const configPath = join13(ocrDir, "config.yaml");
|
|
34189
34902
|
if (!existsSync9(configPath)) {
|
|
34190
34903
|
return { team: [], aliases: {}, defaultModel: null };
|
|
34191
34904
|
}
|
|
34192
|
-
const content =
|
|
34905
|
+
const content = readFileSync7(configPath, "utf-8");
|
|
34193
34906
|
return parseTeamConfigYaml(content);
|
|
34194
34907
|
}
|
|
34195
34908
|
function parseTeamConfigYaml(content) {
|
|
@@ -34470,7 +35183,7 @@ function createTeamRouter(ocrDir) {
|
|
|
34470
35183
|
return;
|
|
34471
35184
|
}
|
|
34472
35185
|
try {
|
|
34473
|
-
const result =
|
|
35186
|
+
const result = spawnSync("ocr", ["team", "set", "--stdin"], {
|
|
34474
35187
|
input: JSON.stringify(body.team),
|
|
34475
35188
|
encoding: "utf-8",
|
|
34476
35189
|
cwd: ocrDir.replace(/\/\.ocr$/, ""),
|
|
@@ -34623,7 +35336,6 @@ function createSessionCaptureService(deps) {
|
|
|
34623
35336
|
return;
|
|
34624
35337
|
}
|
|
34625
35338
|
recordVendorSessionIdForExecution(db, executionId, vendorSessionId);
|
|
34626
|
-
saveDb(db, ocrDir);
|
|
34627
35339
|
} catch (err) {
|
|
34628
35340
|
console.error(
|
|
34629
35341
|
`[session-capture] recordSessionId failed for execution ${executionId} \u2192 ${vendorSessionId}:`,
|
|
@@ -34634,7 +35346,6 @@ function createSessionCaptureService(deps) {
|
|
|
34634
35346
|
function linkInvocationToWorkflow(uid, workflowId) {
|
|
34635
35347
|
try {
|
|
34636
35348
|
linkDashboardInvocationToWorkflow(db, uid, workflowId);
|
|
34637
|
-
saveDb(db, ocrDir);
|
|
34638
35349
|
} catch (err) {
|
|
34639
35350
|
console.error("[session-capture] linkInvocationToWorkflow failed:", err);
|
|
34640
35351
|
}
|
|
@@ -34791,8 +35502,8 @@ function buildDiagnostics(input) {
|
|
|
34791
35502
|
}
|
|
34792
35503
|
|
|
34793
35504
|
// src/server/services/filesystem-sync.ts
|
|
34794
|
-
import { readdirSync, readFileSync as
|
|
34795
|
-
import { join as
|
|
35505
|
+
import { readdirSync, readFileSync as readFileSync8, statSync as statSync2, existsSync as existsSync10 } from "node:fs";
|
|
35506
|
+
import { join as join14, basename as basename2, dirname as dirname9, relative } from "node:path";
|
|
34796
35507
|
import { watch as watch2 } from "chokidar";
|
|
34797
35508
|
|
|
34798
35509
|
// src/server/services/parsers/reviewer-parser.ts
|
|
@@ -34986,15 +35697,13 @@ function queryScalar(db, sql, params = []) {
|
|
|
34986
35697
|
return result[0].values[0]?.[0] ?? null;
|
|
34987
35698
|
}
|
|
34988
35699
|
var FilesystemSync = class {
|
|
34989
|
-
constructor(db, sessionsDir, io2
|
|
35700
|
+
constructor(db, sessionsDir, io2) {
|
|
34990
35701
|
this.db = db;
|
|
34991
35702
|
this.sessionsDir = sessionsDir;
|
|
34992
35703
|
this.io = io2;
|
|
34993
|
-
this.onSync = onSync;
|
|
34994
35704
|
}
|
|
34995
35705
|
watcher = null;
|
|
34996
35706
|
debounceTimers = /* @__PURE__ */ new Map();
|
|
34997
|
-
onSync;
|
|
34998
35707
|
// ── 6.1: Full Scan ──
|
|
34999
35708
|
async fullScan() {
|
|
35000
35709
|
if (!existsSync10(this.sessionsDir)) return;
|
|
@@ -35002,13 +35711,13 @@ var FilesystemSync = class {
|
|
|
35002
35711
|
for (const entry of entries) {
|
|
35003
35712
|
if (!entry.isDirectory()) continue;
|
|
35004
35713
|
const sessionId = entry.name;
|
|
35005
|
-
const sessionDir =
|
|
35714
|
+
const sessionDir = join14(this.sessionsDir, sessionId);
|
|
35006
35715
|
this.syncSession(sessionId, sessionDir);
|
|
35007
35716
|
}
|
|
35008
35717
|
}
|
|
35009
35718
|
syncSession(sessionId, sessionDir) {
|
|
35010
35719
|
this.ensureSessionRow(sessionId, sessionDir);
|
|
35011
|
-
const roundsDir =
|
|
35720
|
+
const roundsDir = join14(sessionDir, "rounds");
|
|
35012
35721
|
if (existsSync10(roundsDir)) {
|
|
35013
35722
|
const rounds = readdirSync(roundsDir, { withFileTypes: true });
|
|
35014
35723
|
for (const roundEntry of rounds) {
|
|
@@ -35016,34 +35725,34 @@ var FilesystemSync = class {
|
|
|
35016
35725
|
const roundMatch = roundEntry.name.match(/^round-(\d+)$/);
|
|
35017
35726
|
if (!roundMatch) continue;
|
|
35018
35727
|
const roundNumber = parseInt(roundMatch[1] ?? "0", 10);
|
|
35019
|
-
const roundDir =
|
|
35020
|
-
const reviewsDir =
|
|
35728
|
+
const roundDir = join14(roundsDir, roundEntry.name);
|
|
35729
|
+
const reviewsDir = join14(roundDir, "reviews");
|
|
35021
35730
|
if (existsSync10(reviewsDir)) {
|
|
35022
35731
|
const reviewFiles = readdirSync(reviewsDir).filter((f) => f.endsWith(".md"));
|
|
35023
35732
|
for (const reviewFile of reviewFiles) {
|
|
35024
|
-
const filePath =
|
|
35733
|
+
const filePath = join14(reviewsDir, reviewFile);
|
|
35025
35734
|
this.processReviewerOutput(sessionId, roundNumber, filePath, reviewFile);
|
|
35026
35735
|
}
|
|
35027
35736
|
}
|
|
35028
|
-
const roundMetaPath =
|
|
35737
|
+
const roundMetaPath = join14(roundDir, "round-meta.json");
|
|
35029
35738
|
if (existsSync10(roundMetaPath)) {
|
|
35030
35739
|
this.processRoundMeta(sessionId, roundNumber, roundMetaPath);
|
|
35031
35740
|
}
|
|
35032
|
-
const finalPath =
|
|
35741
|
+
const finalPath = join14(roundDir, "final.md");
|
|
35033
35742
|
if (existsSync10(finalPath)) {
|
|
35034
35743
|
this.processFinalMd(sessionId, roundNumber, finalPath);
|
|
35035
35744
|
}
|
|
35036
|
-
const finalHumanPath =
|
|
35745
|
+
const finalHumanPath = join14(roundDir, "final-human.md");
|
|
35037
35746
|
if (existsSync10(finalHumanPath)) {
|
|
35038
35747
|
this.processGenericArtifact(sessionId, "final-human", finalHumanPath, roundNumber);
|
|
35039
35748
|
}
|
|
35040
|
-
const discoursePath =
|
|
35749
|
+
const discoursePath = join14(roundDir, "discourse.md");
|
|
35041
35750
|
if (existsSync10(discoursePath)) {
|
|
35042
35751
|
this.processGenericArtifact(sessionId, "discourse", discoursePath, roundNumber);
|
|
35043
35752
|
}
|
|
35044
35753
|
}
|
|
35045
35754
|
}
|
|
35046
|
-
const mapDir =
|
|
35755
|
+
const mapDir = join14(sessionDir, "map", "runs");
|
|
35047
35756
|
if (existsSync10(mapDir)) {
|
|
35048
35757
|
const runs = readdirSync(mapDir, { withFileTypes: true });
|
|
35049
35758
|
for (const runEntry of runs) {
|
|
@@ -35051,12 +35760,12 @@ var FilesystemSync = class {
|
|
|
35051
35760
|
const runMatch = runEntry.name.match(/^run-(\d+)$/);
|
|
35052
35761
|
if (!runMatch) continue;
|
|
35053
35762
|
const runNumber = parseInt(runMatch[1] ?? "0", 10);
|
|
35054
|
-
const runDir =
|
|
35055
|
-
const mapMetaPath =
|
|
35763
|
+
const runDir = join14(mapDir, runEntry.name);
|
|
35764
|
+
const mapMetaPath = join14(runDir, "map-meta.json");
|
|
35056
35765
|
if (existsSync10(mapMetaPath)) {
|
|
35057
35766
|
this.processMapMeta(sessionId, runNumber, mapMetaPath);
|
|
35058
35767
|
}
|
|
35059
|
-
const mapPath =
|
|
35768
|
+
const mapPath = join14(runDir, "map.md");
|
|
35060
35769
|
if (existsSync10(mapPath)) {
|
|
35061
35770
|
this.processMapMd(sessionId, runNumber, mapPath);
|
|
35062
35771
|
}
|
|
@@ -35066,7 +35775,7 @@ var FilesystemSync = class {
|
|
|
35066
35775
|
["requirements-mapping.md", "requirements-mapping"]
|
|
35067
35776
|
];
|
|
35068
35777
|
for (const [fileName, artifactType] of mapArtifacts) {
|
|
35069
|
-
const filePath =
|
|
35778
|
+
const filePath = join14(runDir, fileName);
|
|
35070
35779
|
if (existsSync10(filePath)) {
|
|
35071
35780
|
this.processGenericArtifact(sessionId, artifactType, filePath, void 0, runNumber);
|
|
35072
35781
|
}
|
|
@@ -35078,7 +35787,7 @@ var FilesystemSync = class {
|
|
|
35078
35787
|
["discovered-standards.md", "discovered-standards"]
|
|
35079
35788
|
];
|
|
35080
35789
|
for (const [fileName, artifactType] of sessionArtifacts) {
|
|
35081
|
-
const filePath =
|
|
35790
|
+
const filePath = join14(sessionDir, fileName);
|
|
35082
35791
|
if (existsSync10(filePath)) {
|
|
35083
35792
|
this.processGenericArtifact(sessionId, artifactType, filePath);
|
|
35084
35793
|
}
|
|
@@ -35088,16 +35797,16 @@ var FilesystemSync = class {
|
|
|
35088
35797
|
ensureSessionRow(sessionId, sessionDir) {
|
|
35089
35798
|
const branchMatch = sessionId.match(/^\d{4}-\d{2}-\d{2}-(.+)$/);
|
|
35090
35799
|
const branch = branchMatch?.[1] ?? "unknown";
|
|
35091
|
-
const hasRoundsDir = existsSync10(
|
|
35092
|
-
const hasMapDir = existsSync10(
|
|
35800
|
+
const hasRoundsDir = existsSync10(join14(sessionDir, "rounds"));
|
|
35801
|
+
const hasMapDir = existsSync10(join14(sessionDir, "map"));
|
|
35093
35802
|
const workflowType = hasMapDir && !hasRoundsDir ? "map" : "review";
|
|
35094
35803
|
let currentRound = 1;
|
|
35095
35804
|
if (hasRoundsDir) {
|
|
35096
|
-
const roundDirs = readdirSync(
|
|
35805
|
+
const roundDirs = readdirSync(join14(sessionDir, "rounds")).filter((d) => d.match(/^round-\d+$/));
|
|
35097
35806
|
currentRound = Math.max(1, roundDirs.length);
|
|
35098
35807
|
}
|
|
35099
35808
|
let currentMapRun = 1;
|
|
35100
|
-
const mapRunsDir =
|
|
35809
|
+
const mapRunsDir = join14(sessionDir, "map", "runs");
|
|
35101
35810
|
if (existsSync10(mapRunsDir)) {
|
|
35102
35811
|
const runDirs = readdirSync(mapRunsDir).filter((d) => d.match(/^run-\d+$/));
|
|
35103
35812
|
currentMapRun = Math.max(1, runDirs.length);
|
|
@@ -35106,40 +35815,40 @@ var FilesystemSync = class {
|
|
|
35106
35815
|
let phaseNumber = 1;
|
|
35107
35816
|
let status = "closed";
|
|
35108
35817
|
if (workflowType === "review" && hasRoundsDir) {
|
|
35109
|
-
const roundDir =
|
|
35110
|
-
if (existsSync10(
|
|
35818
|
+
const roundDir = join14(sessionDir, "rounds", `round-${currentRound}`);
|
|
35819
|
+
if (existsSync10(join14(roundDir, "final.md"))) {
|
|
35111
35820
|
phase = "complete";
|
|
35112
35821
|
phaseNumber = 8;
|
|
35113
35822
|
status = "closed";
|
|
35114
|
-
} else if (existsSync10(
|
|
35823
|
+
} else if (existsSync10(join14(roundDir, "discourse.md"))) {
|
|
35115
35824
|
phase = "synthesis";
|
|
35116
35825
|
phaseNumber = 7;
|
|
35117
|
-
} else if (existsSync10(
|
|
35826
|
+
} else if (existsSync10(join14(roundDir, "reviews")) && readdirSync(join14(roundDir, "reviews")).filter((f) => f.endsWith(".md")).length > 0) {
|
|
35118
35827
|
phase = "reviews";
|
|
35119
35828
|
phaseNumber = 4;
|
|
35120
|
-
} else if (existsSync10(
|
|
35829
|
+
} else if (existsSync10(join14(sessionDir, "context.md"))) {
|
|
35121
35830
|
phase = "analysis";
|
|
35122
35831
|
phaseNumber = 3;
|
|
35123
|
-
} else if (existsSync10(
|
|
35832
|
+
} else if (existsSync10(join14(sessionDir, "discovered-standards.md"))) {
|
|
35124
35833
|
phase = "change-context";
|
|
35125
35834
|
phaseNumber = 2;
|
|
35126
35835
|
}
|
|
35127
35836
|
} else if (workflowType === "map" && hasMapDir) {
|
|
35128
|
-
const runDir =
|
|
35129
|
-
if (existsSync10(
|
|
35837
|
+
const runDir = join14(mapRunsDir, `run-${currentMapRun}`);
|
|
35838
|
+
if (existsSync10(join14(runDir, "map.md"))) {
|
|
35130
35839
|
phase = "complete";
|
|
35131
35840
|
phaseNumber = 6;
|
|
35132
35841
|
status = "closed";
|
|
35133
|
-
} else if (existsSync10(
|
|
35842
|
+
} else if (existsSync10(join14(runDir, "requirements-mapping.md"))) {
|
|
35134
35843
|
phase = "synthesis";
|
|
35135
35844
|
phaseNumber = 5;
|
|
35136
|
-
} else if (existsSync10(
|
|
35845
|
+
} else if (existsSync10(join14(runDir, "flow-analysis.md"))) {
|
|
35137
35846
|
phase = "requirements-mapping";
|
|
35138
35847
|
phaseNumber = 4;
|
|
35139
|
-
} else if (existsSync10(
|
|
35848
|
+
} else if (existsSync10(join14(runDir, "topology.md"))) {
|
|
35140
35849
|
phase = "flow-analysis";
|
|
35141
35850
|
phaseNumber = 3;
|
|
35142
|
-
} else if (existsSync10(
|
|
35851
|
+
} else if (existsSync10(join14(sessionDir, "discovered-standards.md"))) {
|
|
35143
35852
|
phase = "topology";
|
|
35144
35853
|
phaseNumber = 2;
|
|
35145
35854
|
}
|
|
@@ -35153,14 +35862,40 @@ var FilesystemSync = class {
|
|
|
35153
35862
|
);
|
|
35154
35863
|
} else {
|
|
35155
35864
|
if (!this.hasArtifacts(sessionDir)) return;
|
|
35156
|
-
this.db.
|
|
35157
|
-
|
|
35158
|
-
|
|
35159
|
-
|
|
35160
|
-
|
|
35865
|
+
this.db.transaction(() => {
|
|
35866
|
+
insertSession(this.db, {
|
|
35867
|
+
id: sessionId,
|
|
35868
|
+
branch,
|
|
35869
|
+
workflow_type: workflowType,
|
|
35870
|
+
current_phase: phase,
|
|
35871
|
+
phase_number: phaseNumber,
|
|
35872
|
+
current_round: currentRound,
|
|
35873
|
+
current_map_run: currentMapRun,
|
|
35874
|
+
session_dir: sessionDir
|
|
35875
|
+
});
|
|
35876
|
+
insertEvent(this.db, {
|
|
35877
|
+
session_id: sessionId,
|
|
35878
|
+
event_type: "session_created",
|
|
35879
|
+
phase,
|
|
35880
|
+
phase_number: 1,
|
|
35881
|
+
round: 1
|
|
35882
|
+
});
|
|
35883
|
+
});
|
|
35884
|
+
if (status === "closed") {
|
|
35885
|
+
commitReasonClose(
|
|
35886
|
+
this.db,
|
|
35887
|
+
sessionId,
|
|
35888
|
+
{
|
|
35889
|
+
event_type: "session_synced",
|
|
35890
|
+
phase,
|
|
35891
|
+
phase_number: phaseNumber,
|
|
35892
|
+
metadata: JSON.stringify({ source: "filesystem_backfill" })
|
|
35893
|
+
},
|
|
35894
|
+
{ status: "closed", current_phase: phase, phase_number: phaseNumber }
|
|
35895
|
+
);
|
|
35896
|
+
}
|
|
35161
35897
|
this.io?.emit("session:created", { id: sessionId, branch, workflow_type: workflowType, status, current_phase: phase });
|
|
35162
35898
|
}
|
|
35163
|
-
this.onSync?.();
|
|
35164
35899
|
}
|
|
35165
35900
|
// ── Artifact Check ──
|
|
35166
35901
|
/** Returns true if the directory contains at least one .md or .json file (recursively). */
|
|
@@ -35168,7 +35903,7 @@ var FilesystemSync = class {
|
|
|
35168
35903
|
try {
|
|
35169
35904
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
35170
35905
|
if (entry.isDirectory()) {
|
|
35171
|
-
if (this.hasArtifacts(
|
|
35906
|
+
if (this.hasArtifacts(join14(dir, entry.name))) return true;
|
|
35172
35907
|
} else if (/\.(md|json)$/.test(entry.name)) {
|
|
35173
35908
|
return true;
|
|
35174
35909
|
}
|
|
@@ -35181,7 +35916,7 @@ var FilesystemSync = class {
|
|
|
35181
35916
|
shouldSkip(filePath, existingParsedAt) {
|
|
35182
35917
|
if (!existingParsedAt) return false;
|
|
35183
35918
|
try {
|
|
35184
|
-
const mtime =
|
|
35919
|
+
const mtime = statSync2(filePath).mtime;
|
|
35185
35920
|
const parsedAt = new Date(existingParsedAt);
|
|
35186
35921
|
return mtime <= parsedAt;
|
|
35187
35922
|
} catch {
|
|
@@ -35218,12 +35953,12 @@ var FilesystemSync = class {
|
|
|
35218
35953
|
);
|
|
35219
35954
|
if (existingRun && this.shouldSkip(filePath, existingRun["parsed_at"] ?? null)) return;
|
|
35220
35955
|
if (existingRun?.["source"] === "orchestrator") {
|
|
35221
|
-
const content2 =
|
|
35956
|
+
const content2 = readFileSync8(filePath, "utf-8");
|
|
35222
35957
|
const action2 = this.upsertMarkdownArtifact(sessionId, "map", filePath, content2, void 0);
|
|
35223
35958
|
this.emitArtifactEvent(action2, { sessionId, artifactType: "map", filePath });
|
|
35224
35959
|
return;
|
|
35225
35960
|
}
|
|
35226
|
-
const content =
|
|
35961
|
+
const content = readFileSync8(filePath, "utf-8");
|
|
35227
35962
|
const parsed = parseMapMd(content);
|
|
35228
35963
|
this.db.run(
|
|
35229
35964
|
`INSERT OR REPLACE INTO map_runs (session_id, run_number, file_count, map_md_path, parsed_at, source)
|
|
@@ -35309,10 +36044,16 @@ var FilesystemSync = class {
|
|
|
35309
36044
|
[sessionId]
|
|
35310
36045
|
);
|
|
35311
36046
|
if (session && session["workflow_type"] === "map" && (session["current_phase"] !== "complete" || session["phase_number"] < 6)) {
|
|
35312
|
-
|
|
35313
|
-
|
|
35314
|
-
|
|
35315
|
-
|
|
36047
|
+
commitReasonClose(
|
|
36048
|
+
this.db,
|
|
36049
|
+
sessionId,
|
|
36050
|
+
{
|
|
36051
|
+
event_type: "session_synced",
|
|
36052
|
+
phase: "complete",
|
|
36053
|
+
phase_number: 6,
|
|
36054
|
+
metadata: JSON.stringify({ source: "filesystem_backfill" })
|
|
36055
|
+
},
|
|
36056
|
+
{ status: "closed", current_phase: "complete", phase_number: 6 }
|
|
35316
36057
|
);
|
|
35317
36058
|
this.io?.emit("session:updated", {
|
|
35318
36059
|
id: sessionId,
|
|
@@ -35343,7 +36084,7 @@ var FilesystemSync = class {
|
|
|
35343
36084
|
const roundId = roundRow?.["id"];
|
|
35344
36085
|
if (!roundId) return;
|
|
35345
36086
|
if (roundRow?.["source"] === "orchestrator") {
|
|
35346
|
-
const content2 =
|
|
36087
|
+
const content2 = readFileSync8(filePath, "utf-8");
|
|
35347
36088
|
const action2 = this.upsertMarkdownArtifact(sessionId, "reviewer-output", filePath, content2, roundNumber);
|
|
35348
36089
|
this.emitArtifactEvent(action2, {
|
|
35349
36090
|
sessionId,
|
|
@@ -35362,7 +36103,7 @@ var FilesystemSync = class {
|
|
|
35362
36103
|
[roundId, reviewerType, instanceNumber]
|
|
35363
36104
|
);
|
|
35364
36105
|
if (existingOutput && this.shouldSkip(filePath, existingOutput["parsed_at"] ?? null)) return;
|
|
35365
|
-
const content =
|
|
36106
|
+
const content = readFileSync8(filePath, "utf-8");
|
|
35366
36107
|
const parsed = parseReviewerOutput(content);
|
|
35367
36108
|
this.db.run(
|
|
35368
36109
|
`INSERT OR REPLACE INTO reviewer_outputs (round_id, reviewer_type, instance_number, file_path, finding_count, parsed_at)
|
|
@@ -35450,7 +36191,7 @@ var FilesystemSync = class {
|
|
|
35450
36191
|
if (existingRound?.["source"] === "orchestrator" && this.shouldSkip(filePath, existingRound["parsed_at"] ?? null)) return;
|
|
35451
36192
|
let raw;
|
|
35452
36193
|
try {
|
|
35453
|
-
raw = JSON.parse(
|
|
36194
|
+
raw = JSON.parse(readFileSync8(filePath, "utf-8"));
|
|
35454
36195
|
} catch {
|
|
35455
36196
|
console.error(`[FilesystemSync] Failed to parse ${filePath}`);
|
|
35456
36197
|
return;
|
|
@@ -35501,7 +36242,7 @@ var FilesystemSync = class {
|
|
|
35501
36242
|
const reviewerType = reviewer.type ?? "unknown";
|
|
35502
36243
|
const instanceNumber = reviewer.instance ?? 1;
|
|
35503
36244
|
const findings = reviewer.findings ?? [];
|
|
35504
|
-
const reviewerMdPath =
|
|
36245
|
+
const reviewerMdPath = join14(roundDir, "reviews", `${reviewerType}-${instanceNumber}.md`);
|
|
35505
36246
|
this.db.run(
|
|
35506
36247
|
`INSERT OR REPLACE INTO reviewer_outputs (round_id, reviewer_type, instance_number, file_path, finding_count, parsed_at)
|
|
35507
36248
|
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
@@ -35599,7 +36340,7 @@ var FilesystemSync = class {
|
|
|
35599
36340
|
if (existingRun?.["source"] === "orchestrator" && this.shouldSkip(filePath, existingRun["parsed_at"] ?? null)) return;
|
|
35600
36341
|
let raw;
|
|
35601
36342
|
try {
|
|
35602
|
-
raw = JSON.parse(
|
|
36343
|
+
raw = JSON.parse(readFileSync8(filePath, "utf-8"));
|
|
35603
36344
|
} catch {
|
|
35604
36345
|
console.error(`[FilesystemSync] Failed to parse ${filePath}`);
|
|
35605
36346
|
return;
|
|
@@ -35727,7 +36468,7 @@ var FilesystemSync = class {
|
|
|
35727
36468
|
);
|
|
35728
36469
|
const isOrchestratorSource = existingRound?.["source"] === "orchestrator";
|
|
35729
36470
|
if (!isOrchestratorSource && existingRound && this.shouldSkip(filePath, existingRound["parsed_at"] ?? null)) return;
|
|
35730
|
-
const content =
|
|
36471
|
+
const content = readFileSync8(filePath, "utf-8");
|
|
35731
36472
|
if (isOrchestratorSource) {
|
|
35732
36473
|
this.db.run(
|
|
35733
36474
|
`UPDATE review_rounds SET final_md_path = ?, parsed_at = ?
|
|
@@ -35771,10 +36512,16 @@ var FilesystemSync = class {
|
|
|
35771
36512
|
[sessionId]
|
|
35772
36513
|
);
|
|
35773
36514
|
if (session && (session["current_phase"] !== "complete" || session["phase_number"] < 8)) {
|
|
35774
|
-
|
|
35775
|
-
|
|
35776
|
-
|
|
35777
|
-
|
|
36515
|
+
commitReasonClose(
|
|
36516
|
+
this.db,
|
|
36517
|
+
sessionId,
|
|
36518
|
+
{
|
|
36519
|
+
event_type: "session_synced",
|
|
36520
|
+
phase: "complete",
|
|
36521
|
+
phase_number: 8,
|
|
36522
|
+
metadata: JSON.stringify({ source: "filesystem_backfill" })
|
|
36523
|
+
},
|
|
36524
|
+
{ status: "closed", current_phase: "complete", phase_number: 8 }
|
|
35778
36525
|
);
|
|
35779
36526
|
this.io?.emit("session:updated", {
|
|
35780
36527
|
id: sessionId,
|
|
@@ -35800,7 +36547,7 @@ var FilesystemSync = class {
|
|
|
35800
36547
|
[sessionId, artifactType, relPath]
|
|
35801
36548
|
);
|
|
35802
36549
|
if (existing && this.shouldSkip(filePath, existing["parsed_at"] ?? null)) return;
|
|
35803
|
-
const content =
|
|
36550
|
+
const content = readFileSync8(filePath, "utf-8");
|
|
35804
36551
|
const action = this.upsertMarkdownArtifact(sessionId, artifactType, filePath, content, roundNumber);
|
|
35805
36552
|
this.emitArtifactEvent(action, {
|
|
35806
36553
|
sessionId,
|
|
@@ -35847,7 +36594,6 @@ var FilesystemSync = class {
|
|
|
35847
36594
|
this.debounceTimers.delete(filePath);
|
|
35848
36595
|
try {
|
|
35849
36596
|
this.processChangedFile(filePath);
|
|
35850
|
-
this.onSync?.();
|
|
35851
36597
|
} catch (err) {
|
|
35852
36598
|
console.error(`[FilesystemSync] Error processing ${filePath}:`, err);
|
|
35853
36599
|
}
|
|
@@ -35859,7 +36605,7 @@ var FilesystemSync = class {
|
|
|
35859
36605
|
const parts = relFromSessions.split("/");
|
|
35860
36606
|
const sessionId = parts[0];
|
|
35861
36607
|
if (!sessionId) return;
|
|
35862
|
-
const sessionDir =
|
|
36608
|
+
const sessionDir = join14(this.sessionsDir, sessionId);
|
|
35863
36609
|
this.ensureSessionRow(sessionId, sessionDir);
|
|
35864
36610
|
const fileName = basename2(filePath);
|
|
35865
36611
|
const reviewerMatch = relFromSessions.match(/rounds\/round-(\d+)\/reviews\/(.+\.md)$/);
|
|
@@ -35931,53 +36677,52 @@ var FilesystemSync = class {
|
|
|
35931
36677
|
};
|
|
35932
36678
|
|
|
35933
36679
|
// src/server/services/db-sync-watcher.ts
|
|
35934
|
-
import { existsSync as existsSync11
|
|
36680
|
+
import { existsSync as existsSync11 } from "node:fs";
|
|
35935
36681
|
import { dirname as dirname10, basename as basename3 } from "node:path";
|
|
35936
36682
|
import { watch as watch3 } from "chokidar";
|
|
35937
|
-
import initSqlJs3 from "sql.js";
|
|
35938
36683
|
function col(row, key) {
|
|
35939
36684
|
return row[key] ?? null;
|
|
35940
36685
|
}
|
|
35941
|
-
var SQLITE_MAGIC = Buffer.from("SQLite format 3\0", "utf-8");
|
|
35942
|
-
function isValidSqliteHeader(buf) {
|
|
35943
|
-
if (buf.length < SQLITE_MAGIC.length) return false;
|
|
35944
|
-
return buf.subarray(0, SQLITE_MAGIC.length).equals(SQLITE_MAGIC);
|
|
35945
|
-
}
|
|
35946
36686
|
var DbSyncWatcher = class {
|
|
35947
|
-
constructor(db, dbFilePath, io2,
|
|
36687
|
+
constructor(db, dbFilePath, io2, onSessionInserted) {
|
|
35948
36688
|
this.db = db;
|
|
35949
36689
|
this.dbFilePath = dbFilePath;
|
|
35950
36690
|
this.io = io2;
|
|
35951
|
-
this.onSync = onSync;
|
|
35952
36691
|
this.onSessionInserted = onSessionInserted;
|
|
35953
36692
|
}
|
|
35954
36693
|
watcher = null;
|
|
35955
36694
|
debounceTimer = null;
|
|
35956
|
-
|
|
35957
|
-
|
|
35958
|
-
|
|
36695
|
+
// Snapshots of last-emitted state, so we only emit on genuine changes.
|
|
36696
|
+
seenSessions = /* @__PURE__ */ new Map();
|
|
36697
|
+
maxEventId = 0;
|
|
36698
|
+
seenCommandRows = /* @__PURE__ */ new Map();
|
|
35959
36699
|
/**
|
|
35960
|
-
*
|
|
36700
|
+
* Prime snapshots from the current database so existing state is not
|
|
36701
|
+
* re-emitted on first watch tick. (No WASM runtime to initialise under
|
|
36702
|
+
* the native engine.)
|
|
35961
36703
|
*/
|
|
35962
36704
|
async init() {
|
|
35963
|
-
|
|
35964
|
-
this.wasmBinary = wasmBuffer.buffer.slice(
|
|
35965
|
-
wasmBuffer.byteOffset,
|
|
35966
|
-
wasmBuffer.byteOffset + wasmBuffer.byteLength
|
|
35967
|
-
);
|
|
35968
|
-
this.SQL = await initSqlJs3({ wasmBinary: this.wasmBinary });
|
|
36705
|
+
this.primeSnapshots();
|
|
35969
36706
|
}
|
|
35970
|
-
|
|
35971
|
-
|
|
35972
|
-
|
|
36707
|
+
primeSnapshots() {
|
|
36708
|
+
for (const row of this.readSessions()) {
|
|
36709
|
+
const id = col(row, "id");
|
|
36710
|
+
if (id) this.seenSessions.set(id, sessionFingerprint(row));
|
|
36711
|
+
}
|
|
36712
|
+
const maxResult = this.db.exec("SELECT MAX(id) FROM orchestration_events");
|
|
36713
|
+
const maxVal = maxResult[0]?.values[0]?.[0];
|
|
36714
|
+
this.maxEventId = typeof maxVal === "number" ? maxVal : 0;
|
|
36715
|
+
for (const row of this.readCommandRows()) {
|
|
36716
|
+
const id = col(row, "id");
|
|
36717
|
+
if (id != null) this.seenCommandRows.set(id, commandFingerprint(row));
|
|
36718
|
+
}
|
|
36719
|
+
}
|
|
36720
|
+
/** Start watching the DB file (and its WAL sidecar) for external writes. */
|
|
35973
36721
|
startWatching() {
|
|
35974
36722
|
if (!existsSync11(this.dbFilePath)) return;
|
|
35975
|
-
try {
|
|
35976
|
-
this.lastMtime = statSync2(this.dbFilePath).mtimeMs;
|
|
35977
|
-
} catch {
|
|
35978
|
-
}
|
|
35979
36723
|
const watchDir = dirname10(this.dbFilePath);
|
|
35980
|
-
const
|
|
36724
|
+
const dbFile = basename3(this.dbFilePath);
|
|
36725
|
+
const walFile = `${dbFile}-wal`;
|
|
35981
36726
|
this.watcher = watch3(watchDir, {
|
|
35982
36727
|
persistent: true,
|
|
35983
36728
|
ignoreInitial: true,
|
|
@@ -35986,15 +36731,13 @@ var DbSyncWatcher = class {
|
|
|
35986
36731
|
interval: 200
|
|
35987
36732
|
});
|
|
35988
36733
|
const onAnyEvent = (path2) => {
|
|
35989
|
-
|
|
36734
|
+
const name = basename3(path2);
|
|
36735
|
+
if (name === dbFile || name === walFile) this.debouncedSync();
|
|
35990
36736
|
};
|
|
35991
36737
|
this.watcher.on("change", onAnyEvent);
|
|
35992
36738
|
this.watcher.on("add", onAnyEvent);
|
|
35993
36739
|
this.watcher.on("unlink", onAnyEvent);
|
|
35994
36740
|
}
|
|
35995
|
-
/**
|
|
35996
|
-
* Stop watching.
|
|
35997
|
-
*/
|
|
35998
36741
|
stopWatching() {
|
|
35999
36742
|
if (this.debounceTimer) {
|
|
36000
36743
|
clearTimeout(this.debounceTimer);
|
|
@@ -36005,9 +36748,6 @@ var DbSyncWatcher = class {
|
|
|
36005
36748
|
this.watcher = null;
|
|
36006
36749
|
}
|
|
36007
36750
|
}
|
|
36008
|
-
/**
|
|
36009
|
-
* Debounce sync to avoid rapid reloads during multi-statement writes.
|
|
36010
|
-
*/
|
|
36011
36751
|
debouncedSync() {
|
|
36012
36752
|
if (this.debounceTimer) clearTimeout(this.debounceTimer);
|
|
36013
36753
|
this.debounceTimer = setTimeout(() => {
|
|
@@ -36015,85 +36755,39 @@ var DbSyncWatcher = class {
|
|
|
36015
36755
|
}, 300);
|
|
36016
36756
|
}
|
|
36017
36757
|
/**
|
|
36018
|
-
*
|
|
36019
|
-
*
|
|
36020
|
-
*
|
|
36021
|
-
* the
|
|
36022
|
-
* dashboard-owned and untouched.
|
|
36023
|
-
*
|
|
36024
|
-
* Public for direct use; also called automatically via registered save hooks
|
|
36025
|
-
* to avoid overwriting CLI changes.
|
|
36026
|
-
*
|
|
36027
|
-
* Resilience:
|
|
36028
|
-
* 1. Validates the SQLite magic header before handing the buffer to
|
|
36029
|
-
* sql.js. If the header is missing — e.g. we caught the file
|
|
36030
|
-
* mid-atomic-rename and read the temp file or a zero-byte stub —
|
|
36031
|
-
* we skip the load and wait for the next watch event. Without this
|
|
36032
|
-
* guard, a torn read corrupts sql.js and surfaces as an opaque WASM
|
|
36033
|
-
* `memory access out of bounds` exception.
|
|
36034
|
-
* 2. Only advances `lastMtime` AFTER a successful load. A failed load
|
|
36035
|
-
* leaves the watermark untouched so the next change event retries.
|
|
36758
|
+
* Rescan the live database and emit notifications for anything that
|
|
36759
|
+
* changed since the last scan. Named `syncFromDisk` for call-site
|
|
36760
|
+
* compatibility; under the native engine it performs no merge writes —
|
|
36761
|
+
* it diffs the live connection against cached snapshots and emits.
|
|
36036
36762
|
*/
|
|
36037
36763
|
syncFromDisk() {
|
|
36038
|
-
if (!this.SQL || !existsSync11(this.dbFilePath)) return;
|
|
36039
|
-
let currentMtime;
|
|
36040
|
-
try {
|
|
36041
|
-
currentMtime = statSync2(this.dbFilePath).mtimeMs;
|
|
36042
|
-
} catch {
|
|
36043
|
-
return;
|
|
36044
|
-
}
|
|
36045
|
-
if (currentMtime <= this.lastMtime) return;
|
|
36046
|
-
let diskDb = null;
|
|
36047
36764
|
try {
|
|
36048
|
-
|
|
36049
|
-
|
|
36050
|
-
|
|
36051
|
-
}
|
|
36052
|
-
diskDb = new this.SQL.Database(fileBuffer);
|
|
36053
|
-
this.syncSessions(diskDb);
|
|
36054
|
-
this.syncEvents(diskDb);
|
|
36055
|
-
this.syncAgentSessions(diskDb);
|
|
36056
|
-
this.lastMtime = currentMtime;
|
|
36057
|
-
this.onSync?.();
|
|
36765
|
+
this.detectSessionChanges();
|
|
36766
|
+
this.detectNewEvents();
|
|
36767
|
+
this.detectCommandChanges();
|
|
36058
36768
|
} catch (err) {
|
|
36059
|
-
console.error("[DbSyncWatcher] Error
|
|
36060
|
-
} finally {
|
|
36061
|
-
diskDb?.close();
|
|
36769
|
+
console.error("[DbSyncWatcher] Error scanning for changes:", err);
|
|
36062
36770
|
}
|
|
36063
36771
|
}
|
|
36064
|
-
|
|
36065
|
-
|
|
36066
|
-
|
|
36067
|
-
|
|
36068
|
-
|
|
36069
|
-
|
|
36070
|
-
|
|
36071
|
-
|
|
36772
|
+
readSessions() {
|
|
36773
|
+
return resultToRows(this.db.exec("SELECT * FROM sessions"));
|
|
36774
|
+
}
|
|
36775
|
+
readCommandRows() {
|
|
36776
|
+
return resultToRows(
|
|
36777
|
+
this.db.exec(
|
|
36778
|
+
`SELECT id, last_heartbeat_at, finished_at, exit_code, workflow_id, vendor_session_id
|
|
36779
|
+
FROM command_executions`
|
|
36780
|
+
)
|
|
36781
|
+
);
|
|
36782
|
+
}
|
|
36783
|
+
detectSessionChanges() {
|
|
36784
|
+
for (const row of this.readSessions()) {
|
|
36072
36785
|
const id = col(row, "id");
|
|
36073
36786
|
if (!id) continue;
|
|
36074
|
-
const
|
|
36075
|
-
|
|
36076
|
-
|
|
36077
|
-
|
|
36078
|
-
const memRows = resultToRows(memResult);
|
|
36079
|
-
if (memRows.length === 0) {
|
|
36080
|
-
this.db.run(
|
|
36081
|
-
`INSERT INTO sessions (id, branch, status, workflow_type, current_phase, phase_number, current_round, current_map_run, started_at, updated_at, session_dir)
|
|
36082
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
36083
|
-
[
|
|
36084
|
-
col(row, "id"),
|
|
36085
|
-
col(row, "branch"),
|
|
36086
|
-
col(row, "status"),
|
|
36087
|
-
col(row, "workflow_type"),
|
|
36088
|
-
col(row, "current_phase"),
|
|
36089
|
-
col(row, "phase_number"),
|
|
36090
|
-
col(row, "current_round"),
|
|
36091
|
-
col(row, "current_map_run"),
|
|
36092
|
-
col(row, "started_at"),
|
|
36093
|
-
col(row, "updated_at"),
|
|
36094
|
-
col(row, "session_dir")
|
|
36095
|
-
]
|
|
36096
|
-
);
|
|
36787
|
+
const fp = sessionFingerprint(row);
|
|
36788
|
+
const prev = this.seenSessions.get(id);
|
|
36789
|
+
if (prev === void 0) {
|
|
36790
|
+
this.seenSessions.set(id, fp);
|
|
36097
36791
|
this.io.emit("session:created", {
|
|
36098
36792
|
id,
|
|
36099
36793
|
branch: col(row, "branch"),
|
|
@@ -36110,81 +36804,33 @@ var DbSyncWatcher = class {
|
|
|
36110
36804
|
} catch (err) {
|
|
36111
36805
|
console.error("[DbSyncWatcher] onSessionInserted hook failed:", err);
|
|
36112
36806
|
}
|
|
36113
|
-
} else {
|
|
36114
|
-
|
|
36115
|
-
|
|
36116
|
-
|
|
36117
|
-
|
|
36118
|
-
|
|
36119
|
-
|
|
36120
|
-
|
|
36121
|
-
if (diskPhase !== memPhase || diskStatus !== memStatus || diskCurrent !== memCurrent) {
|
|
36122
|
-
this.db.run(
|
|
36123
|
-
`UPDATE sessions
|
|
36124
|
-
SET current_phase = ?, phase_number = ?, status = ?,
|
|
36125
|
-
current_round = ?, current_map_run = ?, workflow_type = ?,
|
|
36126
|
-
updated_at = ?
|
|
36127
|
-
WHERE id = ?`,
|
|
36128
|
-
[
|
|
36129
|
-
col(row, "current_phase"),
|
|
36130
|
-
col(row, "phase_number"),
|
|
36131
|
-
col(row, "status"),
|
|
36132
|
-
col(row, "current_round"),
|
|
36133
|
-
col(row, "current_map_run"),
|
|
36134
|
-
col(row, "workflow_type"),
|
|
36135
|
-
col(row, "updated_at"),
|
|
36136
|
-
id
|
|
36137
|
-
]
|
|
36138
|
-
);
|
|
36139
|
-
this.io.emit("session:updated", {
|
|
36140
|
-
id,
|
|
36141
|
-
status: col(row, "status"),
|
|
36142
|
-
current_phase: col(row, "current_phase"),
|
|
36143
|
-
phase_number: col(row, "phase_number")
|
|
36144
|
-
});
|
|
36145
|
-
}
|
|
36807
|
+
} else if (prev !== fp) {
|
|
36808
|
+
this.seenSessions.set(id, fp);
|
|
36809
|
+
this.io.emit("session:updated", {
|
|
36810
|
+
id,
|
|
36811
|
+
status: col(row, "status"),
|
|
36812
|
+
current_phase: col(row, "current_phase"),
|
|
36813
|
+
phase_number: col(row, "phase_number")
|
|
36814
|
+
});
|
|
36146
36815
|
}
|
|
36147
36816
|
}
|
|
36148
36817
|
}
|
|
36149
|
-
|
|
36150
|
-
|
|
36151
|
-
|
|
36152
|
-
|
|
36153
|
-
|
|
36154
|
-
|
|
36155
|
-
diskDb.exec("SELECT * FROM orchestration_events ORDER BY id ASC")
|
|
36818
|
+
detectNewEvents() {
|
|
36819
|
+
const rows = resultToRows(
|
|
36820
|
+
this.db.exec(
|
|
36821
|
+
"SELECT * FROM orchestration_events WHERE id > ? ORDER BY id ASC",
|
|
36822
|
+
[this.maxEventId]
|
|
36823
|
+
)
|
|
36156
36824
|
);
|
|
36157
|
-
|
|
36825
|
+
if (rows.length === 0) return;
|
|
36158
36826
|
const affectedSessions = /* @__PURE__ */ new Set();
|
|
36159
|
-
for (const row of
|
|
36827
|
+
for (const row of rows) {
|
|
36160
36828
|
const eventId = col(row, "id");
|
|
36161
36829
|
const sessionId = col(row, "session_id");
|
|
36162
|
-
const existing = this.db.exec(
|
|
36163
|
-
"SELECT id FROM orchestration_events WHERE id = ?",
|
|
36164
|
-
[eventId]
|
|
36165
|
-
);
|
|
36166
|
-
if (existing.length > 0 && existing[0]?.values.length !== 0) continue;
|
|
36167
|
-
this.db.run(
|
|
36168
|
-
`INSERT OR IGNORE INTO orchestration_events (id, session_id, event_type, phase, phase_number, round, metadata, created_at)
|
|
36169
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
36170
|
-
[
|
|
36171
|
-
eventId,
|
|
36172
|
-
sessionId,
|
|
36173
|
-
col(row, "event_type"),
|
|
36174
|
-
col(row, "phase"),
|
|
36175
|
-
col(row, "phase_number"),
|
|
36176
|
-
col(row, "round"),
|
|
36177
|
-
col(row, "metadata"),
|
|
36178
|
-
col(row, "created_at")
|
|
36179
|
-
]
|
|
36180
|
-
);
|
|
36181
|
-
newEvents.push(row);
|
|
36182
|
-
affectedSessions.add(sessionId);
|
|
36183
|
-
}
|
|
36184
|
-
for (const row of newEvents) {
|
|
36185
36830
|
const eventType = col(row, "event_type");
|
|
36186
|
-
const sessionId = col(row, "session_id");
|
|
36187
36831
|
const metadataStr = col(row, "metadata");
|
|
36832
|
+
if (eventId > this.maxEventId) this.maxEventId = eventId;
|
|
36833
|
+
if (sessionId) affectedSessions.add(sessionId);
|
|
36188
36834
|
if (eventType === "round_completed") {
|
|
36189
36835
|
const roundNumber = col(row, "round");
|
|
36190
36836
|
if (sessionId && roundNumber && metadataStr) {
|
|
@@ -36197,98 +36843,33 @@ var DbSyncWatcher = class {
|
|
|
36197
36843
|
}
|
|
36198
36844
|
}
|
|
36199
36845
|
}
|
|
36200
|
-
|
|
36201
|
-
|
|
36202
|
-
this.io.to(`session:${sessionId}`).emit("session:events", { session_id: sessionId });
|
|
36203
|
-
}
|
|
36846
|
+
for (const sessionId of affectedSessions) {
|
|
36847
|
+
this.io.to(`session:${sessionId}`).emit("session:events", { session_id: sessionId });
|
|
36204
36848
|
}
|
|
36205
36849
|
}
|
|
36206
|
-
|
|
36207
|
-
* Sync the `command_executions` table from disk → in-memory.
|
|
36208
|
-
*
|
|
36209
|
-
* Migration v11 unified `agent_sessions` into `command_executions`. The
|
|
36210
|
-
* journal fields (`vendor`, `vendor_session_id`, `persona`, `resolved_model`,
|
|
36211
|
-
* `last_heartbeat_at`, `notes`, …) live on the same table. Rows the CLI
|
|
36212
|
-
* writes via `ocr session start-instance` / `bind-vendor-id` / `beat` /
|
|
36213
|
-
* `end-instance` flow through atomic file rename, so the disk file is the
|
|
36214
|
-
* source of truth.
|
|
36215
|
-
*
|
|
36216
|
-
* Strategy: full mirror via `INSERT OR REPLACE` keyed on the integer
|
|
36217
|
-
* primary key. The table is small enough that copying every row each
|
|
36218
|
-
* sync is cheap. Emits `agent_session:updated` with affected workflow ids
|
|
36219
|
-
* so the dashboard's query cache invalidates selectively.
|
|
36220
|
-
*/
|
|
36221
|
-
syncAgentSessions(diskDb) {
|
|
36222
|
-
const diskRows = resultToRows(
|
|
36223
|
-
diskDb.exec("SELECT * FROM command_executions")
|
|
36224
|
-
);
|
|
36225
|
-
if (diskRows.length === 0) return;
|
|
36850
|
+
detectCommandChanges() {
|
|
36226
36851
|
const affectedWorkflows = /* @__PURE__ */ new Set();
|
|
36227
|
-
|
|
36228
|
-
for (const row of diskRows) {
|
|
36852
|
+
for (const row of this.readCommandRows()) {
|
|
36229
36853
|
const id = col(row, "id");
|
|
36854
|
+
if (id == null) continue;
|
|
36855
|
+
const fp = commandFingerprint(row);
|
|
36856
|
+
if (this.seenCommandRows.get(id) === fp) continue;
|
|
36857
|
+
this.seenCommandRows.set(id, fp);
|
|
36230
36858
|
const workflowId = col(row, "workflow_id");
|
|
36231
|
-
const vendorSessionId = col(row, "vendor_session_id");
|
|
36232
36859
|
const heartbeat = col(row, "last_heartbeat_at");
|
|
36233
|
-
const finishedAt = col(row, "finished_at");
|
|
36234
|
-
const exitCode = col(row, "exit_code");
|
|
36235
|
-
const existing = this.db.exec(
|
|
36236
|
-
`SELECT last_heartbeat_at, finished_at, exit_code,
|
|
36237
|
-
workflow_id, vendor_session_id
|
|
36238
|
-
FROM command_executions WHERE id = ?`,
|
|
36239
|
-
[id]
|
|
36240
|
-
);
|
|
36241
|
-
const existingRow = existing[0]?.values?.[0];
|
|
36242
|
-
const sameHeartbeat = (existingRow?.[0] ?? null) === heartbeat;
|
|
36243
|
-
const sameFinished = (existingRow?.[1] ?? null) === finishedAt;
|
|
36244
|
-
const sameExit = (existingRow?.[2] ?? null) === exitCode;
|
|
36245
|
-
const sameWorkflow = (existingRow?.[3] ?? null) === workflowId;
|
|
36246
|
-
const sameVendorSession = (existingRow?.[4] ?? null) === vendorSessionId;
|
|
36247
|
-
if (existingRow && sameHeartbeat && sameFinished && sameExit && sameWorkflow && sameVendorSession) continue;
|
|
36248
|
-
this.db.run(
|
|
36249
|
-
`INSERT OR REPLACE INTO command_executions
|
|
36250
|
-
(id, uid, command, args, exit_code, started_at, finished_at, output,
|
|
36251
|
-
pid, is_detached, workflow_id, parent_id, vendor, vendor_session_id,
|
|
36252
|
-
persona, instance_index, name, resolved_model, last_heartbeat_at, notes)
|
|
36253
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
36254
|
-
[
|
|
36255
|
-
id,
|
|
36256
|
-
col(row, "uid"),
|
|
36257
|
-
col(row, "command"),
|
|
36258
|
-
col(row, "args"),
|
|
36259
|
-
exitCode,
|
|
36260
|
-
col(row, "started_at"),
|
|
36261
|
-
finishedAt,
|
|
36262
|
-
col(row, "output"),
|
|
36263
|
-
col(row, "pid"),
|
|
36264
|
-
col(row, "is_detached"),
|
|
36265
|
-
workflowId,
|
|
36266
|
-
col(row, "parent_id"),
|
|
36267
|
-
col(row, "vendor"),
|
|
36268
|
-
col(row, "vendor_session_id"),
|
|
36269
|
-
col(row, "persona"),
|
|
36270
|
-
col(row, "instance_index"),
|
|
36271
|
-
col(row, "name"),
|
|
36272
|
-
col(row, "resolved_model"),
|
|
36273
|
-
heartbeat,
|
|
36274
|
-
col(row, "notes")
|
|
36275
|
-
]
|
|
36276
|
-
);
|
|
36277
36860
|
if (heartbeat !== null && workflowId) {
|
|
36278
36861
|
affectedWorkflows.add(workflowId);
|
|
36279
36862
|
}
|
|
36280
|
-
changed++;
|
|
36281
36863
|
}
|
|
36282
|
-
if (
|
|
36864
|
+
if (affectedWorkflows.size > 0) {
|
|
36283
36865
|
this.io.emit("agent_session:updated", {
|
|
36284
36866
|
workflow_ids: Array.from(affectedWorkflows)
|
|
36285
36867
|
});
|
|
36286
36868
|
}
|
|
36287
36869
|
}
|
|
36288
36870
|
/**
|
|
36289
|
-
*
|
|
36290
|
-
*
|
|
36291
|
-
* Idempotent — skips if round already has source='orchestrator'.
|
|
36871
|
+
* Project a `round_completed` event into `review_rounds` (orchestrator
|
|
36872
|
+
* source latch) and emit a room-scoped `round:updated`. Idempotent.
|
|
36292
36873
|
*/
|
|
36293
36874
|
processRoundCompletedEvent(sessionId, roundNumber, metadataStr) {
|
|
36294
36875
|
let metadata;
|
|
@@ -36306,8 +36887,7 @@ var DbSyncWatcher = class {
|
|
|
36306
36887
|
return;
|
|
36307
36888
|
}
|
|
36308
36889
|
this.db.run(
|
|
36309
|
-
`INSERT OR IGNORE INTO review_rounds (session_id, round_number)
|
|
36310
|
-
VALUES (?, ?)`,
|
|
36890
|
+
`INSERT OR IGNORE INTO review_rounds (session_id, round_number) VALUES (?, ?)`,
|
|
36311
36891
|
[sessionId, roundNumber]
|
|
36312
36892
|
);
|
|
36313
36893
|
this.db.run(
|
|
@@ -36338,9 +36918,8 @@ var DbSyncWatcher = class {
|
|
|
36338
36918
|
});
|
|
36339
36919
|
}
|
|
36340
36920
|
/**
|
|
36341
|
-
*
|
|
36342
|
-
*
|
|
36343
|
-
* Idempotent — skips if run already has source='orchestrator'.
|
|
36921
|
+
* Project a `map_completed` event into `map_runs` and emit a room-scoped
|
|
36922
|
+
* `map:updated`. Idempotent.
|
|
36344
36923
|
*/
|
|
36345
36924
|
processMapCompletedEvent(sessionId, runNumber, metadataStr) {
|
|
36346
36925
|
let metadata;
|
|
@@ -36358,8 +36937,7 @@ var DbSyncWatcher = class {
|
|
|
36358
36937
|
return;
|
|
36359
36938
|
}
|
|
36360
36939
|
this.db.run(
|
|
36361
|
-
`INSERT OR IGNORE INTO map_runs (session_id, run_number)
|
|
36362
|
-
VALUES (?, ?)`,
|
|
36940
|
+
`INSERT OR IGNORE INTO map_runs (session_id, run_number) VALUES (?, ?)`,
|
|
36363
36941
|
[sessionId, runNumber]
|
|
36364
36942
|
);
|
|
36365
36943
|
this.db.run(
|
|
@@ -36382,33 +36960,42 @@ var DbSyncWatcher = class {
|
|
|
36382
36960
|
source: "orchestrator"
|
|
36383
36961
|
});
|
|
36384
36962
|
}
|
|
36385
|
-
/**
|
|
36386
|
-
* Record current mtime after the dashboard writes to disk.
|
|
36387
|
-
* Called automatically via registered save hooks after saveDb().
|
|
36388
|
-
*/
|
|
36389
|
-
markOwnWrite() {
|
|
36390
|
-
try {
|
|
36391
|
-
this.lastMtime = statSync2(this.dbFilePath).mtimeMs;
|
|
36392
|
-
} catch {
|
|
36393
|
-
}
|
|
36394
|
-
}
|
|
36395
36963
|
};
|
|
36964
|
+
function sessionFingerprint(row) {
|
|
36965
|
+
return [
|
|
36966
|
+
col(row, "status"),
|
|
36967
|
+
col(row, "current_phase"),
|
|
36968
|
+
col(row, "phase_number"),
|
|
36969
|
+
col(row, "current_round"),
|
|
36970
|
+
col(row, "current_map_run"),
|
|
36971
|
+
col(row, "updated_at")
|
|
36972
|
+
].join("|");
|
|
36973
|
+
}
|
|
36974
|
+
function commandFingerprint(row) {
|
|
36975
|
+
return [
|
|
36976
|
+
col(row, "last_heartbeat_at"),
|
|
36977
|
+
col(row, "finished_at"),
|
|
36978
|
+
col(row, "exit_code"),
|
|
36979
|
+
col(row, "workflow_id"),
|
|
36980
|
+
col(row, "vendor_session_id")
|
|
36981
|
+
].join("|");
|
|
36982
|
+
}
|
|
36396
36983
|
|
|
36397
36984
|
// src/server/socket/chat-handler.ts
|
|
36398
36985
|
import { dirname as dirname11 } from "node:path";
|
|
36399
36986
|
|
|
36400
36987
|
// src/server/services/chat-context.ts
|
|
36401
|
-
import { readFileSync as
|
|
36402
|
-
import { join as
|
|
36988
|
+
import { readFileSync as readFileSync9, readdirSync as readdirSync2, existsSync as existsSync12 } from "node:fs";
|
|
36989
|
+
import { join as join15 } from "node:path";
|
|
36403
36990
|
function buildChatContext(ocrDir, target) {
|
|
36404
|
-
const sessionsDir =
|
|
36991
|
+
const sessionsDir = join15(ocrDir, "sessions");
|
|
36405
36992
|
if (target.type === "map_run") {
|
|
36406
36993
|
return buildMapRunContext(sessionsDir, target.sessionId, target.runNumber);
|
|
36407
36994
|
}
|
|
36408
36995
|
return buildReviewRoundContext(sessionsDir, target.sessionId, target.roundNumber);
|
|
36409
36996
|
}
|
|
36410
36997
|
function buildMapRunContext(sessionsDir, sessionId, runNumber) {
|
|
36411
|
-
const mapPath =
|
|
36998
|
+
const mapPath = join15(
|
|
36412
36999
|
sessionsDir,
|
|
36413
37000
|
sessionId,
|
|
36414
37001
|
"map",
|
|
@@ -36423,7 +37010,7 @@ function buildMapRunContext(sessionsDir, sessionId, runNumber) {
|
|
|
36423
37010
|
`Below is the Code Review Map that organizes the changeset into reviewable sections:`
|
|
36424
37011
|
];
|
|
36425
37012
|
if (existsSync12(mapPath)) {
|
|
36426
|
-
const content =
|
|
37013
|
+
const content = readFileSync9(mapPath, "utf-8");
|
|
36427
37014
|
parts.push("");
|
|
36428
37015
|
parts.push("<map>");
|
|
36429
37016
|
parts.push(content);
|
|
@@ -36435,9 +37022,9 @@ function buildMapRunContext(sessionsDir, sessionId, runNumber) {
|
|
|
36435
37022
|
return parts.join("\n");
|
|
36436
37023
|
}
|
|
36437
37024
|
function buildReviewRoundContext(sessionsDir, sessionId, roundNumber) {
|
|
36438
|
-
const roundDir =
|
|
36439
|
-
const finalPath =
|
|
36440
|
-
const reviewersDir =
|
|
37025
|
+
const roundDir = join15(sessionsDir, sessionId, "rounds", `round-${roundNumber}`);
|
|
37026
|
+
const finalPath = join15(roundDir, "final.md");
|
|
37027
|
+
const reviewersDir = join15(roundDir, "reviews");
|
|
36441
37028
|
const parts = [
|
|
36442
37029
|
`You are an expert code reviewer assisting with a code review session.`,
|
|
36443
37030
|
`You are looking at review round #${roundNumber} for session "${sessionId}".`,
|
|
@@ -36445,7 +37032,7 @@ function buildReviewRoundContext(sessionsDir, sessionId, roundNumber) {
|
|
|
36445
37032
|
`Below are the review artifacts for this round:`
|
|
36446
37033
|
];
|
|
36447
37034
|
if (existsSync12(finalPath)) {
|
|
36448
|
-
const content =
|
|
37035
|
+
const content = readFileSync9(finalPath, "utf-8");
|
|
36449
37036
|
parts.push("");
|
|
36450
37037
|
parts.push("<final-synthesis>");
|
|
36451
37038
|
parts.push(content);
|
|
@@ -36454,7 +37041,7 @@ function buildReviewRoundContext(sessionsDir, sessionId, roundNumber) {
|
|
|
36454
37041
|
if (existsSync12(reviewersDir)) {
|
|
36455
37042
|
const files = readdirSync2(reviewersDir).filter((f) => f.endsWith(".md")).sort();
|
|
36456
37043
|
for (const file of files) {
|
|
36457
|
-
const content =
|
|
37044
|
+
const content = readFileSync9(join15(reviewersDir, file), "utf-8");
|
|
36458
37045
|
const reviewerName = file.replace(/\.md$/, "");
|
|
36459
37046
|
parts.push("");
|
|
36460
37047
|
parts.push(`<reviewer name="${reviewerName}">`);
|
|
@@ -36521,7 +37108,6 @@ function startTrackedExecution(io2, db, ocrDir, command, args = []) {
|
|
|
36521
37108
|
WHERE id = ?`,
|
|
36522
37109
|
[exitCode, finishedAt, outputBuffer, executionId]
|
|
36523
37110
|
);
|
|
36524
|
-
saveDb(db, ocrDir);
|
|
36525
37111
|
appendCommandLog(ocrDir, {
|
|
36526
37112
|
...baseLogEntry,
|
|
36527
37113
|
is_detached: trackedIsDetached,
|
|
@@ -36551,13 +37137,12 @@ function cleanupChat(conversationId) {
|
|
|
36551
37137
|
activeChats.delete(conversationId);
|
|
36552
37138
|
}
|
|
36553
37139
|
}
|
|
36554
|
-
function resetIdleTimer(conversationId, db
|
|
37140
|
+
function resetIdleTimer(conversationId, db) {
|
|
36555
37141
|
const chat = activeChats.get(conversationId);
|
|
36556
37142
|
if (chat) {
|
|
36557
37143
|
clearTimeout(chat.timer);
|
|
36558
37144
|
chat.timer = setTimeout(() => {
|
|
36559
37145
|
updateConversationStatus(db, conversationId, "expired");
|
|
36560
|
-
saveDb(db, ocrDir);
|
|
36561
37146
|
cleanupChat(conversationId);
|
|
36562
37147
|
}, IDLE_TIMEOUT_MS);
|
|
36563
37148
|
}
|
|
@@ -36581,9 +37166,7 @@ function registerChatHandlers(io2, socket, db, ocrDir, aiCliService) {
|
|
|
36581
37166
|
return;
|
|
36582
37167
|
}
|
|
36583
37168
|
upsertConversation(db, conversationId, sessionId, targetType, targetId);
|
|
36584
|
-
saveDb(db, ocrDir);
|
|
36585
37169
|
insertMessage(db, conversationId, "user", message);
|
|
36586
|
-
saveDb(db, ocrDir);
|
|
36587
37170
|
const conversation = getConversation(db, conversationId);
|
|
36588
37171
|
const claudeSessionId = conversation?.claude_session_id ?? null;
|
|
36589
37172
|
let prompt;
|
|
@@ -36624,7 +37207,6 @@ User: ${message}`;
|
|
|
36624
37207
|
const proc = spawnResult.process;
|
|
36625
37208
|
const timer = setTimeout(() => {
|
|
36626
37209
|
updateConversationStatus(db, conversationId, "expired");
|
|
36627
|
-
saveDb(db, ocrDir);
|
|
36628
37210
|
cleanupChat(conversationId);
|
|
36629
37211
|
}, IDLE_TIMEOUT_MS);
|
|
36630
37212
|
activeChats.set(conversationId, { process: proc, conversationId, timer });
|
|
@@ -36714,7 +37296,6 @@ User: ${message}`;
|
|
|
36714
37296
|
if (assistantText.trim()) {
|
|
36715
37297
|
insertMessage(db, conversationId, "assistant", assistantText.trim());
|
|
36716
37298
|
}
|
|
36717
|
-
saveDb(db, ocrDir);
|
|
36718
37299
|
if (code === 0) {
|
|
36719
37300
|
tracker.appendOutput("\n\u2713 Response complete\n");
|
|
36720
37301
|
tracker.finish(0);
|
|
@@ -36730,7 +37311,7 @@ User: ${message}`;
|
|
|
36730
37311
|
error: errMsg
|
|
36731
37312
|
});
|
|
36732
37313
|
}
|
|
36733
|
-
resetIdleTimer(conversationId, db
|
|
37314
|
+
resetIdleTimer(conversationId, db);
|
|
36734
37315
|
const chat = activeChats.get(conversationId);
|
|
36735
37316
|
if (chat) {
|
|
36736
37317
|
chat.process = null;
|
|
@@ -36790,13 +37371,13 @@ function cleanupAllChats() {
|
|
|
36790
37371
|
}
|
|
36791
37372
|
|
|
36792
37373
|
// src/server/socket/post-handler.ts
|
|
36793
|
-
import { existsSync as existsSync13, mkdirSync as
|
|
37374
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync5, readFileSync as readFileSync10, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "node:fs";
|
|
36794
37375
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
36795
|
-
import { join as
|
|
37376
|
+
import { join as join16, dirname as dirname12, isAbsolute as isAbsolute2 } from "node:path";
|
|
36796
37377
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
36797
|
-
function
|
|
36798
|
-
if (
|
|
36799
|
-
return
|
|
37378
|
+
function resolveSessionDir2(sessionDir, ocrDir) {
|
|
37379
|
+
if (isAbsolute2(sessionDir)) return sessionDir;
|
|
37380
|
+
return join16(dirname12(ocrDir), sessionDir);
|
|
36800
37381
|
}
|
|
36801
37382
|
var BRANCH_PREFIXES = [
|
|
36802
37383
|
"feat",
|
|
@@ -36966,19 +37547,19 @@ function registerPostHandlers(io2, socket, db, ocrDir, aiCliService) {
|
|
|
36966
37547
|
socket.emit("post:error", { error: "Session not found" });
|
|
36967
37548
|
return;
|
|
36968
37549
|
}
|
|
36969
|
-
const sessionDir = session.session_dir ?
|
|
36970
|
-
const roundDir =
|
|
36971
|
-
const finalPath =
|
|
37550
|
+
const sessionDir = session.session_dir ? resolveSessionDir2(session.session_dir, ocrDir) : join16(ocrDir, "sessions", sessionId);
|
|
37551
|
+
const roundDir = join16(sessionDir, "rounds", `round-${roundNumber}`);
|
|
37552
|
+
const finalPath = join16(roundDir, "final.md");
|
|
36972
37553
|
if (!existsSync13(finalPath)) {
|
|
36973
37554
|
socket.emit("post:error", { error: "final.md not found for this round" });
|
|
36974
37555
|
return;
|
|
36975
37556
|
}
|
|
36976
|
-
const humanReviewPath =
|
|
37557
|
+
const humanReviewPath = join16(roundDir, "final-human.md");
|
|
36977
37558
|
const repoRoot = dirname12(ocrDir);
|
|
36978
|
-
const commandMdPath =
|
|
37559
|
+
const commandMdPath = join16(ocrDir, "commands", "translate-review-to-single-human.md");
|
|
36979
37560
|
let commandContent;
|
|
36980
37561
|
try {
|
|
36981
|
-
commandContent =
|
|
37562
|
+
commandContent = readFileSync10(commandMdPath, "utf-8");
|
|
36982
37563
|
} catch {
|
|
36983
37564
|
socket.emit("post:error", {
|
|
36984
37565
|
error: `Command file not found: ${commandMdPath}. Run \`ocr init\` to set up.`
|
|
@@ -37006,8 +37587,8 @@ function registerPostHandlers(io2, socket, db, ocrDir, aiCliService) {
|
|
|
37006
37587
|
"",
|
|
37007
37588
|
"Examples:",
|
|
37008
37589
|
`- Instead of \`ocr state show\`, run: \`node ${localCli} state show\``,
|
|
37009
|
-
`- Instead of \`ocr state
|
|
37010
|
-
`- Instead of \`ocr state
|
|
37590
|
+
`- Instead of \`ocr state begin ...\`, run: \`node ${localCli} state begin ...\``,
|
|
37591
|
+
`- Instead of \`ocr state advance ...\`, run: \`node ${localCli} state advance ...\``,
|
|
37011
37592
|
"",
|
|
37012
37593
|
"This applies to every `ocr` invocation. Do NOT use bare `ocr` commands."
|
|
37013
37594
|
);
|
|
@@ -37060,7 +37641,7 @@ function registerPostHandlers(io2, socket, db, ocrDir, aiCliService) {
|
|
|
37060
37641
|
let generatedContent = "";
|
|
37061
37642
|
if (existsSync13(humanReviewPath)) {
|
|
37062
37643
|
try {
|
|
37063
|
-
generatedContent =
|
|
37644
|
+
generatedContent = readFileSync10(humanReviewPath, "utf-8").trim();
|
|
37064
37645
|
} catch {
|
|
37065
37646
|
}
|
|
37066
37647
|
}
|
|
@@ -37135,12 +37716,11 @@ function registerPostHandlers(io2, socket, db, ocrDir, aiCliService) {
|
|
|
37135
37716
|
socket.emit("post:save-result", { success: false, error: "Session not found" });
|
|
37136
37717
|
return;
|
|
37137
37718
|
}
|
|
37138
|
-
const sessionDir = session.session_dir ?
|
|
37139
|
-
const roundDir =
|
|
37140
|
-
|
|
37141
|
-
const filePath =
|
|
37142
|
-
|
|
37143
|
-
saveDb(db, ocrDir);
|
|
37719
|
+
const sessionDir = session.session_dir ? resolveSessionDir2(session.session_dir, ocrDir) : join16(ocrDir, "sessions", sessionId);
|
|
37720
|
+
const roundDir = join16(sessionDir, "rounds", `round-${roundNumber}`);
|
|
37721
|
+
mkdirSync5(roundDir, { recursive: true });
|
|
37722
|
+
const filePath = join16(roundDir, "final-human.md");
|
|
37723
|
+
writeFileSync4(filePath, content, { mode: 420 });
|
|
37144
37724
|
socket.emit("post:save-result", { success: true });
|
|
37145
37725
|
} catch (err) {
|
|
37146
37726
|
console.error("Error in post:save handler:", err);
|
|
@@ -37166,13 +37746,13 @@ function registerPostHandlers(io2, socket, db, ocrDir, aiCliService) {
|
|
|
37166
37746
|
);
|
|
37167
37747
|
tracker.appendOutput(`\u25B8 Posting review to PR #${prNumber}...
|
|
37168
37748
|
`);
|
|
37169
|
-
const tmpDir =
|
|
37749
|
+
const tmpDir = join16(tmpdir2(), "ocr-post-comments");
|
|
37170
37750
|
try {
|
|
37171
|
-
|
|
37751
|
+
mkdirSync5(tmpDir, { recursive: true, mode: 448 });
|
|
37172
37752
|
} catch {
|
|
37173
37753
|
}
|
|
37174
|
-
const tmpFile =
|
|
37175
|
-
|
|
37754
|
+
const tmpFile = join16(tmpDir, `${randomUUID2()}.md`);
|
|
37755
|
+
writeFileSync4(tmpFile, content, { mode: 384 });
|
|
37176
37756
|
const repoRoot = dirname12(ocrDir);
|
|
37177
37757
|
try {
|
|
37178
37758
|
const { stdout } = await execBinaryAsync(
|
|
@@ -37217,17 +37797,17 @@ function cleanupAllPostGenerations() {
|
|
|
37217
37797
|
}
|
|
37218
37798
|
|
|
37219
37799
|
// ../cli/src/lib/runtime-config.ts
|
|
37220
|
-
import { existsSync as existsSync14, readFileSync as
|
|
37221
|
-
import { join as
|
|
37800
|
+
import { existsSync as existsSync14, readFileSync as readFileSync11 } from "node:fs";
|
|
37801
|
+
import { join as join17 } from "node:path";
|
|
37222
37802
|
var DEFAULT_AGENT_HEARTBEAT_SECONDS = 60;
|
|
37223
37803
|
function getAgentHeartbeatSeconds(ocrDir) {
|
|
37224
|
-
const configPath =
|
|
37804
|
+
const configPath = join17(ocrDir, "config.yaml");
|
|
37225
37805
|
if (!existsSync14(configPath)) {
|
|
37226
37806
|
return DEFAULT_AGENT_HEARTBEAT_SECONDS;
|
|
37227
37807
|
}
|
|
37228
37808
|
let content;
|
|
37229
37809
|
try {
|
|
37230
|
-
content =
|
|
37810
|
+
content = readFileSync11(configPath, "utf-8");
|
|
37231
37811
|
} catch {
|
|
37232
37812
|
return DEFAULT_AGENT_HEARTBEAT_SECONDS;
|
|
37233
37813
|
}
|
|
@@ -37322,23 +37902,23 @@ async function startServer(options = {}) {
|
|
|
37322
37902
|
const port = options.port ?? parseInt(process.env.PORT ?? "4173", 10);
|
|
37323
37903
|
const ocrDir = resolveOcrDir();
|
|
37324
37904
|
const aiCliService = new AiCliService(ocrDir);
|
|
37325
|
-
const dbPathForCheckpoint =
|
|
37905
|
+
const dbPathForCheckpoint = join18(ocrDir, "data", "ocr.db");
|
|
37326
37906
|
const walResult = walCheckpointTruncate(dbPathForCheckpoint);
|
|
37327
37907
|
if (walResult === "checkpointed") {
|
|
37328
37908
|
console.log(" WAL checkpoint: truncated stale write-ahead-log file");
|
|
37329
37909
|
}
|
|
37330
37910
|
const db = await openDb(ocrDir);
|
|
37331
|
-
const dataDir =
|
|
37332
|
-
const pidFilePath =
|
|
37333
|
-
const portFilePath =
|
|
37334
|
-
|
|
37911
|
+
const dataDir = join18(ocrDir, "data");
|
|
37912
|
+
const pidFilePath = join18(dataDir, "dashboard.pid");
|
|
37913
|
+
const portFilePath = join18(dataDir, "server-port");
|
|
37914
|
+
mkdirSync6(dataDir, { recursive: true });
|
|
37335
37915
|
try {
|
|
37336
37916
|
unlinkSync3(portFilePath);
|
|
37337
37917
|
} catch {
|
|
37338
37918
|
}
|
|
37339
37919
|
if (existsSync15(pidFilePath)) {
|
|
37340
37920
|
try {
|
|
37341
|
-
const oldPid = parseInt(
|
|
37921
|
+
const oldPid = parseInt(readFileSync12(pidFilePath, "utf-8").trim(), 10);
|
|
37342
37922
|
if (!isNaN(oldPid)) {
|
|
37343
37923
|
try {
|
|
37344
37924
|
process.kill(oldPid, 0);
|
|
@@ -37351,13 +37931,12 @@ async function startServer(options = {}) {
|
|
|
37351
37931
|
} catch {
|
|
37352
37932
|
}
|
|
37353
37933
|
}
|
|
37354
|
-
|
|
37934
|
+
writeFileSync5(pidFilePath, String(process.pid), { mode: 384 });
|
|
37355
37935
|
const cmdCountResult = db.exec("SELECT COUNT(*) as c FROM command_executions");
|
|
37356
37936
|
const totalCmds = cmdCountResult[0]?.values[0]?.[0] ?? 0;
|
|
37357
37937
|
if (totalCmds === 0) {
|
|
37358
37938
|
const recovered = replayCommandLog(db, ocrDir);
|
|
37359
37939
|
if (recovered > 0) {
|
|
37360
|
-
saveDb(db, ocrDir);
|
|
37361
37940
|
console.log(` Recovered ${recovered} command(s) from JSONL backup`);
|
|
37362
37941
|
}
|
|
37363
37942
|
}
|
|
@@ -37368,15 +37947,14 @@ async function startServer(options = {}) {
|
|
|
37368
37947
|
if (orphanResult.length > 0 && orphanResult[0]) {
|
|
37369
37948
|
const { columns, values: orphanRows } = orphanResult[0];
|
|
37370
37949
|
const colIdx = Object.fromEntries(columns.map((c, i) => [c, i]));
|
|
37371
|
-
const cutoff = Date.now() -
|
|
37950
|
+
const cutoff = Date.now() - PID_REUSE_GUARD_MS;
|
|
37372
37951
|
let killedCount = 0;
|
|
37373
37952
|
for (const row of orphanRows) {
|
|
37374
37953
|
const pid = row[colIdx["pid"]];
|
|
37375
37954
|
const isDetached = row[colIdx["is_detached"]] === 1;
|
|
37376
37955
|
const startedAt = row[colIdx["started_at"]];
|
|
37377
|
-
if (
|
|
37378
|
-
|
|
37379
|
-
process.kill(pid, 0);
|
|
37956
|
+
if (sqliteUtcMs(startedAt) < cutoff) continue;
|
|
37957
|
+
if (defaultIsAlive(pid)) {
|
|
37380
37958
|
if (isDetached) {
|
|
37381
37959
|
try {
|
|
37382
37960
|
process.kill(-pid, "SIGTERM");
|
|
@@ -37400,37 +37978,55 @@ async function startServer(options = {}) {
|
|
|
37400
37978
|
} catch {
|
|
37401
37979
|
}
|
|
37402
37980
|
}, 2e3);
|
|
37403
|
-
} catch {
|
|
37404
37981
|
}
|
|
37405
37982
|
}
|
|
37406
37983
|
if (killedCount > 0) {
|
|
37407
37984
|
console.log(` Cleaned up ${killedCount} orphaned process(es)`);
|
|
37408
37985
|
}
|
|
37409
37986
|
}
|
|
37410
|
-
const
|
|
37411
|
-
"SELECT COUNT(*) as c FROM command_executions WHERE finished_at IS NULL
|
|
37987
|
+
const legacyResult = db.exec(
|
|
37988
|
+
"SELECT COUNT(*) as c FROM command_executions WHERE finished_at IS NOT NULL AND exit_code IS NULL"
|
|
37412
37989
|
);
|
|
37413
|
-
const
|
|
37414
|
-
if (
|
|
37990
|
+
const legacyCount = legacyResult[0]?.values[0]?.[0] ?? 0;
|
|
37991
|
+
if (legacyCount > 0) {
|
|
37415
37992
|
db.run(
|
|
37416
37993
|
`UPDATE command_executions
|
|
37417
|
-
SET exit_code = -2,
|
|
37994
|
+
SET exit_code = -2,
|
|
37418
37995
|
output = COALESCE(output, '') || '
|
|
37419
|
-
[Cancelled]'
|
|
37420
|
-
|
|
37421
|
-
WHERE finished_at IS NULL OR exit_code IS NULL`
|
|
37996
|
+
[Cancelled]'
|
|
37997
|
+
WHERE finished_at IS NOT NULL AND exit_code IS NULL`
|
|
37422
37998
|
);
|
|
37423
|
-
|
|
37424
|
-
console.log(` Cleaned up ${staleCount} stale command(s)`);
|
|
37999
|
+
console.log(` Backfilled ${legacyCount} finished command(s) missing an exit code`);
|
|
37425
38000
|
}
|
|
37426
38001
|
const heartbeatSeconds = getAgentHeartbeatSeconds(ocrDir);
|
|
37427
|
-
const
|
|
37428
|
-
|
|
37429
|
-
|
|
38002
|
+
const logAgentSweep = (result) => {
|
|
38003
|
+
if (result.orphanedIds.length === 0) return;
|
|
38004
|
+
const cascaded = result.cascadedWorkflowIds.length;
|
|
38005
|
+
console.log(
|
|
38006
|
+
` Cleaned up ${result.orphanedIds.length} stale agent session(s) (heartbeat threshold ${heartbeatSeconds}s)` + (cascaded > 0 ? `; cascade-closed dependents of ${cascaded} workflow(s)` : "")
|
|
38007
|
+
);
|
|
38008
|
+
};
|
|
38009
|
+
logAgentSweep(sweepStaleAgentSessions(db, heartbeatSeconds, defaultIsAlive));
|
|
38010
|
+
const STALE_SESSION_THRESHOLD_SECONDS = 7 * 24 * 60 * 60;
|
|
38011
|
+
const staleSessionResult = sweepStaleSessions(
|
|
38012
|
+
db,
|
|
38013
|
+
STALE_SESSION_THRESHOLD_SECONDS
|
|
38014
|
+
);
|
|
38015
|
+
if (staleSessionResult.closedSessionIds.length > 0) {
|
|
37430
38016
|
console.log(
|
|
37431
|
-
`
|
|
38017
|
+
` Auto-closed ${staleSessionResult.closedSessionIds.length} stale active session(s) (threshold 7 days)`
|
|
37432
38018
|
);
|
|
37433
38019
|
}
|
|
38020
|
+
const SWEEP_INTERVAL_MS = 5 * 60 * 1e3;
|
|
38021
|
+
const sweepTimer = setInterval(() => {
|
|
38022
|
+
try {
|
|
38023
|
+
logAgentSweep(sweepStaleAgentSessions(db, heartbeatSeconds, defaultIsAlive));
|
|
38024
|
+
sweepStaleSessions(db, STALE_SESSION_THRESHOLD_SECONDS);
|
|
38025
|
+
} catch (err) {
|
|
38026
|
+
console.error("[sweep] periodic sweep failed:", err);
|
|
38027
|
+
}
|
|
38028
|
+
}, SWEEP_INTERVAL_MS);
|
|
38029
|
+
sweepTimer.unref();
|
|
37434
38030
|
app.get("/api/reviews", (_req, res) => {
|
|
37435
38031
|
try {
|
|
37436
38032
|
const rounds = getAllRounds(db).map((r) => ({
|
|
@@ -37448,12 +38044,12 @@ async function startServer(options = {}) {
|
|
|
37448
38044
|
app.use("/api/sessions", createReviewsRouter(db));
|
|
37449
38045
|
app.use("/api/sessions", createMapsRouter(db));
|
|
37450
38046
|
app.use("/api/sessions", createArtifactsRouter(db));
|
|
37451
|
-
app.use("/api", createProgressRouter(db
|
|
37452
|
-
app.use("/api/notes", createNotesRouter(db
|
|
38047
|
+
app.use("/api", createProgressRouter(db));
|
|
38048
|
+
app.use("/api/notes", createNotesRouter(db));
|
|
37453
38049
|
app.use("/api/stats", createStatsRouter(db));
|
|
37454
38050
|
app.use("/api/commands", createCommandsRouter(db, ocrDir));
|
|
37455
38051
|
app.use("/api/config", createConfigRouter(ocrDir, aiCliService));
|
|
37456
|
-
app.use("/api/sessions", createChatRouter(db
|
|
38052
|
+
app.use("/api/sessions", createChatRouter(db));
|
|
37457
38053
|
app.use("/api/reviewers", createReviewersRouter(ocrDir));
|
|
37458
38054
|
let pullSync = () => {
|
|
37459
38055
|
};
|
|
@@ -37461,11 +38057,11 @@ async function startServer(options = {}) {
|
|
|
37461
38057
|
app.use("/api/agent-sessions", createAgentSessionsRouter(db, () => pullSync()));
|
|
37462
38058
|
app.use("/api/sessions", createHandoffRouter(sessionCapture, ocrDir, () => pullSync()));
|
|
37463
38059
|
app.use("/api/team", createTeamRouter(ocrDir));
|
|
37464
|
-
const clientDir =
|
|
38060
|
+
const clientDir = join18(__dirname3, "client");
|
|
37465
38061
|
if (process.env.NODE_ENV === "production" && existsSync15(clientDir)) {
|
|
37466
38062
|
app.use(import_express15.default.static(clientDir, { index: false }));
|
|
37467
|
-
const indexHtmlPath =
|
|
37468
|
-
const rawIndexHtml = existsSync15(indexHtmlPath) ?
|
|
38063
|
+
const indexHtmlPath = join18(clientDir, "index.html");
|
|
38064
|
+
const rawIndexHtml = existsSync15(indexHtmlPath) ? readFileSync12(indexHtmlPath, "utf-8") : "";
|
|
37469
38065
|
const tokenScript = `<script>window.__OCR_TOKEN__=${JSON.stringify(AUTH_TOKEN)};</script>`;
|
|
37470
38066
|
const injectedIndexHtml = rawIndexHtml.replace(
|
|
37471
38067
|
"</head>",
|
|
@@ -37484,16 +38080,13 @@ async function startServer(options = {}) {
|
|
|
37484
38080
|
registerChatHandlers(io, socket, db, ocrDir, aiCliService);
|
|
37485
38081
|
registerPostHandlers(io, socket, db, ocrDir, aiCliService);
|
|
37486
38082
|
});
|
|
37487
|
-
const dbFilePath =
|
|
38083
|
+
const dbFilePath = join18(ocrDir, "data", "ocr.db");
|
|
37488
38084
|
const dbSyncWatcher = new DbSyncWatcher(
|
|
37489
38085
|
db,
|
|
37490
38086
|
dbFilePath,
|
|
37491
38087
|
io,
|
|
37492
|
-
() => {
|
|
37493
|
-
saveDb(db, ocrDir);
|
|
37494
|
-
},
|
|
37495
38088
|
// Auto-link the dashboard's parent execution row when the AI
|
|
37496
|
-
// creates a new session via `ocr state
|
|
38089
|
+
// creates a new session via `ocr state begin`. Eliminates the
|
|
37497
38090
|
// dependency on env-var/flag propagation through the AI's shell.
|
|
37498
38091
|
(session) => {
|
|
37499
38092
|
sessionCapture.autoLinkPendingDashboardExecution(session.id);
|
|
@@ -37503,14 +38096,9 @@ async function startServer(options = {}) {
|
|
|
37503
38096
|
dbSyncWatcher.startWatching();
|
|
37504
38097
|
pullSync = () => dbSyncWatcher.syncFromDisk();
|
|
37505
38098
|
console.log(` Watching DB: ${shortenPath(dbFilePath)}`);
|
|
37506
|
-
|
|
37507
|
-
|
|
37508
|
-
() => dbSyncWatcher.markOwnWrite()
|
|
37509
|
-
);
|
|
37510
|
-
const sessionsDir = join17(ocrDir, "sessions");
|
|
37511
|
-
const fsSync = new FilesystemSync(db, sessionsDir, io, () => saveDb(db, ocrDir));
|
|
38099
|
+
const sessionsDir = join18(ocrDir, "sessions");
|
|
38100
|
+
const fsSync = new FilesystemSync(db, sessionsDir, io);
|
|
37512
38101
|
await fsSync.fullScan();
|
|
37513
|
-
saveDb(db, ocrDir);
|
|
37514
38102
|
fsSync.startWatching();
|
|
37515
38103
|
console.log(` Watching sessions: ${shortenPath(sessionsDir)}`);
|
|
37516
38104
|
const stopReviewersWatch = watchReviewersMeta(ocrDir, io);
|
|
@@ -37547,7 +38135,7 @@ async function startServer(options = {}) {
|
|
|
37547
38135
|
if (actualPort !== port) {
|
|
37548
38136
|
console.log(` Note: using port ${actualPort} (${port} was in use)`);
|
|
37549
38137
|
}
|
|
37550
|
-
|
|
38138
|
+
writeFileSync5(portFilePath, String(actualPort), { mode: 384 });
|
|
37551
38139
|
console.log(` Server: http://localhost:${actualPort}`);
|
|
37552
38140
|
console.log(` OCR directory: ${shortenPath(ocrDir)}`);
|
|
37553
38141
|
console.log();
|
|
@@ -37573,7 +38161,7 @@ async function startServer(options = {}) {
|
|
|
37573
38161
|
} catch {
|
|
37574
38162
|
}
|
|
37575
38163
|
try {
|
|
37576
|
-
unlinkSync3(
|
|
38164
|
+
unlinkSync3(join18(dataDir, "dashboard-active-spawn.json"));
|
|
37577
38165
|
} catch {
|
|
37578
38166
|
}
|
|
37579
38167
|
try {
|
|
@@ -37614,24 +38202,12 @@ async function startServer(options = {}) {
|
|
|
37614
38202
|
}
|
|
37615
38203
|
cleanupAllChats();
|
|
37616
38204
|
cleanupAllPostGenerations();
|
|
37617
|
-
try {
|
|
37618
|
-
flushSave();
|
|
37619
|
-
} catch {
|
|
37620
|
-
}
|
|
37621
|
-
try {
|
|
37622
|
-
saveDb(db, ocrDir);
|
|
37623
|
-
} catch {
|
|
37624
|
-
}
|
|
37625
38205
|
dbSyncWatcher.stopWatching();
|
|
37626
38206
|
fsSync.stopWatching();
|
|
37627
38207
|
stopReviewersWatch();
|
|
37628
38208
|
io.close();
|
|
37629
38209
|
httpServer.closeAllConnections();
|
|
37630
38210
|
httpServer.close(() => {
|
|
37631
|
-
try {
|
|
37632
|
-
saveDb(db, ocrDir);
|
|
37633
|
-
} catch {
|
|
37634
|
-
}
|
|
37635
38211
|
closeDb();
|
|
37636
38212
|
console.log("Server stopped.");
|
|
37637
38213
|
process.exit(0);
|