@open-code-review/cli 2.1.0 → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dashboard/client/assets/{_basePickBy-B3ALyupE.js → _basePickBy-BAlGnwHG.js} +1 -1
- package/dist/dashboard/client/assets/{_baseUniq-b2RALAWc.js → _baseUniq-CoauyOeL.js} +1 -1
- package/dist/dashboard/client/assets/{arc-DcSVvhUd.js → arc-DtS0aHfP.js} +1 -1
- package/dist/dashboard/client/assets/{architectureDiagram-VXUJARFQ-BNUlmSCS.js → architectureDiagram-VXUJARFQ-CnWmtRTh.js} +1 -1
- package/dist/dashboard/client/assets/{blockDiagram-VD42YOAC-BmhiQVwa.js → blockDiagram-VD42YOAC-DgPp4oGV.js} +1 -1
- package/dist/dashboard/client/assets/{c4Diagram-YG6GDRKO-jyJ3WOv5.js → c4Diagram-YG6GDRKO--LV4qQaE.js} +1 -1
- package/dist/dashboard/client/assets/channel-BU2129fl.js +1 -0
- package/dist/dashboard/client/assets/{chunk-4BX2VUAB-x1dQU_s3.js → chunk-4BX2VUAB-BRglpc7Z.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-55IACEB6-CwbsE2XQ.js → chunk-55IACEB6-Bgx06_CV.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-B4BG7PRW-BaE7c-ti.js → chunk-B4BG7PRW-D6HN3Yiy.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-DI55MBZ5-Bw5PUaMK.js → chunk-DI55MBZ5-NH9EgN9T.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-FMBD7UC4-B7cF6P3s.js → chunk-FMBD7UC4-xriO6WNP.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-QN33PNHL-OY4evNHd.js → chunk-QN33PNHL-CV1h6_Zl.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-QZHKN3VN-BpjQwIWz.js → chunk-QZHKN3VN-CV4VzxNq.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-TZMSLE5B-D8b_Oq9B.js → chunk-TZMSLE5B-isdklocW.js} +1 -1
- package/dist/dashboard/client/assets/classDiagram-2ON5EDUG-CVftFGiR.js +1 -0
- package/dist/dashboard/client/assets/classDiagram-v2-WZHVMYZB-CVftFGiR.js +1 -0
- package/dist/dashboard/client/assets/clone-DC6LEEC5.js +1 -0
- package/dist/dashboard/client/assets/{cose-bilkent-S5V4N54A-C-sfP8PN.js → cose-bilkent-S5V4N54A-CCzlFSJf.js} +1 -1
- package/dist/dashboard/client/assets/{dagre-6UL2VRFP-Cqfo0NRg.js → dagre-6UL2VRFP-DVN3PkjZ.js} +1 -1
- package/dist/dashboard/client/assets/{diagram-PSM6KHXK-BR3ppxqI.js → diagram-PSM6KHXK-SzJVoSsb.js} +1 -1
- package/dist/dashboard/client/assets/{diagram-QEK2KX5R-Dvcx6x3R.js → diagram-QEK2KX5R-CgGn7ts-.js} +1 -1
- package/dist/dashboard/client/assets/{diagram-S2PKOQOG-DoyBLnVN.js → diagram-S2PKOQOG-Bz1ukSx8.js} +1 -1
- package/dist/dashboard/client/assets/{erDiagram-Q2GNP2WA-hy77l1cL.js → erDiagram-Q2GNP2WA-CpstUTMZ.js} +1 -1
- package/dist/dashboard/client/assets/{flowDiagram-NV44I4VS-Bz0B1rKM.js → flowDiagram-NV44I4VS-aYVydGhp.js} +1 -1
- package/dist/dashboard/client/assets/{ganttDiagram-JELNMOA3-CLgrZPoC.js → ganttDiagram-JELNMOA3-Cb2DUSRk.js} +1 -1
- package/dist/dashboard/client/assets/{gitGraphDiagram-V2S2FVAM-DwJ-1f-v.js → gitGraphDiagram-V2S2FVAM-BUOnwA2w.js} +1 -1
- package/dist/dashboard/client/assets/{graph-DDBMM_t2.js → graph-4X5ddhLp.js} +1 -1
- package/dist/dashboard/client/assets/index-CKWqYAfu.js +581 -0
- package/dist/dashboard/client/assets/index-CzxeSSaQ.css +1 -0
- package/dist/dashboard/client/assets/{infoDiagram-HS3SLOUP-Bhn1FmAk.js → infoDiagram-HS3SLOUP-BlMqcrwm.js} +1 -1
- package/dist/dashboard/client/assets/{journeyDiagram-XKPGCS4Q-CzGbjX1y.js → journeyDiagram-XKPGCS4Q-DF2ew7ju.js} +1 -1
- package/dist/dashboard/client/assets/{kanban-definition-3W4ZIXB7-Da77-WYk.js → kanban-definition-3W4ZIXB7-BKQMx0-n.js} +1 -1
- package/dist/dashboard/client/assets/{layout-CVwSB-GS.js → layout-DNcn2g9w.js} +1 -1
- package/dist/dashboard/client/assets/{linear-CTRAc5Jn.js → linear-Bqy9gvqb.js} +1 -1
- package/dist/dashboard/client/assets/{mermaid-renderer-Bjo170ax.js → mermaid-renderer-dJ71wgld.js} +4 -4
- package/dist/dashboard/client/assets/{mindmap-definition-VGOIOE7T-B55C2odl.js → mindmap-definition-VGOIOE7T-BARc8sqJ.js} +1 -1
- package/dist/dashboard/client/assets/{pieDiagram-ADFJNKIX-5lrQLrSz.js → pieDiagram-ADFJNKIX-CULlNZTd.js} +1 -1
- package/dist/dashboard/client/assets/{quadrantDiagram-AYHSOK5B-Bg55gC30.js → quadrantDiagram-AYHSOK5B-BJEZPVe9.js} +1 -1
- package/dist/dashboard/client/assets/{requirementDiagram-UZGBJVZJ-CyR4YFJY.js → requirementDiagram-UZGBJVZJ-BhMsmUIs.js} +1 -1
- package/dist/dashboard/client/assets/{sankeyDiagram-TZEHDZUN-BVWKr9_-.js → sankeyDiagram-TZEHDZUN-BYbNgogG.js} +1 -1
- package/dist/dashboard/client/assets/{sequenceDiagram-WL72ISMW-D0AJg_tE.js → sequenceDiagram-WL72ISMW-MoM_NwWk.js} +1 -1
- package/dist/dashboard/client/assets/{stateDiagram-FKZM4ZOC-BuHpTgim.js → stateDiagram-FKZM4ZOC-ditrlbM3.js} +1 -1
- package/dist/dashboard/client/assets/stateDiagram-v2-4FDKWEC3-SqoG2LCn.js +1 -0
- package/dist/dashboard/client/assets/{timeline-definition-IT6M3QCI-LDhpAmDd.js → timeline-definition-IT6M3QCI-DOAJyjuz.js} +1 -1
- package/dist/dashboard/client/assets/{treemap-GDKQZRPO-Dd4gjvUl.js → treemap-GDKQZRPO-BBJkjnJl.js} +1 -1
- package/dist/dashboard/client/assets/{xychartDiagram-PRI3JC2R-B9RDod39.js → xychartDiagram-PRI3JC2R-CPW4s5vm.js} +1 -1
- package/dist/dashboard/client/index.html +2 -2
- package/dist/dashboard/server.js +1188 -579
- package/dist/index.js +1395 -335
- package/dist/lib/db/index.js +485 -24
- package/dist/lib/models.js +125 -50
- package/dist/lib/runtime-config.js +29 -13
- package/dist/lib/state/index.js +2196 -0
- package/package.json +8 -2
- package/dist/dashboard/client/assets/channel-D3J8-GF_.js +0 -1
- package/dist/dashboard/client/assets/classDiagram-2ON5EDUG-tkFUL-1Y.js +0 -1
- package/dist/dashboard/client/assets/classDiagram-v2-WZHVMYZB-tkFUL-1Y.js +0 -1
- package/dist/dashboard/client/assets/clone-CkY5ajLr.js +0 -1
- package/dist/dashboard/client/assets/index-Cr9yEo_B.js +0 -576
- package/dist/dashboard/client/assets/index-Z1pPudAt.css +0 -1
- package/dist/dashboard/client/assets/stateDiagram-v2-4FDKWEC3-DwAPhteN.js +0 -1
package/dist/dashboard/server.js
CHANGED
|
@@ -1579,14 +1579,14 @@ var require_internal = __commonJS({
|
|
|
1579
1579
|
}
|
|
1580
1580
|
InternalCodec.prototype.encoder = InternalEncoder;
|
|
1581
1581
|
InternalCodec.prototype.decoder = InternalDecoder;
|
|
1582
|
-
var
|
|
1583
|
-
if (!
|
|
1584
|
-
|
|
1582
|
+
var StringDecoder2 = __require("string_decoder").StringDecoder;
|
|
1583
|
+
if (!StringDecoder2.prototype.end)
|
|
1584
|
+
StringDecoder2.prototype.end = function() {
|
|
1585
1585
|
};
|
|
1586
1586
|
function InternalDecoder(options, codec) {
|
|
1587
|
-
|
|
1587
|
+
StringDecoder2.call(this, codec.enc);
|
|
1588
1588
|
}
|
|
1589
|
-
InternalDecoder.prototype =
|
|
1589
|
+
InternalDecoder.prototype = StringDecoder2.prototype;
|
|
1590
1590
|
function InternalEncoder(options, codec) {
|
|
1591
1591
|
this.enc = codec.enc;
|
|
1592
1592
|
}
|
|
@@ -18410,10 +18410,10 @@ var require_view = __commonJS({
|
|
|
18410
18410
|
var debug = require_src()("express:view");
|
|
18411
18411
|
var path2 = __require("path");
|
|
18412
18412
|
var fs6 = __require("fs");
|
|
18413
|
-
var
|
|
18414
|
-
var
|
|
18413
|
+
var dirname15 = path2.dirname;
|
|
18414
|
+
var basename5 = path2.basename;
|
|
18415
18415
|
var extname = path2.extname;
|
|
18416
|
-
var
|
|
18416
|
+
var join20 = path2.join;
|
|
18417
18417
|
var resolve3 = path2.resolve;
|
|
18418
18418
|
module.exports = View;
|
|
18419
18419
|
function View(name, options) {
|
|
@@ -18449,8 +18449,8 @@ var require_view = __commonJS({
|
|
|
18449
18449
|
for (var i = 0; i < roots.length && !path3; i++) {
|
|
18450
18450
|
var root = roots[i];
|
|
18451
18451
|
var loc = resolve3(root, name);
|
|
18452
|
-
var dir =
|
|
18453
|
-
var file =
|
|
18452
|
+
var dir = dirname15(loc);
|
|
18453
|
+
var file = basename5(loc);
|
|
18454
18454
|
path3 = this.resolve(dir, file);
|
|
18455
18455
|
}
|
|
18456
18456
|
return path3;
|
|
@@ -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 = join20(dir, file);
|
|
18465
18465
|
var stat = tryStat(path3);
|
|
18466
18466
|
if (stat && stat.isFile()) {
|
|
18467
18467
|
return path3;
|
|
18468
18468
|
}
|
|
18469
|
-
path3 =
|
|
18469
|
+
path3 = join20(dir, basename5(file, ext), "index" + ext);
|
|
18470
18470
|
stat = tryStat(path3);
|
|
18471
18471
|
if (stat && stat.isFile()) {
|
|
18472
18472
|
return path3;
|
|
@@ -18547,7 +18547,7 @@ var require_content_disposition = __commonJS({
|
|
|
18547
18547
|
"use strict";
|
|
18548
18548
|
module.exports = contentDisposition;
|
|
18549
18549
|
module.exports.parse = parse;
|
|
18550
|
-
var
|
|
18550
|
+
var basename5 = __require("path").basename;
|
|
18551
18551
|
var Buffer3 = require_safe_buffer().Buffer;
|
|
18552
18552
|
var ENCODE_URL_ATTR_CHAR_REGEXP = /[\x00-\x20"'()*,/:;<=>?@[\\\]{}\x7f]/g;
|
|
18553
18553
|
var HEX_ESCAPE_REGEXP = /%[0-9A-Fa-f]{2}/;
|
|
@@ -18583,9 +18583,9 @@ var require_content_disposition = __commonJS({
|
|
|
18583
18583
|
if (typeof fallback === "string" && NON_LATIN1_REGEXP.test(fallback)) {
|
|
18584
18584
|
throw new TypeError("fallback must be ISO-8859-1 string");
|
|
18585
18585
|
}
|
|
18586
|
-
var name =
|
|
18586
|
+
var name = basename5(filename);
|
|
18587
18587
|
var isQuotedString = TEXT_REGEXP.test(name);
|
|
18588
|
-
var fallbackName = typeof fallback !== "string" ? fallback && getlatin1(name) :
|
|
18588
|
+
var fallbackName = typeof fallback !== "string" ? fallback && getlatin1(name) : basename5(fallback);
|
|
18589
18589
|
var hasFallback = typeof fallbackName === "string" && fallbackName !== name;
|
|
18590
18590
|
if (hasFallback || !isQuotedString || HEX_ESCAPE_REGEXP.test(name)) {
|
|
18591
18591
|
params["filename*"] = name;
|
|
@@ -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 join20 = 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(join20(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 = join20(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);
|
|
@@ -20806,7 +20806,7 @@ var require_application = __commonJS({
|
|
|
20806
20806
|
};
|
|
20807
20807
|
app2.del = deprecate.function(app2.delete, "app.del: Use app.delete instead");
|
|
20808
20808
|
app2.render = function render(name, options, callback) {
|
|
20809
|
-
var
|
|
20809
|
+
var cache2 = this.cache;
|
|
20810
20810
|
var done = callback;
|
|
20811
20811
|
var engines = this.engines;
|
|
20812
20812
|
var opts = options;
|
|
@@ -20825,7 +20825,7 @@ var require_application = __commonJS({
|
|
|
20825
20825
|
renderOptions.cache = this.enabled("view cache");
|
|
20826
20826
|
}
|
|
20827
20827
|
if (renderOptions.cache) {
|
|
20828
|
-
view =
|
|
20828
|
+
view = cache2[name];
|
|
20829
20829
|
}
|
|
20830
20830
|
if (!view) {
|
|
20831
20831
|
var View2 = this.get("view");
|
|
@@ -20841,7 +20841,7 @@ var require_application = __commonJS({
|
|
|
20841
20841
|
return done(err);
|
|
20842
20842
|
}
|
|
20843
20843
|
if (renderOptions.cache) {
|
|
20844
|
-
|
|
20844
|
+
cache2[name] = view;
|
|
20845
20845
|
}
|
|
20846
20846
|
}
|
|
20847
20847
|
tryRender(view, renderOptions, done);
|
|
@@ -30483,10 +30483,152 @@ 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
|
|
30487
|
-
import { join as
|
|
30486
|
+
import { existsSync as existsSync17, readFileSync as readFileSync12, writeFileSync as writeFileSync6, unlinkSync as unlinkSync5, mkdirSync as mkdirSync7 } from "node:fs";
|
|
30487
|
+
import { join as join19, dirname as dirname14, resolve as resolve2 } from "node:path";
|
|
30488
|
+
|
|
30489
|
+
// ../shared/platform/src/index.ts
|
|
30490
|
+
import {
|
|
30491
|
+
execFile,
|
|
30492
|
+
execFileSync,
|
|
30493
|
+
spawn
|
|
30494
|
+
} from "node:child_process";
|
|
30495
|
+
import { promisify } from "node:util";
|
|
30496
|
+
var execFilePromise = promisify(execFile);
|
|
30497
|
+
var isWindows = process.platform === "win32";
|
|
30498
|
+
function execBinary(binary, args, opts) {
|
|
30499
|
+
return execFileSync(binary, args, {
|
|
30500
|
+
...opts,
|
|
30501
|
+
shell: isWindows
|
|
30502
|
+
});
|
|
30503
|
+
}
|
|
30504
|
+
async function execBinaryAsync(binary, args, opts) {
|
|
30505
|
+
return execFilePromise(binary, args, {
|
|
30506
|
+
...opts,
|
|
30507
|
+
shell: isWindows
|
|
30508
|
+
});
|
|
30509
|
+
}
|
|
30510
|
+
function spawnBinary(binary, args, opts) {
|
|
30511
|
+
return spawn(binary, args, {
|
|
30512
|
+
...opts,
|
|
30513
|
+
...isWindows && { shell: true, windowsHide: true }
|
|
30514
|
+
});
|
|
30515
|
+
}
|
|
30516
|
+
function isProcessAlive(pid) {
|
|
30517
|
+
try {
|
|
30518
|
+
process.kill(pid, 0);
|
|
30519
|
+
return true;
|
|
30520
|
+
} catch (err) {
|
|
30521
|
+
return !(err instanceof Error && "code" in err && err.code === "ESRCH");
|
|
30522
|
+
}
|
|
30523
|
+
}
|
|
30524
|
+
function walkDescendants(rootPid) {
|
|
30525
|
+
if (isWindows) return { pids: [], psAvailable: false };
|
|
30526
|
+
let out;
|
|
30527
|
+
try {
|
|
30528
|
+
out = execFileSync("ps", ["-A", "-o", "pid=,ppid="], {
|
|
30529
|
+
encoding: "utf-8",
|
|
30530
|
+
timeout: 5e3
|
|
30531
|
+
});
|
|
30532
|
+
} catch {
|
|
30533
|
+
return { pids: [], psAvailable: false };
|
|
30534
|
+
}
|
|
30535
|
+
const children = /* @__PURE__ */ new Map();
|
|
30536
|
+
for (const line of out.split("\n")) {
|
|
30537
|
+
const m = line.trim().match(/^(\d+)\s+(\d+)$/);
|
|
30538
|
+
if (!m) continue;
|
|
30539
|
+
const pid = Number(m[1]);
|
|
30540
|
+
const ppid = Number(m[2]);
|
|
30541
|
+
if (!children.has(ppid)) children.set(ppid, []);
|
|
30542
|
+
children.get(ppid).push(pid);
|
|
30543
|
+
}
|
|
30544
|
+
const acc = [];
|
|
30545
|
+
const queue = [rootPid];
|
|
30546
|
+
const seen = /* @__PURE__ */ new Set([rootPid]);
|
|
30547
|
+
while (queue.length) {
|
|
30548
|
+
const p = queue.shift();
|
|
30549
|
+
for (const c of children.get(p) ?? []) {
|
|
30550
|
+
if (seen.has(c)) continue;
|
|
30551
|
+
seen.add(c);
|
|
30552
|
+
acc.push(c);
|
|
30553
|
+
queue.push(c);
|
|
30554
|
+
}
|
|
30555
|
+
}
|
|
30556
|
+
return { pids: acc, psAvailable: true };
|
|
30557
|
+
}
|
|
30558
|
+
function descendantPids(rootPid) {
|
|
30559
|
+
return walkDescendants(rootPid).pids;
|
|
30560
|
+
}
|
|
30561
|
+
function reapTree(rootPid, graceMs = 5e3) {
|
|
30562
|
+
if (isWindows) {
|
|
30563
|
+
try {
|
|
30564
|
+
execFileSync("taskkill", ["/PID", String(rootPid), "/T", "/F"], {
|
|
30565
|
+
timeout: 5e3
|
|
30566
|
+
});
|
|
30567
|
+
} catch {
|
|
30568
|
+
}
|
|
30569
|
+
return { signaled: 1, psAvailable: false };
|
|
30570
|
+
}
|
|
30571
|
+
const { pids: descendants, psAvailable } = walkDescendants(rootPid);
|
|
30572
|
+
if (!psAvailable) {
|
|
30573
|
+
console.warn(
|
|
30574
|
+
`[reapTree] 'ps' unavailable \u2014 signalling only the root (PID ${rootPid}); escaped descendants cannot be enumerated on this system.`
|
|
30575
|
+
);
|
|
30576
|
+
}
|
|
30577
|
+
const term = [...descendants, rootPid];
|
|
30578
|
+
for (const pid of term) {
|
|
30579
|
+
try {
|
|
30580
|
+
process.kill(pid, "SIGTERM");
|
|
30581
|
+
} catch {
|
|
30582
|
+
}
|
|
30583
|
+
}
|
|
30584
|
+
setTimeout(() => {
|
|
30585
|
+
const kill = [...descendantPids(rootPid), rootPid];
|
|
30586
|
+
let stragglers = 0;
|
|
30587
|
+
for (const pid of kill) {
|
|
30588
|
+
if (!isProcessAlive(pid)) continue;
|
|
30589
|
+
stragglers++;
|
|
30590
|
+
try {
|
|
30591
|
+
process.kill(pid, "SIGKILL");
|
|
30592
|
+
} catch {
|
|
30593
|
+
}
|
|
30594
|
+
}
|
|
30595
|
+
if (stragglers > 0) {
|
|
30596
|
+
console.warn(
|
|
30597
|
+
`[reapTree] ${stragglers} process(es) under PID ${rootPid} survived SIGTERM after ${graceMs}ms; sending SIGKILL \u2014 investigate a leaked daemon.`
|
|
30598
|
+
);
|
|
30599
|
+
}
|
|
30600
|
+
}, graceMs).unref();
|
|
30601
|
+
return { signaled: term.length, psAvailable };
|
|
30602
|
+
}
|
|
30603
|
+
var BUILTIN_ICON_MAP = {
|
|
30604
|
+
architect: "blocks",
|
|
30605
|
+
fullstack: "layers",
|
|
30606
|
+
reliability: "activity",
|
|
30607
|
+
"staff-engineer": "compass",
|
|
30608
|
+
principal: "crown",
|
|
30609
|
+
frontend: "layout",
|
|
30610
|
+
backend: "server",
|
|
30611
|
+
infrastructure: "cloud",
|
|
30612
|
+
performance: "gauge",
|
|
30613
|
+
accessibility: "accessibility",
|
|
30614
|
+
data: "database",
|
|
30615
|
+
devops: "rocket",
|
|
30616
|
+
dx: "terminal",
|
|
30617
|
+
mobile: "smartphone",
|
|
30618
|
+
security: "shield-alert",
|
|
30619
|
+
quality: "sparkles",
|
|
30620
|
+
testing: "test-tubes",
|
|
30621
|
+
ai: "bot",
|
|
30622
|
+
"docs-writer": "file-text"
|
|
30623
|
+
};
|
|
30624
|
+
function defaultIconFor(id, tier) {
|
|
30625
|
+
return BUILTIN_ICON_MAP[id] ?? (tier === "persona" ? "brain" : "user");
|
|
30626
|
+
}
|
|
30627
|
+
|
|
30628
|
+
// src/server/index.ts
|
|
30488
30629
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
30489
30630
|
import { randomBytes } from "node:crypto";
|
|
30631
|
+
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
30490
30632
|
import { Server as SocketIOServer } from "socket.io";
|
|
30491
30633
|
|
|
30492
30634
|
// src/server/services/ocr-resolver.ts
|
|
@@ -30511,14 +30653,14 @@ function resolveOcrDir(startDir) {
|
|
|
30511
30653
|
|
|
30512
30654
|
// ../cli/src/lib/db/index.ts
|
|
30513
30655
|
import {
|
|
30514
|
-
existsSync as
|
|
30656
|
+
existsSync as existsSync5,
|
|
30515
30657
|
mkdirSync as mkdirSync2,
|
|
30516
|
-
copyFileSync,
|
|
30517
|
-
statSync,
|
|
30658
|
+
copyFileSync as copyFileSync2,
|
|
30659
|
+
statSync as statSync2,
|
|
30518
30660
|
mkdtempSync,
|
|
30519
30661
|
rmSync
|
|
30520
30662
|
} from "node:fs";
|
|
30521
|
-
import { dirname as
|
|
30663
|
+
import { dirname as dirname5, join as join5 } from "node:path";
|
|
30522
30664
|
|
|
30523
30665
|
// ../cli/src/lib/db/engine.ts
|
|
30524
30666
|
import { createRequire } from "node:module";
|
|
@@ -31167,6 +31309,35 @@ var MIGRATIONS = [
|
|
|
31167
31309
|
db.run("DROP INDEX IF EXISTS idx_command_executions_parent;");
|
|
31168
31310
|
db.run("ALTER TABLE command_executions DROP COLUMN parent_id;");
|
|
31169
31311
|
}
|
|
31312
|
+
},
|
|
31313
|
+
{
|
|
31314
|
+
version: 14,
|
|
31315
|
+
description: "Self-heal markdown_artifacts duplication: collapse NULL-round duplicate rows and add a NULL-safe unique index so the dedup bug cannot recur",
|
|
31316
|
+
// The table's `UNIQUE(session_id, artifact_type, round_number, file_path)`
|
|
31317
|
+
// never deduped session-level artifacts because SQLite treats NULL ≠ NULL,
|
|
31318
|
+
// and the writer used `INSERT OR REPLACE` — so every re-parse of a
|
|
31319
|
+
// NULL-round artifact (context.md, map.md, …) appended a duplicate (one
|
|
31320
|
+
// context.md reached 775 identical rows, ~177 MB). The writer is now an
|
|
31321
|
+
// explicit UPDATE-or-INSERT; this migration heals existing DBs and adds a
|
|
31322
|
+
// NULL-collapsing unique index as a DB-level backstop.
|
|
31323
|
+
//
|
|
31324
|
+
// Orphan-row sweep (FK-dangling children from the pre-FK-enforcement era)
|
|
31325
|
+
// is intentionally NOT done here — it needs `PRAGMA foreign_keys = OFF`,
|
|
31326
|
+
// which is a no-op inside the migration transaction. `ocr db doctor --fix`
|
|
31327
|
+
// performs it outside a transaction.
|
|
31328
|
+
run: (db) => {
|
|
31329
|
+
db.run(`
|
|
31330
|
+
DELETE FROM markdown_artifacts
|
|
31331
|
+
WHERE rowid NOT IN (
|
|
31332
|
+
SELECT MAX(rowid) FROM markdown_artifacts
|
|
31333
|
+
GROUP BY session_id, artifact_type, IFNULL(round_number, -1), file_path
|
|
31334
|
+
)
|
|
31335
|
+
`);
|
|
31336
|
+
db.run(`
|
|
31337
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_markdown_artifacts_logical
|
|
31338
|
+
ON markdown_artifacts(session_id, artifact_type, IFNULL(round_number, -1), file_path)
|
|
31339
|
+
`);
|
|
31340
|
+
}
|
|
31170
31341
|
}
|
|
31171
31342
|
];
|
|
31172
31343
|
function columnExists(db, table, column) {
|
|
@@ -31481,9 +31652,28 @@ function sqliteUtcMs(ts) {
|
|
|
31481
31652
|
}
|
|
31482
31653
|
|
|
31483
31654
|
// ../cli/src/lib/state/exit-codes.ts
|
|
31655
|
+
var STATE_EXIT = {
|
|
31656
|
+
OK: 0,
|
|
31657
|
+
USAGE: 2,
|
|
31658
|
+
AMBIGUOUS: 3,
|
|
31659
|
+
NOT_FOUND: 4,
|
|
31660
|
+
ILLEGAL_TRANSITION: 5,
|
|
31661
|
+
INVARIANT_UNMET: 6,
|
|
31662
|
+
SCHEMA_INVALID: 7,
|
|
31663
|
+
/** Database was locked past the bounded retry budget (SQLITE_BUSY). */
|
|
31664
|
+
BUSY: 8
|
|
31665
|
+
};
|
|
31666
|
+
var StateError = class extends Error {
|
|
31667
|
+
constructor(code, message) {
|
|
31668
|
+
super(message);
|
|
31669
|
+
this.code = code;
|
|
31670
|
+
this.name = "StateError";
|
|
31671
|
+
}
|
|
31672
|
+
};
|
|
31484
31673
|
var CANCELLED_EXIT_CODE = -2;
|
|
31485
31674
|
var ORPHAN_EXIT_CODE = -3;
|
|
31486
31675
|
var CASCADE_CLOSE_EXIT_CODE = -4;
|
|
31676
|
+
var WATCHDOG_DEADLINE_EXIT_CODE = -5;
|
|
31487
31677
|
|
|
31488
31678
|
// ../cli/src/lib/db/agent-sessions.ts
|
|
31489
31679
|
var NOTE_ORPHAN_PREFIX = "orphaned by liveness sweep";
|
|
@@ -31678,9 +31868,84 @@ function sweepStaleSessions(db, thresholdSeconds) {
|
|
|
31678
31868
|
return { closedSessionIds: rows.map((r) => r.id) };
|
|
31679
31869
|
}
|
|
31680
31870
|
|
|
31871
|
+
// ../cli/src/lib/db/maintenance.ts
|
|
31872
|
+
import {
|
|
31873
|
+
existsSync as existsSync3,
|
|
31874
|
+
readdirSync,
|
|
31875
|
+
statSync,
|
|
31876
|
+
unlinkSync,
|
|
31877
|
+
copyFileSync
|
|
31878
|
+
} from "node:fs";
|
|
31879
|
+
import { dirname as dirname3, join as join3, basename } from "node:path";
|
|
31880
|
+
var ONE_HOUR_MS = 60 * 60 * 1e3;
|
|
31881
|
+
function scanOrphanTempFiles(dataDir) {
|
|
31882
|
+
let entries;
|
|
31883
|
+
try {
|
|
31884
|
+
entries = readdirSync(dataDir);
|
|
31885
|
+
} catch {
|
|
31886
|
+
return [];
|
|
31887
|
+
}
|
|
31888
|
+
const out = [];
|
|
31889
|
+
for (const name of entries) {
|
|
31890
|
+
const m = name.match(/^ocr\.db\.(\d+)\.tmp$/);
|
|
31891
|
+
if (!m) continue;
|
|
31892
|
+
const pid = Number(m[1]);
|
|
31893
|
+
let ageMs = 0;
|
|
31894
|
+
try {
|
|
31895
|
+
ageMs = Date.now() - statSync(join3(dataDir, name)).mtimeMs;
|
|
31896
|
+
} catch {
|
|
31897
|
+
continue;
|
|
31898
|
+
}
|
|
31899
|
+
const alive = isProcessAlive(pid);
|
|
31900
|
+
out.push({
|
|
31901
|
+
name,
|
|
31902
|
+
pid,
|
|
31903
|
+
ageMs,
|
|
31904
|
+
// Reapable only when the writer PID is dead AND the file is old enough
|
|
31905
|
+
// that no live mid-write could plausibly own it.
|
|
31906
|
+
reapable: !alive && ageMs > ONE_HOUR_MS
|
|
31907
|
+
});
|
|
31908
|
+
}
|
|
31909
|
+
return out;
|
|
31910
|
+
}
|
|
31911
|
+
function reapOrphanDbFiles(dataDir) {
|
|
31912
|
+
const reaped = [];
|
|
31913
|
+
for (const f of scanOrphanTempFiles(dataDir)) {
|
|
31914
|
+
if (!f.reapable) continue;
|
|
31915
|
+
try {
|
|
31916
|
+
unlinkSync(join3(dataDir, f.name));
|
|
31917
|
+
reaped.push(f.name);
|
|
31918
|
+
} catch {
|
|
31919
|
+
}
|
|
31920
|
+
}
|
|
31921
|
+
return reaped;
|
|
31922
|
+
}
|
|
31923
|
+
var SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
31924
|
+
function reapStaleExecLogs(execLogsDir, maxAgeMs = SEVEN_DAYS_MS) {
|
|
31925
|
+
let entries;
|
|
31926
|
+
try {
|
|
31927
|
+
entries = readdirSync(execLogsDir);
|
|
31928
|
+
} catch {
|
|
31929
|
+
return [];
|
|
31930
|
+
}
|
|
31931
|
+
const cutoff = Date.now() - maxAgeMs;
|
|
31932
|
+
const reaped = [];
|
|
31933
|
+
for (const name of entries) {
|
|
31934
|
+
if (!name.endsWith(".log")) continue;
|
|
31935
|
+
const full = join3(execLogsDir, name);
|
|
31936
|
+
try {
|
|
31937
|
+
if (statSync(full).mtimeMs > cutoff) continue;
|
|
31938
|
+
unlinkSync(full);
|
|
31939
|
+
reaped.push(name);
|
|
31940
|
+
} catch {
|
|
31941
|
+
}
|
|
31942
|
+
}
|
|
31943
|
+
return reaped;
|
|
31944
|
+
}
|
|
31945
|
+
|
|
31681
31946
|
// ../cli/src/lib/db/command-log.ts
|
|
31682
|
-
import { appendFileSync, existsSync as
|
|
31683
|
-
import { dirname as
|
|
31947
|
+
import { appendFileSync, existsSync as existsSync4, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
31948
|
+
import { dirname as dirname4, join as join4 } from "node:path";
|
|
31684
31949
|
import { randomUUID } from "node:crypto";
|
|
31685
31950
|
var CACHE_DIR = ".cache";
|
|
31686
31951
|
var FILENAME = "command-history.jsonl";
|
|
@@ -31691,16 +31956,16 @@ function generateCommandUid() {
|
|
|
31691
31956
|
return randomUUID();
|
|
31692
31957
|
}
|
|
31693
31958
|
function cacheDir(ocrDir) {
|
|
31694
|
-
return
|
|
31959
|
+
return join4(ocrDir, "data", CACHE_DIR);
|
|
31695
31960
|
}
|
|
31696
31961
|
function commandLogPath(ocrDir) {
|
|
31697
|
-
return
|
|
31962
|
+
return join4(cacheDir(ocrDir), FILENAME);
|
|
31698
31963
|
}
|
|
31699
31964
|
function appendCommandLog(ocrDir, entry) {
|
|
31700
31965
|
try {
|
|
31701
31966
|
const filePath = commandLogPath(ocrDir);
|
|
31702
|
-
const dir =
|
|
31703
|
-
if (!
|
|
31967
|
+
const dir = dirname4(filePath);
|
|
31968
|
+
if (!existsSync4(dir)) mkdirSync(dir, { recursive: true });
|
|
31704
31969
|
const line = JSON.stringify(entry) + "\n";
|
|
31705
31970
|
appendFileSync(filePath, line, { encoding: "utf-8" });
|
|
31706
31971
|
if (approxLineCount >= 0) approxLineCount++;
|
|
@@ -31710,7 +31975,7 @@ function appendCommandLog(ocrDir, entry) {
|
|
|
31710
31975
|
}
|
|
31711
31976
|
function readCommandLog(ocrDir) {
|
|
31712
31977
|
const filePath = commandLogPath(ocrDir);
|
|
31713
|
-
if (!
|
|
31978
|
+
if (!existsSync4(filePath)) return [];
|
|
31714
31979
|
const content = readFileSync(filePath, "utf-8");
|
|
31715
31980
|
const entries = [];
|
|
31716
31981
|
for (const line of content.split("\n")) {
|
|
@@ -31780,11 +32045,11 @@ var V2_SCHEMA_VERSION = 12;
|
|
|
31780
32045
|
function maybeSnapshotBeforeUpgrade(db, dbPath, fromVersion) {
|
|
31781
32046
|
if (fromVersion < 1 || fromVersion >= V2_SCHEMA_VERSION) return null;
|
|
31782
32047
|
const bakPath = `${dbPath}.bak.v${fromVersion}`;
|
|
31783
|
-
if (
|
|
32048
|
+
if (existsSync5(bakPath)) return bakPath;
|
|
31784
32049
|
try {
|
|
31785
|
-
if (!
|
|
32050
|
+
if (!existsSync5(dbPath) || statSync2(dbPath).size === 0) return null;
|
|
31786
32051
|
db.pragma("wal_checkpoint(TRUNCATE)");
|
|
31787
|
-
|
|
32052
|
+
copyFileSync2(dbPath, bakPath);
|
|
31788
32053
|
return bakPath;
|
|
31789
32054
|
} catch {
|
|
31790
32055
|
return null;
|
|
@@ -31818,8 +32083,8 @@ async function openDatabase(dbPath) {
|
|
|
31818
32083
|
if (cached) {
|
|
31819
32084
|
return cached;
|
|
31820
32085
|
}
|
|
31821
|
-
const dir =
|
|
31822
|
-
if (!
|
|
32086
|
+
const dir = dirname5(dbPath);
|
|
32087
|
+
if (!existsSync5(dir)) {
|
|
31823
32088
|
mkdirSync2(dir, { recursive: true });
|
|
31824
32089
|
}
|
|
31825
32090
|
const db = openEngine(dbPath);
|
|
@@ -31827,11 +32092,11 @@ async function openDatabase(dbPath) {
|
|
|
31827
32092
|
return db;
|
|
31828
32093
|
}
|
|
31829
32094
|
async function ensureDatabase(ocrDir) {
|
|
31830
|
-
const dataDir =
|
|
31831
|
-
if (!
|
|
32095
|
+
const dataDir = join5(ocrDir, "data");
|
|
32096
|
+
if (!existsSync5(dataDir)) {
|
|
31832
32097
|
mkdirSync2(dataDir, { recursive: true });
|
|
31833
32098
|
}
|
|
31834
|
-
const dbPath =
|
|
32099
|
+
const dbPath = join5(dataDir, "ocr.db");
|
|
31835
32100
|
const db = await openDatabase(dbPath);
|
|
31836
32101
|
let before = 0;
|
|
31837
32102
|
try {
|
|
@@ -31859,7 +32124,7 @@ async function ensureDatabase(ocrDir) {
|
|
|
31859
32124
|
return db;
|
|
31860
32125
|
}
|
|
31861
32126
|
function walCheckpointTruncate(dbPath) {
|
|
31862
|
-
if (!
|
|
32127
|
+
if (!existsSync5(dbPath)) {
|
|
31863
32128
|
return "skipped";
|
|
31864
32129
|
}
|
|
31865
32130
|
const cached = connections.get(dbPath);
|
|
@@ -31894,11 +32159,11 @@ function closeDatabase(dbPath) {
|
|
|
31894
32159
|
}
|
|
31895
32160
|
|
|
31896
32161
|
// src/server/db.ts
|
|
31897
|
-
import { join as
|
|
32162
|
+
import { join as join6 } from "node:path";
|
|
31898
32163
|
var cachedDb = null;
|
|
31899
32164
|
var cachedDbPath = null;
|
|
31900
32165
|
async function openDb(ocrDir) {
|
|
31901
|
-
const dbPath =
|
|
32166
|
+
const dbPath = join6(ocrDir, "data", "ocr.db");
|
|
31902
32167
|
const db = await ensureDatabase(ocrDir);
|
|
31903
32168
|
cachedDb = db;
|
|
31904
32169
|
cachedDbPath = dbPath;
|
|
@@ -33080,37 +33345,9 @@ function createStatsRouter(db) {
|
|
|
33080
33345
|
// src/server/routes/commands.ts
|
|
33081
33346
|
var import_express8 = __toESM(require_express2(), 1);
|
|
33082
33347
|
|
|
33083
|
-
// ../shared/platform/src/index.ts
|
|
33084
|
-
import {
|
|
33085
|
-
execFile,
|
|
33086
|
-
execFileSync,
|
|
33087
|
-
spawn
|
|
33088
|
-
} from "node:child_process";
|
|
33089
|
-
import { promisify } from "node:util";
|
|
33090
|
-
var execFilePromise = promisify(execFile);
|
|
33091
|
-
var isWindows = process.platform === "win32";
|
|
33092
|
-
function execBinary(binary, args, opts) {
|
|
33093
|
-
return execFileSync(binary, args, {
|
|
33094
|
-
...opts,
|
|
33095
|
-
shell: isWindows
|
|
33096
|
-
});
|
|
33097
|
-
}
|
|
33098
|
-
async function execBinaryAsync(binary, args, opts) {
|
|
33099
|
-
return execFilePromise(binary, args, {
|
|
33100
|
-
...opts,
|
|
33101
|
-
shell: isWindows
|
|
33102
|
-
});
|
|
33103
|
-
}
|
|
33104
|
-
function spawnBinary(binary, args, opts) {
|
|
33105
|
-
return spawn(binary, args, {
|
|
33106
|
-
...opts,
|
|
33107
|
-
...isWindows && { shell: true, windowsHide: true }
|
|
33108
|
-
});
|
|
33109
|
-
}
|
|
33110
|
-
|
|
33111
33348
|
// src/server/socket/command-runner.ts
|
|
33112
|
-
import { readFileSync as
|
|
33113
|
-
import { dirname as
|
|
33349
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, unlinkSync as unlinkSync3, mkdirSync as mkdirSync5, existsSync as existsSync10 } from "node:fs";
|
|
33350
|
+
import { dirname as dirname7, join as join12 } from "node:path";
|
|
33114
33351
|
|
|
33115
33352
|
// src/server/services/command-outcome.ts
|
|
33116
33353
|
function deriveCommandOutcome(exitCode, completeness) {
|
|
@@ -33118,6 +33355,7 @@ function deriveCommandOutcome(exitCode, completeness) {
|
|
|
33118
33355
|
if (exitCode === CANCELLED_EXIT_CODE || exitCode === CASCADE_CLOSE_EXIT_CODE) {
|
|
33119
33356
|
return "cancelled";
|
|
33120
33357
|
}
|
|
33358
|
+
if (exitCode === WATCHDOG_DEADLINE_EXIT_CODE) return "failed";
|
|
33121
33359
|
if (exitCode !== 0) return "failed";
|
|
33122
33360
|
if (completeness === null || completeness === "complete") return "success";
|
|
33123
33361
|
return "incomplete";
|
|
@@ -33146,7 +33384,50 @@ function getWorkflowCompletenessForExecution(db, executionId) {
|
|
|
33146
33384
|
|
|
33147
33385
|
// src/server/services/ai-cli/index.ts
|
|
33148
33386
|
import { readFileSync as readFileSync3 } from "node:fs";
|
|
33149
|
-
import { join as
|
|
33387
|
+
import { join as join9 } from "node:path";
|
|
33388
|
+
|
|
33389
|
+
// src/server/services/ai-cli/helpers.ts
|
|
33390
|
+
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync2, unlinkSync as unlinkSync2, openSync, closeSync } from "node:fs";
|
|
33391
|
+
import { tmpdir } from "node:os";
|
|
33392
|
+
import { join as join7 } from "node:path";
|
|
33393
|
+
function buildFileStdio(stdin, logFile) {
|
|
33394
|
+
if (!logFile) {
|
|
33395
|
+
return { stdio: [stdin, "pipe", "pipe"], logFd: null, logPath: void 0 };
|
|
33396
|
+
}
|
|
33397
|
+
const logFd = openSync(logFile, "a");
|
|
33398
|
+
return { stdio: [stdin, logFd, logFd], logFd, logPath: logFile };
|
|
33399
|
+
}
|
|
33400
|
+
function closeFileStdio(logFd) {
|
|
33401
|
+
if (logFd === null) return;
|
|
33402
|
+
try {
|
|
33403
|
+
closeSync(logFd);
|
|
33404
|
+
} catch {
|
|
33405
|
+
}
|
|
33406
|
+
}
|
|
33407
|
+
function formatToolDetail(tool, input) {
|
|
33408
|
+
switch (tool) {
|
|
33409
|
+
case "Read":
|
|
33410
|
+
return `Reading ${input["file_path"] ?? "file"}`;
|
|
33411
|
+
case "Write":
|
|
33412
|
+
return `Writing ${input["file_path"] ?? "file"}`;
|
|
33413
|
+
case "Edit":
|
|
33414
|
+
return `Editing ${input["file_path"] ?? "file"}`;
|
|
33415
|
+
case "Grep":
|
|
33416
|
+
return `Searching for "${input["pattern"] ?? "..."}"`;
|
|
33417
|
+
case "Glob":
|
|
33418
|
+
return `Finding files matching ${input["pattern"] ?? "..."}`;
|
|
33419
|
+
case "Bash": {
|
|
33420
|
+
let cmd = input["command"] ?? "...";
|
|
33421
|
+
cmd = cmd.replace(/^cd\s+\S+\s*&&\s*/, "");
|
|
33422
|
+
return `Running: ${cmd.slice(0, 120)}`;
|
|
33423
|
+
}
|
|
33424
|
+
case "Agent":
|
|
33425
|
+
return `Spawning agent: ${input["description"] ?? "..."}`;
|
|
33426
|
+
default:
|
|
33427
|
+
return `Using ${tool}`;
|
|
33428
|
+
}
|
|
33429
|
+
}
|
|
33430
|
+
var TEMP_BASE = join7(tmpdir(), "ocr-ai-prompts");
|
|
33150
33431
|
|
|
33151
33432
|
// src/server/socket/env.ts
|
|
33152
33433
|
var ENV_ALLOWLIST = [
|
|
@@ -33208,17 +33489,14 @@ function buildResumeCommand(vendor, vendorSessionId) {
|
|
|
33208
33489
|
// src/server/services/ai-cli/claude-adapter.ts
|
|
33209
33490
|
var WORKFLOW_TOOLS = ["Read", "Write", "Edit", "Bash", "Glob", "Grep", "TodoWrite", "TodoRead", "Task"];
|
|
33210
33491
|
var QUERY_TOOLS = ["Read", "Grep", "Glob"];
|
|
33211
|
-
var BUNDLED_CLAUDE_MODELS = [
|
|
33212
|
-
{ id: "claude-opus-4-7", displayName: "Claude Opus 4.7" },
|
|
33213
|
-
{ id: "claude-sonnet-4-6", displayName: "Claude Sonnet 4.6" },
|
|
33214
|
-
{ id: "claude-haiku-4-5-20251001", displayName: "Claude Haiku 4.5" }
|
|
33215
|
-
];
|
|
33216
33492
|
var ClaudeCodeAdapter = class {
|
|
33217
33493
|
name = "Claude Code";
|
|
33218
33494
|
binary = "claude";
|
|
33219
33495
|
// Claude Code subagent definitions support per-subagent model frontmatter,
|
|
33220
33496
|
// so per-task model overrides are honored at the host level.
|
|
33221
33497
|
supportsPerTaskModel = true;
|
|
33498
|
+
// Claude Code can spawn reviewer sub-agents via its Task tool.
|
|
33499
|
+
supportsSubagentSpawn = true;
|
|
33222
33500
|
buildResumeArgs(vendorSessionId) {
|
|
33223
33501
|
return buildResumeArgs("claude", vendorSessionId);
|
|
33224
33502
|
}
|
|
@@ -33259,47 +33537,25 @@ var ClaudeCodeAdapter = class {
|
|
|
33259
33537
|
if (opts.model) {
|
|
33260
33538
|
flags.push("--model", opts.model);
|
|
33261
33539
|
}
|
|
33540
|
+
const { stdio, logFd, logPath } = buildFileStdio(
|
|
33541
|
+
"pipe",
|
|
33542
|
+
isWorkflow ? opts.logFile : void 0
|
|
33543
|
+
);
|
|
33262
33544
|
const proc = spawnBinary("claude", flags, {
|
|
33263
33545
|
cwd: opts.cwd,
|
|
33264
33546
|
env: { ...cleanEnv(), ...opts.env ?? {} },
|
|
33265
33547
|
detached: isWorkflow,
|
|
33266
|
-
stdio
|
|
33548
|
+
stdio
|
|
33267
33549
|
});
|
|
33550
|
+
closeFileStdio(logFd);
|
|
33551
|
+
if (isWorkflow) proc.unref();
|
|
33268
33552
|
proc.stdin?.write(opts.prompt);
|
|
33269
33553
|
proc.stdin?.end();
|
|
33270
|
-
return {
|
|
33271
|
-
|
|
33272
|
-
|
|
33273
|
-
|
|
33274
|
-
|
|
33275
|
-
encoding: "utf-8",
|
|
33276
|
-
timeout: 5e3,
|
|
33277
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
33278
|
-
});
|
|
33279
|
-
const parsed = JSON.parse(output);
|
|
33280
|
-
if (Array.isArray(parsed)) {
|
|
33281
|
-
const models = [];
|
|
33282
|
-
for (const item of parsed) {
|
|
33283
|
-
if (typeof item === "string") {
|
|
33284
|
-
models.push({ id: item });
|
|
33285
|
-
} else if (typeof item === "object" && item !== null && "id" in item && typeof item.id === "string") {
|
|
33286
|
-
const obj = item;
|
|
33287
|
-
const desc = { id: obj.id };
|
|
33288
|
-
if (typeof obj.displayName === "string") desc.displayName = obj.displayName;
|
|
33289
|
-
if (typeof obj.provider === "string") desc.provider = obj.provider;
|
|
33290
|
-
if (Array.isArray(obj.tags)) {
|
|
33291
|
-
desc.tags = obj.tags.filter((t) => typeof t === "string");
|
|
33292
|
-
}
|
|
33293
|
-
models.push(desc);
|
|
33294
|
-
}
|
|
33295
|
-
}
|
|
33296
|
-
if (models.length > 0) {
|
|
33297
|
-
return models;
|
|
33298
|
-
}
|
|
33299
|
-
}
|
|
33300
|
-
} catch {
|
|
33301
|
-
}
|
|
33302
|
-
return BUNDLED_CLAUDE_MODELS;
|
|
33554
|
+
return {
|
|
33555
|
+
process: proc,
|
|
33556
|
+
detached: isWorkflow,
|
|
33557
|
+
...logPath ? { logPath } : {}
|
|
33558
|
+
};
|
|
33303
33559
|
}
|
|
33304
33560
|
createParser() {
|
|
33305
33561
|
return new ClaudeLineParser();
|
|
@@ -33426,6 +33682,13 @@ var ClaudeLineParser = class {
|
|
|
33426
33682
|
const message = typeof parsed["message"] === "string" ? parsed["message"] : "Agent error";
|
|
33427
33683
|
events.push({ type: "error", source: "agent", message });
|
|
33428
33684
|
}
|
|
33685
|
+
if (type === "result") {
|
|
33686
|
+
events.push({
|
|
33687
|
+
type: "result",
|
|
33688
|
+
isError: parsed["is_error"] === true,
|
|
33689
|
+
subtype: typeof parsed["subtype"] === "string" ? parsed["subtype"] : void 0
|
|
33690
|
+
});
|
|
33691
|
+
}
|
|
33429
33692
|
return events;
|
|
33430
33693
|
}
|
|
33431
33694
|
};
|
|
@@ -33447,11 +33710,6 @@ function extractToolResultOutput(content) {
|
|
|
33447
33710
|
function capitalize(s) {
|
|
33448
33711
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
33449
33712
|
}
|
|
33450
|
-
var BUNDLED_OPENCODE_MODELS = [
|
|
33451
|
-
{ id: "anthropic/claude-opus-4-7", provider: "anthropic" },
|
|
33452
|
-
{ id: "anthropic/claude-sonnet-4-6", provider: "anthropic" },
|
|
33453
|
-
{ id: "anthropic/claude-haiku-4-5-20251001", provider: "anthropic" }
|
|
33454
|
-
];
|
|
33455
33713
|
var OpenCodeAdapter = class {
|
|
33456
33714
|
name = "OpenCode";
|
|
33457
33715
|
binary = "opencode";
|
|
@@ -33461,6 +33719,9 @@ var OpenCodeAdapter = class {
|
|
|
33461
33719
|
// until OpenCode adds per-task model support; OCR surfaces a warning to
|
|
33462
33720
|
// the user when this happens.
|
|
33463
33721
|
supportsPerTaskModel = false;
|
|
33722
|
+
// OpenCode exposes a sub-agent primitive (`--agent`), so reviewer sub-agents
|
|
33723
|
+
// can be spawned in-agent (uniform model — see supportsPerTaskModel above).
|
|
33724
|
+
supportsSubagentSpawn = true;
|
|
33464
33725
|
buildResumeArgs(vendorSessionId) {
|
|
33465
33726
|
return buildResumeArgs("opencode", vendorSessionId);
|
|
33466
33727
|
}
|
|
@@ -33498,45 +33759,23 @@ var OpenCodeAdapter = class {
|
|
|
33498
33759
|
if (opts.model) {
|
|
33499
33760
|
args.push("--model", opts.model);
|
|
33500
33761
|
}
|
|
33762
|
+
const { stdio, logFd, logPath } = buildFileStdio(
|
|
33763
|
+
"ignore",
|
|
33764
|
+
isWorkflow ? opts.logFile : void 0
|
|
33765
|
+
);
|
|
33501
33766
|
const proc = spawnBinary("opencode", args, {
|
|
33502
33767
|
cwd: opts.cwd,
|
|
33503
33768
|
env: { ...cleanEnv(), ...opts.env ?? {} },
|
|
33504
33769
|
detached: isWorkflow,
|
|
33505
|
-
stdio
|
|
33770
|
+
stdio
|
|
33506
33771
|
});
|
|
33507
|
-
|
|
33508
|
-
|
|
33509
|
-
|
|
33510
|
-
|
|
33511
|
-
|
|
33512
|
-
|
|
33513
|
-
|
|
33514
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
33515
|
-
});
|
|
33516
|
-
const parsed = JSON.parse(output);
|
|
33517
|
-
if (Array.isArray(parsed)) {
|
|
33518
|
-
const models = [];
|
|
33519
|
-
for (const item of parsed) {
|
|
33520
|
-
if (typeof item === "string") {
|
|
33521
|
-
models.push({ id: item });
|
|
33522
|
-
} else if (typeof item === "object" && item !== null && "id" in item && typeof item.id === "string") {
|
|
33523
|
-
const obj = item;
|
|
33524
|
-
const desc = { id: obj.id };
|
|
33525
|
-
if (typeof obj.displayName === "string") desc.displayName = obj.displayName;
|
|
33526
|
-
if (typeof obj.provider === "string") desc.provider = obj.provider;
|
|
33527
|
-
if (Array.isArray(obj.tags)) {
|
|
33528
|
-
desc.tags = obj.tags.filter((t) => typeof t === "string");
|
|
33529
|
-
}
|
|
33530
|
-
models.push(desc);
|
|
33531
|
-
}
|
|
33532
|
-
}
|
|
33533
|
-
if (models.length > 0) {
|
|
33534
|
-
return models;
|
|
33535
|
-
}
|
|
33536
|
-
}
|
|
33537
|
-
} catch {
|
|
33538
|
-
}
|
|
33539
|
-
return BUNDLED_OPENCODE_MODELS;
|
|
33772
|
+
closeFileStdio(logFd);
|
|
33773
|
+
if (isWorkflow) proc.unref();
|
|
33774
|
+
return {
|
|
33775
|
+
process: proc,
|
|
33776
|
+
detached: isWorkflow,
|
|
33777
|
+
...logPath ? { logPath } : {}
|
|
33778
|
+
};
|
|
33540
33779
|
}
|
|
33541
33780
|
/**
|
|
33542
33781
|
* OpenCode emits each event with all its content already resolved (tool
|
|
@@ -33635,20 +33874,20 @@ function extractToolOutput(part) {
|
|
|
33635
33874
|
// src/server/services/event-journal.ts
|
|
33636
33875
|
import {
|
|
33637
33876
|
createWriteStream,
|
|
33638
|
-
existsSync as
|
|
33639
|
-
mkdirSync as
|
|
33877
|
+
existsSync as existsSync6,
|
|
33878
|
+
mkdirSync as mkdirSync4,
|
|
33640
33879
|
readFileSync as readFileSync2
|
|
33641
33880
|
} from "node:fs";
|
|
33642
|
-
import { join as
|
|
33881
|
+
import { join as join8 } from "node:path";
|
|
33643
33882
|
function eventsDir(ocrDir) {
|
|
33644
|
-
const dir =
|
|
33645
|
-
if (!
|
|
33646
|
-
|
|
33883
|
+
const dir = join8(ocrDir, "data", "events");
|
|
33884
|
+
if (!existsSync6(dir)) {
|
|
33885
|
+
mkdirSync4(dir, { recursive: true });
|
|
33647
33886
|
}
|
|
33648
33887
|
return dir;
|
|
33649
33888
|
}
|
|
33650
33889
|
function eventJournalPath(ocrDir, executionId) {
|
|
33651
|
-
return
|
|
33890
|
+
return join8(eventsDir(ocrDir), `${executionId}.jsonl`);
|
|
33652
33891
|
}
|
|
33653
33892
|
var EventJournalAppender = class {
|
|
33654
33893
|
stream;
|
|
@@ -33685,7 +33924,7 @@ var EventJournalAppender = class {
|
|
|
33685
33924
|
};
|
|
33686
33925
|
function readEventJournal(ocrDir, executionId) {
|
|
33687
33926
|
const path2 = eventJournalPath(ocrDir, executionId);
|
|
33688
|
-
if (!
|
|
33927
|
+
if (!existsSync6(path2)) return [];
|
|
33689
33928
|
let raw;
|
|
33690
33929
|
try {
|
|
33691
33930
|
raw = readFileSync2(path2, "utf-8");
|
|
@@ -33705,38 +33944,13 @@ function readEventJournal(ocrDir, executionId) {
|
|
|
33705
33944
|
return events;
|
|
33706
33945
|
}
|
|
33707
33946
|
|
|
33708
|
-
// src/server/services/ai-cli/helpers.ts
|
|
33709
|
-
import { tmpdir } from "node:os";
|
|
33710
|
-
import { join as join7 } from "node:path";
|
|
33711
|
-
function formatToolDetail(tool, input) {
|
|
33712
|
-
switch (tool) {
|
|
33713
|
-
case "Read":
|
|
33714
|
-
return `Reading ${input["file_path"] ?? "file"}`;
|
|
33715
|
-
case "Write":
|
|
33716
|
-
return `Writing ${input["file_path"] ?? "file"}`;
|
|
33717
|
-
case "Edit":
|
|
33718
|
-
return `Editing ${input["file_path"] ?? "file"}`;
|
|
33719
|
-
case "Grep":
|
|
33720
|
-
return `Searching for "${input["pattern"] ?? "..."}"`;
|
|
33721
|
-
case "Glob":
|
|
33722
|
-
return `Finding files matching ${input["pattern"] ?? "..."}`;
|
|
33723
|
-
case "Bash": {
|
|
33724
|
-
let cmd = input["command"] ?? "...";
|
|
33725
|
-
cmd = cmd.replace(/^cd\s+\S+\s*&&\s*/, "");
|
|
33726
|
-
return `Running: ${cmd.slice(0, 120)}`;
|
|
33727
|
-
}
|
|
33728
|
-
case "Agent":
|
|
33729
|
-
return `Spawning agent: ${input["description"] ?? "..."}`;
|
|
33730
|
-
default:
|
|
33731
|
-
return `Using ${tool}`;
|
|
33732
|
-
}
|
|
33733
|
-
}
|
|
33734
|
-
var TEMP_BASE = join7(tmpdir(), "ocr-ai-prompts");
|
|
33735
|
-
|
|
33736
33947
|
// src/server/services/ai-cli/index.ts
|
|
33948
|
+
function createRegisteredAdapters() {
|
|
33949
|
+
return [new ClaudeCodeAdapter(), new OpenCodeAdapter()];
|
|
33950
|
+
}
|
|
33737
33951
|
function readAiCliPreference(ocrDir) {
|
|
33738
33952
|
try {
|
|
33739
|
-
const configPath =
|
|
33953
|
+
const configPath = join9(ocrDir, "config.yaml");
|
|
33740
33954
|
const content = readFileSync3(configPath, "utf-8");
|
|
33741
33955
|
const match = content.match(/^\s*ai_cli:\s*(\S+)/m);
|
|
33742
33956
|
const value = match?.[1] ?? "auto";
|
|
@@ -33753,10 +33967,7 @@ var AiCliService = class {
|
|
|
33753
33967
|
status;
|
|
33754
33968
|
constructor(ocrDir) {
|
|
33755
33969
|
this.preference = readAiCliPreference(ocrDir);
|
|
33756
|
-
const adapters =
|
|
33757
|
-
new ClaudeCodeAdapter(),
|
|
33758
|
-
new OpenCodeAdapter()
|
|
33759
|
-
];
|
|
33970
|
+
const adapters = createRegisteredAdapters();
|
|
33760
33971
|
this.entries = adapters.map((adapter) => ({
|
|
33761
33972
|
adapter,
|
|
33762
33973
|
detection: adapter.detect()
|
|
@@ -33845,31 +34056,245 @@ var AiCliService = class {
|
|
|
33845
34056
|
}
|
|
33846
34057
|
};
|
|
33847
34058
|
|
|
34059
|
+
// src/server/services/ai-cli/file-tailer.ts
|
|
34060
|
+
import { openSync as openSync2, readSync, closeSync as closeSync2, existsSync as existsSync7 } from "node:fs";
|
|
34061
|
+
import { StringDecoder } from "node:string_decoder";
|
|
34062
|
+
var DEFAULT_POLL_MS = 100;
|
|
34063
|
+
var READ_CHUNK_BYTES = 64 * 1024;
|
|
34064
|
+
var FileTailer = class {
|
|
34065
|
+
constructor(path2, onChunk, pollMs = DEFAULT_POLL_MS) {
|
|
34066
|
+
this.path = path2;
|
|
34067
|
+
this.onChunk = onChunk;
|
|
34068
|
+
this.pollMs = pollMs;
|
|
34069
|
+
}
|
|
34070
|
+
fd = null;
|
|
34071
|
+
offset = 0;
|
|
34072
|
+
decoder = new StringDecoder("utf8");
|
|
34073
|
+
timer = null;
|
|
34074
|
+
buf = Buffer.allocUnsafe(READ_CHUNK_BYTES);
|
|
34075
|
+
stopped = false;
|
|
34076
|
+
/** Begin polling for appended bytes. Idempotent. */
|
|
34077
|
+
start() {
|
|
34078
|
+
if (this.timer || this.stopped) return;
|
|
34079
|
+
this.timer = setInterval(() => this.poll(), this.pollMs);
|
|
34080
|
+
this.timer.unref?.();
|
|
34081
|
+
}
|
|
34082
|
+
ensureOpen() {
|
|
34083
|
+
if (this.fd !== null) return true;
|
|
34084
|
+
if (!existsSync7(this.path)) return false;
|
|
34085
|
+
try {
|
|
34086
|
+
this.fd = openSync2(this.path, "r");
|
|
34087
|
+
} catch {
|
|
34088
|
+
return false;
|
|
34089
|
+
}
|
|
34090
|
+
return true;
|
|
34091
|
+
}
|
|
34092
|
+
/** Read everything currently available from `offset` to EOF. */
|
|
34093
|
+
poll() {
|
|
34094
|
+
if (!this.ensureOpen()) return;
|
|
34095
|
+
let bytes;
|
|
34096
|
+
do {
|
|
34097
|
+
try {
|
|
34098
|
+
bytes = readSync(this.fd, this.buf, 0, this.buf.length, this.offset);
|
|
34099
|
+
} catch {
|
|
34100
|
+
return;
|
|
34101
|
+
}
|
|
34102
|
+
if (bytes > 0) {
|
|
34103
|
+
this.offset += bytes;
|
|
34104
|
+
const chunk = this.decoder.write(this.buf.subarray(0, bytes));
|
|
34105
|
+
if (chunk) this.onChunk(chunk);
|
|
34106
|
+
}
|
|
34107
|
+
} while (bytes === this.buf.length);
|
|
34108
|
+
}
|
|
34109
|
+
/**
|
|
34110
|
+
* Stop tailing: do one final drain to EOF, flush any partial multi-byte
|
|
34111
|
+
* remainder, and close the fd. Safe to call more than once. Synchronous so a
|
|
34112
|
+
* `close` handler can finalize the stream with no lost-tail race.
|
|
34113
|
+
*/
|
|
34114
|
+
stop() {
|
|
34115
|
+
if (this.stopped) return;
|
|
34116
|
+
this.stopped = true;
|
|
34117
|
+
if (this.timer) {
|
|
34118
|
+
clearInterval(this.timer);
|
|
34119
|
+
this.timer = null;
|
|
34120
|
+
}
|
|
34121
|
+
this.poll();
|
|
34122
|
+
const tail = this.decoder.end();
|
|
34123
|
+
if (tail) this.onChunk(tail);
|
|
34124
|
+
if (this.fd !== null) {
|
|
34125
|
+
try {
|
|
34126
|
+
closeSync2(this.fd);
|
|
34127
|
+
} catch {
|
|
34128
|
+
}
|
|
34129
|
+
this.fd = null;
|
|
34130
|
+
}
|
|
34131
|
+
}
|
|
34132
|
+
};
|
|
34133
|
+
|
|
33848
34134
|
// src/server/socket/cli-resolver.ts
|
|
33849
|
-
import { existsSync as
|
|
33850
|
-
import { dirname as
|
|
34135
|
+
import { existsSync as existsSync8 } from "node:fs";
|
|
34136
|
+
import { dirname as dirname6, join as join10 } from "node:path";
|
|
33851
34137
|
import { fileURLToPath } from "node:url";
|
|
33852
|
-
var __dirname =
|
|
34138
|
+
var __dirname = dirname6(fileURLToPath(import.meta.url));
|
|
33853
34139
|
function resolveLocalCli() {
|
|
33854
|
-
const parentDir =
|
|
33855
|
-
const bundledCli =
|
|
33856
|
-
if (
|
|
34140
|
+
const parentDir = join10(__dirname, "..");
|
|
34141
|
+
const bundledCli = join10(parentDir, "index.js");
|
|
34142
|
+
if (existsSync8(bundledCli) && existsSync8(join10(parentDir, "dashboard", "server.js"))) {
|
|
33857
34143
|
return bundledCli;
|
|
33858
34144
|
}
|
|
33859
34145
|
let dir = __dirname;
|
|
33860
34146
|
for (let i = 0; i < 8; i++) {
|
|
33861
|
-
if (
|
|
33862
|
-
const candidate =
|
|
33863
|
-
if (
|
|
34147
|
+
if (existsSync8(join10(dir, "nx.json"))) {
|
|
34148
|
+
const candidate = join10(dir, "packages", "cli", "dist", "index.js");
|
|
34149
|
+
if (existsSync8(candidate)) return candidate;
|
|
33864
34150
|
break;
|
|
33865
34151
|
}
|
|
33866
|
-
const parent =
|
|
34152
|
+
const parent = dirname6(dir);
|
|
33867
34153
|
if (parent === dir) break;
|
|
33868
34154
|
dir = parent;
|
|
33869
34155
|
}
|
|
33870
34156
|
return null;
|
|
33871
34157
|
}
|
|
33872
34158
|
|
|
34159
|
+
// ../cli/src/lib/state/projection.ts
|
|
34160
|
+
var REASON_EVENT_TYPES = [
|
|
34161
|
+
"session_aborted",
|
|
34162
|
+
"session_auto_closed_stale",
|
|
34163
|
+
"session_synced",
|
|
34164
|
+
"session_legacy_import"
|
|
34165
|
+
];
|
|
34166
|
+
var TERMINAL_EVENT_TYPES = /* @__PURE__ */ new Set([
|
|
34167
|
+
"session_closed",
|
|
34168
|
+
...REASON_EVENT_TYPES
|
|
34169
|
+
]);
|
|
34170
|
+
function hasCompletionInvariant(db, session) {
|
|
34171
|
+
const eventType = session.workflow_type === "map" ? "map_completed" : "round_completed";
|
|
34172
|
+
const round = session.workflow_type === "map" ? session.current_map_run : session.current_round;
|
|
34173
|
+
const r = db.exec(
|
|
34174
|
+
`SELECT 1 FROM orchestration_events
|
|
34175
|
+
WHERE session_id = ? AND event_type = ? AND round = ? LIMIT 1`,
|
|
34176
|
+
[session.id, eventType, round]
|
|
34177
|
+
);
|
|
34178
|
+
return (r[0]?.values.length ?? 0) > 0;
|
|
34179
|
+
}
|
|
34180
|
+
|
|
34181
|
+
// ../cli/src/lib/state/index.ts
|
|
34182
|
+
async function stateClose(params) {
|
|
34183
|
+
const { sessionId, ocrDir, abort } = params;
|
|
34184
|
+
const db = await ensureDatabase(ocrDir);
|
|
34185
|
+
const existing = getSession(db, sessionId);
|
|
34186
|
+
if (!existing) {
|
|
34187
|
+
throw new StateError(STATE_EXIT.NOT_FOUND, `Session not found: ${sessionId}`);
|
|
34188
|
+
}
|
|
34189
|
+
if (existing.status === "closed") {
|
|
34190
|
+
console.error(`[ocr] Session already closed: ${sessionId}`);
|
|
34191
|
+
return;
|
|
34192
|
+
}
|
|
34193
|
+
if (!abort && !hasCompletionInvariant(db, existing)) {
|
|
34194
|
+
const what = existing.workflow_type === "map" ? `map run ${existing.current_map_run} has no map_completed event` : `round ${existing.current_round} has no round_completed event`;
|
|
34195
|
+
throw new StateError(
|
|
34196
|
+
STATE_EXIT.INVARIANT_UNMET,
|
|
34197
|
+
`Cannot close session ${sessionId}: ${what}. Run 'ocr state complete-round' to finalize it, or pass --abort to record an abandoned session.`
|
|
34198
|
+
);
|
|
34199
|
+
}
|
|
34200
|
+
const note = "closed by parent workflow close";
|
|
34201
|
+
db.transaction(() => {
|
|
34202
|
+
if (abort) {
|
|
34203
|
+
insertEvent(db, {
|
|
34204
|
+
session_id: sessionId,
|
|
34205
|
+
event_type: "session_aborted",
|
|
34206
|
+
phase: existing.current_phase,
|
|
34207
|
+
phase_number: existing.phase_number,
|
|
34208
|
+
round: existing.current_round
|
|
34209
|
+
});
|
|
34210
|
+
}
|
|
34211
|
+
updateSession(db, sessionId, {
|
|
34212
|
+
status: "closed",
|
|
34213
|
+
current_phase: "complete"
|
|
34214
|
+
});
|
|
34215
|
+
if (!abort) {
|
|
34216
|
+
insertEvent(db, {
|
|
34217
|
+
session_id: sessionId,
|
|
34218
|
+
event_type: "session_closed",
|
|
34219
|
+
phase: "complete",
|
|
34220
|
+
phase_number: existing.phase_number,
|
|
34221
|
+
round: existing.current_round
|
|
34222
|
+
});
|
|
34223
|
+
}
|
|
34224
|
+
cascadeTerminateExecutions(db, sessionId, CASCADE_CLOSE_EXIT_CODE, note);
|
|
34225
|
+
});
|
|
34226
|
+
}
|
|
34227
|
+
async function reconcileWorkflowOnExit(ocrDir, sessionId, db) {
|
|
34228
|
+
db ??= await ensureDatabase(ocrDir);
|
|
34229
|
+
const existing = getSession(db, sessionId);
|
|
34230
|
+
if (!existing) return "not-found";
|
|
34231
|
+
if (existing.status === "closed") return "already-closed";
|
|
34232
|
+
if (!hasCompletionInvariant(db, existing)) return "incomplete";
|
|
34233
|
+
if (hasInFlightDependents(db, sessionId)) return "in-flight";
|
|
34234
|
+
await stateClose({ sessionId, ocrDir, abort: false });
|
|
34235
|
+
return "closed";
|
|
34236
|
+
}
|
|
34237
|
+
async function reconcileCompletedSessions(ocrDir) {
|
|
34238
|
+
const db = await ensureDatabase(ocrDir);
|
|
34239
|
+
const closed = [];
|
|
34240
|
+
for (const s of getAllSessions(db)) {
|
|
34241
|
+
if (s.status !== "active") continue;
|
|
34242
|
+
const outcome = await reconcileWorkflowOnExit(ocrDir, s.id, db);
|
|
34243
|
+
if (outcome === "closed") closed.push(s.id);
|
|
34244
|
+
}
|
|
34245
|
+
return closed;
|
|
34246
|
+
}
|
|
34247
|
+
|
|
34248
|
+
// ../cli/src/lib/runtime-config.ts
|
|
34249
|
+
import { existsSync as existsSync9, readFileSync as readFileSync4 } from "node:fs";
|
|
34250
|
+
import { join as join11 } from "node:path";
|
|
34251
|
+
var DEFAULT_AGENT_HEARTBEAT_SECONDS = 60;
|
|
34252
|
+
var DEFAULT_WORKFLOW_HARD_DEADLINE_MINUTES = 60;
|
|
34253
|
+
function readRuntimePositiveInt(ocrDir, key, defaultValue) {
|
|
34254
|
+
const configPath = join11(ocrDir, "config.yaml");
|
|
34255
|
+
if (!existsSync9(configPath)) return defaultValue;
|
|
34256
|
+
let content;
|
|
34257
|
+
try {
|
|
34258
|
+
content = readFileSync4(configPath, "utf-8");
|
|
34259
|
+
} catch {
|
|
34260
|
+
return defaultValue;
|
|
34261
|
+
}
|
|
34262
|
+
const blockMatch = content.match(
|
|
34263
|
+
new RegExp(
|
|
34264
|
+
String.raw`^runtime:\s*\n(?:\s+[^\n]*\n)*?\s+${key}:\s*([^\s#\n]+)`,
|
|
34265
|
+
"m"
|
|
34266
|
+
)
|
|
34267
|
+
);
|
|
34268
|
+
const inlineMatch = content.match(
|
|
34269
|
+
new RegExp(String.raw`^runtime:\s*\{[^}]*\b${key}:\s*([^\s,}]+)`, "m")
|
|
34270
|
+
);
|
|
34271
|
+
const raw = blockMatch?.[1] ?? inlineMatch?.[1];
|
|
34272
|
+
if (!raw) return defaultValue;
|
|
34273
|
+
const parsed = Number(raw);
|
|
34274
|
+
if (!Number.isFinite(parsed) || parsed <= 0 || !Number.isInteger(parsed)) {
|
|
34275
|
+
process.stderr.write(
|
|
34276
|
+
`[ocr] runtime.${key} is not a positive integer (got "${raw}"); falling back to ${defaultValue}.
|
|
34277
|
+
`
|
|
34278
|
+
);
|
|
34279
|
+
return defaultValue;
|
|
34280
|
+
}
|
|
34281
|
+
return parsed;
|
|
34282
|
+
}
|
|
34283
|
+
function getAgentHeartbeatSeconds(ocrDir) {
|
|
34284
|
+
return readRuntimePositiveInt(
|
|
34285
|
+
ocrDir,
|
|
34286
|
+
"agent_heartbeat_seconds",
|
|
34287
|
+
DEFAULT_AGENT_HEARTBEAT_SECONDS
|
|
34288
|
+
);
|
|
34289
|
+
}
|
|
34290
|
+
function getWorkflowHardDeadlineMs(ocrDir) {
|
|
34291
|
+
return readRuntimePositiveInt(
|
|
34292
|
+
ocrDir,
|
|
34293
|
+
"workflow_hard_deadline_minutes",
|
|
34294
|
+
DEFAULT_WORKFLOW_HARD_DEADLINE_MINUTES
|
|
34295
|
+
) * 60 * 1e3;
|
|
34296
|
+
}
|
|
34297
|
+
|
|
33873
34298
|
// src/server/socket/command-runner.ts
|
|
33874
34299
|
function shellSplit(str) {
|
|
33875
34300
|
const tokens = [];
|
|
@@ -33903,7 +34328,7 @@ var ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
33903
34328
|
]);
|
|
33904
34329
|
var AI_COMMANDS = /* @__PURE__ */ new Set(["map", "review", "translate-review-to-single-human", "address", "create-reviewer", "sync-reviewers"]);
|
|
33905
34330
|
function escapeUserHeaders(value) {
|
|
33906
|
-
return value.replace(/^([ \t]{0,3})(#+)/gm, "$1\\$2").replace(/^([ \t]{0,3})(#+)/gm, "$1\\$2").replace(/^([ \t]{0,3})(={3,}|-{3,})\s*$/gm, "$1\\$2").replace(/^([ \t]{0,3})(```+)/gm, "$1\\$2");
|
|
34331
|
+
return value.normalize("NFKC").replace(/[\u2028\u2029]/g, "\n").replace(/\p{Cf}/gu, "").replace(/^([ \t]{0,3})(#+)/gm, "$1\\$2").replace(/^([ \t]{0,3})(#+)/gm, "$1\\$2").replace(/^([ \t]{0,3})(={3,}|-{3,})\s*$/gm, "$1\\$2").replace(/^([ \t]{0,3})(```+)/gm, "$1\\$2");
|
|
33907
34332
|
}
|
|
33908
34333
|
function buildPrompt(opts) {
|
|
33909
34334
|
const { baseCommand, subArgs, commandContent, executionUid, localCli } = opts;
|
|
@@ -34050,23 +34475,45 @@ function extractPerInstanceModels(subArgs) {
|
|
|
34050
34475
|
return [...models];
|
|
34051
34476
|
}
|
|
34052
34477
|
var MAX_CONCURRENT = 3;
|
|
34478
|
+
var WATCHDOG_TICK_MS = 1e4;
|
|
34479
|
+
var POST_RESULT_GRACE_MS = 3e4;
|
|
34480
|
+
var HEARTBEAT_THROTTLE_MS = 5e3;
|
|
34481
|
+
function decideWatchdogTick(i) {
|
|
34482
|
+
if (i.resultSeenAt !== void 0 && i.nowMs - i.resultSeenAt > i.postResultGraceMs) {
|
|
34483
|
+
return {
|
|
34484
|
+
action: "finalize",
|
|
34485
|
+
reap: !i.exited,
|
|
34486
|
+
exitCode: i.resultIsError ? 1 : 0,
|
|
34487
|
+
reason: "result-grace"
|
|
34488
|
+
};
|
|
34489
|
+
}
|
|
34490
|
+
if (i.nowMs - i.startedAtMs > i.hardDeadlineMs) {
|
|
34491
|
+
return {
|
|
34492
|
+
action: "finalize",
|
|
34493
|
+
reap: !i.exited,
|
|
34494
|
+
exitCode: WATCHDOG_DEADLINE_EXIT_CODE,
|
|
34495
|
+
reason: "hard-deadline"
|
|
34496
|
+
};
|
|
34497
|
+
}
|
|
34498
|
+
return i.exited ? { action: "wait" } : { action: "beat" };
|
|
34499
|
+
}
|
|
34053
34500
|
var activeCommands = /* @__PURE__ */ new Map();
|
|
34054
34501
|
function spawnMarkerPath(ocrDir) {
|
|
34055
|
-
return
|
|
34502
|
+
return join12(ocrDir, "data", "dashboard-active-spawn.json");
|
|
34056
34503
|
}
|
|
34057
34504
|
function writeSpawnMarker(ocrDir, executionUid, pid) {
|
|
34058
|
-
const dataDir =
|
|
34059
|
-
if (!
|
|
34505
|
+
const dataDir = join12(ocrDir, "data");
|
|
34506
|
+
if (!existsSync10(dataDir)) mkdirSync5(dataDir, { recursive: true });
|
|
34060
34507
|
const payload = JSON.stringify({
|
|
34061
34508
|
execution_uid: executionUid,
|
|
34062
34509
|
pid,
|
|
34063
34510
|
started_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
34064
34511
|
});
|
|
34065
|
-
|
|
34512
|
+
writeFileSync3(spawnMarkerPath(ocrDir), payload, { mode: 384 });
|
|
34066
34513
|
}
|
|
34067
34514
|
function clearSpawnMarker(ocrDir) {
|
|
34068
34515
|
try {
|
|
34069
|
-
|
|
34516
|
+
unlinkSync3(spawnMarkerPath(ocrDir));
|
|
34070
34517
|
} catch {
|
|
34071
34518
|
}
|
|
34072
34519
|
}
|
|
@@ -34193,25 +34640,14 @@ function registerCommandHandlers(io2, socket, db, ocrDir, aiCliService, sessionC
|
|
|
34193
34640
|
if (!proc) return;
|
|
34194
34641
|
const pid = proc.pid;
|
|
34195
34642
|
if (entry.detached && pid) {
|
|
34196
|
-
|
|
34197
|
-
process.kill(-pid, "SIGTERM");
|
|
34198
|
-
} catch {
|
|
34199
|
-
proc.kill("SIGTERM");
|
|
34200
|
-
}
|
|
34643
|
+
reapTree(pid);
|
|
34201
34644
|
} else {
|
|
34202
34645
|
proc.kill("SIGTERM");
|
|
34646
|
+
const killTimer = setTimeout(() => {
|
|
34647
|
+
if (activeCommands.has(targetId)) proc.kill("SIGKILL");
|
|
34648
|
+
}, 5e3);
|
|
34649
|
+
proc.once("close", () => clearTimeout(killTimer));
|
|
34203
34650
|
}
|
|
34204
|
-
const killTimer = setTimeout(() => {
|
|
34205
|
-
if (!activeCommands.has(targetId)) return;
|
|
34206
|
-
if (entry.detached && pid) {
|
|
34207
|
-
try {
|
|
34208
|
-
process.kill(-pid, "SIGKILL");
|
|
34209
|
-
} catch {
|
|
34210
|
-
}
|
|
34211
|
-
}
|
|
34212
|
-
proc.kill("SIGKILL");
|
|
34213
|
-
}, 5e3);
|
|
34214
|
-
proc.once("close", () => clearTimeout(killTimer));
|
|
34215
34651
|
} catch (err) {
|
|
34216
34652
|
console.error("Error in command:cancel handler:", err);
|
|
34217
34653
|
socket.emit("error", { message: "Internal error" });
|
|
@@ -34220,7 +34656,7 @@ function registerCommandHandlers(io2, socket, db, ocrDir, aiCliService, sessionC
|
|
|
34220
34656
|
}
|
|
34221
34657
|
function spawnCliCommand(io2, db, ocrDir, executionId, baseCommand, subArgs, entry) {
|
|
34222
34658
|
const localCli = resolveLocalCli();
|
|
34223
|
-
const repoRoot =
|
|
34659
|
+
const repoRoot = dirname7(ocrDir);
|
|
34224
34660
|
const proc = localCli ? spawnBinary("node", [localCli, baseCommand, ...subArgs], {
|
|
34225
34661
|
cwd: repoRoot,
|
|
34226
34662
|
env: cleanEnv()
|
|
@@ -34246,8 +34682,7 @@ function spawnCliCommand(io2, db, ocrDir, executionId, baseCommand, subArgs, ent
|
|
|
34246
34682
|
io2.emit("command:output", { execution_id: executionId, content: chunk });
|
|
34247
34683
|
});
|
|
34248
34684
|
proc.on("close", (code) => {
|
|
34249
|
-
|
|
34250
|
-
finishExecution(io2, db, ocrDir, executionId, finalCode, entry.outputBuffer);
|
|
34685
|
+
finishExecution(io2, db, ocrDir, executionId, code ?? -1, entry.outputBuffer);
|
|
34251
34686
|
});
|
|
34252
34687
|
proc.on("error", (err) => {
|
|
34253
34688
|
entry.outputBuffer += `Process error: ${err.message}`;
|
|
@@ -34270,10 +34705,10 @@ function spawnAiCommand(io2, _socket, db, ocrDir, executionId, baseCommand, subA
|
|
|
34270
34705
|
io2.emit("command:output", { execution_id: executionId, content: warning });
|
|
34271
34706
|
}
|
|
34272
34707
|
}
|
|
34273
|
-
const commandMdPath =
|
|
34708
|
+
const commandMdPath = join12(ocrDir, "commands", `${baseCommand}.md`);
|
|
34274
34709
|
let commandContent;
|
|
34275
34710
|
try {
|
|
34276
|
-
commandContent =
|
|
34711
|
+
commandContent = readFileSync5(commandMdPath, "utf-8");
|
|
34277
34712
|
} catch {
|
|
34278
34713
|
const content = `Error: Could not read command file at ${commandMdPath}
|
|
34279
34714
|
`;
|
|
@@ -34317,7 +34752,20 @@ function spawnAiCommand(io2, _socket, db, ocrDir, executionId, baseCommand, subA
|
|
|
34317
34752
|
console.error("Failed to resolve resume context:", err);
|
|
34318
34753
|
}
|
|
34319
34754
|
}
|
|
34320
|
-
const repoRoot =
|
|
34755
|
+
const repoRoot = dirname7(ocrDir);
|
|
34756
|
+
let logFile;
|
|
34757
|
+
if (entry.uid) {
|
|
34758
|
+
try {
|
|
34759
|
+
const logDir = join12(ocrDir, "data", "exec-logs");
|
|
34760
|
+
mkdirSync5(logDir, { recursive: true });
|
|
34761
|
+
logFile = join12(logDir, `${entry.uid}.log`);
|
|
34762
|
+
} catch (err) {
|
|
34763
|
+
console.error(
|
|
34764
|
+
"[command-runner] could not prepare exec-log dir \u2014 falling back to pipe stdio (degraded: close may be withheld by a leaked grandchild; watchdog deadlines still finalize):",
|
|
34765
|
+
err
|
|
34766
|
+
);
|
|
34767
|
+
}
|
|
34768
|
+
}
|
|
34321
34769
|
const spawnOpts = {
|
|
34322
34770
|
mode: "workflow",
|
|
34323
34771
|
prompt,
|
|
@@ -34327,7 +34775,10 @@ function spawnAiCommand(io2, _socket, db, ocrDir, executionId, baseCommand, subA
|
|
|
34327
34775
|
if (resumeSessionId) {
|
|
34328
34776
|
spawnOpts.resumeSessionId = resumeSessionId;
|
|
34329
34777
|
}
|
|
34330
|
-
|
|
34778
|
+
if (logFile) {
|
|
34779
|
+
spawnOpts.logFile = logFile;
|
|
34780
|
+
}
|
|
34781
|
+
const { process: proc, detached, logPath } = adapter.spawn(spawnOpts);
|
|
34331
34782
|
entry.process = proc;
|
|
34332
34783
|
entry.detached = detached;
|
|
34333
34784
|
if (proc.pid) {
|
|
@@ -34363,6 +34814,61 @@ function spawnAiCommand(io2, _socket, db, ocrDir, executionId, baseCommand, subA
|
|
|
34363
34814
|
}
|
|
34364
34815
|
}, POLL_INTERVAL_MS);
|
|
34365
34816
|
entry.linkPoll = linkPoll;
|
|
34817
|
+
const bumpHeartbeat = () => {
|
|
34818
|
+
if (entry.finalized) return;
|
|
34819
|
+
const now = Date.now();
|
|
34820
|
+
if (now - (entry.lastBeatWrite ?? 0) < HEARTBEAT_THROTTLE_MS) return;
|
|
34821
|
+
entry.lastBeatWrite = now;
|
|
34822
|
+
try {
|
|
34823
|
+
db.run(
|
|
34824
|
+
`UPDATE command_executions SET last_heartbeat_at = datetime('now') WHERE id = ? AND finished_at IS NULL`,
|
|
34825
|
+
[executionId]
|
|
34826
|
+
);
|
|
34827
|
+
} catch (err) {
|
|
34828
|
+
console.error("[command-runner] heartbeat bump failed:", err);
|
|
34829
|
+
}
|
|
34830
|
+
};
|
|
34831
|
+
const hardDeadlineMs = getWorkflowHardDeadlineMs(ocrDir);
|
|
34832
|
+
entry.watchdog = setInterval(() => {
|
|
34833
|
+
if (entry.finalized) return;
|
|
34834
|
+
const child = entry.process;
|
|
34835
|
+
const pid = child?.pid;
|
|
34836
|
+
if (!child || !pid) return;
|
|
34837
|
+
const exited = child.exitCode !== null || child.signalCode !== null;
|
|
34838
|
+
const decision = decideWatchdogTick({
|
|
34839
|
+
exited,
|
|
34840
|
+
resultSeenAt: entry.resultSeenAt,
|
|
34841
|
+
resultIsError: entry.resultIsError,
|
|
34842
|
+
startedAtMs: Date.parse(entry.startedAt),
|
|
34843
|
+
nowMs: Date.now(),
|
|
34844
|
+
postResultGraceMs: POST_RESULT_GRACE_MS,
|
|
34845
|
+
hardDeadlineMs
|
|
34846
|
+
});
|
|
34847
|
+
switch (decision.action) {
|
|
34848
|
+
case "beat":
|
|
34849
|
+
bumpHeartbeat();
|
|
34850
|
+
return;
|
|
34851
|
+
case "wait":
|
|
34852
|
+
return;
|
|
34853
|
+
case "finalize": {
|
|
34854
|
+
if (decision.reason === "hard-deadline") {
|
|
34855
|
+
const minutes = Math.round(hardDeadlineMs / 6e4);
|
|
34856
|
+
console.warn(`[watchdog] execution ${executionId}: exceeded hard deadline (${minutes}m) \u2014 finalizing${decision.reap ? " + reaping tree" : ""}`);
|
|
34857
|
+
const notice = `
|
|
34858
|
+
[watchdog] Reaped after exceeding the ${minutes}-minute hard deadline. Raise runtime.workflow_hard_deadline_minutes in .ocr/config.yaml for large reviewer fleets.
|
|
34859
|
+
`;
|
|
34860
|
+
entry.outputBuffer += notice;
|
|
34861
|
+
io2.emit("command:output", { execution_id: executionId, content: notice });
|
|
34862
|
+
} else {
|
|
34863
|
+
console.warn(`[watchdog] execution ${executionId}: result seen but no close after grace \u2014 finalizing${decision.reap ? " + reaping tree" : ""}`);
|
|
34864
|
+
}
|
|
34865
|
+
if (decision.reap) reapTree(pid);
|
|
34866
|
+
finishExecution(io2, db, ocrDir, executionId, decision.exitCode, entry.outputBuffer);
|
|
34867
|
+
return;
|
|
34868
|
+
}
|
|
34869
|
+
}
|
|
34870
|
+
}, WATCHDOG_TICK_MS);
|
|
34871
|
+
entry.watchdog.unref();
|
|
34366
34872
|
io2.emit("command:output", {
|
|
34367
34873
|
execution_id: executionId,
|
|
34368
34874
|
content: `\u25B8 Starting OCR ${baseCommand} workflow...
|
|
@@ -34427,11 +34933,16 @@ function spawnAiCommand(io2, _socket, db, ocrDir, executionId, baseCommand, subA
|
|
|
34427
34933
|
emitStreamEvent(evt);
|
|
34428
34934
|
break;
|
|
34429
34935
|
}
|
|
34936
|
+
case "result": {
|
|
34937
|
+
entry.resultSeenAt = Date.now();
|
|
34938
|
+
entry.resultIsError = evt.isError;
|
|
34939
|
+
emitStreamEvent(evt);
|
|
34940
|
+
break;
|
|
34941
|
+
}
|
|
34430
34942
|
}
|
|
34431
34943
|
}
|
|
34432
|
-
|
|
34433
|
-
|
|
34434
|
-
proc.stdout?.on("data", (chunk) => {
|
|
34944
|
+
function onOutputChunk(chunk) {
|
|
34945
|
+
bumpHeartbeat();
|
|
34435
34946
|
lineBuffer += chunk;
|
|
34436
34947
|
const lines = lineBuffer.split("\n");
|
|
34437
34948
|
lineBuffer = lines.pop() ?? "";
|
|
@@ -34446,17 +34957,30 @@ function spawnAiCommand(io2, _socket, db, ocrDir, executionId, baseCommand, subA
|
|
|
34446
34957
|
handleEvent(evt);
|
|
34447
34958
|
}
|
|
34448
34959
|
}
|
|
34449
|
-
}
|
|
34960
|
+
}
|
|
34450
34961
|
let stderrBuffer = "";
|
|
34451
|
-
|
|
34452
|
-
|
|
34453
|
-
|
|
34962
|
+
if (logPath) {
|
|
34963
|
+
const tailer = new FileTailer(logPath, onOutputChunk);
|
|
34964
|
+
tailer.start();
|
|
34965
|
+
entry.tailer = tailer;
|
|
34966
|
+
} else {
|
|
34967
|
+
proc.stdout?.setEncoding("utf-8");
|
|
34968
|
+
proc.stderr?.setEncoding("utf-8");
|
|
34969
|
+
proc.stdout?.on("data", onOutputChunk);
|
|
34970
|
+
proc.stderr?.on("data", (chunk) => {
|
|
34971
|
+
stderrBuffer += chunk;
|
|
34972
|
+
});
|
|
34973
|
+
}
|
|
34454
34974
|
proc.on("close", (code) => {
|
|
34455
34975
|
if (entry.linkPoll) {
|
|
34456
34976
|
clearInterval(entry.linkPoll);
|
|
34457
34977
|
entry.linkPoll = void 0;
|
|
34458
34978
|
}
|
|
34459
34979
|
clearSpawnMarker(ocrDir);
|
|
34980
|
+
if (entry.tailer) {
|
|
34981
|
+
entry.tailer.stop();
|
|
34982
|
+
entry.tailer = void 0;
|
|
34983
|
+
}
|
|
34460
34984
|
if (lineBuffer.trim()) {
|
|
34461
34985
|
const events = parser.parseLine(lineBuffer);
|
|
34462
34986
|
for (const evt of events) {
|
|
@@ -34480,8 +35004,7 @@ ${stderrBuffer}`;
|
|
|
34480
35004
|
journal.close().catch((err) => {
|
|
34481
35005
|
console.error("[event-journal] close failed:", err);
|
|
34482
35006
|
});
|
|
34483
|
-
|
|
34484
|
-
finishExecution(io2, db, ocrDir, executionId, finalCode, entry.outputBuffer);
|
|
35007
|
+
finishExecution(io2, db, ocrDir, executionId, code ?? -1, entry.outputBuffer);
|
|
34485
35008
|
});
|
|
34486
35009
|
proc.on("error", (err) => {
|
|
34487
35010
|
if (entry.linkPoll) {
|
|
@@ -34495,15 +35018,28 @@ ${stderrBuffer}`;
|
|
|
34495
35018
|
finishExecution(io2, db, ocrDir, executionId, -1, entry.outputBuffer);
|
|
34496
35019
|
});
|
|
34497
35020
|
}
|
|
34498
|
-
function finishExecution(io2, db, ocrDir, executionId,
|
|
35021
|
+
function finishExecution(io2, db, ocrDir, executionId, rawCode, output) {
|
|
34499
35022
|
const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
34500
35023
|
const entry = activeCommands.get(executionId);
|
|
34501
|
-
|
|
35024
|
+
const code = entry?.cancelled ? CANCELLED_EXIT_CODE : rawCode;
|
|
35025
|
+
if (entry?.finalized) return;
|
|
35026
|
+
if (entry) {
|
|
35027
|
+
entry.finalized = true;
|
|
35028
|
+
if (entry.watchdog) {
|
|
35029
|
+
clearInterval(entry.watchdog);
|
|
35030
|
+
entry.watchdog = void 0;
|
|
35031
|
+
}
|
|
35032
|
+
if (entry.tailer) {
|
|
35033
|
+
entry.tailer.stop();
|
|
35034
|
+
entry.tailer = void 0;
|
|
35035
|
+
}
|
|
35036
|
+
}
|
|
35037
|
+
const res = db.prepare(
|
|
34502
35038
|
`UPDATE command_executions
|
|
34503
|
-
|
|
34504
|
-
|
|
34505
|
-
|
|
34506
|
-
);
|
|
35039
|
+
SET exit_code = ?, finished_at = ?, output = ?, pid = NULL
|
|
35040
|
+
WHERE id = ? AND finished_at IS NULL`
|
|
35041
|
+
).run(code, finishedAt, output, executionId);
|
|
35042
|
+
if (Number(res.changes) === 0 && !entry) return;
|
|
34507
35043
|
const completeness = getWorkflowCompletenessForExecution(db, executionId);
|
|
34508
35044
|
const outcome = deriveCommandOutcome(code, completeness);
|
|
34509
35045
|
const cancellationReason = deriveCancellationReason(code);
|
|
@@ -34518,7 +35054,7 @@ function finishExecution(io2, db, ocrDir, executionId, code, output) {
|
|
|
34518
35054
|
started_at: entry.startedAt,
|
|
34519
35055
|
finished_at: finishedAt,
|
|
34520
35056
|
is_detached: entry.detached ? 1 : 0,
|
|
34521
|
-
event: code ===
|
|
35057
|
+
event: code === CANCELLED_EXIT_CODE ? "cancel" : "finish",
|
|
34522
35058
|
writer: "dashboard"
|
|
34523
35059
|
});
|
|
34524
35060
|
}
|
|
@@ -34530,6 +35066,27 @@ function finishExecution(io2, db, ocrDir, executionId, code, output) {
|
|
|
34530
35066
|
cancellation_reason: cancellationReason
|
|
34531
35067
|
});
|
|
34532
35068
|
activeCommands.delete(executionId);
|
|
35069
|
+
const workflowRow = db.exec(
|
|
35070
|
+
"SELECT workflow_id FROM command_executions WHERE id = ?",
|
|
35071
|
+
[executionId]
|
|
35072
|
+
);
|
|
35073
|
+
const workflowId = workflowRow[0]?.values[0]?.[0];
|
|
35074
|
+
if (typeof workflowId === "string" && workflowId.length > 0) {
|
|
35075
|
+
void reconcileWorkflowOnExit(ocrDir, workflowId, db).then((outcome2) => {
|
|
35076
|
+
if (outcome2 === "closed") {
|
|
35077
|
+
console.log(`[command-runner] auto-finalized workflow ${workflowId}`);
|
|
35078
|
+
} else if (outcome2 === "incomplete" || outcome2 === "in-flight") {
|
|
35079
|
+
console.debug(
|
|
35080
|
+
`[command-runner] workflow ${workflowId} not finalized: ${outcome2}`
|
|
35081
|
+
);
|
|
35082
|
+
}
|
|
35083
|
+
}).catch((err) => {
|
|
35084
|
+
console.error(
|
|
35085
|
+
`[command-runner] reconcileWorkflowOnExit(${workflowId}) failed:`,
|
|
35086
|
+
err instanceof Error ? err.message : err
|
|
35087
|
+
);
|
|
35088
|
+
});
|
|
35089
|
+
}
|
|
34533
35090
|
}
|
|
34534
35091
|
|
|
34535
35092
|
// src/server/routes/commands.ts
|
|
@@ -34617,8 +35174,8 @@ function createCommandsRouter(db, ocrDir) {
|
|
|
34617
35174
|
|
|
34618
35175
|
// src/server/routes/config.ts
|
|
34619
35176
|
var import_express9 = __toESM(require_express2(), 1);
|
|
34620
|
-
import { readFileSync as
|
|
34621
|
-
import { join as
|
|
35177
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "node:fs";
|
|
35178
|
+
import { join as join13, dirname as dirname8, basename as basename2 } from "node:path";
|
|
34622
35179
|
var VALID_IDES = ["vscode", "cursor", "windsurf", "jetbrains", "sublime"];
|
|
34623
35180
|
function detectIde() {
|
|
34624
35181
|
const termProgram = process.env.TERM_PROGRAM?.toLowerCase() ?? "";
|
|
@@ -34635,8 +35192,8 @@ function detectIde() {
|
|
|
34635
35192
|
}
|
|
34636
35193
|
function readIdeFromConfig(ocrDir) {
|
|
34637
35194
|
try {
|
|
34638
|
-
const configPath =
|
|
34639
|
-
const content =
|
|
35195
|
+
const configPath = join13(ocrDir, "config.yaml");
|
|
35196
|
+
const content = readFileSync6(configPath, "utf-8");
|
|
34640
35197
|
const match = content.match(/^\s*ide:\s*(\S+)/m);
|
|
34641
35198
|
return match?.[1] ?? "auto";
|
|
34642
35199
|
} catch {
|
|
@@ -34663,8 +35220,8 @@ function detectGitBranch(cwd) {
|
|
|
34663
35220
|
}
|
|
34664
35221
|
function createConfigRouter(ocrDir, aiCliService) {
|
|
34665
35222
|
const router = (0, import_express9.Router)();
|
|
34666
|
-
const projectRoot =
|
|
34667
|
-
const workspaceName =
|
|
35223
|
+
const projectRoot = dirname8(ocrDir);
|
|
35224
|
+
const workspaceName = basename2(projectRoot);
|
|
34668
35225
|
const gitBranch = detectGitBranch(projectRoot);
|
|
34669
35226
|
router.get("/", (_req, res) => {
|
|
34670
35227
|
res.json({
|
|
@@ -34682,8 +35239,8 @@ function createConfigRouter(ocrDir, aiCliService) {
|
|
|
34682
35239
|
return;
|
|
34683
35240
|
}
|
|
34684
35241
|
try {
|
|
34685
|
-
const configPath =
|
|
34686
|
-
let content =
|
|
35242
|
+
const configPath = join13(ocrDir, "config.yaml");
|
|
35243
|
+
let content = readFileSync6(configPath, "utf-8");
|
|
34687
35244
|
if (content.match(/^\s*ide:\s*\S+/m)) {
|
|
34688
35245
|
content = content.replace(/^(\s*ide:\s*)\S+/m, `$1${ide}`);
|
|
34689
35246
|
} else if (content.includes("dashboard:")) {
|
|
@@ -34695,7 +35252,7 @@ dashboard:
|
|
|
34695
35252
|
ide: ${ide}
|
|
34696
35253
|
`;
|
|
34697
35254
|
}
|
|
34698
|
-
|
|
35255
|
+
writeFileSync4(configPath, content);
|
|
34699
35256
|
res.json({ ide });
|
|
34700
35257
|
} catch (err) {
|
|
34701
35258
|
console.error("Failed to update config:", err);
|
|
@@ -34771,17 +35328,20 @@ function createChatRouter(db) {
|
|
|
34771
35328
|
|
|
34772
35329
|
// src/server/routes/reviewers.ts
|
|
34773
35330
|
var import_express11 = __toESM(require_express2(), 1);
|
|
34774
|
-
import { readFileSync as
|
|
34775
|
-
import { join as
|
|
35331
|
+
import { readFileSync as readFileSync7, existsSync as existsSync11, watch } from "node:fs";
|
|
35332
|
+
import { join as join14 } from "node:path";
|
|
34776
35333
|
function readReviewersMeta(ocrDir) {
|
|
34777
|
-
const metaPath =
|
|
34778
|
-
if (!
|
|
35334
|
+
const metaPath = join14(ocrDir, "reviewers-meta.json");
|
|
35335
|
+
if (!existsSync11(metaPath)) {
|
|
34779
35336
|
return { reviewers: [], defaults: [] };
|
|
34780
35337
|
}
|
|
34781
35338
|
try {
|
|
34782
|
-
const raw =
|
|
35339
|
+
const raw = readFileSync7(metaPath, "utf-8");
|
|
34783
35340
|
const meta = JSON.parse(raw);
|
|
34784
|
-
const reviewers = meta.reviewers ?? []
|
|
35341
|
+
const reviewers = (meta.reviewers ?? []).map((r) => ({
|
|
35342
|
+
...r,
|
|
35343
|
+
icon: r.icon || defaultIconFor(r.id, r.tier)
|
|
35344
|
+
}));
|
|
34785
35345
|
const defaults = reviewers.filter((r) => r.is_default).map((r) => r.id);
|
|
34786
35346
|
return { reviewers, defaults };
|
|
34787
35347
|
} catch {
|
|
@@ -34800,13 +35360,13 @@ function createReviewersRouter(ocrDir) {
|
|
|
34800
35360
|
res.status(400).json({ error: "Invalid reviewer ID" });
|
|
34801
35361
|
return;
|
|
34802
35362
|
}
|
|
34803
|
-
const filePath =
|
|
34804
|
-
if (!
|
|
35363
|
+
const filePath = join14(ocrDir, "skills", "references", "reviewers", `${id}.md`);
|
|
35364
|
+
if (!existsSync11(filePath)) {
|
|
34805
35365
|
res.status(404).json({ error: "Reviewer not found", id });
|
|
34806
35366
|
return;
|
|
34807
35367
|
}
|
|
34808
35368
|
try {
|
|
34809
|
-
const content =
|
|
35369
|
+
const content = readFileSync7(filePath, "utf-8");
|
|
34810
35370
|
res.json({ id, content });
|
|
34811
35371
|
} catch {
|
|
34812
35372
|
res.status(500).json({ error: "Failed to read reviewer file", id });
|
|
@@ -34815,7 +35375,7 @@ function createReviewersRouter(ocrDir) {
|
|
|
34815
35375
|
return router;
|
|
34816
35376
|
}
|
|
34817
35377
|
function watchReviewersMeta(ocrDir, io2) {
|
|
34818
|
-
const metaPath =
|
|
35378
|
+
const metaPath = join14(ocrDir, "reviewers-meta.json");
|
|
34819
35379
|
let watcher = null;
|
|
34820
35380
|
let debounce;
|
|
34821
35381
|
try {
|
|
@@ -34861,11 +35421,11 @@ function createAgentSessionsRouter(db, syncFromDisk = () => {
|
|
|
34861
35421
|
|
|
34862
35422
|
// src/server/routes/handoff.ts
|
|
34863
35423
|
var import_express13 = __toESM(require_express2(), 1);
|
|
34864
|
-
import { dirname as
|
|
35424
|
+
import { dirname as dirname9 } from "node:path";
|
|
34865
35425
|
function createHandoffRouter(sessionCapture, ocrDir, syncFromDisk = () => {
|
|
34866
35426
|
}) {
|
|
34867
35427
|
const router = (0, import_express13.Router)();
|
|
34868
|
-
const projectDir =
|
|
35428
|
+
const projectDir = dirname9(ocrDir);
|
|
34869
35429
|
router.get("/:id/handoff", (req, res) => {
|
|
34870
35430
|
const workflowId = req.params["id"];
|
|
34871
35431
|
if (!workflowId) {
|
|
@@ -34895,14 +35455,14 @@ import { spawnSync } from "node:child_process";
|
|
|
34895
35455
|
|
|
34896
35456
|
// ../cli/src/lib/team-config.ts
|
|
34897
35457
|
var import_yaml = __toESM(require_dist(), 1);
|
|
34898
|
-
import { existsSync as
|
|
34899
|
-
import { join as
|
|
35458
|
+
import { existsSync as existsSync12, readFileSync as readFileSync8 } from "node:fs";
|
|
35459
|
+
import { join as join15 } from "node:path";
|
|
34900
35460
|
function loadTeamConfig(ocrDir) {
|
|
34901
|
-
const configPath =
|
|
34902
|
-
if (!
|
|
35461
|
+
const configPath = join15(ocrDir, "config.yaml");
|
|
35462
|
+
if (!existsSync12(configPath)) {
|
|
34903
35463
|
return { team: [], aliases: {}, defaultModel: null };
|
|
34904
35464
|
}
|
|
34905
|
-
const content =
|
|
35465
|
+
const content = readFileSync8(configPath, "utf-8");
|
|
34906
35466
|
return parseTeamConfigYaml(content);
|
|
34907
35467
|
}
|
|
34908
35468
|
function parseTeamConfigYaml(content) {
|
|
@@ -35065,23 +35625,62 @@ function resolveTeamComposition(team, override) {
|
|
|
35065
35625
|
}
|
|
35066
35626
|
|
|
35067
35627
|
// ../cli/src/lib/models.ts
|
|
35068
|
-
|
|
35069
|
-
|
|
35070
|
-
|
|
35071
|
-
|
|
35072
|
-
];
|
|
35073
|
-
|
|
35074
|
-
|
|
35075
|
-
|
|
35076
|
-
|
|
35077
|
-
|
|
35078
|
-
|
|
35079
|
-
|
|
35628
|
+
function parseOpenCodeModelList(stdout) {
|
|
35629
|
+
const models = [];
|
|
35630
|
+
for (const rawLine of stdout.split(/\r?\n/)) {
|
|
35631
|
+
const line = rawLine.trim();
|
|
35632
|
+
if (!/^[^\s:]+\/\S+$/.test(line)) continue;
|
|
35633
|
+
const provider = line.slice(0, line.indexOf("/"));
|
|
35634
|
+
models.push({ id: line, provider });
|
|
35635
|
+
}
|
|
35636
|
+
return models.length > 0 ? models : null;
|
|
35637
|
+
}
|
|
35638
|
+
var VENDOR_MODEL_STRATEGIES = {
|
|
35639
|
+
claude: {
|
|
35640
|
+
displayName: "Claude Code",
|
|
35641
|
+
native: {
|
|
35642
|
+
// Verified against Claude Code 2.1.x: the CLI has no model-listing
|
|
35643
|
+
// subcommand (`claude models --json` → "unknown option"). Revisit if
|
|
35644
|
+
// a future release adds one.
|
|
35645
|
+
unavailableReason: "Claude Code does not provide a model-listing command; showing its documented model aliases instead"
|
|
35646
|
+
},
|
|
35647
|
+
// Vendor-documented aliases that always track the latest generation —
|
|
35648
|
+
// dated ids here would go stale by construction (the exact bug class of
|
|
35649
|
+
// issue #39). Pinned dated ids remain available via free-text entry.
|
|
35650
|
+
bundled: [
|
|
35651
|
+
{ id: "opus", displayName: "Claude Opus (latest)" },
|
|
35652
|
+
{ id: "sonnet", displayName: "Claude Sonnet (latest)" },
|
|
35653
|
+
{ id: "haiku", displayName: "Claude Haiku (latest)" }
|
|
35654
|
+
]
|
|
35655
|
+
},
|
|
35656
|
+
opencode: {
|
|
35657
|
+
displayName: "OpenCode",
|
|
35658
|
+
native: {
|
|
35659
|
+
// Plain `opencode models` — newline-delimited ids. (`--json` is not a
|
|
35660
|
+
// real flag, and `--verbose` interleaves JSON metadata blocks that
|
|
35661
|
+
// defeat line parsing.)
|
|
35662
|
+
args: ["models"],
|
|
35663
|
+
parse: parseOpenCodeModelList
|
|
35664
|
+
},
|
|
35665
|
+
bundled: [
|
|
35666
|
+
{ id: "anthropic/claude-opus-4-8", provider: "anthropic" },
|
|
35667
|
+
{ id: "anthropic/claude-sonnet-4-6", provider: "anthropic" },
|
|
35668
|
+
{ id: "anthropic/claude-haiku-4-5", provider: "anthropic" }
|
|
35669
|
+
]
|
|
35670
|
+
}
|
|
35671
|
+
};
|
|
35672
|
+
var SUPPORTED_VENDORS = Object.keys(
|
|
35673
|
+
VENDOR_MODEL_STRATEGIES
|
|
35674
|
+
);
|
|
35675
|
+
function isModelVendor(value) {
|
|
35676
|
+
return Object.hasOwn(VENDOR_MODEL_STRATEGIES, value);
|
|
35677
|
+
}
|
|
35678
|
+
async function detectActiveVendor() {
|
|
35679
|
+
for (const vendor of SUPPORTED_VENDORS) {
|
|
35080
35680
|
try {
|
|
35081
|
-
|
|
35681
|
+
await execBinaryAsync(vendor, ["--version"], {
|
|
35082
35682
|
encoding: "utf-8",
|
|
35083
|
-
timeout: 3e3
|
|
35084
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
35683
|
+
timeout: 3e3
|
|
35085
35684
|
});
|
|
35086
35685
|
return vendor;
|
|
35087
35686
|
} catch {
|
|
@@ -35089,45 +35688,73 @@ function detectActiveVendor() {
|
|
|
35089
35688
|
}
|
|
35090
35689
|
return null;
|
|
35091
35690
|
}
|
|
35092
|
-
function
|
|
35691
|
+
function describeProbeFailure(vendor, args, err) {
|
|
35692
|
+
const command = `${vendor} ${args.join(" ")}`;
|
|
35693
|
+
const e = err;
|
|
35694
|
+
if (e.code === "ENOENT") {
|
|
35695
|
+
return `\`${vendor}\` is not installed or not on PATH`;
|
|
35696
|
+
}
|
|
35697
|
+
if (e.killed) {
|
|
35698
|
+
return `\`${command}\` timed out or exceeded output limits`;
|
|
35699
|
+
}
|
|
35700
|
+
const stderr = typeof e.stderr === "string" ? e.stderr.trim() : "";
|
|
35701
|
+
const firstLine = (stderr.split(/\r?\n/)[0] ?? "").replace(/\u001b\[[0-9;]*[A-Za-z]/g, "").replace(/[\u0000-\u001f\u007f]/g, "").slice(0, 200);
|
|
35702
|
+
const detail = firstLine ? `: ${firstLine}` : "";
|
|
35703
|
+
const exit = typeof e.code === "number" ? ` with exit code ${e.code}` : "";
|
|
35704
|
+
return `\`${command}\` failed${exit}${detail}`;
|
|
35705
|
+
}
|
|
35706
|
+
async function tryNativeEnumeration(vendor, probe) {
|
|
35707
|
+
let stdout;
|
|
35093
35708
|
try {
|
|
35094
|
-
const
|
|
35709
|
+
const result = await execBinaryAsync(vendor, probe.args, {
|
|
35095
35710
|
encoding: "utf-8",
|
|
35096
|
-
timeout: 5e3
|
|
35097
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
35711
|
+
timeout: 5e3
|
|
35098
35712
|
});
|
|
35099
|
-
|
|
35100
|
-
|
|
35101
|
-
|
|
35102
|
-
for (const item of parsed) {
|
|
35103
|
-
if (typeof item === "string") {
|
|
35104
|
-
models.push({ id: item });
|
|
35105
|
-
} else if (typeof item === "object" && item !== null && "id" in item && typeof item.id === "string") {
|
|
35106
|
-
const obj = item;
|
|
35107
|
-
const desc = { id: obj.id };
|
|
35108
|
-
if (typeof obj.displayName === "string") desc.displayName = obj.displayName;
|
|
35109
|
-
if (typeof obj.provider === "string") desc.provider = obj.provider;
|
|
35110
|
-
if (Array.isArray(obj.tags)) {
|
|
35111
|
-
desc.tags = obj.tags.filter((t) => typeof t === "string");
|
|
35112
|
-
}
|
|
35113
|
-
models.push(desc);
|
|
35114
|
-
}
|
|
35115
|
-
}
|
|
35116
|
-
return models.length > 0 ? models : null;
|
|
35117
|
-
} catch {
|
|
35118
|
-
return null;
|
|
35713
|
+
stdout = result.stdout;
|
|
35714
|
+
} catch (err) {
|
|
35715
|
+
return { models: null, reason: describeProbeFailure(vendor, probe.args, err) };
|
|
35119
35716
|
}
|
|
35717
|
+
const models = probe.parse(stdout);
|
|
35718
|
+
if (!models) {
|
|
35719
|
+
return {
|
|
35720
|
+
models: null,
|
|
35721
|
+
reason: `\`${vendor} ${probe.args.join(" ")}\` output did not contain any model identifiers`
|
|
35722
|
+
};
|
|
35723
|
+
}
|
|
35724
|
+
return { models };
|
|
35120
35725
|
}
|
|
35121
|
-
|
|
35122
|
-
|
|
35123
|
-
|
|
35124
|
-
|
|
35125
|
-
|
|
35126
|
-
|
|
35127
|
-
|
|
35128
|
-
|
|
35726
|
+
var SUCCESS_TTL_MS = 6e4;
|
|
35727
|
+
var FAILURE_TTL_MS = 1e4;
|
|
35728
|
+
var cache = /* @__PURE__ */ new Map();
|
|
35729
|
+
async function listModelsForVendor(vendor) {
|
|
35730
|
+
const cached = cache.get(vendor);
|
|
35731
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
35732
|
+
return cached.result;
|
|
35733
|
+
}
|
|
35734
|
+
const strategy = VENDOR_MODEL_STRATEGIES[vendor];
|
|
35735
|
+
if (!strategy) {
|
|
35736
|
+
throw new Error(`Unknown vendor: ${vendor}`);
|
|
35737
|
+
}
|
|
35738
|
+
let result;
|
|
35739
|
+
if ("unavailableReason" in strategy.native) {
|
|
35740
|
+
result = {
|
|
35741
|
+
vendor,
|
|
35742
|
+
source: "bundled",
|
|
35743
|
+
models: strategy.bundled,
|
|
35744
|
+
nativeUnavailableReason: strategy.native.unavailableReason
|
|
35745
|
+
};
|
|
35746
|
+
} else {
|
|
35747
|
+
const native = await tryNativeEnumeration(vendor, strategy.native);
|
|
35748
|
+
result = native.models ? { vendor, source: "native", models: native.models } : {
|
|
35749
|
+
vendor,
|
|
35750
|
+
source: "bundled",
|
|
35751
|
+
models: strategy.bundled,
|
|
35752
|
+
nativeUnavailableReason: native.reason
|
|
35753
|
+
};
|
|
35129
35754
|
}
|
|
35130
|
-
|
|
35755
|
+
const ttl = result.source === "native" ? SUCCESS_TTL_MS : FAILURE_TTL_MS;
|
|
35756
|
+
cache.set(vendor, { result, expiresAt: Date.now() + ttl });
|
|
35757
|
+
return result;
|
|
35131
35758
|
}
|
|
35132
35759
|
|
|
35133
35760
|
// src/server/routes/team.ts
|
|
@@ -35213,30 +35840,41 @@ function createTeamRouter(ocrDir) {
|
|
|
35213
35840
|
}
|
|
35214
35841
|
});
|
|
35215
35842
|
router.get("/models", (req, res) => {
|
|
35216
|
-
|
|
35217
|
-
|
|
35218
|
-
|
|
35219
|
-
|
|
35220
|
-
|
|
35221
|
-
|
|
35222
|
-
|
|
35223
|
-
|
|
35224
|
-
|
|
35225
|
-
|
|
35226
|
-
|
|
35227
|
-
|
|
35228
|
-
|
|
35229
|
-
|
|
35230
|
-
|
|
35231
|
-
|
|
35232
|
-
|
|
35233
|
-
|
|
35234
|
-
|
|
35235
|
-
|
|
35236
|
-
|
|
35237
|
-
|
|
35238
|
-
|
|
35239
|
-
|
|
35843
|
+
void (async () => {
|
|
35844
|
+
try {
|
|
35845
|
+
const raw = req.query["vendor"];
|
|
35846
|
+
if (raw !== void 0 && typeof raw !== "string") {
|
|
35847
|
+
res.status(400).json({ error: "vendor must be a single string" });
|
|
35848
|
+
return;
|
|
35849
|
+
}
|
|
35850
|
+
const requested = raw?.toLowerCase();
|
|
35851
|
+
let vendor;
|
|
35852
|
+
if (requested && isModelVendor(requested)) {
|
|
35853
|
+
vendor = requested;
|
|
35854
|
+
} else if (!requested || requested === "auto") {
|
|
35855
|
+
vendor = await detectActiveVendor();
|
|
35856
|
+
} else {
|
|
35857
|
+
res.status(400).json({
|
|
35858
|
+
error: `Unknown vendor: ${requested}. Supported: ${SUPPORTED_VENDORS.join(", ")}`
|
|
35859
|
+
});
|
|
35860
|
+
return;
|
|
35861
|
+
}
|
|
35862
|
+
if (!vendor) {
|
|
35863
|
+
res.json({ vendor: null, source: null, models: [] });
|
|
35864
|
+
return;
|
|
35865
|
+
}
|
|
35866
|
+
const result = await listModelsForVendor(vendor);
|
|
35867
|
+
res.json(result);
|
|
35868
|
+
} catch (err) {
|
|
35869
|
+
console.error("Failed to list models:", err);
|
|
35870
|
+
if (!res.headersSent) {
|
|
35871
|
+
res.status(500).json({
|
|
35872
|
+
error: "Failed to list models",
|
|
35873
|
+
detail: err instanceof Error ? err.message : String(err)
|
|
35874
|
+
});
|
|
35875
|
+
}
|
|
35876
|
+
}
|
|
35877
|
+
})();
|
|
35240
35878
|
});
|
|
35241
35879
|
return router;
|
|
35242
35880
|
}
|
|
@@ -35502,8 +36140,8 @@ function buildDiagnostics(input) {
|
|
|
35502
36140
|
}
|
|
35503
36141
|
|
|
35504
36142
|
// src/server/services/filesystem-sync.ts
|
|
35505
|
-
import { readdirSync, readFileSync as
|
|
35506
|
-
import { join as
|
|
36143
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync9, statSync as statSync3, existsSync as existsSync13 } from "node:fs";
|
|
36144
|
+
import { join as join16, basename as basename3, dirname as dirname10, relative } from "node:path";
|
|
35507
36145
|
import { watch as watch2 } from "chokidar";
|
|
35508
36146
|
|
|
35509
36147
|
// src/server/services/parsers/reviewer-parser.ts
|
|
@@ -35706,67 +36344,67 @@ var FilesystemSync = class {
|
|
|
35706
36344
|
debounceTimers = /* @__PURE__ */ new Map();
|
|
35707
36345
|
// ── 6.1: Full Scan ──
|
|
35708
36346
|
async fullScan() {
|
|
35709
|
-
if (!
|
|
35710
|
-
const entries =
|
|
36347
|
+
if (!existsSync13(this.sessionsDir)) return;
|
|
36348
|
+
const entries = readdirSync2(this.sessionsDir, { withFileTypes: true });
|
|
35711
36349
|
for (const entry of entries) {
|
|
35712
36350
|
if (!entry.isDirectory()) continue;
|
|
35713
36351
|
const sessionId = entry.name;
|
|
35714
|
-
const sessionDir =
|
|
36352
|
+
const sessionDir = join16(this.sessionsDir, sessionId);
|
|
35715
36353
|
this.syncSession(sessionId, sessionDir);
|
|
35716
36354
|
}
|
|
35717
36355
|
}
|
|
35718
36356
|
syncSession(sessionId, sessionDir) {
|
|
35719
36357
|
this.ensureSessionRow(sessionId, sessionDir);
|
|
35720
|
-
const roundsDir =
|
|
35721
|
-
if (
|
|
35722
|
-
const rounds =
|
|
36358
|
+
const roundsDir = join16(sessionDir, "rounds");
|
|
36359
|
+
if (existsSync13(roundsDir)) {
|
|
36360
|
+
const rounds = readdirSync2(roundsDir, { withFileTypes: true });
|
|
35723
36361
|
for (const roundEntry of rounds) {
|
|
35724
36362
|
if (!roundEntry.isDirectory()) continue;
|
|
35725
36363
|
const roundMatch = roundEntry.name.match(/^round-(\d+)$/);
|
|
35726
36364
|
if (!roundMatch) continue;
|
|
35727
36365
|
const roundNumber = parseInt(roundMatch[1] ?? "0", 10);
|
|
35728
|
-
const roundDir =
|
|
35729
|
-
const reviewsDir =
|
|
35730
|
-
if (
|
|
35731
|
-
const reviewFiles =
|
|
36366
|
+
const roundDir = join16(roundsDir, roundEntry.name);
|
|
36367
|
+
const reviewsDir = join16(roundDir, "reviews");
|
|
36368
|
+
if (existsSync13(reviewsDir)) {
|
|
36369
|
+
const reviewFiles = readdirSync2(reviewsDir).filter((f) => f.endsWith(".md"));
|
|
35732
36370
|
for (const reviewFile of reviewFiles) {
|
|
35733
|
-
const filePath =
|
|
36371
|
+
const filePath = join16(reviewsDir, reviewFile);
|
|
35734
36372
|
this.processReviewerOutput(sessionId, roundNumber, filePath, reviewFile);
|
|
35735
36373
|
}
|
|
35736
36374
|
}
|
|
35737
|
-
const roundMetaPath =
|
|
35738
|
-
if (
|
|
36375
|
+
const roundMetaPath = join16(roundDir, "round-meta.json");
|
|
36376
|
+
if (existsSync13(roundMetaPath)) {
|
|
35739
36377
|
this.processRoundMeta(sessionId, roundNumber, roundMetaPath);
|
|
35740
36378
|
}
|
|
35741
|
-
const finalPath =
|
|
35742
|
-
if (
|
|
36379
|
+
const finalPath = join16(roundDir, "final.md");
|
|
36380
|
+
if (existsSync13(finalPath)) {
|
|
35743
36381
|
this.processFinalMd(sessionId, roundNumber, finalPath);
|
|
35744
36382
|
}
|
|
35745
|
-
const finalHumanPath =
|
|
35746
|
-
if (
|
|
36383
|
+
const finalHumanPath = join16(roundDir, "final-human.md");
|
|
36384
|
+
if (existsSync13(finalHumanPath)) {
|
|
35747
36385
|
this.processGenericArtifact(sessionId, "final-human", finalHumanPath, roundNumber);
|
|
35748
36386
|
}
|
|
35749
|
-
const discoursePath =
|
|
35750
|
-
if (
|
|
36387
|
+
const discoursePath = join16(roundDir, "discourse.md");
|
|
36388
|
+
if (existsSync13(discoursePath)) {
|
|
35751
36389
|
this.processGenericArtifact(sessionId, "discourse", discoursePath, roundNumber);
|
|
35752
36390
|
}
|
|
35753
36391
|
}
|
|
35754
36392
|
}
|
|
35755
|
-
const mapDir =
|
|
35756
|
-
if (
|
|
35757
|
-
const runs =
|
|
36393
|
+
const mapDir = join16(sessionDir, "map", "runs");
|
|
36394
|
+
if (existsSync13(mapDir)) {
|
|
36395
|
+
const runs = readdirSync2(mapDir, { withFileTypes: true });
|
|
35758
36396
|
for (const runEntry of runs) {
|
|
35759
36397
|
if (!runEntry.isDirectory()) continue;
|
|
35760
36398
|
const runMatch = runEntry.name.match(/^run-(\d+)$/);
|
|
35761
36399
|
if (!runMatch) continue;
|
|
35762
36400
|
const runNumber = parseInt(runMatch[1] ?? "0", 10);
|
|
35763
|
-
const runDir =
|
|
35764
|
-
const mapMetaPath =
|
|
35765
|
-
if (
|
|
36401
|
+
const runDir = join16(mapDir, runEntry.name);
|
|
36402
|
+
const mapMetaPath = join16(runDir, "map-meta.json");
|
|
36403
|
+
if (existsSync13(mapMetaPath)) {
|
|
35766
36404
|
this.processMapMeta(sessionId, runNumber, mapMetaPath);
|
|
35767
36405
|
}
|
|
35768
|
-
const mapPath =
|
|
35769
|
-
if (
|
|
36406
|
+
const mapPath = join16(runDir, "map.md");
|
|
36407
|
+
if (existsSync13(mapPath)) {
|
|
35770
36408
|
this.processMapMd(sessionId, runNumber, mapPath);
|
|
35771
36409
|
}
|
|
35772
36410
|
const mapArtifacts = [
|
|
@@ -35775,8 +36413,8 @@ var FilesystemSync = class {
|
|
|
35775
36413
|
["requirements-mapping.md", "requirements-mapping"]
|
|
35776
36414
|
];
|
|
35777
36415
|
for (const [fileName, artifactType] of mapArtifacts) {
|
|
35778
|
-
const filePath =
|
|
35779
|
-
if (
|
|
36416
|
+
const filePath = join16(runDir, fileName);
|
|
36417
|
+
if (existsSync13(filePath)) {
|
|
35780
36418
|
this.processGenericArtifact(sessionId, artifactType, filePath, void 0, runNumber);
|
|
35781
36419
|
}
|
|
35782
36420
|
}
|
|
@@ -35787,8 +36425,8 @@ var FilesystemSync = class {
|
|
|
35787
36425
|
["discovered-standards.md", "discovered-standards"]
|
|
35788
36426
|
];
|
|
35789
36427
|
for (const [fileName, artifactType] of sessionArtifacts) {
|
|
35790
|
-
const filePath =
|
|
35791
|
-
if (
|
|
36428
|
+
const filePath = join16(sessionDir, fileName);
|
|
36429
|
+
if (existsSync13(filePath)) {
|
|
35792
36430
|
this.processGenericArtifact(sessionId, artifactType, filePath);
|
|
35793
36431
|
}
|
|
35794
36432
|
}
|
|
@@ -35797,58 +36435,58 @@ var FilesystemSync = class {
|
|
|
35797
36435
|
ensureSessionRow(sessionId, sessionDir) {
|
|
35798
36436
|
const branchMatch = sessionId.match(/^\d{4}-\d{2}-\d{2}-(.+)$/);
|
|
35799
36437
|
const branch = branchMatch?.[1] ?? "unknown";
|
|
35800
|
-
const hasRoundsDir =
|
|
35801
|
-
const hasMapDir =
|
|
36438
|
+
const hasRoundsDir = existsSync13(join16(sessionDir, "rounds"));
|
|
36439
|
+
const hasMapDir = existsSync13(join16(sessionDir, "map"));
|
|
35802
36440
|
const workflowType = hasMapDir && !hasRoundsDir ? "map" : "review";
|
|
35803
36441
|
let currentRound = 1;
|
|
35804
36442
|
if (hasRoundsDir) {
|
|
35805
|
-
const roundDirs =
|
|
36443
|
+
const roundDirs = readdirSync2(join16(sessionDir, "rounds")).filter((d) => d.match(/^round-\d+$/));
|
|
35806
36444
|
currentRound = Math.max(1, roundDirs.length);
|
|
35807
36445
|
}
|
|
35808
36446
|
let currentMapRun = 1;
|
|
35809
|
-
const mapRunsDir =
|
|
35810
|
-
if (
|
|
35811
|
-
const runDirs =
|
|
36447
|
+
const mapRunsDir = join16(sessionDir, "map", "runs");
|
|
36448
|
+
if (existsSync13(mapRunsDir)) {
|
|
36449
|
+
const runDirs = readdirSync2(mapRunsDir).filter((d) => d.match(/^run-\d+$/));
|
|
35812
36450
|
currentMapRun = Math.max(1, runDirs.length);
|
|
35813
36451
|
}
|
|
35814
36452
|
let phase = "context";
|
|
35815
36453
|
let phaseNumber = 1;
|
|
35816
36454
|
let status = "closed";
|
|
35817
36455
|
if (workflowType === "review" && hasRoundsDir) {
|
|
35818
|
-
const roundDir =
|
|
35819
|
-
if (
|
|
36456
|
+
const roundDir = join16(sessionDir, "rounds", `round-${currentRound}`);
|
|
36457
|
+
if (existsSync13(join16(roundDir, "final.md"))) {
|
|
35820
36458
|
phase = "complete";
|
|
35821
36459
|
phaseNumber = 8;
|
|
35822
36460
|
status = "closed";
|
|
35823
|
-
} else if (
|
|
36461
|
+
} else if (existsSync13(join16(roundDir, "discourse.md"))) {
|
|
35824
36462
|
phase = "synthesis";
|
|
35825
36463
|
phaseNumber = 7;
|
|
35826
|
-
} else if (
|
|
36464
|
+
} else if (existsSync13(join16(roundDir, "reviews")) && readdirSync2(join16(roundDir, "reviews")).filter((f) => f.endsWith(".md")).length > 0) {
|
|
35827
36465
|
phase = "reviews";
|
|
35828
36466
|
phaseNumber = 4;
|
|
35829
|
-
} else if (
|
|
36467
|
+
} else if (existsSync13(join16(sessionDir, "context.md"))) {
|
|
35830
36468
|
phase = "analysis";
|
|
35831
36469
|
phaseNumber = 3;
|
|
35832
|
-
} else if (
|
|
36470
|
+
} else if (existsSync13(join16(sessionDir, "discovered-standards.md"))) {
|
|
35833
36471
|
phase = "change-context";
|
|
35834
36472
|
phaseNumber = 2;
|
|
35835
36473
|
}
|
|
35836
36474
|
} else if (workflowType === "map" && hasMapDir) {
|
|
35837
|
-
const runDir =
|
|
35838
|
-
if (
|
|
36475
|
+
const runDir = join16(mapRunsDir, `run-${currentMapRun}`);
|
|
36476
|
+
if (existsSync13(join16(runDir, "map.md"))) {
|
|
35839
36477
|
phase = "complete";
|
|
35840
36478
|
phaseNumber = 6;
|
|
35841
36479
|
status = "closed";
|
|
35842
|
-
} else if (
|
|
36480
|
+
} else if (existsSync13(join16(runDir, "requirements-mapping.md"))) {
|
|
35843
36481
|
phase = "synthesis";
|
|
35844
36482
|
phaseNumber = 5;
|
|
35845
|
-
} else if (
|
|
36483
|
+
} else if (existsSync13(join16(runDir, "flow-analysis.md"))) {
|
|
35846
36484
|
phase = "requirements-mapping";
|
|
35847
36485
|
phaseNumber = 4;
|
|
35848
|
-
} else if (
|
|
36486
|
+
} else if (existsSync13(join16(runDir, "topology.md"))) {
|
|
35849
36487
|
phase = "flow-analysis";
|
|
35850
36488
|
phaseNumber = 3;
|
|
35851
|
-
} else if (
|
|
36489
|
+
} else if (existsSync13(join16(sessionDir, "discovered-standards.md"))) {
|
|
35852
36490
|
phase = "topology";
|
|
35853
36491
|
phaseNumber = 2;
|
|
35854
36492
|
}
|
|
@@ -35901,9 +36539,9 @@ var FilesystemSync = class {
|
|
|
35901
36539
|
/** Returns true if the directory contains at least one .md or .json file (recursively). */
|
|
35902
36540
|
hasArtifacts(dir) {
|
|
35903
36541
|
try {
|
|
35904
|
-
for (const entry of
|
|
36542
|
+
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
35905
36543
|
if (entry.isDirectory()) {
|
|
35906
|
-
if (this.hasArtifacts(
|
|
36544
|
+
if (this.hasArtifacts(join16(dir, entry.name))) return true;
|
|
35907
36545
|
} else if (/\.(md|json)$/.test(entry.name)) {
|
|
35908
36546
|
return true;
|
|
35909
36547
|
}
|
|
@@ -35916,7 +36554,7 @@ var FilesystemSync = class {
|
|
|
35916
36554
|
shouldSkip(filePath, existingParsedAt) {
|
|
35917
36555
|
if (!existingParsedAt) return false;
|
|
35918
36556
|
try {
|
|
35919
|
-
const mtime =
|
|
36557
|
+
const mtime = statSync3(filePath).mtime;
|
|
35920
36558
|
const parsedAt = new Date(existingParsedAt);
|
|
35921
36559
|
return mtime <= parsedAt;
|
|
35922
36560
|
} catch {
|
|
@@ -35931,13 +36569,19 @@ var FilesystemSync = class {
|
|
|
35931
36569
|
"SELECT id FROM markdown_artifacts WHERE session_id = ? AND artifact_type = ? AND round_number IS ? AND file_path = ?",
|
|
35932
36570
|
[sessionId, artifactType, roundNumber ?? null, relPath]
|
|
35933
36571
|
);
|
|
35934
|
-
|
|
36572
|
+
if (existing !== null) {
|
|
36573
|
+
this.db.run(
|
|
36574
|
+
`UPDATE markdown_artifacts SET content = ?, parsed_at = datetime('now') WHERE id = ?`,
|
|
36575
|
+
[content, existing]
|
|
36576
|
+
);
|
|
36577
|
+
return "updated";
|
|
36578
|
+
}
|
|
35935
36579
|
this.db.run(
|
|
35936
|
-
`INSERT
|
|
36580
|
+
`INSERT INTO markdown_artifacts (session_id, artifact_type, round_number, file_path, content, parsed_at)
|
|
35937
36581
|
VALUES (?, ?, ?, ?, ?, datetime('now'))`,
|
|
35938
36582
|
[sessionId, artifactType, roundNumber ?? null, relPath, content]
|
|
35939
36583
|
);
|
|
35940
|
-
return
|
|
36584
|
+
return "created";
|
|
35941
36585
|
}
|
|
35942
36586
|
// ── 6.7: Socket.IO Emission ──
|
|
35943
36587
|
emitArtifactEvent(action, event) {
|
|
@@ -35953,12 +36597,12 @@ var FilesystemSync = class {
|
|
|
35953
36597
|
);
|
|
35954
36598
|
if (existingRun && this.shouldSkip(filePath, existingRun["parsed_at"] ?? null)) return;
|
|
35955
36599
|
if (existingRun?.["source"] === "orchestrator") {
|
|
35956
|
-
const content2 =
|
|
36600
|
+
const content2 = readFileSync9(filePath, "utf-8");
|
|
35957
36601
|
const action2 = this.upsertMarkdownArtifact(sessionId, "map", filePath, content2, void 0);
|
|
35958
36602
|
this.emitArtifactEvent(action2, { sessionId, artifactType: "map", filePath });
|
|
35959
36603
|
return;
|
|
35960
36604
|
}
|
|
35961
|
-
const content =
|
|
36605
|
+
const content = readFileSync9(filePath, "utf-8");
|
|
35962
36606
|
const parsed = parseMapMd(content);
|
|
35963
36607
|
this.db.run(
|
|
35964
36608
|
`INSERT OR REPLACE INTO map_runs (session_id, run_number, file_count, map_md_path, parsed_at, source)
|
|
@@ -36084,7 +36728,7 @@ var FilesystemSync = class {
|
|
|
36084
36728
|
const roundId = roundRow?.["id"];
|
|
36085
36729
|
if (!roundId) return;
|
|
36086
36730
|
if (roundRow?.["source"] === "orchestrator") {
|
|
36087
|
-
const content2 =
|
|
36731
|
+
const content2 = readFileSync9(filePath, "utf-8");
|
|
36088
36732
|
const action2 = this.upsertMarkdownArtifact(sessionId, "reviewer-output", filePath, content2, roundNumber);
|
|
36089
36733
|
this.emitArtifactEvent(action2, {
|
|
36090
36734
|
sessionId,
|
|
@@ -36103,7 +36747,7 @@ var FilesystemSync = class {
|
|
|
36103
36747
|
[roundId, reviewerType, instanceNumber]
|
|
36104
36748
|
);
|
|
36105
36749
|
if (existingOutput && this.shouldSkip(filePath, existingOutput["parsed_at"] ?? null)) return;
|
|
36106
|
-
const content =
|
|
36750
|
+
const content = readFileSync9(filePath, "utf-8");
|
|
36107
36751
|
const parsed = parseReviewerOutput(content);
|
|
36108
36752
|
this.db.run(
|
|
36109
36753
|
`INSERT OR REPLACE INTO reviewer_outputs (round_id, reviewer_type, instance_number, file_path, finding_count, parsed_at)
|
|
@@ -36191,7 +36835,7 @@ var FilesystemSync = class {
|
|
|
36191
36835
|
if (existingRound?.["source"] === "orchestrator" && this.shouldSkip(filePath, existingRound["parsed_at"] ?? null)) return;
|
|
36192
36836
|
let raw;
|
|
36193
36837
|
try {
|
|
36194
|
-
raw = JSON.parse(
|
|
36838
|
+
raw = JSON.parse(readFileSync9(filePath, "utf-8"));
|
|
36195
36839
|
} catch {
|
|
36196
36840
|
console.error(`[FilesystemSync] Failed to parse ${filePath}`);
|
|
36197
36841
|
return;
|
|
@@ -36237,12 +36881,12 @@ var FilesystemSync = class {
|
|
|
36237
36881
|
this.db.run("COMMIT");
|
|
36238
36882
|
return;
|
|
36239
36883
|
}
|
|
36240
|
-
const roundDir =
|
|
36884
|
+
const roundDir = dirname10(filePath);
|
|
36241
36885
|
for (const reviewer of meta.reviewers) {
|
|
36242
36886
|
const reviewerType = reviewer.type ?? "unknown";
|
|
36243
36887
|
const instanceNumber = reviewer.instance ?? 1;
|
|
36244
36888
|
const findings = reviewer.findings ?? [];
|
|
36245
|
-
const reviewerMdPath =
|
|
36889
|
+
const reviewerMdPath = join16(roundDir, "reviews", `${reviewerType}-${instanceNumber}.md`);
|
|
36246
36890
|
this.db.run(
|
|
36247
36891
|
`INSERT OR REPLACE INTO reviewer_outputs (round_id, reviewer_type, instance_number, file_path, finding_count, parsed_at)
|
|
36248
36892
|
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
@@ -36340,7 +36984,7 @@ var FilesystemSync = class {
|
|
|
36340
36984
|
if (existingRun?.["source"] === "orchestrator" && this.shouldSkip(filePath, existingRun["parsed_at"] ?? null)) return;
|
|
36341
36985
|
let raw;
|
|
36342
36986
|
try {
|
|
36343
|
-
raw = JSON.parse(
|
|
36987
|
+
raw = JSON.parse(readFileSync9(filePath, "utf-8"));
|
|
36344
36988
|
} catch {
|
|
36345
36989
|
console.error(`[FilesystemSync] Failed to parse ${filePath}`);
|
|
36346
36990
|
return;
|
|
@@ -36468,7 +37112,7 @@ var FilesystemSync = class {
|
|
|
36468
37112
|
);
|
|
36469
37113
|
const isOrchestratorSource = existingRound?.["source"] === "orchestrator";
|
|
36470
37114
|
if (!isOrchestratorSource && existingRound && this.shouldSkip(filePath, existingRound["parsed_at"] ?? null)) return;
|
|
36471
|
-
const content =
|
|
37115
|
+
const content = readFileSync9(filePath, "utf-8");
|
|
36472
37116
|
if (isOrchestratorSource) {
|
|
36473
37117
|
this.db.run(
|
|
36474
37118
|
`UPDATE review_rounds SET final_md_path = ?, parsed_at = ?
|
|
@@ -36547,7 +37191,7 @@ var FilesystemSync = class {
|
|
|
36547
37191
|
[sessionId, artifactType, relPath]
|
|
36548
37192
|
);
|
|
36549
37193
|
if (existing && this.shouldSkip(filePath, existing["parsed_at"] ?? null)) return;
|
|
36550
|
-
const content =
|
|
37194
|
+
const content = readFileSync9(filePath, "utf-8");
|
|
36551
37195
|
const action = this.upsertMarkdownArtifact(sessionId, artifactType, filePath, content, roundNumber);
|
|
36552
37196
|
this.emitArtifactEvent(action, {
|
|
36553
37197
|
sessionId,
|
|
@@ -36566,7 +37210,7 @@ var FilesystemSync = class {
|
|
|
36566
37210
|
ignored: [
|
|
36567
37211
|
// Only ignore entries whose own name starts with a dot — the old regex
|
|
36568
37212
|
// /(^|[/\\])\../ matched `.ocr` in the parent path, silencing ALL events.
|
|
36569
|
-
(filePath) =>
|
|
37213
|
+
(filePath) => basename3(filePath).startsWith("."),
|
|
36570
37214
|
/node_modules/,
|
|
36571
37215
|
/\.db$/
|
|
36572
37216
|
]
|
|
@@ -36605,9 +37249,9 @@ var FilesystemSync = class {
|
|
|
36605
37249
|
const parts = relFromSessions.split("/");
|
|
36606
37250
|
const sessionId = parts[0];
|
|
36607
37251
|
if (!sessionId) return;
|
|
36608
|
-
const sessionDir =
|
|
37252
|
+
const sessionDir = join16(this.sessionsDir, sessionId);
|
|
36609
37253
|
this.ensureSessionRow(sessionId, sessionDir);
|
|
36610
|
-
const fileName =
|
|
37254
|
+
const fileName = basename3(filePath);
|
|
36611
37255
|
const reviewerMatch = relFromSessions.match(/rounds\/round-(\d+)\/reviews\/(.+\.md)$/);
|
|
36612
37256
|
if (reviewerMatch) {
|
|
36613
37257
|
const roundNumber = parseInt(reviewerMatch[1] ?? "0", 10);
|
|
@@ -36677,8 +37321,8 @@ var FilesystemSync = class {
|
|
|
36677
37321
|
};
|
|
36678
37322
|
|
|
36679
37323
|
// src/server/services/db-sync-watcher.ts
|
|
36680
|
-
import { existsSync as
|
|
36681
|
-
import { dirname as
|
|
37324
|
+
import { existsSync as existsSync14 } from "node:fs";
|
|
37325
|
+
import { dirname as dirname11, basename as basename4 } from "node:path";
|
|
36682
37326
|
import { watch as watch3 } from "chokidar";
|
|
36683
37327
|
function col(row, key) {
|
|
36684
37328
|
return row[key] ?? null;
|
|
@@ -36719,9 +37363,9 @@ var DbSyncWatcher = class {
|
|
|
36719
37363
|
}
|
|
36720
37364
|
/** Start watching the DB file (and its WAL sidecar) for external writes. */
|
|
36721
37365
|
startWatching() {
|
|
36722
|
-
if (!
|
|
36723
|
-
const watchDir =
|
|
36724
|
-
const dbFile =
|
|
37366
|
+
if (!existsSync14(this.dbFilePath)) return;
|
|
37367
|
+
const watchDir = dirname11(this.dbFilePath);
|
|
37368
|
+
const dbFile = basename4(this.dbFilePath);
|
|
36725
37369
|
const walFile = `${dbFile}-wal`;
|
|
36726
37370
|
this.watcher = watch3(watchDir, {
|
|
36727
37371
|
persistent: true,
|
|
@@ -36731,7 +37375,7 @@ var DbSyncWatcher = class {
|
|
|
36731
37375
|
interval: 200
|
|
36732
37376
|
});
|
|
36733
37377
|
const onAnyEvent = (path2) => {
|
|
36734
|
-
const name =
|
|
37378
|
+
const name = basename4(path2);
|
|
36735
37379
|
if (name === dbFile || name === walFile) this.debouncedSync();
|
|
36736
37380
|
};
|
|
36737
37381
|
this.watcher.on("change", onAnyEvent);
|
|
@@ -36982,20 +37626,20 @@ function commandFingerprint(row) {
|
|
|
36982
37626
|
}
|
|
36983
37627
|
|
|
36984
37628
|
// src/server/socket/chat-handler.ts
|
|
36985
|
-
import { dirname as
|
|
37629
|
+
import { dirname as dirname12 } from "node:path";
|
|
36986
37630
|
|
|
36987
37631
|
// src/server/services/chat-context.ts
|
|
36988
|
-
import { readFileSync as
|
|
36989
|
-
import { join as
|
|
37632
|
+
import { readFileSync as readFileSync10, readdirSync as readdirSync3, existsSync as existsSync15 } from "node:fs";
|
|
37633
|
+
import { join as join17 } from "node:path";
|
|
36990
37634
|
function buildChatContext(ocrDir, target) {
|
|
36991
|
-
const sessionsDir =
|
|
37635
|
+
const sessionsDir = join17(ocrDir, "sessions");
|
|
36992
37636
|
if (target.type === "map_run") {
|
|
36993
37637
|
return buildMapRunContext(sessionsDir, target.sessionId, target.runNumber);
|
|
36994
37638
|
}
|
|
36995
37639
|
return buildReviewRoundContext(sessionsDir, target.sessionId, target.roundNumber);
|
|
36996
37640
|
}
|
|
36997
37641
|
function buildMapRunContext(sessionsDir, sessionId, runNumber) {
|
|
36998
|
-
const mapPath =
|
|
37642
|
+
const mapPath = join17(
|
|
36999
37643
|
sessionsDir,
|
|
37000
37644
|
sessionId,
|
|
37001
37645
|
"map",
|
|
@@ -37009,8 +37653,8 @@ function buildMapRunContext(sessionsDir, sessionId, runNumber) {
|
|
|
37009
37653
|
"",
|
|
37010
37654
|
`Below is the Code Review Map that organizes the changeset into reviewable sections:`
|
|
37011
37655
|
];
|
|
37012
|
-
if (
|
|
37013
|
-
const content =
|
|
37656
|
+
if (existsSync15(mapPath)) {
|
|
37657
|
+
const content = readFileSync10(mapPath, "utf-8");
|
|
37014
37658
|
parts.push("");
|
|
37015
37659
|
parts.push("<map>");
|
|
37016
37660
|
parts.push(content);
|
|
@@ -37022,26 +37666,26 @@ function buildMapRunContext(sessionsDir, sessionId, runNumber) {
|
|
|
37022
37666
|
return parts.join("\n");
|
|
37023
37667
|
}
|
|
37024
37668
|
function buildReviewRoundContext(sessionsDir, sessionId, roundNumber) {
|
|
37025
|
-
const roundDir =
|
|
37026
|
-
const finalPath =
|
|
37027
|
-
const reviewersDir =
|
|
37669
|
+
const roundDir = join17(sessionsDir, sessionId, "rounds", `round-${roundNumber}`);
|
|
37670
|
+
const finalPath = join17(roundDir, "final.md");
|
|
37671
|
+
const reviewersDir = join17(roundDir, "reviews");
|
|
37028
37672
|
const parts = [
|
|
37029
37673
|
`You are an expert code reviewer assisting with a code review session.`,
|
|
37030
37674
|
`You are looking at review round #${roundNumber} for session "${sessionId}".`,
|
|
37031
37675
|
"",
|
|
37032
37676
|
`Below are the review artifacts for this round:`
|
|
37033
37677
|
];
|
|
37034
|
-
if (
|
|
37035
|
-
const content =
|
|
37678
|
+
if (existsSync15(finalPath)) {
|
|
37679
|
+
const content = readFileSync10(finalPath, "utf-8");
|
|
37036
37680
|
parts.push("");
|
|
37037
37681
|
parts.push("<final-synthesis>");
|
|
37038
37682
|
parts.push(content);
|
|
37039
37683
|
parts.push("</final-synthesis>");
|
|
37040
37684
|
}
|
|
37041
|
-
if (
|
|
37042
|
-
const files =
|
|
37685
|
+
if (existsSync15(reviewersDir)) {
|
|
37686
|
+
const files = readdirSync3(reviewersDir).filter((f) => f.endsWith(".md")).sort();
|
|
37043
37687
|
for (const file of files) {
|
|
37044
|
-
const content =
|
|
37688
|
+
const content = readFileSync10(join17(reviewersDir, file), "utf-8");
|
|
37045
37689
|
const reviewerName = file.replace(/\.md$/, "");
|
|
37046
37690
|
parts.push("");
|
|
37047
37691
|
parts.push(`<reviewer name="${reviewerName}">`);
|
|
@@ -37049,7 +37693,7 @@ function buildReviewRoundContext(sessionsDir, sessionId, roundNumber) {
|
|
|
37049
37693
|
parts.push("</reviewer>");
|
|
37050
37694
|
}
|
|
37051
37695
|
}
|
|
37052
|
-
if (!
|
|
37696
|
+
if (!existsSync15(finalPath) && !existsSync15(reviewersDir)) {
|
|
37053
37697
|
parts.push("");
|
|
37054
37698
|
parts.push("(No review artifacts found on disk for this round.)");
|
|
37055
37699
|
}
|
|
@@ -37195,7 +37839,7 @@ User: ${message}`;
|
|
|
37195
37839
|
});
|
|
37196
37840
|
return;
|
|
37197
37841
|
}
|
|
37198
|
-
const repoRoot =
|
|
37842
|
+
const repoRoot = dirname12(ocrDir);
|
|
37199
37843
|
const spawnResult = adapter.spawn({
|
|
37200
37844
|
prompt,
|
|
37201
37845
|
cwd: repoRoot,
|
|
@@ -37371,13 +38015,13 @@ function cleanupAllChats() {
|
|
|
37371
38015
|
}
|
|
37372
38016
|
|
|
37373
38017
|
// src/server/socket/post-handler.ts
|
|
37374
|
-
import { existsSync as
|
|
38018
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync6, readFileSync as readFileSync11, unlinkSync as unlinkSync4, writeFileSync as writeFileSync5 } from "node:fs";
|
|
37375
38019
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
37376
|
-
import { join as
|
|
38020
|
+
import { join as join18, dirname as dirname13, isAbsolute as isAbsolute2 } from "node:path";
|
|
37377
38021
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
37378
38022
|
function resolveSessionDir2(sessionDir, ocrDir) {
|
|
37379
38023
|
if (isAbsolute2(sessionDir)) return sessionDir;
|
|
37380
|
-
return
|
|
38024
|
+
return join18(dirname13(ocrDir), sessionDir);
|
|
37381
38025
|
}
|
|
37382
38026
|
var BRANCH_PREFIXES = [
|
|
37383
38027
|
"feat",
|
|
@@ -37446,7 +38090,7 @@ function registerPostHandlers(io2, socket, db, ocrDir, aiCliService) {
|
|
|
37446
38090
|
return;
|
|
37447
38091
|
}
|
|
37448
38092
|
const branch = session.branch;
|
|
37449
|
-
const repoRoot =
|
|
38093
|
+
const repoRoot = dirname13(ocrDir);
|
|
37450
38094
|
try {
|
|
37451
38095
|
await execBinaryAsync("gh", ["auth", "status"], { env: cleanEnv(), cwd: repoRoot, encoding: "utf-8" });
|
|
37452
38096
|
} catch {
|
|
@@ -37547,19 +38191,19 @@ function registerPostHandlers(io2, socket, db, ocrDir, aiCliService) {
|
|
|
37547
38191
|
socket.emit("post:error", { error: "Session not found" });
|
|
37548
38192
|
return;
|
|
37549
38193
|
}
|
|
37550
|
-
const sessionDir = session.session_dir ? resolveSessionDir2(session.session_dir, ocrDir) :
|
|
37551
|
-
const roundDir =
|
|
37552
|
-
const finalPath =
|
|
37553
|
-
if (!
|
|
38194
|
+
const sessionDir = session.session_dir ? resolveSessionDir2(session.session_dir, ocrDir) : join18(ocrDir, "sessions", sessionId);
|
|
38195
|
+
const roundDir = join18(sessionDir, "rounds", `round-${roundNumber}`);
|
|
38196
|
+
const finalPath = join18(roundDir, "final.md");
|
|
38197
|
+
if (!existsSync16(finalPath)) {
|
|
37554
38198
|
socket.emit("post:error", { error: "final.md not found for this round" });
|
|
37555
38199
|
return;
|
|
37556
38200
|
}
|
|
37557
|
-
const humanReviewPath =
|
|
37558
|
-
const repoRoot =
|
|
37559
|
-
const commandMdPath =
|
|
38201
|
+
const humanReviewPath = join18(roundDir, "final-human.md");
|
|
38202
|
+
const repoRoot = dirname13(ocrDir);
|
|
38203
|
+
const commandMdPath = join18(ocrDir, "commands", "translate-review-to-single-human.md");
|
|
37560
38204
|
let commandContent;
|
|
37561
38205
|
try {
|
|
37562
|
-
commandContent =
|
|
38206
|
+
commandContent = readFileSync11(commandMdPath, "utf-8");
|
|
37563
38207
|
} catch {
|
|
37564
38208
|
socket.emit("post:error", {
|
|
37565
38209
|
error: `Command file not found: ${commandMdPath}. Run \`ocr init\` to set up.`
|
|
@@ -37639,9 +38283,9 @@ function registerPostHandlers(io2, socket, db, ocrDir, aiCliService) {
|
|
|
37639
38283
|
}
|
|
37640
38284
|
}
|
|
37641
38285
|
let generatedContent = "";
|
|
37642
|
-
if (
|
|
38286
|
+
if (existsSync16(humanReviewPath)) {
|
|
37643
38287
|
try {
|
|
37644
|
-
generatedContent =
|
|
38288
|
+
generatedContent = readFileSync11(humanReviewPath, "utf-8").trim();
|
|
37645
38289
|
} catch {
|
|
37646
38290
|
}
|
|
37647
38291
|
}
|
|
@@ -37716,11 +38360,11 @@ function registerPostHandlers(io2, socket, db, ocrDir, aiCliService) {
|
|
|
37716
38360
|
socket.emit("post:save-result", { success: false, error: "Session not found" });
|
|
37717
38361
|
return;
|
|
37718
38362
|
}
|
|
37719
|
-
const sessionDir = session.session_dir ? resolveSessionDir2(session.session_dir, ocrDir) :
|
|
37720
|
-
const roundDir =
|
|
37721
|
-
|
|
37722
|
-
const filePath =
|
|
37723
|
-
|
|
38363
|
+
const sessionDir = session.session_dir ? resolveSessionDir2(session.session_dir, ocrDir) : join18(ocrDir, "sessions", sessionId);
|
|
38364
|
+
const roundDir = join18(sessionDir, "rounds", `round-${roundNumber}`);
|
|
38365
|
+
mkdirSync6(roundDir, { recursive: true });
|
|
38366
|
+
const filePath = join18(roundDir, "final-human.md");
|
|
38367
|
+
writeFileSync5(filePath, content, { mode: 420 });
|
|
37724
38368
|
socket.emit("post:save-result", { success: true });
|
|
37725
38369
|
} catch (err) {
|
|
37726
38370
|
console.error("Error in post:save handler:", err);
|
|
@@ -37746,14 +38390,14 @@ function registerPostHandlers(io2, socket, db, ocrDir, aiCliService) {
|
|
|
37746
38390
|
);
|
|
37747
38391
|
tracker.appendOutput(`\u25B8 Posting review to PR #${prNumber}...
|
|
37748
38392
|
`);
|
|
37749
|
-
const tmpDir =
|
|
38393
|
+
const tmpDir = join18(tmpdir2(), "ocr-post-comments");
|
|
37750
38394
|
try {
|
|
37751
|
-
|
|
38395
|
+
mkdirSync6(tmpDir, { recursive: true, mode: 448 });
|
|
37752
38396
|
} catch {
|
|
37753
38397
|
}
|
|
37754
|
-
const tmpFile =
|
|
37755
|
-
|
|
37756
|
-
const repoRoot =
|
|
38398
|
+
const tmpFile = join18(tmpDir, `${randomUUID2()}.md`);
|
|
38399
|
+
writeFileSync5(tmpFile, content, { mode: 384 });
|
|
38400
|
+
const repoRoot = dirname13(ocrDir);
|
|
37757
38401
|
try {
|
|
37758
38402
|
const { stdout } = await execBinaryAsync(
|
|
37759
38403
|
"gh",
|
|
@@ -37776,7 +38420,7 @@ function registerPostHandlers(io2, socket, db, ocrDir, aiCliService) {
|
|
|
37776
38420
|
});
|
|
37777
38421
|
} finally {
|
|
37778
38422
|
try {
|
|
37779
|
-
|
|
38423
|
+
unlinkSync4(tmpFile);
|
|
37780
38424
|
} catch {
|
|
37781
38425
|
}
|
|
37782
38426
|
}
|
|
@@ -37796,45 +38440,9 @@ function cleanupAllPostGenerations() {
|
|
|
37796
38440
|
}
|
|
37797
38441
|
}
|
|
37798
38442
|
|
|
37799
|
-
// ../cli/src/lib/runtime-config.ts
|
|
37800
|
-
import { existsSync as existsSync14, readFileSync as readFileSync11 } from "node:fs";
|
|
37801
|
-
import { join as join17 } from "node:path";
|
|
37802
|
-
var DEFAULT_AGENT_HEARTBEAT_SECONDS = 60;
|
|
37803
|
-
function getAgentHeartbeatSeconds(ocrDir) {
|
|
37804
|
-
const configPath = join17(ocrDir, "config.yaml");
|
|
37805
|
-
if (!existsSync14(configPath)) {
|
|
37806
|
-
return DEFAULT_AGENT_HEARTBEAT_SECONDS;
|
|
37807
|
-
}
|
|
37808
|
-
let content;
|
|
37809
|
-
try {
|
|
37810
|
-
content = readFileSync11(configPath, "utf-8");
|
|
37811
|
-
} catch {
|
|
37812
|
-
return DEFAULT_AGENT_HEARTBEAT_SECONDS;
|
|
37813
|
-
}
|
|
37814
|
-
const blockMatch = content.match(
|
|
37815
|
-
/^runtime:\s*\n(?:\s+[^\n]*\n)*?\s+agent_heartbeat_seconds:\s*([^\s#\n]+)/m
|
|
37816
|
-
);
|
|
37817
|
-
const inlineMatch = content.match(
|
|
37818
|
-
/^runtime:\s*\{[^}]*\bagent_heartbeat_seconds:\s*([^\s,}]+)/m
|
|
37819
|
-
);
|
|
37820
|
-
const raw = blockMatch?.[1] ?? inlineMatch?.[1];
|
|
37821
|
-
if (!raw) {
|
|
37822
|
-
return DEFAULT_AGENT_HEARTBEAT_SECONDS;
|
|
37823
|
-
}
|
|
37824
|
-
const parsed = Number(raw);
|
|
37825
|
-
if (!Number.isFinite(parsed) || parsed <= 0 || !Number.isInteger(parsed)) {
|
|
37826
|
-
process.stderr.write(
|
|
37827
|
-
`[ocr] runtime.agent_heartbeat_seconds is not a positive integer (got "${raw}"); falling back to ${DEFAULT_AGENT_HEARTBEAT_SECONDS}s.
|
|
37828
|
-
`
|
|
37829
|
-
);
|
|
37830
|
-
return DEFAULT_AGENT_HEARTBEAT_SECONDS;
|
|
37831
|
-
}
|
|
37832
|
-
return parsed;
|
|
37833
|
-
}
|
|
37834
|
-
|
|
37835
38443
|
// src/server/index.ts
|
|
37836
38444
|
import { homedir } from "node:os";
|
|
37837
|
-
var __dirname3 =
|
|
38445
|
+
var __dirname3 = dirname14(fileURLToPath3(import.meta.url));
|
|
37838
38446
|
function shortenPath(p) {
|
|
37839
38447
|
const home = homedir();
|
|
37840
38448
|
return p.startsWith(home) ? "~" + p.slice(home.length) : p;
|
|
@@ -37898,40 +38506,61 @@ if (process.env.NODE_ENV !== "production") {
|
|
|
37898
38506
|
res.json({ token: AUTH_TOKEN });
|
|
37899
38507
|
});
|
|
37900
38508
|
}
|
|
38509
|
+
function isOcrDashboardProcess(pid) {
|
|
38510
|
+
if (process.platform === "win32") return false;
|
|
38511
|
+
try {
|
|
38512
|
+
const cmd = execFileSync3("ps", ["-p", String(pid), "-o", "command="], {
|
|
38513
|
+
encoding: "utf-8",
|
|
38514
|
+
timeout: 3e3
|
|
38515
|
+
}).trim();
|
|
38516
|
+
if (/^ocr-dashboard\b/.test(cmd)) return true;
|
|
38517
|
+
return /dashboard\/server\.js|server\/index\.ts/.test(cmd);
|
|
38518
|
+
} catch {
|
|
38519
|
+
return false;
|
|
38520
|
+
}
|
|
38521
|
+
}
|
|
37901
38522
|
async function startServer(options = {}) {
|
|
37902
38523
|
const port = options.port ?? parseInt(process.env.PORT ?? "4173", 10);
|
|
38524
|
+
process.title = "ocr-dashboard";
|
|
37903
38525
|
const ocrDir = resolveOcrDir();
|
|
37904
38526
|
const aiCliService = new AiCliService(ocrDir);
|
|
37905
|
-
const dbPathForCheckpoint =
|
|
38527
|
+
const dbPathForCheckpoint = join19(ocrDir, "data", "ocr.db");
|
|
37906
38528
|
const walResult = walCheckpointTruncate(dbPathForCheckpoint);
|
|
37907
38529
|
if (walResult === "checkpointed") {
|
|
37908
38530
|
console.log(" WAL checkpoint: truncated stale write-ahead-log file");
|
|
37909
38531
|
}
|
|
38532
|
+
for (const reaped of reapOrphanDbFiles(join19(ocrDir, "data"))) {
|
|
38533
|
+
console.log(` Orphan reap: removed stale ${reaped}`);
|
|
38534
|
+
}
|
|
38535
|
+
const staleLogs = reapStaleExecLogs(join19(ocrDir, "data", "exec-logs"));
|
|
38536
|
+
if (staleLogs.length > 0) {
|
|
38537
|
+
console.log(` Exec-log reap: removed ${staleLogs.length} stale agent log(s)`);
|
|
38538
|
+
}
|
|
37910
38539
|
const db = await openDb(ocrDir);
|
|
37911
|
-
const dataDir =
|
|
37912
|
-
const pidFilePath =
|
|
37913
|
-
const portFilePath =
|
|
37914
|
-
|
|
38540
|
+
const dataDir = join19(ocrDir, "data");
|
|
38541
|
+
const pidFilePath = join19(dataDir, "dashboard.pid");
|
|
38542
|
+
const portFilePath = join19(dataDir, "server-port");
|
|
38543
|
+
mkdirSync7(dataDir, { recursive: true });
|
|
37915
38544
|
try {
|
|
37916
|
-
|
|
38545
|
+
unlinkSync5(portFilePath);
|
|
37917
38546
|
} catch {
|
|
37918
38547
|
}
|
|
37919
|
-
if (
|
|
38548
|
+
if (existsSync17(pidFilePath)) {
|
|
37920
38549
|
try {
|
|
37921
38550
|
const oldPid = parseInt(readFileSync12(pidFilePath, "utf-8").trim(), 10);
|
|
37922
|
-
if (!isNaN(oldPid)) {
|
|
37923
|
-
|
|
37924
|
-
|
|
37925
|
-
|
|
37926
|
-
|
|
37927
|
-
);
|
|
37928
|
-
} catch {
|
|
38551
|
+
if (!isNaN(oldPid) && oldPid !== process.pid && isProcessAlive(oldPid) && isOcrDashboardProcess(oldPid)) {
|
|
38552
|
+
console.log(` Single-instance: reaping prior dashboard server (PID ${oldPid}) and taking over`);
|
|
38553
|
+
reapTree(oldPid);
|
|
38554
|
+
const deadline = Date.now() + 6e3;
|
|
38555
|
+
while (isProcessAlive(oldPid) && Date.now() < deadline) {
|
|
38556
|
+
await new Promise((resolve3) => setTimeout(resolve3, 100));
|
|
37929
38557
|
}
|
|
38558
|
+
walCheckpointTruncate(dbPathForCheckpoint);
|
|
37930
38559
|
}
|
|
37931
38560
|
} catch {
|
|
37932
38561
|
}
|
|
37933
38562
|
}
|
|
37934
|
-
|
|
38563
|
+
writeFileSync6(pidFilePath, String(process.pid), { mode: 384 });
|
|
37935
38564
|
const cmdCountResult = db.exec("SELECT COUNT(*) as c FROM command_executions");
|
|
37936
38565
|
const totalCmds = cmdCountResult[0]?.values[0]?.[0] ?? 0;
|
|
37937
38566
|
if (totalCmds === 0) {
|
|
@@ -37941,7 +38570,7 @@ async function startServer(options = {}) {
|
|
|
37941
38570
|
}
|
|
37942
38571
|
}
|
|
37943
38572
|
const orphanResult = db.exec(
|
|
37944
|
-
`SELECT id, pid,
|
|
38573
|
+
`SELECT id, pid, started_at FROM command_executions
|
|
37945
38574
|
WHERE pid IS NOT NULL AND finished_at IS NULL`
|
|
37946
38575
|
);
|
|
37947
38576
|
if (orphanResult.length > 0 && orphanResult[0]) {
|
|
@@ -37951,37 +38580,15 @@ async function startServer(options = {}) {
|
|
|
37951
38580
|
let killedCount = 0;
|
|
37952
38581
|
for (const row of orphanRows) {
|
|
37953
38582
|
const pid = row[colIdx["pid"]];
|
|
37954
|
-
const isDetached = row[colIdx["is_detached"]] === 1;
|
|
37955
38583
|
const startedAt = row[colIdx["started_at"]];
|
|
37956
38584
|
if (sqliteUtcMs(startedAt) < cutoff) continue;
|
|
37957
38585
|
if (defaultIsAlive(pid)) {
|
|
37958
|
-
|
|
37959
|
-
try {
|
|
37960
|
-
process.kill(-pid, "SIGTERM");
|
|
37961
|
-
} catch {
|
|
37962
|
-
process.kill(pid, "SIGTERM");
|
|
37963
|
-
}
|
|
37964
|
-
} else {
|
|
37965
|
-
process.kill(pid, "SIGTERM");
|
|
37966
|
-
}
|
|
38586
|
+
reapTree(pid);
|
|
37967
38587
|
killedCount++;
|
|
37968
|
-
setTimeout(() => {
|
|
37969
|
-
try {
|
|
37970
|
-
process.kill(pid, 0);
|
|
37971
|
-
if (isDetached) {
|
|
37972
|
-
try {
|
|
37973
|
-
process.kill(-pid, "SIGKILL");
|
|
37974
|
-
} catch {
|
|
37975
|
-
}
|
|
37976
|
-
}
|
|
37977
|
-
process.kill(pid, "SIGKILL");
|
|
37978
|
-
} catch {
|
|
37979
|
-
}
|
|
37980
|
-
}, 2e3);
|
|
37981
38588
|
}
|
|
37982
38589
|
}
|
|
37983
38590
|
if (killedCount > 0) {
|
|
37984
|
-
console.log(`
|
|
38591
|
+
console.log(` Reaped ${killedCount} orphaned process tree(s)`);
|
|
37985
38592
|
}
|
|
37986
38593
|
}
|
|
37987
38594
|
const legacyResult = db.exec(
|
|
@@ -37991,10 +38598,11 @@ async function startServer(options = {}) {
|
|
|
37991
38598
|
if (legacyCount > 0) {
|
|
37992
38599
|
db.run(
|
|
37993
38600
|
`UPDATE command_executions
|
|
37994
|
-
SET exit_code =
|
|
38601
|
+
SET exit_code = ?,
|
|
37995
38602
|
output = COALESCE(output, '') || '
|
|
37996
38603
|
[Cancelled]'
|
|
37997
|
-
WHERE finished_at IS NOT NULL AND exit_code IS NULL
|
|
38604
|
+
WHERE finished_at IS NOT NULL AND exit_code IS NULL`,
|
|
38605
|
+
[CANCELLED_EXIT_CODE]
|
|
37998
38606
|
);
|
|
37999
38607
|
console.log(` Backfilled ${legacyCount} finished command(s) missing an exit code`);
|
|
38000
38608
|
}
|
|
@@ -38017,11 +38625,25 @@ async function startServer(options = {}) {
|
|
|
38017
38625
|
` Auto-closed ${staleSessionResult.closedSessionIds.length} stale active session(s) (threshold 7 days)`
|
|
38018
38626
|
);
|
|
38019
38627
|
}
|
|
38628
|
+
const reconcileCompleted = async () => {
|
|
38629
|
+
try {
|
|
38630
|
+
const closed = await reconcileCompletedSessions(ocrDir);
|
|
38631
|
+
if (closed.length > 0) {
|
|
38632
|
+
console.log(
|
|
38633
|
+
` Auto-finalized ${closed.length} completed-but-open session(s)`
|
|
38634
|
+
);
|
|
38635
|
+
}
|
|
38636
|
+
} catch (err) {
|
|
38637
|
+
console.error("[reconcile] completed-session reconciliation failed:", err);
|
|
38638
|
+
}
|
|
38639
|
+
};
|
|
38640
|
+
await reconcileCompleted();
|
|
38020
38641
|
const SWEEP_INTERVAL_MS = 5 * 60 * 1e3;
|
|
38021
38642
|
const sweepTimer = setInterval(() => {
|
|
38022
38643
|
try {
|
|
38023
38644
|
logAgentSweep(sweepStaleAgentSessions(db, heartbeatSeconds, defaultIsAlive));
|
|
38024
38645
|
sweepStaleSessions(db, STALE_SESSION_THRESHOLD_SECONDS);
|
|
38646
|
+
void reconcileCompleted();
|
|
38025
38647
|
} catch (err) {
|
|
38026
38648
|
console.error("[sweep] periodic sweep failed:", err);
|
|
38027
38649
|
}
|
|
@@ -38057,11 +38679,11 @@ async function startServer(options = {}) {
|
|
|
38057
38679
|
app.use("/api/agent-sessions", createAgentSessionsRouter(db, () => pullSync()));
|
|
38058
38680
|
app.use("/api/sessions", createHandoffRouter(sessionCapture, ocrDir, () => pullSync()));
|
|
38059
38681
|
app.use("/api/team", createTeamRouter(ocrDir));
|
|
38060
|
-
const clientDir =
|
|
38061
|
-
if (process.env.NODE_ENV === "production" &&
|
|
38682
|
+
const clientDir = join19(__dirname3, "client");
|
|
38683
|
+
if (process.env.NODE_ENV === "production" && existsSync17(clientDir)) {
|
|
38062
38684
|
app.use(import_express15.default.static(clientDir, { index: false }));
|
|
38063
|
-
const indexHtmlPath =
|
|
38064
|
-
const rawIndexHtml =
|
|
38685
|
+
const indexHtmlPath = join19(clientDir, "index.html");
|
|
38686
|
+
const rawIndexHtml = existsSync17(indexHtmlPath) ? readFileSync12(indexHtmlPath, "utf-8") : "";
|
|
38065
38687
|
const tokenScript = `<script>window.__OCR_TOKEN__=${JSON.stringify(AUTH_TOKEN)};</script>`;
|
|
38066
38688
|
const injectedIndexHtml = rawIndexHtml.replace(
|
|
38067
38689
|
"</head>",
|
|
@@ -38080,7 +38702,7 @@ async function startServer(options = {}) {
|
|
|
38080
38702
|
registerChatHandlers(io, socket, db, ocrDir, aiCliService);
|
|
38081
38703
|
registerPostHandlers(io, socket, db, ocrDir, aiCliService);
|
|
38082
38704
|
});
|
|
38083
|
-
const dbFilePath =
|
|
38705
|
+
const dbFilePath = join19(ocrDir, "data", "ocr.db");
|
|
38084
38706
|
const dbSyncWatcher = new DbSyncWatcher(
|
|
38085
38707
|
db,
|
|
38086
38708
|
dbFilePath,
|
|
@@ -38096,7 +38718,7 @@ async function startServer(options = {}) {
|
|
|
38096
38718
|
dbSyncWatcher.startWatching();
|
|
38097
38719
|
pullSync = () => dbSyncWatcher.syncFromDisk();
|
|
38098
38720
|
console.log(` Watching DB: ${shortenPath(dbFilePath)}`);
|
|
38099
|
-
const sessionsDir =
|
|
38721
|
+
const sessionsDir = join19(ocrDir, "sessions");
|
|
38100
38722
|
const fsSync = new FilesystemSync(db, sessionsDir, io);
|
|
38101
38723
|
await fsSync.fullScan();
|
|
38102
38724
|
fsSync.startWatching();
|
|
@@ -38135,7 +38757,7 @@ async function startServer(options = {}) {
|
|
|
38135
38757
|
if (actualPort !== port) {
|
|
38136
38758
|
console.log(` Note: using port ${actualPort} (${port} was in use)`);
|
|
38137
38759
|
}
|
|
38138
|
-
|
|
38760
|
+
writeFileSync6(portFilePath, String(actualPort), { mode: 384 });
|
|
38139
38761
|
console.log(` Server: http://localhost:${actualPort}`);
|
|
38140
38762
|
console.log(` OCR directory: ${shortenPath(ocrDir)}`);
|
|
38141
38763
|
console.log();
|
|
@@ -38148,53 +38770,40 @@ async function startServer(options = {}) {
|
|
|
38148
38770
|
} catch {
|
|
38149
38771
|
}
|
|
38150
38772
|
}
|
|
38151
|
-
const shutdown = (signal) => {
|
|
38773
|
+
const shutdown = async (signal) => {
|
|
38152
38774
|
console.log(
|
|
38153
38775
|
`Shutting down dashboard server${signal ? ` (received ${signal})` : ""}...`
|
|
38154
38776
|
);
|
|
38155
38777
|
try {
|
|
38156
|
-
|
|
38157
|
-
} catch {
|
|
38158
|
-
}
|
|
38159
|
-
try {
|
|
38160
|
-
unlinkSync3(portFilePath);
|
|
38778
|
+
unlinkSync5(pidFilePath);
|
|
38161
38779
|
} catch {
|
|
38162
38780
|
}
|
|
38163
38781
|
try {
|
|
38164
|
-
|
|
38782
|
+
unlinkSync5(portFilePath);
|
|
38165
38783
|
} catch {
|
|
38166
38784
|
}
|
|
38785
|
+
clearSpawnMarker(ocrDir);
|
|
38167
38786
|
try {
|
|
38168
38787
|
const activeResult = db.exec(
|
|
38169
|
-
"SELECT id, pid
|
|
38788
|
+
"SELECT id, pid FROM command_executions WHERE pid IS NOT NULL AND finished_at IS NULL"
|
|
38170
38789
|
);
|
|
38171
38790
|
if (activeResult.length > 0 && activeResult[0]) {
|
|
38172
38791
|
const { columns, values: activeRows } = activeResult[0];
|
|
38173
38792
|
const colIdx = Object.fromEntries(columns.map((c, i) => [c, i]));
|
|
38174
38793
|
for (const row of activeRows) {
|
|
38175
38794
|
const pid = row[colIdx["pid"]];
|
|
38176
|
-
|
|
38177
|
-
|
|
38178
|
-
if (isDetached) {
|
|
38179
|
-
try {
|
|
38180
|
-
process.kill(-pid, "SIGTERM");
|
|
38181
|
-
} catch {
|
|
38182
|
-
process.kill(pid, "SIGTERM");
|
|
38183
|
-
}
|
|
38184
|
-
} else {
|
|
38185
|
-
process.kill(pid, "SIGTERM");
|
|
38186
|
-
}
|
|
38187
|
-
console.log(`Sent SIGTERM to child process (PID ${pid})`);
|
|
38188
|
-
} catch {
|
|
38189
|
-
}
|
|
38795
|
+
reapTree(pid, 750);
|
|
38796
|
+
console.log(`Reaping child process tree (PID ${pid})`);
|
|
38190
38797
|
}
|
|
38798
|
+
await new Promise((resolve3) => setTimeout(resolve3, 1e3));
|
|
38191
38799
|
db.run(
|
|
38192
38800
|
`UPDATE command_executions
|
|
38193
|
-
SET exit_code =
|
|
38801
|
+
SET exit_code = ?, finished_at = datetime('now'),
|
|
38194
38802
|
output = COALESCE(output, '') || '
|
|
38195
38803
|
[Cancelled \u2014 server shutdown]',
|
|
38196
38804
|
pid = NULL
|
|
38197
|
-
WHERE pid IS NOT NULL AND finished_at IS NULL
|
|
38805
|
+
WHERE pid IS NOT NULL AND finished_at IS NULL`,
|
|
38806
|
+
[CANCELLED_EXIT_CODE]
|
|
38198
38807
|
);
|
|
38199
38808
|
}
|
|
38200
38809
|
} catch (err) {
|
|
@@ -38217,9 +38826,9 @@ async function startServer(options = {}) {
|
|
|
38217
38826
|
process.exit(1);
|
|
38218
38827
|
}, 2e3).unref();
|
|
38219
38828
|
};
|
|
38220
|
-
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
38221
|
-
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
38222
|
-
process.on("SIGHUP", () => shutdown("SIGHUP"));
|
|
38829
|
+
process.on("SIGINT", () => void shutdown("SIGINT"));
|
|
38830
|
+
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
|
38831
|
+
process.on("SIGHUP", () => void shutdown("SIGHUP"));
|
|
38223
38832
|
process.on("uncaughtException", (err) => {
|
|
38224
38833
|
console.error("[dashboard] uncaughtException:", err);
|
|
38225
38834
|
});
|