@open-code-review/cli 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dashboard/client/assets/{_basePickBy-B3ALyupE.js → _basePickBy-BBPb8BJA.js} +1 -1
- package/dist/dashboard/client/assets/{_baseUniq-b2RALAWc.js → _baseUniq-CFHdos6T.js} +1 -1
- package/dist/dashboard/client/assets/{arc-DcSVvhUd.js → arc-BKGGWA2F.js} +1 -1
- package/dist/dashboard/client/assets/{architectureDiagram-VXUJARFQ-BNUlmSCS.js → architectureDiagram-VXUJARFQ-B_ovNjX1.js} +1 -1
- package/dist/dashboard/client/assets/{blockDiagram-VD42YOAC-BmhiQVwa.js → blockDiagram-VD42YOAC-C2M-avVp.js} +1 -1
- package/dist/dashboard/client/assets/{c4Diagram-YG6GDRKO-jyJ3WOv5.js → c4Diagram-YG6GDRKO-BtOBpAzH.js} +1 -1
- package/dist/dashboard/client/assets/channel-rgw7C1e7.js +1 -0
- package/dist/dashboard/client/assets/{chunk-4BX2VUAB-x1dQU_s3.js → chunk-4BX2VUAB-Cz2EbHPl.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-55IACEB6-CwbsE2XQ.js → chunk-55IACEB6-C8xpXw9G.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-B4BG7PRW-BaE7c-ti.js → chunk-B4BG7PRW-BSRfOovX.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-DI55MBZ5-Bw5PUaMK.js → chunk-DI55MBZ5-CEUbYQWn.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-FMBD7UC4-B7cF6P3s.js → chunk-FMBD7UC4-5xWP6GRj.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-QN33PNHL-OY4evNHd.js → chunk-QN33PNHL-DfNCVcy8.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-QZHKN3VN-BpjQwIWz.js → chunk-QZHKN3VN--OdToKKu.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-TZMSLE5B-D8b_Oq9B.js → chunk-TZMSLE5B-B_0K0Qso.js} +1 -1
- package/dist/dashboard/client/assets/classDiagram-2ON5EDUG-DTGi7d9X.js +1 -0
- package/dist/dashboard/client/assets/classDiagram-v2-WZHVMYZB-DTGi7d9X.js +1 -0
- package/dist/dashboard/client/assets/clone-Cz7hswqi.js +1 -0
- package/dist/dashboard/client/assets/{cose-bilkent-S5V4N54A-C-sfP8PN.js → cose-bilkent-S5V4N54A-Cc_Dmnxz.js} +1 -1
- package/dist/dashboard/client/assets/{dagre-6UL2VRFP-Cqfo0NRg.js → dagre-6UL2VRFP-DaAfvUXU.js} +1 -1
- package/dist/dashboard/client/assets/{diagram-PSM6KHXK-BR3ppxqI.js → diagram-PSM6KHXK-7idwN0rC.js} +1 -1
- package/dist/dashboard/client/assets/{diagram-QEK2KX5R-Dvcx6x3R.js → diagram-QEK2KX5R-D9j9H13n.js} +1 -1
- package/dist/dashboard/client/assets/{diagram-S2PKOQOG-DoyBLnVN.js → diagram-S2PKOQOG-SMF5SB0K.js} +1 -1
- package/dist/dashboard/client/assets/{erDiagram-Q2GNP2WA-hy77l1cL.js → erDiagram-Q2GNP2WA-EVJ4Qa2F.js} +1 -1
- package/dist/dashboard/client/assets/{flowDiagram-NV44I4VS-Bz0B1rKM.js → flowDiagram-NV44I4VS-tZ7SFE77.js} +1 -1
- package/dist/dashboard/client/assets/{ganttDiagram-JELNMOA3-CLgrZPoC.js → ganttDiagram-JELNMOA3-DFSqguY7.js} +1 -1
- package/dist/dashboard/client/assets/{gitGraphDiagram-V2S2FVAM-DwJ-1f-v.js → gitGraphDiagram-V2S2FVAM-CqHdP3HE.js} +1 -1
- package/dist/dashboard/client/assets/{graph-DDBMM_t2.js → graph-C0XnkNkk.js} +1 -1
- package/dist/dashboard/client/assets/{index-Cr9yEo_B.js → index-C3NEq704.js} +133 -138
- package/dist/dashboard/client/assets/index-CzxeSSaQ.css +1 -0
- package/dist/dashboard/client/assets/{infoDiagram-HS3SLOUP-Bhn1FmAk.js → infoDiagram-HS3SLOUP-DlXZo9U2.js} +1 -1
- package/dist/dashboard/client/assets/{journeyDiagram-XKPGCS4Q-CzGbjX1y.js → journeyDiagram-XKPGCS4Q-CgC8_7eN.js} +1 -1
- package/dist/dashboard/client/assets/{kanban-definition-3W4ZIXB7-Da77-WYk.js → kanban-definition-3W4ZIXB7-BMAw_jNp.js} +1 -1
- package/dist/dashboard/client/assets/{layout-CVwSB-GS.js → layout-XjM3Q-ka.js} +1 -1
- package/dist/dashboard/client/assets/{linear-CTRAc5Jn.js → linear-CMUrrr1X.js} +1 -1
- package/dist/dashboard/client/assets/{mermaid-renderer-Bjo170ax.js → mermaid-renderer-D2jYNs7K.js} +4 -4
- package/dist/dashboard/client/assets/{mindmap-definition-VGOIOE7T-B55C2odl.js → mindmap-definition-VGOIOE7T-CL4hv-vg.js} +1 -1
- package/dist/dashboard/client/assets/{pieDiagram-ADFJNKIX-5lrQLrSz.js → pieDiagram-ADFJNKIX-DTqv-1h1.js} +1 -1
- package/dist/dashboard/client/assets/{quadrantDiagram-AYHSOK5B-Bg55gC30.js → quadrantDiagram-AYHSOK5B-BpFlSW9N.js} +1 -1
- package/dist/dashboard/client/assets/{requirementDiagram-UZGBJVZJ-CyR4YFJY.js → requirementDiagram-UZGBJVZJ-BqYqqXL4.js} +1 -1
- package/dist/dashboard/client/assets/{sankeyDiagram-TZEHDZUN-BVWKr9_-.js → sankeyDiagram-TZEHDZUN-kEI9kntR.js} +1 -1
- package/dist/dashboard/client/assets/{sequenceDiagram-WL72ISMW-D0AJg_tE.js → sequenceDiagram-WL72ISMW-Cnu_1j-N.js} +1 -1
- package/dist/dashboard/client/assets/{stateDiagram-FKZM4ZOC-BuHpTgim.js → stateDiagram-FKZM4ZOC-BoC-rqoG.js} +1 -1
- package/dist/dashboard/client/assets/stateDiagram-v2-4FDKWEC3-COR3QD3v.js +1 -0
- package/dist/dashboard/client/assets/{timeline-definition-IT6M3QCI-LDhpAmDd.js → timeline-definition-IT6M3QCI-CXMWuzDL.js} +1 -1
- package/dist/dashboard/client/assets/{treemap-GDKQZRPO-Dd4gjvUl.js → treemap-GDKQZRPO-o9ZFgpbJ.js} +1 -1
- package/dist/dashboard/client/assets/{xychartDiagram-PRI3JC2R-B9RDod39.js → xychartDiagram-PRI3JC2R-CfIuUpeA.js} +1 -1
- package/dist/dashboard/client/index.html +2 -2
- package/dist/dashboard/server.js +1031 -426
- package/dist/index.js +1252 -268
- package/dist/lib/db/index.js +485 -24
- 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-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);
|
|
@@ -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 = [
|
|
@@ -33219,6 +33500,8 @@ var ClaudeCodeAdapter = class {
|
|
|
33219
33500
|
// Claude Code subagent definitions support per-subagent model frontmatter,
|
|
33220
33501
|
// so per-task model overrides are honored at the host level.
|
|
33221
33502
|
supportsPerTaskModel = true;
|
|
33503
|
+
// Claude Code can spawn reviewer sub-agents via its Task tool.
|
|
33504
|
+
supportsSubagentSpawn = true;
|
|
33222
33505
|
buildResumeArgs(vendorSessionId) {
|
|
33223
33506
|
return buildResumeArgs("claude", vendorSessionId);
|
|
33224
33507
|
}
|
|
@@ -33259,15 +33542,25 @@ var ClaudeCodeAdapter = class {
|
|
|
33259
33542
|
if (opts.model) {
|
|
33260
33543
|
flags.push("--model", opts.model);
|
|
33261
33544
|
}
|
|
33545
|
+
const { stdio, logFd, logPath } = buildFileStdio(
|
|
33546
|
+
"pipe",
|
|
33547
|
+
isWorkflow ? opts.logFile : void 0
|
|
33548
|
+
);
|
|
33262
33549
|
const proc = spawnBinary("claude", flags, {
|
|
33263
33550
|
cwd: opts.cwd,
|
|
33264
33551
|
env: { ...cleanEnv(), ...opts.env ?? {} },
|
|
33265
33552
|
detached: isWorkflow,
|
|
33266
|
-
stdio
|
|
33553
|
+
stdio
|
|
33267
33554
|
});
|
|
33555
|
+
closeFileStdio(logFd);
|
|
33556
|
+
if (isWorkflow) proc.unref();
|
|
33268
33557
|
proc.stdin?.write(opts.prompt);
|
|
33269
33558
|
proc.stdin?.end();
|
|
33270
|
-
return {
|
|
33559
|
+
return {
|
|
33560
|
+
process: proc,
|
|
33561
|
+
detached: isWorkflow,
|
|
33562
|
+
...logPath ? { logPath } : {}
|
|
33563
|
+
};
|
|
33271
33564
|
}
|
|
33272
33565
|
async listModels() {
|
|
33273
33566
|
try {
|
|
@@ -33426,6 +33719,13 @@ var ClaudeLineParser = class {
|
|
|
33426
33719
|
const message = typeof parsed["message"] === "string" ? parsed["message"] : "Agent error";
|
|
33427
33720
|
events.push({ type: "error", source: "agent", message });
|
|
33428
33721
|
}
|
|
33722
|
+
if (type === "result") {
|
|
33723
|
+
events.push({
|
|
33724
|
+
type: "result",
|
|
33725
|
+
isError: parsed["is_error"] === true,
|
|
33726
|
+
subtype: typeof parsed["subtype"] === "string" ? parsed["subtype"] : void 0
|
|
33727
|
+
});
|
|
33728
|
+
}
|
|
33429
33729
|
return events;
|
|
33430
33730
|
}
|
|
33431
33731
|
};
|
|
@@ -33461,6 +33761,9 @@ var OpenCodeAdapter = class {
|
|
|
33461
33761
|
// until OpenCode adds per-task model support; OCR surfaces a warning to
|
|
33462
33762
|
// the user when this happens.
|
|
33463
33763
|
supportsPerTaskModel = false;
|
|
33764
|
+
// OpenCode exposes a sub-agent primitive (`--agent`), so reviewer sub-agents
|
|
33765
|
+
// can be spawned in-agent (uniform model — see supportsPerTaskModel above).
|
|
33766
|
+
supportsSubagentSpawn = true;
|
|
33464
33767
|
buildResumeArgs(vendorSessionId) {
|
|
33465
33768
|
return buildResumeArgs("opencode", vendorSessionId);
|
|
33466
33769
|
}
|
|
@@ -33498,13 +33801,23 @@ var OpenCodeAdapter = class {
|
|
|
33498
33801
|
if (opts.model) {
|
|
33499
33802
|
args.push("--model", opts.model);
|
|
33500
33803
|
}
|
|
33804
|
+
const { stdio, logFd, logPath } = buildFileStdio(
|
|
33805
|
+
"ignore",
|
|
33806
|
+
isWorkflow ? opts.logFile : void 0
|
|
33807
|
+
);
|
|
33501
33808
|
const proc = spawnBinary("opencode", args, {
|
|
33502
33809
|
cwd: opts.cwd,
|
|
33503
33810
|
env: { ...cleanEnv(), ...opts.env ?? {} },
|
|
33504
33811
|
detached: isWorkflow,
|
|
33505
|
-
stdio
|
|
33812
|
+
stdio
|
|
33506
33813
|
});
|
|
33507
|
-
|
|
33814
|
+
closeFileStdio(logFd);
|
|
33815
|
+
if (isWorkflow) proc.unref();
|
|
33816
|
+
return {
|
|
33817
|
+
process: proc,
|
|
33818
|
+
detached: isWorkflow,
|
|
33819
|
+
...logPath ? { logPath } : {}
|
|
33820
|
+
};
|
|
33508
33821
|
}
|
|
33509
33822
|
async listModels() {
|
|
33510
33823
|
try {
|
|
@@ -33635,20 +33948,20 @@ function extractToolOutput(part) {
|
|
|
33635
33948
|
// src/server/services/event-journal.ts
|
|
33636
33949
|
import {
|
|
33637
33950
|
createWriteStream,
|
|
33638
|
-
existsSync as
|
|
33639
|
-
mkdirSync as
|
|
33951
|
+
existsSync as existsSync6,
|
|
33952
|
+
mkdirSync as mkdirSync4,
|
|
33640
33953
|
readFileSync as readFileSync2
|
|
33641
33954
|
} from "node:fs";
|
|
33642
|
-
import { join as
|
|
33955
|
+
import { join as join8 } from "node:path";
|
|
33643
33956
|
function eventsDir(ocrDir) {
|
|
33644
|
-
const dir =
|
|
33645
|
-
if (!
|
|
33646
|
-
|
|
33957
|
+
const dir = join8(ocrDir, "data", "events");
|
|
33958
|
+
if (!existsSync6(dir)) {
|
|
33959
|
+
mkdirSync4(dir, { recursive: true });
|
|
33647
33960
|
}
|
|
33648
33961
|
return dir;
|
|
33649
33962
|
}
|
|
33650
33963
|
function eventJournalPath(ocrDir, executionId) {
|
|
33651
|
-
return
|
|
33964
|
+
return join8(eventsDir(ocrDir), `${executionId}.jsonl`);
|
|
33652
33965
|
}
|
|
33653
33966
|
var EventJournalAppender = class {
|
|
33654
33967
|
stream;
|
|
@@ -33685,7 +33998,7 @@ var EventJournalAppender = class {
|
|
|
33685
33998
|
};
|
|
33686
33999
|
function readEventJournal(ocrDir, executionId) {
|
|
33687
34000
|
const path2 = eventJournalPath(ocrDir, executionId);
|
|
33688
|
-
if (!
|
|
34001
|
+
if (!existsSync6(path2)) return [];
|
|
33689
34002
|
let raw;
|
|
33690
34003
|
try {
|
|
33691
34004
|
raw = readFileSync2(path2, "utf-8");
|
|
@@ -33705,38 +34018,10 @@ function readEventJournal(ocrDir, executionId) {
|
|
|
33705
34018
|
return events;
|
|
33706
34019
|
}
|
|
33707
34020
|
|
|
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
34021
|
// src/server/services/ai-cli/index.ts
|
|
33737
34022
|
function readAiCliPreference(ocrDir) {
|
|
33738
34023
|
try {
|
|
33739
|
-
const configPath =
|
|
34024
|
+
const configPath = join9(ocrDir, "config.yaml");
|
|
33740
34025
|
const content = readFileSync3(configPath, "utf-8");
|
|
33741
34026
|
const match = content.match(/^\s*ai_cli:\s*(\S+)/m);
|
|
33742
34027
|
const value = match?.[1] ?? "auto";
|
|
@@ -33845,31 +34130,245 @@ var AiCliService = class {
|
|
|
33845
34130
|
}
|
|
33846
34131
|
};
|
|
33847
34132
|
|
|
34133
|
+
// src/server/services/ai-cli/file-tailer.ts
|
|
34134
|
+
import { openSync as openSync2, readSync, closeSync as closeSync2, existsSync as existsSync7 } from "node:fs";
|
|
34135
|
+
import { StringDecoder } from "node:string_decoder";
|
|
34136
|
+
var DEFAULT_POLL_MS = 100;
|
|
34137
|
+
var READ_CHUNK_BYTES = 64 * 1024;
|
|
34138
|
+
var FileTailer = class {
|
|
34139
|
+
constructor(path2, onChunk, pollMs = DEFAULT_POLL_MS) {
|
|
34140
|
+
this.path = path2;
|
|
34141
|
+
this.onChunk = onChunk;
|
|
34142
|
+
this.pollMs = pollMs;
|
|
34143
|
+
}
|
|
34144
|
+
fd = null;
|
|
34145
|
+
offset = 0;
|
|
34146
|
+
decoder = new StringDecoder("utf8");
|
|
34147
|
+
timer = null;
|
|
34148
|
+
buf = Buffer.allocUnsafe(READ_CHUNK_BYTES);
|
|
34149
|
+
stopped = false;
|
|
34150
|
+
/** Begin polling for appended bytes. Idempotent. */
|
|
34151
|
+
start() {
|
|
34152
|
+
if (this.timer || this.stopped) return;
|
|
34153
|
+
this.timer = setInterval(() => this.poll(), this.pollMs);
|
|
34154
|
+
this.timer.unref?.();
|
|
34155
|
+
}
|
|
34156
|
+
ensureOpen() {
|
|
34157
|
+
if (this.fd !== null) return true;
|
|
34158
|
+
if (!existsSync7(this.path)) return false;
|
|
34159
|
+
try {
|
|
34160
|
+
this.fd = openSync2(this.path, "r");
|
|
34161
|
+
} catch {
|
|
34162
|
+
return false;
|
|
34163
|
+
}
|
|
34164
|
+
return true;
|
|
34165
|
+
}
|
|
34166
|
+
/** Read everything currently available from `offset` to EOF. */
|
|
34167
|
+
poll() {
|
|
34168
|
+
if (!this.ensureOpen()) return;
|
|
34169
|
+
let bytes;
|
|
34170
|
+
do {
|
|
34171
|
+
try {
|
|
34172
|
+
bytes = readSync(this.fd, this.buf, 0, this.buf.length, this.offset);
|
|
34173
|
+
} catch {
|
|
34174
|
+
return;
|
|
34175
|
+
}
|
|
34176
|
+
if (bytes > 0) {
|
|
34177
|
+
this.offset += bytes;
|
|
34178
|
+
const chunk = this.decoder.write(this.buf.subarray(0, bytes));
|
|
34179
|
+
if (chunk) this.onChunk(chunk);
|
|
34180
|
+
}
|
|
34181
|
+
} while (bytes === this.buf.length);
|
|
34182
|
+
}
|
|
34183
|
+
/**
|
|
34184
|
+
* Stop tailing: do one final drain to EOF, flush any partial multi-byte
|
|
34185
|
+
* remainder, and close the fd. Safe to call more than once. Synchronous so a
|
|
34186
|
+
* `close` handler can finalize the stream with no lost-tail race.
|
|
34187
|
+
*/
|
|
34188
|
+
stop() {
|
|
34189
|
+
if (this.stopped) return;
|
|
34190
|
+
this.stopped = true;
|
|
34191
|
+
if (this.timer) {
|
|
34192
|
+
clearInterval(this.timer);
|
|
34193
|
+
this.timer = null;
|
|
34194
|
+
}
|
|
34195
|
+
this.poll();
|
|
34196
|
+
const tail = this.decoder.end();
|
|
34197
|
+
if (tail) this.onChunk(tail);
|
|
34198
|
+
if (this.fd !== null) {
|
|
34199
|
+
try {
|
|
34200
|
+
closeSync2(this.fd);
|
|
34201
|
+
} catch {
|
|
34202
|
+
}
|
|
34203
|
+
this.fd = null;
|
|
34204
|
+
}
|
|
34205
|
+
}
|
|
34206
|
+
};
|
|
34207
|
+
|
|
33848
34208
|
// src/server/socket/cli-resolver.ts
|
|
33849
|
-
import { existsSync as
|
|
33850
|
-
import { dirname as
|
|
34209
|
+
import { existsSync as existsSync8 } from "node:fs";
|
|
34210
|
+
import { dirname as dirname6, join as join10 } from "node:path";
|
|
33851
34211
|
import { fileURLToPath } from "node:url";
|
|
33852
|
-
var __dirname =
|
|
34212
|
+
var __dirname = dirname6(fileURLToPath(import.meta.url));
|
|
33853
34213
|
function resolveLocalCli() {
|
|
33854
|
-
const parentDir =
|
|
33855
|
-
const bundledCli =
|
|
33856
|
-
if (
|
|
34214
|
+
const parentDir = join10(__dirname, "..");
|
|
34215
|
+
const bundledCli = join10(parentDir, "index.js");
|
|
34216
|
+
if (existsSync8(bundledCli) && existsSync8(join10(parentDir, "dashboard", "server.js"))) {
|
|
33857
34217
|
return bundledCli;
|
|
33858
34218
|
}
|
|
33859
34219
|
let dir = __dirname;
|
|
33860
34220
|
for (let i = 0; i < 8; i++) {
|
|
33861
|
-
if (
|
|
33862
|
-
const candidate =
|
|
33863
|
-
if (
|
|
34221
|
+
if (existsSync8(join10(dir, "nx.json"))) {
|
|
34222
|
+
const candidate = join10(dir, "packages", "cli", "dist", "index.js");
|
|
34223
|
+
if (existsSync8(candidate)) return candidate;
|
|
33864
34224
|
break;
|
|
33865
34225
|
}
|
|
33866
|
-
const parent =
|
|
34226
|
+
const parent = dirname6(dir);
|
|
33867
34227
|
if (parent === dir) break;
|
|
33868
34228
|
dir = parent;
|
|
33869
34229
|
}
|
|
33870
34230
|
return null;
|
|
33871
34231
|
}
|
|
33872
34232
|
|
|
34233
|
+
// ../cli/src/lib/state/projection.ts
|
|
34234
|
+
var REASON_EVENT_TYPES = [
|
|
34235
|
+
"session_aborted",
|
|
34236
|
+
"session_auto_closed_stale",
|
|
34237
|
+
"session_synced",
|
|
34238
|
+
"session_legacy_import"
|
|
34239
|
+
];
|
|
34240
|
+
var TERMINAL_EVENT_TYPES = /* @__PURE__ */ new Set([
|
|
34241
|
+
"session_closed",
|
|
34242
|
+
...REASON_EVENT_TYPES
|
|
34243
|
+
]);
|
|
34244
|
+
function hasCompletionInvariant(db, session) {
|
|
34245
|
+
const eventType = session.workflow_type === "map" ? "map_completed" : "round_completed";
|
|
34246
|
+
const round = session.workflow_type === "map" ? session.current_map_run : session.current_round;
|
|
34247
|
+
const r = db.exec(
|
|
34248
|
+
`SELECT 1 FROM orchestration_events
|
|
34249
|
+
WHERE session_id = ? AND event_type = ? AND round = ? LIMIT 1`,
|
|
34250
|
+
[session.id, eventType, round]
|
|
34251
|
+
);
|
|
34252
|
+
return (r[0]?.values.length ?? 0) > 0;
|
|
34253
|
+
}
|
|
34254
|
+
|
|
34255
|
+
// ../cli/src/lib/state/index.ts
|
|
34256
|
+
async function stateClose(params) {
|
|
34257
|
+
const { sessionId, ocrDir, abort } = params;
|
|
34258
|
+
const db = await ensureDatabase(ocrDir);
|
|
34259
|
+
const existing = getSession(db, sessionId);
|
|
34260
|
+
if (!existing) {
|
|
34261
|
+
throw new StateError(STATE_EXIT.NOT_FOUND, `Session not found: ${sessionId}`);
|
|
34262
|
+
}
|
|
34263
|
+
if (existing.status === "closed") {
|
|
34264
|
+
console.error(`[ocr] Session already closed: ${sessionId}`);
|
|
34265
|
+
return;
|
|
34266
|
+
}
|
|
34267
|
+
if (!abort && !hasCompletionInvariant(db, existing)) {
|
|
34268
|
+
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`;
|
|
34269
|
+
throw new StateError(
|
|
34270
|
+
STATE_EXIT.INVARIANT_UNMET,
|
|
34271
|
+
`Cannot close session ${sessionId}: ${what}. Run 'ocr state complete-round' to finalize it, or pass --abort to record an abandoned session.`
|
|
34272
|
+
);
|
|
34273
|
+
}
|
|
34274
|
+
const note = "closed by parent workflow close";
|
|
34275
|
+
db.transaction(() => {
|
|
34276
|
+
if (abort) {
|
|
34277
|
+
insertEvent(db, {
|
|
34278
|
+
session_id: sessionId,
|
|
34279
|
+
event_type: "session_aborted",
|
|
34280
|
+
phase: existing.current_phase,
|
|
34281
|
+
phase_number: existing.phase_number,
|
|
34282
|
+
round: existing.current_round
|
|
34283
|
+
});
|
|
34284
|
+
}
|
|
34285
|
+
updateSession(db, sessionId, {
|
|
34286
|
+
status: "closed",
|
|
34287
|
+
current_phase: "complete"
|
|
34288
|
+
});
|
|
34289
|
+
if (!abort) {
|
|
34290
|
+
insertEvent(db, {
|
|
34291
|
+
session_id: sessionId,
|
|
34292
|
+
event_type: "session_closed",
|
|
34293
|
+
phase: "complete",
|
|
34294
|
+
phase_number: existing.phase_number,
|
|
34295
|
+
round: existing.current_round
|
|
34296
|
+
});
|
|
34297
|
+
}
|
|
34298
|
+
cascadeTerminateExecutions(db, sessionId, CASCADE_CLOSE_EXIT_CODE, note);
|
|
34299
|
+
});
|
|
34300
|
+
}
|
|
34301
|
+
async function reconcileWorkflowOnExit(ocrDir, sessionId, db) {
|
|
34302
|
+
db ??= await ensureDatabase(ocrDir);
|
|
34303
|
+
const existing = getSession(db, sessionId);
|
|
34304
|
+
if (!existing) return "not-found";
|
|
34305
|
+
if (existing.status === "closed") return "already-closed";
|
|
34306
|
+
if (!hasCompletionInvariant(db, existing)) return "incomplete";
|
|
34307
|
+
if (hasInFlightDependents(db, sessionId)) return "in-flight";
|
|
34308
|
+
await stateClose({ sessionId, ocrDir, abort: false });
|
|
34309
|
+
return "closed";
|
|
34310
|
+
}
|
|
34311
|
+
async function reconcileCompletedSessions(ocrDir) {
|
|
34312
|
+
const db = await ensureDatabase(ocrDir);
|
|
34313
|
+
const closed = [];
|
|
34314
|
+
for (const s of getAllSessions(db)) {
|
|
34315
|
+
if (s.status !== "active") continue;
|
|
34316
|
+
const outcome = await reconcileWorkflowOnExit(ocrDir, s.id, db);
|
|
34317
|
+
if (outcome === "closed") closed.push(s.id);
|
|
34318
|
+
}
|
|
34319
|
+
return closed;
|
|
34320
|
+
}
|
|
34321
|
+
|
|
34322
|
+
// ../cli/src/lib/runtime-config.ts
|
|
34323
|
+
import { existsSync as existsSync9, readFileSync as readFileSync4 } from "node:fs";
|
|
34324
|
+
import { join as join11 } from "node:path";
|
|
34325
|
+
var DEFAULT_AGENT_HEARTBEAT_SECONDS = 60;
|
|
34326
|
+
var DEFAULT_WORKFLOW_HARD_DEADLINE_MINUTES = 60;
|
|
34327
|
+
function readRuntimePositiveInt(ocrDir, key, defaultValue) {
|
|
34328
|
+
const configPath = join11(ocrDir, "config.yaml");
|
|
34329
|
+
if (!existsSync9(configPath)) return defaultValue;
|
|
34330
|
+
let content;
|
|
34331
|
+
try {
|
|
34332
|
+
content = readFileSync4(configPath, "utf-8");
|
|
34333
|
+
} catch {
|
|
34334
|
+
return defaultValue;
|
|
34335
|
+
}
|
|
34336
|
+
const blockMatch = content.match(
|
|
34337
|
+
new RegExp(
|
|
34338
|
+
String.raw`^runtime:\s*\n(?:\s+[^\n]*\n)*?\s+${key}:\s*([^\s#\n]+)`,
|
|
34339
|
+
"m"
|
|
34340
|
+
)
|
|
34341
|
+
);
|
|
34342
|
+
const inlineMatch = content.match(
|
|
34343
|
+
new RegExp(String.raw`^runtime:\s*\{[^}]*\b${key}:\s*([^\s,}]+)`, "m")
|
|
34344
|
+
);
|
|
34345
|
+
const raw = blockMatch?.[1] ?? inlineMatch?.[1];
|
|
34346
|
+
if (!raw) return defaultValue;
|
|
34347
|
+
const parsed = Number(raw);
|
|
34348
|
+
if (!Number.isFinite(parsed) || parsed <= 0 || !Number.isInteger(parsed)) {
|
|
34349
|
+
process.stderr.write(
|
|
34350
|
+
`[ocr] runtime.${key} is not a positive integer (got "${raw}"); falling back to ${defaultValue}.
|
|
34351
|
+
`
|
|
34352
|
+
);
|
|
34353
|
+
return defaultValue;
|
|
34354
|
+
}
|
|
34355
|
+
return parsed;
|
|
34356
|
+
}
|
|
34357
|
+
function getAgentHeartbeatSeconds(ocrDir) {
|
|
34358
|
+
return readRuntimePositiveInt(
|
|
34359
|
+
ocrDir,
|
|
34360
|
+
"agent_heartbeat_seconds",
|
|
34361
|
+
DEFAULT_AGENT_HEARTBEAT_SECONDS
|
|
34362
|
+
);
|
|
34363
|
+
}
|
|
34364
|
+
function getWorkflowHardDeadlineMs(ocrDir) {
|
|
34365
|
+
return readRuntimePositiveInt(
|
|
34366
|
+
ocrDir,
|
|
34367
|
+
"workflow_hard_deadline_minutes",
|
|
34368
|
+
DEFAULT_WORKFLOW_HARD_DEADLINE_MINUTES
|
|
34369
|
+
) * 60 * 1e3;
|
|
34370
|
+
}
|
|
34371
|
+
|
|
33873
34372
|
// src/server/socket/command-runner.ts
|
|
33874
34373
|
function shellSplit(str) {
|
|
33875
34374
|
const tokens = [];
|
|
@@ -33903,7 +34402,7 @@ var ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
33903
34402
|
]);
|
|
33904
34403
|
var AI_COMMANDS = /* @__PURE__ */ new Set(["map", "review", "translate-review-to-single-human", "address", "create-reviewer", "sync-reviewers"]);
|
|
33905
34404
|
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");
|
|
34405
|
+
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
34406
|
}
|
|
33908
34407
|
function buildPrompt(opts) {
|
|
33909
34408
|
const { baseCommand, subArgs, commandContent, executionUid, localCli } = opts;
|
|
@@ -34050,23 +34549,45 @@ function extractPerInstanceModels(subArgs) {
|
|
|
34050
34549
|
return [...models];
|
|
34051
34550
|
}
|
|
34052
34551
|
var MAX_CONCURRENT = 3;
|
|
34552
|
+
var WATCHDOG_TICK_MS = 1e4;
|
|
34553
|
+
var POST_RESULT_GRACE_MS = 3e4;
|
|
34554
|
+
var HEARTBEAT_THROTTLE_MS = 5e3;
|
|
34555
|
+
function decideWatchdogTick(i) {
|
|
34556
|
+
if (i.resultSeenAt !== void 0 && i.nowMs - i.resultSeenAt > i.postResultGraceMs) {
|
|
34557
|
+
return {
|
|
34558
|
+
action: "finalize",
|
|
34559
|
+
reap: !i.exited,
|
|
34560
|
+
exitCode: i.resultIsError ? 1 : 0,
|
|
34561
|
+
reason: "result-grace"
|
|
34562
|
+
};
|
|
34563
|
+
}
|
|
34564
|
+
if (i.nowMs - i.startedAtMs > i.hardDeadlineMs) {
|
|
34565
|
+
return {
|
|
34566
|
+
action: "finalize",
|
|
34567
|
+
reap: !i.exited,
|
|
34568
|
+
exitCode: WATCHDOG_DEADLINE_EXIT_CODE,
|
|
34569
|
+
reason: "hard-deadline"
|
|
34570
|
+
};
|
|
34571
|
+
}
|
|
34572
|
+
return i.exited ? { action: "wait" } : { action: "beat" };
|
|
34573
|
+
}
|
|
34053
34574
|
var activeCommands = /* @__PURE__ */ new Map();
|
|
34054
34575
|
function spawnMarkerPath(ocrDir) {
|
|
34055
|
-
return
|
|
34576
|
+
return join12(ocrDir, "data", "dashboard-active-spawn.json");
|
|
34056
34577
|
}
|
|
34057
34578
|
function writeSpawnMarker(ocrDir, executionUid, pid) {
|
|
34058
|
-
const dataDir =
|
|
34059
|
-
if (!
|
|
34579
|
+
const dataDir = join12(ocrDir, "data");
|
|
34580
|
+
if (!existsSync10(dataDir)) mkdirSync5(dataDir, { recursive: true });
|
|
34060
34581
|
const payload = JSON.stringify({
|
|
34061
34582
|
execution_uid: executionUid,
|
|
34062
34583
|
pid,
|
|
34063
34584
|
started_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
34064
34585
|
});
|
|
34065
|
-
|
|
34586
|
+
writeFileSync3(spawnMarkerPath(ocrDir), payload, { mode: 384 });
|
|
34066
34587
|
}
|
|
34067
34588
|
function clearSpawnMarker(ocrDir) {
|
|
34068
34589
|
try {
|
|
34069
|
-
|
|
34590
|
+
unlinkSync3(spawnMarkerPath(ocrDir));
|
|
34070
34591
|
} catch {
|
|
34071
34592
|
}
|
|
34072
34593
|
}
|
|
@@ -34193,25 +34714,14 @@ function registerCommandHandlers(io2, socket, db, ocrDir, aiCliService, sessionC
|
|
|
34193
34714
|
if (!proc) return;
|
|
34194
34715
|
const pid = proc.pid;
|
|
34195
34716
|
if (entry.detached && pid) {
|
|
34196
|
-
|
|
34197
|
-
process.kill(-pid, "SIGTERM");
|
|
34198
|
-
} catch {
|
|
34199
|
-
proc.kill("SIGTERM");
|
|
34200
|
-
}
|
|
34717
|
+
reapTree(pid);
|
|
34201
34718
|
} else {
|
|
34202
34719
|
proc.kill("SIGTERM");
|
|
34720
|
+
const killTimer = setTimeout(() => {
|
|
34721
|
+
if (activeCommands.has(targetId)) proc.kill("SIGKILL");
|
|
34722
|
+
}, 5e3);
|
|
34723
|
+
proc.once("close", () => clearTimeout(killTimer));
|
|
34203
34724
|
}
|
|
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
34725
|
} catch (err) {
|
|
34216
34726
|
console.error("Error in command:cancel handler:", err);
|
|
34217
34727
|
socket.emit("error", { message: "Internal error" });
|
|
@@ -34220,7 +34730,7 @@ function registerCommandHandlers(io2, socket, db, ocrDir, aiCliService, sessionC
|
|
|
34220
34730
|
}
|
|
34221
34731
|
function spawnCliCommand(io2, db, ocrDir, executionId, baseCommand, subArgs, entry) {
|
|
34222
34732
|
const localCli = resolveLocalCli();
|
|
34223
|
-
const repoRoot =
|
|
34733
|
+
const repoRoot = dirname7(ocrDir);
|
|
34224
34734
|
const proc = localCli ? spawnBinary("node", [localCli, baseCommand, ...subArgs], {
|
|
34225
34735
|
cwd: repoRoot,
|
|
34226
34736
|
env: cleanEnv()
|
|
@@ -34246,8 +34756,7 @@ function spawnCliCommand(io2, db, ocrDir, executionId, baseCommand, subArgs, ent
|
|
|
34246
34756
|
io2.emit("command:output", { execution_id: executionId, content: chunk });
|
|
34247
34757
|
});
|
|
34248
34758
|
proc.on("close", (code) => {
|
|
34249
|
-
|
|
34250
|
-
finishExecution(io2, db, ocrDir, executionId, finalCode, entry.outputBuffer);
|
|
34759
|
+
finishExecution(io2, db, ocrDir, executionId, code ?? -1, entry.outputBuffer);
|
|
34251
34760
|
});
|
|
34252
34761
|
proc.on("error", (err) => {
|
|
34253
34762
|
entry.outputBuffer += `Process error: ${err.message}`;
|
|
@@ -34270,10 +34779,10 @@ function spawnAiCommand(io2, _socket, db, ocrDir, executionId, baseCommand, subA
|
|
|
34270
34779
|
io2.emit("command:output", { execution_id: executionId, content: warning });
|
|
34271
34780
|
}
|
|
34272
34781
|
}
|
|
34273
|
-
const commandMdPath =
|
|
34782
|
+
const commandMdPath = join12(ocrDir, "commands", `${baseCommand}.md`);
|
|
34274
34783
|
let commandContent;
|
|
34275
34784
|
try {
|
|
34276
|
-
commandContent =
|
|
34785
|
+
commandContent = readFileSync5(commandMdPath, "utf-8");
|
|
34277
34786
|
} catch {
|
|
34278
34787
|
const content = `Error: Could not read command file at ${commandMdPath}
|
|
34279
34788
|
`;
|
|
@@ -34317,7 +34826,20 @@ function spawnAiCommand(io2, _socket, db, ocrDir, executionId, baseCommand, subA
|
|
|
34317
34826
|
console.error("Failed to resolve resume context:", err);
|
|
34318
34827
|
}
|
|
34319
34828
|
}
|
|
34320
|
-
const repoRoot =
|
|
34829
|
+
const repoRoot = dirname7(ocrDir);
|
|
34830
|
+
let logFile;
|
|
34831
|
+
if (entry.uid) {
|
|
34832
|
+
try {
|
|
34833
|
+
const logDir = join12(ocrDir, "data", "exec-logs");
|
|
34834
|
+
mkdirSync5(logDir, { recursive: true });
|
|
34835
|
+
logFile = join12(logDir, `${entry.uid}.log`);
|
|
34836
|
+
} catch (err) {
|
|
34837
|
+
console.error(
|
|
34838
|
+
"[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):",
|
|
34839
|
+
err
|
|
34840
|
+
);
|
|
34841
|
+
}
|
|
34842
|
+
}
|
|
34321
34843
|
const spawnOpts = {
|
|
34322
34844
|
mode: "workflow",
|
|
34323
34845
|
prompt,
|
|
@@ -34327,7 +34849,10 @@ function spawnAiCommand(io2, _socket, db, ocrDir, executionId, baseCommand, subA
|
|
|
34327
34849
|
if (resumeSessionId) {
|
|
34328
34850
|
spawnOpts.resumeSessionId = resumeSessionId;
|
|
34329
34851
|
}
|
|
34330
|
-
|
|
34852
|
+
if (logFile) {
|
|
34853
|
+
spawnOpts.logFile = logFile;
|
|
34854
|
+
}
|
|
34855
|
+
const { process: proc, detached, logPath } = adapter.spawn(spawnOpts);
|
|
34331
34856
|
entry.process = proc;
|
|
34332
34857
|
entry.detached = detached;
|
|
34333
34858
|
if (proc.pid) {
|
|
@@ -34363,6 +34888,61 @@ function spawnAiCommand(io2, _socket, db, ocrDir, executionId, baseCommand, subA
|
|
|
34363
34888
|
}
|
|
34364
34889
|
}, POLL_INTERVAL_MS);
|
|
34365
34890
|
entry.linkPoll = linkPoll;
|
|
34891
|
+
const bumpHeartbeat = () => {
|
|
34892
|
+
if (entry.finalized) return;
|
|
34893
|
+
const now = Date.now();
|
|
34894
|
+
if (now - (entry.lastBeatWrite ?? 0) < HEARTBEAT_THROTTLE_MS) return;
|
|
34895
|
+
entry.lastBeatWrite = now;
|
|
34896
|
+
try {
|
|
34897
|
+
db.run(
|
|
34898
|
+
`UPDATE command_executions SET last_heartbeat_at = datetime('now') WHERE id = ? AND finished_at IS NULL`,
|
|
34899
|
+
[executionId]
|
|
34900
|
+
);
|
|
34901
|
+
} catch (err) {
|
|
34902
|
+
console.error("[command-runner] heartbeat bump failed:", err);
|
|
34903
|
+
}
|
|
34904
|
+
};
|
|
34905
|
+
const hardDeadlineMs = getWorkflowHardDeadlineMs(ocrDir);
|
|
34906
|
+
entry.watchdog = setInterval(() => {
|
|
34907
|
+
if (entry.finalized) return;
|
|
34908
|
+
const child = entry.process;
|
|
34909
|
+
const pid = child?.pid;
|
|
34910
|
+
if (!child || !pid) return;
|
|
34911
|
+
const exited = child.exitCode !== null || child.signalCode !== null;
|
|
34912
|
+
const decision = decideWatchdogTick({
|
|
34913
|
+
exited,
|
|
34914
|
+
resultSeenAt: entry.resultSeenAt,
|
|
34915
|
+
resultIsError: entry.resultIsError,
|
|
34916
|
+
startedAtMs: Date.parse(entry.startedAt),
|
|
34917
|
+
nowMs: Date.now(),
|
|
34918
|
+
postResultGraceMs: POST_RESULT_GRACE_MS,
|
|
34919
|
+
hardDeadlineMs
|
|
34920
|
+
});
|
|
34921
|
+
switch (decision.action) {
|
|
34922
|
+
case "beat":
|
|
34923
|
+
bumpHeartbeat();
|
|
34924
|
+
return;
|
|
34925
|
+
case "wait":
|
|
34926
|
+
return;
|
|
34927
|
+
case "finalize": {
|
|
34928
|
+
if (decision.reason === "hard-deadline") {
|
|
34929
|
+
const minutes = Math.round(hardDeadlineMs / 6e4);
|
|
34930
|
+
console.warn(`[watchdog] execution ${executionId}: exceeded hard deadline (${minutes}m) \u2014 finalizing${decision.reap ? " + reaping tree" : ""}`);
|
|
34931
|
+
const notice = `
|
|
34932
|
+
[watchdog] Reaped after exceeding the ${minutes}-minute hard deadline. Raise runtime.workflow_hard_deadline_minutes in .ocr/config.yaml for large reviewer fleets.
|
|
34933
|
+
`;
|
|
34934
|
+
entry.outputBuffer += notice;
|
|
34935
|
+
io2.emit("command:output", { execution_id: executionId, content: notice });
|
|
34936
|
+
} else {
|
|
34937
|
+
console.warn(`[watchdog] execution ${executionId}: result seen but no close after grace \u2014 finalizing${decision.reap ? " + reaping tree" : ""}`);
|
|
34938
|
+
}
|
|
34939
|
+
if (decision.reap) reapTree(pid);
|
|
34940
|
+
finishExecution(io2, db, ocrDir, executionId, decision.exitCode, entry.outputBuffer);
|
|
34941
|
+
return;
|
|
34942
|
+
}
|
|
34943
|
+
}
|
|
34944
|
+
}, WATCHDOG_TICK_MS);
|
|
34945
|
+
entry.watchdog.unref();
|
|
34366
34946
|
io2.emit("command:output", {
|
|
34367
34947
|
execution_id: executionId,
|
|
34368
34948
|
content: `\u25B8 Starting OCR ${baseCommand} workflow...
|
|
@@ -34427,11 +35007,16 @@ function spawnAiCommand(io2, _socket, db, ocrDir, executionId, baseCommand, subA
|
|
|
34427
35007
|
emitStreamEvent(evt);
|
|
34428
35008
|
break;
|
|
34429
35009
|
}
|
|
35010
|
+
case "result": {
|
|
35011
|
+
entry.resultSeenAt = Date.now();
|
|
35012
|
+
entry.resultIsError = evt.isError;
|
|
35013
|
+
emitStreamEvent(evt);
|
|
35014
|
+
break;
|
|
35015
|
+
}
|
|
34430
35016
|
}
|
|
34431
35017
|
}
|
|
34432
|
-
|
|
34433
|
-
|
|
34434
|
-
proc.stdout?.on("data", (chunk) => {
|
|
35018
|
+
function onOutputChunk(chunk) {
|
|
35019
|
+
bumpHeartbeat();
|
|
34435
35020
|
lineBuffer += chunk;
|
|
34436
35021
|
const lines = lineBuffer.split("\n");
|
|
34437
35022
|
lineBuffer = lines.pop() ?? "";
|
|
@@ -34446,17 +35031,30 @@ function spawnAiCommand(io2, _socket, db, ocrDir, executionId, baseCommand, subA
|
|
|
34446
35031
|
handleEvent(evt);
|
|
34447
35032
|
}
|
|
34448
35033
|
}
|
|
34449
|
-
}
|
|
35034
|
+
}
|
|
34450
35035
|
let stderrBuffer = "";
|
|
34451
|
-
|
|
34452
|
-
|
|
34453
|
-
|
|
35036
|
+
if (logPath) {
|
|
35037
|
+
const tailer = new FileTailer(logPath, onOutputChunk);
|
|
35038
|
+
tailer.start();
|
|
35039
|
+
entry.tailer = tailer;
|
|
35040
|
+
} else {
|
|
35041
|
+
proc.stdout?.setEncoding("utf-8");
|
|
35042
|
+
proc.stderr?.setEncoding("utf-8");
|
|
35043
|
+
proc.stdout?.on("data", onOutputChunk);
|
|
35044
|
+
proc.stderr?.on("data", (chunk) => {
|
|
35045
|
+
stderrBuffer += chunk;
|
|
35046
|
+
});
|
|
35047
|
+
}
|
|
34454
35048
|
proc.on("close", (code) => {
|
|
34455
35049
|
if (entry.linkPoll) {
|
|
34456
35050
|
clearInterval(entry.linkPoll);
|
|
34457
35051
|
entry.linkPoll = void 0;
|
|
34458
35052
|
}
|
|
34459
35053
|
clearSpawnMarker(ocrDir);
|
|
35054
|
+
if (entry.tailer) {
|
|
35055
|
+
entry.tailer.stop();
|
|
35056
|
+
entry.tailer = void 0;
|
|
35057
|
+
}
|
|
34460
35058
|
if (lineBuffer.trim()) {
|
|
34461
35059
|
const events = parser.parseLine(lineBuffer);
|
|
34462
35060
|
for (const evt of events) {
|
|
@@ -34480,8 +35078,7 @@ ${stderrBuffer}`;
|
|
|
34480
35078
|
journal.close().catch((err) => {
|
|
34481
35079
|
console.error("[event-journal] close failed:", err);
|
|
34482
35080
|
});
|
|
34483
|
-
|
|
34484
|
-
finishExecution(io2, db, ocrDir, executionId, finalCode, entry.outputBuffer);
|
|
35081
|
+
finishExecution(io2, db, ocrDir, executionId, code ?? -1, entry.outputBuffer);
|
|
34485
35082
|
});
|
|
34486
35083
|
proc.on("error", (err) => {
|
|
34487
35084
|
if (entry.linkPoll) {
|
|
@@ -34495,15 +35092,28 @@ ${stderrBuffer}`;
|
|
|
34495
35092
|
finishExecution(io2, db, ocrDir, executionId, -1, entry.outputBuffer);
|
|
34496
35093
|
});
|
|
34497
35094
|
}
|
|
34498
|
-
function finishExecution(io2, db, ocrDir, executionId,
|
|
35095
|
+
function finishExecution(io2, db, ocrDir, executionId, rawCode, output) {
|
|
34499
35096
|
const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
34500
35097
|
const entry = activeCommands.get(executionId);
|
|
34501
|
-
|
|
35098
|
+
const code = entry?.cancelled ? CANCELLED_EXIT_CODE : rawCode;
|
|
35099
|
+
if (entry?.finalized) return;
|
|
35100
|
+
if (entry) {
|
|
35101
|
+
entry.finalized = true;
|
|
35102
|
+
if (entry.watchdog) {
|
|
35103
|
+
clearInterval(entry.watchdog);
|
|
35104
|
+
entry.watchdog = void 0;
|
|
35105
|
+
}
|
|
35106
|
+
if (entry.tailer) {
|
|
35107
|
+
entry.tailer.stop();
|
|
35108
|
+
entry.tailer = void 0;
|
|
35109
|
+
}
|
|
35110
|
+
}
|
|
35111
|
+
const res = db.prepare(
|
|
34502
35112
|
`UPDATE command_executions
|
|
34503
|
-
|
|
34504
|
-
|
|
34505
|
-
|
|
34506
|
-
);
|
|
35113
|
+
SET exit_code = ?, finished_at = ?, output = ?, pid = NULL
|
|
35114
|
+
WHERE id = ? AND finished_at IS NULL`
|
|
35115
|
+
).run(code, finishedAt, output, executionId);
|
|
35116
|
+
if (Number(res.changes) === 0 && !entry) return;
|
|
34507
35117
|
const completeness = getWorkflowCompletenessForExecution(db, executionId);
|
|
34508
35118
|
const outcome = deriveCommandOutcome(code, completeness);
|
|
34509
35119
|
const cancellationReason = deriveCancellationReason(code);
|
|
@@ -34518,7 +35128,7 @@ function finishExecution(io2, db, ocrDir, executionId, code, output) {
|
|
|
34518
35128
|
started_at: entry.startedAt,
|
|
34519
35129
|
finished_at: finishedAt,
|
|
34520
35130
|
is_detached: entry.detached ? 1 : 0,
|
|
34521
|
-
event: code ===
|
|
35131
|
+
event: code === CANCELLED_EXIT_CODE ? "cancel" : "finish",
|
|
34522
35132
|
writer: "dashboard"
|
|
34523
35133
|
});
|
|
34524
35134
|
}
|
|
@@ -34530,6 +35140,27 @@ function finishExecution(io2, db, ocrDir, executionId, code, output) {
|
|
|
34530
35140
|
cancellation_reason: cancellationReason
|
|
34531
35141
|
});
|
|
34532
35142
|
activeCommands.delete(executionId);
|
|
35143
|
+
const workflowRow = db.exec(
|
|
35144
|
+
"SELECT workflow_id FROM command_executions WHERE id = ?",
|
|
35145
|
+
[executionId]
|
|
35146
|
+
);
|
|
35147
|
+
const workflowId = workflowRow[0]?.values[0]?.[0];
|
|
35148
|
+
if (typeof workflowId === "string" && workflowId.length > 0) {
|
|
35149
|
+
void reconcileWorkflowOnExit(ocrDir, workflowId, db).then((outcome2) => {
|
|
35150
|
+
if (outcome2 === "closed") {
|
|
35151
|
+
console.log(`[command-runner] auto-finalized workflow ${workflowId}`);
|
|
35152
|
+
} else if (outcome2 === "incomplete" || outcome2 === "in-flight") {
|
|
35153
|
+
console.debug(
|
|
35154
|
+
`[command-runner] workflow ${workflowId} not finalized: ${outcome2}`
|
|
35155
|
+
);
|
|
35156
|
+
}
|
|
35157
|
+
}).catch((err) => {
|
|
35158
|
+
console.error(
|
|
35159
|
+
`[command-runner] reconcileWorkflowOnExit(${workflowId}) failed:`,
|
|
35160
|
+
err instanceof Error ? err.message : err
|
|
35161
|
+
);
|
|
35162
|
+
});
|
|
35163
|
+
}
|
|
34533
35164
|
}
|
|
34534
35165
|
|
|
34535
35166
|
// src/server/routes/commands.ts
|
|
@@ -34617,8 +35248,8 @@ function createCommandsRouter(db, ocrDir) {
|
|
|
34617
35248
|
|
|
34618
35249
|
// src/server/routes/config.ts
|
|
34619
35250
|
var import_express9 = __toESM(require_express2(), 1);
|
|
34620
|
-
import { readFileSync as
|
|
34621
|
-
import { join as
|
|
35251
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "node:fs";
|
|
35252
|
+
import { join as join13, dirname as dirname8, basename as basename2 } from "node:path";
|
|
34622
35253
|
var VALID_IDES = ["vscode", "cursor", "windsurf", "jetbrains", "sublime"];
|
|
34623
35254
|
function detectIde() {
|
|
34624
35255
|
const termProgram = process.env.TERM_PROGRAM?.toLowerCase() ?? "";
|
|
@@ -34635,8 +35266,8 @@ function detectIde() {
|
|
|
34635
35266
|
}
|
|
34636
35267
|
function readIdeFromConfig(ocrDir) {
|
|
34637
35268
|
try {
|
|
34638
|
-
const configPath =
|
|
34639
|
-
const content =
|
|
35269
|
+
const configPath = join13(ocrDir, "config.yaml");
|
|
35270
|
+
const content = readFileSync6(configPath, "utf-8");
|
|
34640
35271
|
const match = content.match(/^\s*ide:\s*(\S+)/m);
|
|
34641
35272
|
return match?.[1] ?? "auto";
|
|
34642
35273
|
} catch {
|
|
@@ -34663,8 +35294,8 @@ function detectGitBranch(cwd) {
|
|
|
34663
35294
|
}
|
|
34664
35295
|
function createConfigRouter(ocrDir, aiCliService) {
|
|
34665
35296
|
const router = (0, import_express9.Router)();
|
|
34666
|
-
const projectRoot =
|
|
34667
|
-
const workspaceName =
|
|
35297
|
+
const projectRoot = dirname8(ocrDir);
|
|
35298
|
+
const workspaceName = basename2(projectRoot);
|
|
34668
35299
|
const gitBranch = detectGitBranch(projectRoot);
|
|
34669
35300
|
router.get("/", (_req, res) => {
|
|
34670
35301
|
res.json({
|
|
@@ -34682,8 +35313,8 @@ function createConfigRouter(ocrDir, aiCliService) {
|
|
|
34682
35313
|
return;
|
|
34683
35314
|
}
|
|
34684
35315
|
try {
|
|
34685
|
-
const configPath =
|
|
34686
|
-
let content =
|
|
35316
|
+
const configPath = join13(ocrDir, "config.yaml");
|
|
35317
|
+
let content = readFileSync6(configPath, "utf-8");
|
|
34687
35318
|
if (content.match(/^\s*ide:\s*\S+/m)) {
|
|
34688
35319
|
content = content.replace(/^(\s*ide:\s*)\S+/m, `$1${ide}`);
|
|
34689
35320
|
} else if (content.includes("dashboard:")) {
|
|
@@ -34695,7 +35326,7 @@ dashboard:
|
|
|
34695
35326
|
ide: ${ide}
|
|
34696
35327
|
`;
|
|
34697
35328
|
}
|
|
34698
|
-
|
|
35329
|
+
writeFileSync4(configPath, content);
|
|
34699
35330
|
res.json({ ide });
|
|
34700
35331
|
} catch (err) {
|
|
34701
35332
|
console.error("Failed to update config:", err);
|
|
@@ -34771,17 +35402,20 @@ function createChatRouter(db) {
|
|
|
34771
35402
|
|
|
34772
35403
|
// src/server/routes/reviewers.ts
|
|
34773
35404
|
var import_express11 = __toESM(require_express2(), 1);
|
|
34774
|
-
import { readFileSync as
|
|
34775
|
-
import { join as
|
|
35405
|
+
import { readFileSync as readFileSync7, existsSync as existsSync11, watch } from "node:fs";
|
|
35406
|
+
import { join as join14 } from "node:path";
|
|
34776
35407
|
function readReviewersMeta(ocrDir) {
|
|
34777
|
-
const metaPath =
|
|
34778
|
-
if (!
|
|
35408
|
+
const metaPath = join14(ocrDir, "reviewers-meta.json");
|
|
35409
|
+
if (!existsSync11(metaPath)) {
|
|
34779
35410
|
return { reviewers: [], defaults: [] };
|
|
34780
35411
|
}
|
|
34781
35412
|
try {
|
|
34782
|
-
const raw =
|
|
35413
|
+
const raw = readFileSync7(metaPath, "utf-8");
|
|
34783
35414
|
const meta = JSON.parse(raw);
|
|
34784
|
-
const reviewers = meta.reviewers ?? []
|
|
35415
|
+
const reviewers = (meta.reviewers ?? []).map((r) => ({
|
|
35416
|
+
...r,
|
|
35417
|
+
icon: r.icon || defaultIconFor(r.id, r.tier)
|
|
35418
|
+
}));
|
|
34785
35419
|
const defaults = reviewers.filter((r) => r.is_default).map((r) => r.id);
|
|
34786
35420
|
return { reviewers, defaults };
|
|
34787
35421
|
} catch {
|
|
@@ -34800,13 +35434,13 @@ function createReviewersRouter(ocrDir) {
|
|
|
34800
35434
|
res.status(400).json({ error: "Invalid reviewer ID" });
|
|
34801
35435
|
return;
|
|
34802
35436
|
}
|
|
34803
|
-
const filePath =
|
|
34804
|
-
if (!
|
|
35437
|
+
const filePath = join14(ocrDir, "skills", "references", "reviewers", `${id}.md`);
|
|
35438
|
+
if (!existsSync11(filePath)) {
|
|
34805
35439
|
res.status(404).json({ error: "Reviewer not found", id });
|
|
34806
35440
|
return;
|
|
34807
35441
|
}
|
|
34808
35442
|
try {
|
|
34809
|
-
const content =
|
|
35443
|
+
const content = readFileSync7(filePath, "utf-8");
|
|
34810
35444
|
res.json({ id, content });
|
|
34811
35445
|
} catch {
|
|
34812
35446
|
res.status(500).json({ error: "Failed to read reviewer file", id });
|
|
@@ -34815,7 +35449,7 @@ function createReviewersRouter(ocrDir) {
|
|
|
34815
35449
|
return router;
|
|
34816
35450
|
}
|
|
34817
35451
|
function watchReviewersMeta(ocrDir, io2) {
|
|
34818
|
-
const metaPath =
|
|
35452
|
+
const metaPath = join14(ocrDir, "reviewers-meta.json");
|
|
34819
35453
|
let watcher = null;
|
|
34820
35454
|
let debounce;
|
|
34821
35455
|
try {
|
|
@@ -34861,11 +35495,11 @@ function createAgentSessionsRouter(db, syncFromDisk = () => {
|
|
|
34861
35495
|
|
|
34862
35496
|
// src/server/routes/handoff.ts
|
|
34863
35497
|
var import_express13 = __toESM(require_express2(), 1);
|
|
34864
|
-
import { dirname as
|
|
35498
|
+
import { dirname as dirname9 } from "node:path";
|
|
34865
35499
|
function createHandoffRouter(sessionCapture, ocrDir, syncFromDisk = () => {
|
|
34866
35500
|
}) {
|
|
34867
35501
|
const router = (0, import_express13.Router)();
|
|
34868
|
-
const projectDir =
|
|
35502
|
+
const projectDir = dirname9(ocrDir);
|
|
34869
35503
|
router.get("/:id/handoff", (req, res) => {
|
|
34870
35504
|
const workflowId = req.params["id"];
|
|
34871
35505
|
if (!workflowId) {
|
|
@@ -34895,14 +35529,14 @@ import { spawnSync } from "node:child_process";
|
|
|
34895
35529
|
|
|
34896
35530
|
// ../cli/src/lib/team-config.ts
|
|
34897
35531
|
var import_yaml = __toESM(require_dist(), 1);
|
|
34898
|
-
import { existsSync as
|
|
34899
|
-
import { join as
|
|
35532
|
+
import { existsSync as existsSync12, readFileSync as readFileSync8 } from "node:fs";
|
|
35533
|
+
import { join as join15 } from "node:path";
|
|
34900
35534
|
function loadTeamConfig(ocrDir) {
|
|
34901
|
-
const configPath =
|
|
34902
|
-
if (!
|
|
35535
|
+
const configPath = join15(ocrDir, "config.yaml");
|
|
35536
|
+
if (!existsSync12(configPath)) {
|
|
34903
35537
|
return { team: [], aliases: {}, defaultModel: null };
|
|
34904
35538
|
}
|
|
34905
|
-
const content =
|
|
35539
|
+
const content = readFileSync8(configPath, "utf-8");
|
|
34906
35540
|
return parseTeamConfigYaml(content);
|
|
34907
35541
|
}
|
|
34908
35542
|
function parseTeamConfigYaml(content) {
|
|
@@ -35502,8 +36136,8 @@ function buildDiagnostics(input) {
|
|
|
35502
36136
|
}
|
|
35503
36137
|
|
|
35504
36138
|
// src/server/services/filesystem-sync.ts
|
|
35505
|
-
import { readdirSync, readFileSync as
|
|
35506
|
-
import { join as
|
|
36139
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync9, statSync as statSync3, existsSync as existsSync13 } from "node:fs";
|
|
36140
|
+
import { join as join16, basename as basename3, dirname as dirname10, relative } from "node:path";
|
|
35507
36141
|
import { watch as watch2 } from "chokidar";
|
|
35508
36142
|
|
|
35509
36143
|
// src/server/services/parsers/reviewer-parser.ts
|
|
@@ -35706,67 +36340,67 @@ var FilesystemSync = class {
|
|
|
35706
36340
|
debounceTimers = /* @__PURE__ */ new Map();
|
|
35707
36341
|
// ── 6.1: Full Scan ──
|
|
35708
36342
|
async fullScan() {
|
|
35709
|
-
if (!
|
|
35710
|
-
const entries =
|
|
36343
|
+
if (!existsSync13(this.sessionsDir)) return;
|
|
36344
|
+
const entries = readdirSync2(this.sessionsDir, { withFileTypes: true });
|
|
35711
36345
|
for (const entry of entries) {
|
|
35712
36346
|
if (!entry.isDirectory()) continue;
|
|
35713
36347
|
const sessionId = entry.name;
|
|
35714
|
-
const sessionDir =
|
|
36348
|
+
const sessionDir = join16(this.sessionsDir, sessionId);
|
|
35715
36349
|
this.syncSession(sessionId, sessionDir);
|
|
35716
36350
|
}
|
|
35717
36351
|
}
|
|
35718
36352
|
syncSession(sessionId, sessionDir) {
|
|
35719
36353
|
this.ensureSessionRow(sessionId, sessionDir);
|
|
35720
|
-
const roundsDir =
|
|
35721
|
-
if (
|
|
35722
|
-
const rounds =
|
|
36354
|
+
const roundsDir = join16(sessionDir, "rounds");
|
|
36355
|
+
if (existsSync13(roundsDir)) {
|
|
36356
|
+
const rounds = readdirSync2(roundsDir, { withFileTypes: true });
|
|
35723
36357
|
for (const roundEntry of rounds) {
|
|
35724
36358
|
if (!roundEntry.isDirectory()) continue;
|
|
35725
36359
|
const roundMatch = roundEntry.name.match(/^round-(\d+)$/);
|
|
35726
36360
|
if (!roundMatch) continue;
|
|
35727
36361
|
const roundNumber = parseInt(roundMatch[1] ?? "0", 10);
|
|
35728
|
-
const roundDir =
|
|
35729
|
-
const reviewsDir =
|
|
35730
|
-
if (
|
|
35731
|
-
const reviewFiles =
|
|
36362
|
+
const roundDir = join16(roundsDir, roundEntry.name);
|
|
36363
|
+
const reviewsDir = join16(roundDir, "reviews");
|
|
36364
|
+
if (existsSync13(reviewsDir)) {
|
|
36365
|
+
const reviewFiles = readdirSync2(reviewsDir).filter((f) => f.endsWith(".md"));
|
|
35732
36366
|
for (const reviewFile of reviewFiles) {
|
|
35733
|
-
const filePath =
|
|
36367
|
+
const filePath = join16(reviewsDir, reviewFile);
|
|
35734
36368
|
this.processReviewerOutput(sessionId, roundNumber, filePath, reviewFile);
|
|
35735
36369
|
}
|
|
35736
36370
|
}
|
|
35737
|
-
const roundMetaPath =
|
|
35738
|
-
if (
|
|
36371
|
+
const roundMetaPath = join16(roundDir, "round-meta.json");
|
|
36372
|
+
if (existsSync13(roundMetaPath)) {
|
|
35739
36373
|
this.processRoundMeta(sessionId, roundNumber, roundMetaPath);
|
|
35740
36374
|
}
|
|
35741
|
-
const finalPath =
|
|
35742
|
-
if (
|
|
36375
|
+
const finalPath = join16(roundDir, "final.md");
|
|
36376
|
+
if (existsSync13(finalPath)) {
|
|
35743
36377
|
this.processFinalMd(sessionId, roundNumber, finalPath);
|
|
35744
36378
|
}
|
|
35745
|
-
const finalHumanPath =
|
|
35746
|
-
if (
|
|
36379
|
+
const finalHumanPath = join16(roundDir, "final-human.md");
|
|
36380
|
+
if (existsSync13(finalHumanPath)) {
|
|
35747
36381
|
this.processGenericArtifact(sessionId, "final-human", finalHumanPath, roundNumber);
|
|
35748
36382
|
}
|
|
35749
|
-
const discoursePath =
|
|
35750
|
-
if (
|
|
36383
|
+
const discoursePath = join16(roundDir, "discourse.md");
|
|
36384
|
+
if (existsSync13(discoursePath)) {
|
|
35751
36385
|
this.processGenericArtifact(sessionId, "discourse", discoursePath, roundNumber);
|
|
35752
36386
|
}
|
|
35753
36387
|
}
|
|
35754
36388
|
}
|
|
35755
|
-
const mapDir =
|
|
35756
|
-
if (
|
|
35757
|
-
const runs =
|
|
36389
|
+
const mapDir = join16(sessionDir, "map", "runs");
|
|
36390
|
+
if (existsSync13(mapDir)) {
|
|
36391
|
+
const runs = readdirSync2(mapDir, { withFileTypes: true });
|
|
35758
36392
|
for (const runEntry of runs) {
|
|
35759
36393
|
if (!runEntry.isDirectory()) continue;
|
|
35760
36394
|
const runMatch = runEntry.name.match(/^run-(\d+)$/);
|
|
35761
36395
|
if (!runMatch) continue;
|
|
35762
36396
|
const runNumber = parseInt(runMatch[1] ?? "0", 10);
|
|
35763
|
-
const runDir =
|
|
35764
|
-
const mapMetaPath =
|
|
35765
|
-
if (
|
|
36397
|
+
const runDir = join16(mapDir, runEntry.name);
|
|
36398
|
+
const mapMetaPath = join16(runDir, "map-meta.json");
|
|
36399
|
+
if (existsSync13(mapMetaPath)) {
|
|
35766
36400
|
this.processMapMeta(sessionId, runNumber, mapMetaPath);
|
|
35767
36401
|
}
|
|
35768
|
-
const mapPath =
|
|
35769
|
-
if (
|
|
36402
|
+
const mapPath = join16(runDir, "map.md");
|
|
36403
|
+
if (existsSync13(mapPath)) {
|
|
35770
36404
|
this.processMapMd(sessionId, runNumber, mapPath);
|
|
35771
36405
|
}
|
|
35772
36406
|
const mapArtifacts = [
|
|
@@ -35775,8 +36409,8 @@ var FilesystemSync = class {
|
|
|
35775
36409
|
["requirements-mapping.md", "requirements-mapping"]
|
|
35776
36410
|
];
|
|
35777
36411
|
for (const [fileName, artifactType] of mapArtifacts) {
|
|
35778
|
-
const filePath =
|
|
35779
|
-
if (
|
|
36412
|
+
const filePath = join16(runDir, fileName);
|
|
36413
|
+
if (existsSync13(filePath)) {
|
|
35780
36414
|
this.processGenericArtifact(sessionId, artifactType, filePath, void 0, runNumber);
|
|
35781
36415
|
}
|
|
35782
36416
|
}
|
|
@@ -35787,8 +36421,8 @@ var FilesystemSync = class {
|
|
|
35787
36421
|
["discovered-standards.md", "discovered-standards"]
|
|
35788
36422
|
];
|
|
35789
36423
|
for (const [fileName, artifactType] of sessionArtifacts) {
|
|
35790
|
-
const filePath =
|
|
35791
|
-
if (
|
|
36424
|
+
const filePath = join16(sessionDir, fileName);
|
|
36425
|
+
if (existsSync13(filePath)) {
|
|
35792
36426
|
this.processGenericArtifact(sessionId, artifactType, filePath);
|
|
35793
36427
|
}
|
|
35794
36428
|
}
|
|
@@ -35797,58 +36431,58 @@ var FilesystemSync = class {
|
|
|
35797
36431
|
ensureSessionRow(sessionId, sessionDir) {
|
|
35798
36432
|
const branchMatch = sessionId.match(/^\d{4}-\d{2}-\d{2}-(.+)$/);
|
|
35799
36433
|
const branch = branchMatch?.[1] ?? "unknown";
|
|
35800
|
-
const hasRoundsDir =
|
|
35801
|
-
const hasMapDir =
|
|
36434
|
+
const hasRoundsDir = existsSync13(join16(sessionDir, "rounds"));
|
|
36435
|
+
const hasMapDir = existsSync13(join16(sessionDir, "map"));
|
|
35802
36436
|
const workflowType = hasMapDir && !hasRoundsDir ? "map" : "review";
|
|
35803
36437
|
let currentRound = 1;
|
|
35804
36438
|
if (hasRoundsDir) {
|
|
35805
|
-
const roundDirs =
|
|
36439
|
+
const roundDirs = readdirSync2(join16(sessionDir, "rounds")).filter((d) => d.match(/^round-\d+$/));
|
|
35806
36440
|
currentRound = Math.max(1, roundDirs.length);
|
|
35807
36441
|
}
|
|
35808
36442
|
let currentMapRun = 1;
|
|
35809
|
-
const mapRunsDir =
|
|
35810
|
-
if (
|
|
35811
|
-
const runDirs =
|
|
36443
|
+
const mapRunsDir = join16(sessionDir, "map", "runs");
|
|
36444
|
+
if (existsSync13(mapRunsDir)) {
|
|
36445
|
+
const runDirs = readdirSync2(mapRunsDir).filter((d) => d.match(/^run-\d+$/));
|
|
35812
36446
|
currentMapRun = Math.max(1, runDirs.length);
|
|
35813
36447
|
}
|
|
35814
36448
|
let phase = "context";
|
|
35815
36449
|
let phaseNumber = 1;
|
|
35816
36450
|
let status = "closed";
|
|
35817
36451
|
if (workflowType === "review" && hasRoundsDir) {
|
|
35818
|
-
const roundDir =
|
|
35819
|
-
if (
|
|
36452
|
+
const roundDir = join16(sessionDir, "rounds", `round-${currentRound}`);
|
|
36453
|
+
if (existsSync13(join16(roundDir, "final.md"))) {
|
|
35820
36454
|
phase = "complete";
|
|
35821
36455
|
phaseNumber = 8;
|
|
35822
36456
|
status = "closed";
|
|
35823
|
-
} else if (
|
|
36457
|
+
} else if (existsSync13(join16(roundDir, "discourse.md"))) {
|
|
35824
36458
|
phase = "synthesis";
|
|
35825
36459
|
phaseNumber = 7;
|
|
35826
|
-
} else if (
|
|
36460
|
+
} else if (existsSync13(join16(roundDir, "reviews")) && readdirSync2(join16(roundDir, "reviews")).filter((f) => f.endsWith(".md")).length > 0) {
|
|
35827
36461
|
phase = "reviews";
|
|
35828
36462
|
phaseNumber = 4;
|
|
35829
|
-
} else if (
|
|
36463
|
+
} else if (existsSync13(join16(sessionDir, "context.md"))) {
|
|
35830
36464
|
phase = "analysis";
|
|
35831
36465
|
phaseNumber = 3;
|
|
35832
|
-
} else if (
|
|
36466
|
+
} else if (existsSync13(join16(sessionDir, "discovered-standards.md"))) {
|
|
35833
36467
|
phase = "change-context";
|
|
35834
36468
|
phaseNumber = 2;
|
|
35835
36469
|
}
|
|
35836
36470
|
} else if (workflowType === "map" && hasMapDir) {
|
|
35837
|
-
const runDir =
|
|
35838
|
-
if (
|
|
36471
|
+
const runDir = join16(mapRunsDir, `run-${currentMapRun}`);
|
|
36472
|
+
if (existsSync13(join16(runDir, "map.md"))) {
|
|
35839
36473
|
phase = "complete";
|
|
35840
36474
|
phaseNumber = 6;
|
|
35841
36475
|
status = "closed";
|
|
35842
|
-
} else if (
|
|
36476
|
+
} else if (existsSync13(join16(runDir, "requirements-mapping.md"))) {
|
|
35843
36477
|
phase = "synthesis";
|
|
35844
36478
|
phaseNumber = 5;
|
|
35845
|
-
} else if (
|
|
36479
|
+
} else if (existsSync13(join16(runDir, "flow-analysis.md"))) {
|
|
35846
36480
|
phase = "requirements-mapping";
|
|
35847
36481
|
phaseNumber = 4;
|
|
35848
|
-
} else if (
|
|
36482
|
+
} else if (existsSync13(join16(runDir, "topology.md"))) {
|
|
35849
36483
|
phase = "flow-analysis";
|
|
35850
36484
|
phaseNumber = 3;
|
|
35851
|
-
} else if (
|
|
36485
|
+
} else if (existsSync13(join16(sessionDir, "discovered-standards.md"))) {
|
|
35852
36486
|
phase = "topology";
|
|
35853
36487
|
phaseNumber = 2;
|
|
35854
36488
|
}
|
|
@@ -35901,9 +36535,9 @@ var FilesystemSync = class {
|
|
|
35901
36535
|
/** Returns true if the directory contains at least one .md or .json file (recursively). */
|
|
35902
36536
|
hasArtifacts(dir) {
|
|
35903
36537
|
try {
|
|
35904
|
-
for (const entry of
|
|
36538
|
+
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
35905
36539
|
if (entry.isDirectory()) {
|
|
35906
|
-
if (this.hasArtifacts(
|
|
36540
|
+
if (this.hasArtifacts(join16(dir, entry.name))) return true;
|
|
35907
36541
|
} else if (/\.(md|json)$/.test(entry.name)) {
|
|
35908
36542
|
return true;
|
|
35909
36543
|
}
|
|
@@ -35916,7 +36550,7 @@ var FilesystemSync = class {
|
|
|
35916
36550
|
shouldSkip(filePath, existingParsedAt) {
|
|
35917
36551
|
if (!existingParsedAt) return false;
|
|
35918
36552
|
try {
|
|
35919
|
-
const mtime =
|
|
36553
|
+
const mtime = statSync3(filePath).mtime;
|
|
35920
36554
|
const parsedAt = new Date(existingParsedAt);
|
|
35921
36555
|
return mtime <= parsedAt;
|
|
35922
36556
|
} catch {
|
|
@@ -35931,13 +36565,19 @@ var FilesystemSync = class {
|
|
|
35931
36565
|
"SELECT id FROM markdown_artifacts WHERE session_id = ? AND artifact_type = ? AND round_number IS ? AND file_path = ?",
|
|
35932
36566
|
[sessionId, artifactType, roundNumber ?? null, relPath]
|
|
35933
36567
|
);
|
|
35934
|
-
|
|
36568
|
+
if (existing !== null) {
|
|
36569
|
+
this.db.run(
|
|
36570
|
+
`UPDATE markdown_artifacts SET content = ?, parsed_at = datetime('now') WHERE id = ?`,
|
|
36571
|
+
[content, existing]
|
|
36572
|
+
);
|
|
36573
|
+
return "updated";
|
|
36574
|
+
}
|
|
35935
36575
|
this.db.run(
|
|
35936
|
-
`INSERT
|
|
36576
|
+
`INSERT INTO markdown_artifacts (session_id, artifact_type, round_number, file_path, content, parsed_at)
|
|
35937
36577
|
VALUES (?, ?, ?, ?, ?, datetime('now'))`,
|
|
35938
36578
|
[sessionId, artifactType, roundNumber ?? null, relPath, content]
|
|
35939
36579
|
);
|
|
35940
|
-
return
|
|
36580
|
+
return "created";
|
|
35941
36581
|
}
|
|
35942
36582
|
// ── 6.7: Socket.IO Emission ──
|
|
35943
36583
|
emitArtifactEvent(action, event) {
|
|
@@ -35953,12 +36593,12 @@ var FilesystemSync = class {
|
|
|
35953
36593
|
);
|
|
35954
36594
|
if (existingRun && this.shouldSkip(filePath, existingRun["parsed_at"] ?? null)) return;
|
|
35955
36595
|
if (existingRun?.["source"] === "orchestrator") {
|
|
35956
|
-
const content2 =
|
|
36596
|
+
const content2 = readFileSync9(filePath, "utf-8");
|
|
35957
36597
|
const action2 = this.upsertMarkdownArtifact(sessionId, "map", filePath, content2, void 0);
|
|
35958
36598
|
this.emitArtifactEvent(action2, { sessionId, artifactType: "map", filePath });
|
|
35959
36599
|
return;
|
|
35960
36600
|
}
|
|
35961
|
-
const content =
|
|
36601
|
+
const content = readFileSync9(filePath, "utf-8");
|
|
35962
36602
|
const parsed = parseMapMd(content);
|
|
35963
36603
|
this.db.run(
|
|
35964
36604
|
`INSERT OR REPLACE INTO map_runs (session_id, run_number, file_count, map_md_path, parsed_at, source)
|
|
@@ -36084,7 +36724,7 @@ var FilesystemSync = class {
|
|
|
36084
36724
|
const roundId = roundRow?.["id"];
|
|
36085
36725
|
if (!roundId) return;
|
|
36086
36726
|
if (roundRow?.["source"] === "orchestrator") {
|
|
36087
|
-
const content2 =
|
|
36727
|
+
const content2 = readFileSync9(filePath, "utf-8");
|
|
36088
36728
|
const action2 = this.upsertMarkdownArtifact(sessionId, "reviewer-output", filePath, content2, roundNumber);
|
|
36089
36729
|
this.emitArtifactEvent(action2, {
|
|
36090
36730
|
sessionId,
|
|
@@ -36103,7 +36743,7 @@ var FilesystemSync = class {
|
|
|
36103
36743
|
[roundId, reviewerType, instanceNumber]
|
|
36104
36744
|
);
|
|
36105
36745
|
if (existingOutput && this.shouldSkip(filePath, existingOutput["parsed_at"] ?? null)) return;
|
|
36106
|
-
const content =
|
|
36746
|
+
const content = readFileSync9(filePath, "utf-8");
|
|
36107
36747
|
const parsed = parseReviewerOutput(content);
|
|
36108
36748
|
this.db.run(
|
|
36109
36749
|
`INSERT OR REPLACE INTO reviewer_outputs (round_id, reviewer_type, instance_number, file_path, finding_count, parsed_at)
|
|
@@ -36191,7 +36831,7 @@ var FilesystemSync = class {
|
|
|
36191
36831
|
if (existingRound?.["source"] === "orchestrator" && this.shouldSkip(filePath, existingRound["parsed_at"] ?? null)) return;
|
|
36192
36832
|
let raw;
|
|
36193
36833
|
try {
|
|
36194
|
-
raw = JSON.parse(
|
|
36834
|
+
raw = JSON.parse(readFileSync9(filePath, "utf-8"));
|
|
36195
36835
|
} catch {
|
|
36196
36836
|
console.error(`[FilesystemSync] Failed to parse ${filePath}`);
|
|
36197
36837
|
return;
|
|
@@ -36237,12 +36877,12 @@ var FilesystemSync = class {
|
|
|
36237
36877
|
this.db.run("COMMIT");
|
|
36238
36878
|
return;
|
|
36239
36879
|
}
|
|
36240
|
-
const roundDir =
|
|
36880
|
+
const roundDir = dirname10(filePath);
|
|
36241
36881
|
for (const reviewer of meta.reviewers) {
|
|
36242
36882
|
const reviewerType = reviewer.type ?? "unknown";
|
|
36243
36883
|
const instanceNumber = reviewer.instance ?? 1;
|
|
36244
36884
|
const findings = reviewer.findings ?? [];
|
|
36245
|
-
const reviewerMdPath =
|
|
36885
|
+
const reviewerMdPath = join16(roundDir, "reviews", `${reviewerType}-${instanceNumber}.md`);
|
|
36246
36886
|
this.db.run(
|
|
36247
36887
|
`INSERT OR REPLACE INTO reviewer_outputs (round_id, reviewer_type, instance_number, file_path, finding_count, parsed_at)
|
|
36248
36888
|
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
@@ -36340,7 +36980,7 @@ var FilesystemSync = class {
|
|
|
36340
36980
|
if (existingRun?.["source"] === "orchestrator" && this.shouldSkip(filePath, existingRun["parsed_at"] ?? null)) return;
|
|
36341
36981
|
let raw;
|
|
36342
36982
|
try {
|
|
36343
|
-
raw = JSON.parse(
|
|
36983
|
+
raw = JSON.parse(readFileSync9(filePath, "utf-8"));
|
|
36344
36984
|
} catch {
|
|
36345
36985
|
console.error(`[FilesystemSync] Failed to parse ${filePath}`);
|
|
36346
36986
|
return;
|
|
@@ -36468,7 +37108,7 @@ var FilesystemSync = class {
|
|
|
36468
37108
|
);
|
|
36469
37109
|
const isOrchestratorSource = existingRound?.["source"] === "orchestrator";
|
|
36470
37110
|
if (!isOrchestratorSource && existingRound && this.shouldSkip(filePath, existingRound["parsed_at"] ?? null)) return;
|
|
36471
|
-
const content =
|
|
37111
|
+
const content = readFileSync9(filePath, "utf-8");
|
|
36472
37112
|
if (isOrchestratorSource) {
|
|
36473
37113
|
this.db.run(
|
|
36474
37114
|
`UPDATE review_rounds SET final_md_path = ?, parsed_at = ?
|
|
@@ -36547,7 +37187,7 @@ var FilesystemSync = class {
|
|
|
36547
37187
|
[sessionId, artifactType, relPath]
|
|
36548
37188
|
);
|
|
36549
37189
|
if (existing && this.shouldSkip(filePath, existing["parsed_at"] ?? null)) return;
|
|
36550
|
-
const content =
|
|
37190
|
+
const content = readFileSync9(filePath, "utf-8");
|
|
36551
37191
|
const action = this.upsertMarkdownArtifact(sessionId, artifactType, filePath, content, roundNumber);
|
|
36552
37192
|
this.emitArtifactEvent(action, {
|
|
36553
37193
|
sessionId,
|
|
@@ -36566,7 +37206,7 @@ var FilesystemSync = class {
|
|
|
36566
37206
|
ignored: [
|
|
36567
37207
|
// Only ignore entries whose own name starts with a dot — the old regex
|
|
36568
37208
|
// /(^|[/\\])\../ matched `.ocr` in the parent path, silencing ALL events.
|
|
36569
|
-
(filePath) =>
|
|
37209
|
+
(filePath) => basename3(filePath).startsWith("."),
|
|
36570
37210
|
/node_modules/,
|
|
36571
37211
|
/\.db$/
|
|
36572
37212
|
]
|
|
@@ -36605,9 +37245,9 @@ var FilesystemSync = class {
|
|
|
36605
37245
|
const parts = relFromSessions.split("/");
|
|
36606
37246
|
const sessionId = parts[0];
|
|
36607
37247
|
if (!sessionId) return;
|
|
36608
|
-
const sessionDir =
|
|
37248
|
+
const sessionDir = join16(this.sessionsDir, sessionId);
|
|
36609
37249
|
this.ensureSessionRow(sessionId, sessionDir);
|
|
36610
|
-
const fileName =
|
|
37250
|
+
const fileName = basename3(filePath);
|
|
36611
37251
|
const reviewerMatch = relFromSessions.match(/rounds\/round-(\d+)\/reviews\/(.+\.md)$/);
|
|
36612
37252
|
if (reviewerMatch) {
|
|
36613
37253
|
const roundNumber = parseInt(reviewerMatch[1] ?? "0", 10);
|
|
@@ -36677,8 +37317,8 @@ var FilesystemSync = class {
|
|
|
36677
37317
|
};
|
|
36678
37318
|
|
|
36679
37319
|
// src/server/services/db-sync-watcher.ts
|
|
36680
|
-
import { existsSync as
|
|
36681
|
-
import { dirname as
|
|
37320
|
+
import { existsSync as existsSync14 } from "node:fs";
|
|
37321
|
+
import { dirname as dirname11, basename as basename4 } from "node:path";
|
|
36682
37322
|
import { watch as watch3 } from "chokidar";
|
|
36683
37323
|
function col(row, key) {
|
|
36684
37324
|
return row[key] ?? null;
|
|
@@ -36719,9 +37359,9 @@ var DbSyncWatcher = class {
|
|
|
36719
37359
|
}
|
|
36720
37360
|
/** Start watching the DB file (and its WAL sidecar) for external writes. */
|
|
36721
37361
|
startWatching() {
|
|
36722
|
-
if (!
|
|
36723
|
-
const watchDir =
|
|
36724
|
-
const dbFile =
|
|
37362
|
+
if (!existsSync14(this.dbFilePath)) return;
|
|
37363
|
+
const watchDir = dirname11(this.dbFilePath);
|
|
37364
|
+
const dbFile = basename4(this.dbFilePath);
|
|
36725
37365
|
const walFile = `${dbFile}-wal`;
|
|
36726
37366
|
this.watcher = watch3(watchDir, {
|
|
36727
37367
|
persistent: true,
|
|
@@ -36731,7 +37371,7 @@ var DbSyncWatcher = class {
|
|
|
36731
37371
|
interval: 200
|
|
36732
37372
|
});
|
|
36733
37373
|
const onAnyEvent = (path2) => {
|
|
36734
|
-
const name =
|
|
37374
|
+
const name = basename4(path2);
|
|
36735
37375
|
if (name === dbFile || name === walFile) this.debouncedSync();
|
|
36736
37376
|
};
|
|
36737
37377
|
this.watcher.on("change", onAnyEvent);
|
|
@@ -36982,20 +37622,20 @@ function commandFingerprint(row) {
|
|
|
36982
37622
|
}
|
|
36983
37623
|
|
|
36984
37624
|
// src/server/socket/chat-handler.ts
|
|
36985
|
-
import { dirname as
|
|
37625
|
+
import { dirname as dirname12 } from "node:path";
|
|
36986
37626
|
|
|
36987
37627
|
// src/server/services/chat-context.ts
|
|
36988
|
-
import { readFileSync as
|
|
36989
|
-
import { join as
|
|
37628
|
+
import { readFileSync as readFileSync10, readdirSync as readdirSync3, existsSync as existsSync15 } from "node:fs";
|
|
37629
|
+
import { join as join17 } from "node:path";
|
|
36990
37630
|
function buildChatContext(ocrDir, target) {
|
|
36991
|
-
const sessionsDir =
|
|
37631
|
+
const sessionsDir = join17(ocrDir, "sessions");
|
|
36992
37632
|
if (target.type === "map_run") {
|
|
36993
37633
|
return buildMapRunContext(sessionsDir, target.sessionId, target.runNumber);
|
|
36994
37634
|
}
|
|
36995
37635
|
return buildReviewRoundContext(sessionsDir, target.sessionId, target.roundNumber);
|
|
36996
37636
|
}
|
|
36997
37637
|
function buildMapRunContext(sessionsDir, sessionId, runNumber) {
|
|
36998
|
-
const mapPath =
|
|
37638
|
+
const mapPath = join17(
|
|
36999
37639
|
sessionsDir,
|
|
37000
37640
|
sessionId,
|
|
37001
37641
|
"map",
|
|
@@ -37009,8 +37649,8 @@ function buildMapRunContext(sessionsDir, sessionId, runNumber) {
|
|
|
37009
37649
|
"",
|
|
37010
37650
|
`Below is the Code Review Map that organizes the changeset into reviewable sections:`
|
|
37011
37651
|
];
|
|
37012
|
-
if (
|
|
37013
|
-
const content =
|
|
37652
|
+
if (existsSync15(mapPath)) {
|
|
37653
|
+
const content = readFileSync10(mapPath, "utf-8");
|
|
37014
37654
|
parts.push("");
|
|
37015
37655
|
parts.push("<map>");
|
|
37016
37656
|
parts.push(content);
|
|
@@ -37022,26 +37662,26 @@ function buildMapRunContext(sessionsDir, sessionId, runNumber) {
|
|
|
37022
37662
|
return parts.join("\n");
|
|
37023
37663
|
}
|
|
37024
37664
|
function buildReviewRoundContext(sessionsDir, sessionId, roundNumber) {
|
|
37025
|
-
const roundDir =
|
|
37026
|
-
const finalPath =
|
|
37027
|
-
const reviewersDir =
|
|
37665
|
+
const roundDir = join17(sessionsDir, sessionId, "rounds", `round-${roundNumber}`);
|
|
37666
|
+
const finalPath = join17(roundDir, "final.md");
|
|
37667
|
+
const reviewersDir = join17(roundDir, "reviews");
|
|
37028
37668
|
const parts = [
|
|
37029
37669
|
`You are an expert code reviewer assisting with a code review session.`,
|
|
37030
37670
|
`You are looking at review round #${roundNumber} for session "${sessionId}".`,
|
|
37031
37671
|
"",
|
|
37032
37672
|
`Below are the review artifacts for this round:`
|
|
37033
37673
|
];
|
|
37034
|
-
if (
|
|
37035
|
-
const content =
|
|
37674
|
+
if (existsSync15(finalPath)) {
|
|
37675
|
+
const content = readFileSync10(finalPath, "utf-8");
|
|
37036
37676
|
parts.push("");
|
|
37037
37677
|
parts.push("<final-synthesis>");
|
|
37038
37678
|
parts.push(content);
|
|
37039
37679
|
parts.push("</final-synthesis>");
|
|
37040
37680
|
}
|
|
37041
|
-
if (
|
|
37042
|
-
const files =
|
|
37681
|
+
if (existsSync15(reviewersDir)) {
|
|
37682
|
+
const files = readdirSync3(reviewersDir).filter((f) => f.endsWith(".md")).sort();
|
|
37043
37683
|
for (const file of files) {
|
|
37044
|
-
const content =
|
|
37684
|
+
const content = readFileSync10(join17(reviewersDir, file), "utf-8");
|
|
37045
37685
|
const reviewerName = file.replace(/\.md$/, "");
|
|
37046
37686
|
parts.push("");
|
|
37047
37687
|
parts.push(`<reviewer name="${reviewerName}">`);
|
|
@@ -37049,7 +37689,7 @@ function buildReviewRoundContext(sessionsDir, sessionId, roundNumber) {
|
|
|
37049
37689
|
parts.push("</reviewer>");
|
|
37050
37690
|
}
|
|
37051
37691
|
}
|
|
37052
|
-
if (!
|
|
37692
|
+
if (!existsSync15(finalPath) && !existsSync15(reviewersDir)) {
|
|
37053
37693
|
parts.push("");
|
|
37054
37694
|
parts.push("(No review artifacts found on disk for this round.)");
|
|
37055
37695
|
}
|
|
@@ -37195,7 +37835,7 @@ User: ${message}`;
|
|
|
37195
37835
|
});
|
|
37196
37836
|
return;
|
|
37197
37837
|
}
|
|
37198
|
-
const repoRoot =
|
|
37838
|
+
const repoRoot = dirname12(ocrDir);
|
|
37199
37839
|
const spawnResult = adapter.spawn({
|
|
37200
37840
|
prompt,
|
|
37201
37841
|
cwd: repoRoot,
|
|
@@ -37371,13 +38011,13 @@ function cleanupAllChats() {
|
|
|
37371
38011
|
}
|
|
37372
38012
|
|
|
37373
38013
|
// src/server/socket/post-handler.ts
|
|
37374
|
-
import { existsSync as
|
|
38014
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync6, readFileSync as readFileSync11, unlinkSync as unlinkSync4, writeFileSync as writeFileSync5 } from "node:fs";
|
|
37375
38015
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
37376
|
-
import { join as
|
|
38016
|
+
import { join as join18, dirname as dirname13, isAbsolute as isAbsolute2 } from "node:path";
|
|
37377
38017
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
37378
38018
|
function resolveSessionDir2(sessionDir, ocrDir) {
|
|
37379
38019
|
if (isAbsolute2(sessionDir)) return sessionDir;
|
|
37380
|
-
return
|
|
38020
|
+
return join18(dirname13(ocrDir), sessionDir);
|
|
37381
38021
|
}
|
|
37382
38022
|
var BRANCH_PREFIXES = [
|
|
37383
38023
|
"feat",
|
|
@@ -37446,7 +38086,7 @@ function registerPostHandlers(io2, socket, db, ocrDir, aiCliService) {
|
|
|
37446
38086
|
return;
|
|
37447
38087
|
}
|
|
37448
38088
|
const branch = session.branch;
|
|
37449
|
-
const repoRoot =
|
|
38089
|
+
const repoRoot = dirname13(ocrDir);
|
|
37450
38090
|
try {
|
|
37451
38091
|
await execBinaryAsync("gh", ["auth", "status"], { env: cleanEnv(), cwd: repoRoot, encoding: "utf-8" });
|
|
37452
38092
|
} catch {
|
|
@@ -37547,19 +38187,19 @@ function registerPostHandlers(io2, socket, db, ocrDir, aiCliService) {
|
|
|
37547
38187
|
socket.emit("post:error", { error: "Session not found" });
|
|
37548
38188
|
return;
|
|
37549
38189
|
}
|
|
37550
|
-
const sessionDir = session.session_dir ? resolveSessionDir2(session.session_dir, ocrDir) :
|
|
37551
|
-
const roundDir =
|
|
37552
|
-
const finalPath =
|
|
37553
|
-
if (!
|
|
38190
|
+
const sessionDir = session.session_dir ? resolveSessionDir2(session.session_dir, ocrDir) : join18(ocrDir, "sessions", sessionId);
|
|
38191
|
+
const roundDir = join18(sessionDir, "rounds", `round-${roundNumber}`);
|
|
38192
|
+
const finalPath = join18(roundDir, "final.md");
|
|
38193
|
+
if (!existsSync16(finalPath)) {
|
|
37554
38194
|
socket.emit("post:error", { error: "final.md not found for this round" });
|
|
37555
38195
|
return;
|
|
37556
38196
|
}
|
|
37557
|
-
const humanReviewPath =
|
|
37558
|
-
const repoRoot =
|
|
37559
|
-
const commandMdPath =
|
|
38197
|
+
const humanReviewPath = join18(roundDir, "final-human.md");
|
|
38198
|
+
const repoRoot = dirname13(ocrDir);
|
|
38199
|
+
const commandMdPath = join18(ocrDir, "commands", "translate-review-to-single-human.md");
|
|
37560
38200
|
let commandContent;
|
|
37561
38201
|
try {
|
|
37562
|
-
commandContent =
|
|
38202
|
+
commandContent = readFileSync11(commandMdPath, "utf-8");
|
|
37563
38203
|
} catch {
|
|
37564
38204
|
socket.emit("post:error", {
|
|
37565
38205
|
error: `Command file not found: ${commandMdPath}. Run \`ocr init\` to set up.`
|
|
@@ -37639,9 +38279,9 @@ function registerPostHandlers(io2, socket, db, ocrDir, aiCliService) {
|
|
|
37639
38279
|
}
|
|
37640
38280
|
}
|
|
37641
38281
|
let generatedContent = "";
|
|
37642
|
-
if (
|
|
38282
|
+
if (existsSync16(humanReviewPath)) {
|
|
37643
38283
|
try {
|
|
37644
|
-
generatedContent =
|
|
38284
|
+
generatedContent = readFileSync11(humanReviewPath, "utf-8").trim();
|
|
37645
38285
|
} catch {
|
|
37646
38286
|
}
|
|
37647
38287
|
}
|
|
@@ -37716,11 +38356,11 @@ function registerPostHandlers(io2, socket, db, ocrDir, aiCliService) {
|
|
|
37716
38356
|
socket.emit("post:save-result", { success: false, error: "Session not found" });
|
|
37717
38357
|
return;
|
|
37718
38358
|
}
|
|
37719
|
-
const sessionDir = session.session_dir ? resolveSessionDir2(session.session_dir, ocrDir) :
|
|
37720
|
-
const roundDir =
|
|
37721
|
-
|
|
37722
|
-
const filePath =
|
|
37723
|
-
|
|
38359
|
+
const sessionDir = session.session_dir ? resolveSessionDir2(session.session_dir, ocrDir) : join18(ocrDir, "sessions", sessionId);
|
|
38360
|
+
const roundDir = join18(sessionDir, "rounds", `round-${roundNumber}`);
|
|
38361
|
+
mkdirSync6(roundDir, { recursive: true });
|
|
38362
|
+
const filePath = join18(roundDir, "final-human.md");
|
|
38363
|
+
writeFileSync5(filePath, content, { mode: 420 });
|
|
37724
38364
|
socket.emit("post:save-result", { success: true });
|
|
37725
38365
|
} catch (err) {
|
|
37726
38366
|
console.error("Error in post:save handler:", err);
|
|
@@ -37746,14 +38386,14 @@ function registerPostHandlers(io2, socket, db, ocrDir, aiCliService) {
|
|
|
37746
38386
|
);
|
|
37747
38387
|
tracker.appendOutput(`\u25B8 Posting review to PR #${prNumber}...
|
|
37748
38388
|
`);
|
|
37749
|
-
const tmpDir =
|
|
38389
|
+
const tmpDir = join18(tmpdir2(), "ocr-post-comments");
|
|
37750
38390
|
try {
|
|
37751
|
-
|
|
38391
|
+
mkdirSync6(tmpDir, { recursive: true, mode: 448 });
|
|
37752
38392
|
} catch {
|
|
37753
38393
|
}
|
|
37754
|
-
const tmpFile =
|
|
37755
|
-
|
|
37756
|
-
const repoRoot =
|
|
38394
|
+
const tmpFile = join18(tmpDir, `${randomUUID2()}.md`);
|
|
38395
|
+
writeFileSync5(tmpFile, content, { mode: 384 });
|
|
38396
|
+
const repoRoot = dirname13(ocrDir);
|
|
37757
38397
|
try {
|
|
37758
38398
|
const { stdout } = await execBinaryAsync(
|
|
37759
38399
|
"gh",
|
|
@@ -37776,7 +38416,7 @@ function registerPostHandlers(io2, socket, db, ocrDir, aiCliService) {
|
|
|
37776
38416
|
});
|
|
37777
38417
|
} finally {
|
|
37778
38418
|
try {
|
|
37779
|
-
|
|
38419
|
+
unlinkSync4(tmpFile);
|
|
37780
38420
|
} catch {
|
|
37781
38421
|
}
|
|
37782
38422
|
}
|
|
@@ -37796,45 +38436,9 @@ function cleanupAllPostGenerations() {
|
|
|
37796
38436
|
}
|
|
37797
38437
|
}
|
|
37798
38438
|
|
|
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
38439
|
// src/server/index.ts
|
|
37836
38440
|
import { homedir } from "node:os";
|
|
37837
|
-
var __dirname3 =
|
|
38441
|
+
var __dirname3 = dirname14(fileURLToPath3(import.meta.url));
|
|
37838
38442
|
function shortenPath(p) {
|
|
37839
38443
|
const home = homedir();
|
|
37840
38444
|
return p.startsWith(home) ? "~" + p.slice(home.length) : p;
|
|
@@ -37898,40 +38502,61 @@ if (process.env.NODE_ENV !== "production") {
|
|
|
37898
38502
|
res.json({ token: AUTH_TOKEN });
|
|
37899
38503
|
});
|
|
37900
38504
|
}
|
|
38505
|
+
function isOcrDashboardProcess(pid) {
|
|
38506
|
+
if (process.platform === "win32") return false;
|
|
38507
|
+
try {
|
|
38508
|
+
const cmd = execFileSync3("ps", ["-p", String(pid), "-o", "command="], {
|
|
38509
|
+
encoding: "utf-8",
|
|
38510
|
+
timeout: 3e3
|
|
38511
|
+
}).trim();
|
|
38512
|
+
if (/^ocr-dashboard\b/.test(cmd)) return true;
|
|
38513
|
+
return /dashboard\/server\.js|server\/index\.ts/.test(cmd);
|
|
38514
|
+
} catch {
|
|
38515
|
+
return false;
|
|
38516
|
+
}
|
|
38517
|
+
}
|
|
37901
38518
|
async function startServer(options = {}) {
|
|
37902
38519
|
const port = options.port ?? parseInt(process.env.PORT ?? "4173", 10);
|
|
38520
|
+
process.title = "ocr-dashboard";
|
|
37903
38521
|
const ocrDir = resolveOcrDir();
|
|
37904
38522
|
const aiCliService = new AiCliService(ocrDir);
|
|
37905
|
-
const dbPathForCheckpoint =
|
|
38523
|
+
const dbPathForCheckpoint = join19(ocrDir, "data", "ocr.db");
|
|
37906
38524
|
const walResult = walCheckpointTruncate(dbPathForCheckpoint);
|
|
37907
38525
|
if (walResult === "checkpointed") {
|
|
37908
38526
|
console.log(" WAL checkpoint: truncated stale write-ahead-log file");
|
|
37909
38527
|
}
|
|
38528
|
+
for (const reaped of reapOrphanDbFiles(join19(ocrDir, "data"))) {
|
|
38529
|
+
console.log(` Orphan reap: removed stale ${reaped}`);
|
|
38530
|
+
}
|
|
38531
|
+
const staleLogs = reapStaleExecLogs(join19(ocrDir, "data", "exec-logs"));
|
|
38532
|
+
if (staleLogs.length > 0) {
|
|
38533
|
+
console.log(` Exec-log reap: removed ${staleLogs.length} stale agent log(s)`);
|
|
38534
|
+
}
|
|
37910
38535
|
const db = await openDb(ocrDir);
|
|
37911
|
-
const dataDir =
|
|
37912
|
-
const pidFilePath =
|
|
37913
|
-
const portFilePath =
|
|
37914
|
-
|
|
38536
|
+
const dataDir = join19(ocrDir, "data");
|
|
38537
|
+
const pidFilePath = join19(dataDir, "dashboard.pid");
|
|
38538
|
+
const portFilePath = join19(dataDir, "server-port");
|
|
38539
|
+
mkdirSync7(dataDir, { recursive: true });
|
|
37915
38540
|
try {
|
|
37916
|
-
|
|
38541
|
+
unlinkSync5(portFilePath);
|
|
37917
38542
|
} catch {
|
|
37918
38543
|
}
|
|
37919
|
-
if (
|
|
38544
|
+
if (existsSync17(pidFilePath)) {
|
|
37920
38545
|
try {
|
|
37921
38546
|
const oldPid = parseInt(readFileSync12(pidFilePath, "utf-8").trim(), 10);
|
|
37922
|
-
if (!isNaN(oldPid)) {
|
|
37923
|
-
|
|
37924
|
-
|
|
37925
|
-
|
|
37926
|
-
|
|
37927
|
-
);
|
|
37928
|
-
} catch {
|
|
38547
|
+
if (!isNaN(oldPid) && oldPid !== process.pid && isProcessAlive(oldPid) && isOcrDashboardProcess(oldPid)) {
|
|
38548
|
+
console.log(` Single-instance: reaping prior dashboard server (PID ${oldPid}) and taking over`);
|
|
38549
|
+
reapTree(oldPid);
|
|
38550
|
+
const deadline = Date.now() + 6e3;
|
|
38551
|
+
while (isProcessAlive(oldPid) && Date.now() < deadline) {
|
|
38552
|
+
await new Promise((resolve3) => setTimeout(resolve3, 100));
|
|
37929
38553
|
}
|
|
38554
|
+
walCheckpointTruncate(dbPathForCheckpoint);
|
|
37930
38555
|
}
|
|
37931
38556
|
} catch {
|
|
37932
38557
|
}
|
|
37933
38558
|
}
|
|
37934
|
-
|
|
38559
|
+
writeFileSync6(pidFilePath, String(process.pid), { mode: 384 });
|
|
37935
38560
|
const cmdCountResult = db.exec("SELECT COUNT(*) as c FROM command_executions");
|
|
37936
38561
|
const totalCmds = cmdCountResult[0]?.values[0]?.[0] ?? 0;
|
|
37937
38562
|
if (totalCmds === 0) {
|
|
@@ -37941,7 +38566,7 @@ async function startServer(options = {}) {
|
|
|
37941
38566
|
}
|
|
37942
38567
|
}
|
|
37943
38568
|
const orphanResult = db.exec(
|
|
37944
|
-
`SELECT id, pid,
|
|
38569
|
+
`SELECT id, pid, started_at FROM command_executions
|
|
37945
38570
|
WHERE pid IS NOT NULL AND finished_at IS NULL`
|
|
37946
38571
|
);
|
|
37947
38572
|
if (orphanResult.length > 0 && orphanResult[0]) {
|
|
@@ -37951,37 +38576,15 @@ async function startServer(options = {}) {
|
|
|
37951
38576
|
let killedCount = 0;
|
|
37952
38577
|
for (const row of orphanRows) {
|
|
37953
38578
|
const pid = row[colIdx["pid"]];
|
|
37954
|
-
const isDetached = row[colIdx["is_detached"]] === 1;
|
|
37955
38579
|
const startedAt = row[colIdx["started_at"]];
|
|
37956
38580
|
if (sqliteUtcMs(startedAt) < cutoff) continue;
|
|
37957
38581
|
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
|
-
}
|
|
38582
|
+
reapTree(pid);
|
|
37967
38583
|
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
38584
|
}
|
|
37982
38585
|
}
|
|
37983
38586
|
if (killedCount > 0) {
|
|
37984
|
-
console.log(`
|
|
38587
|
+
console.log(` Reaped ${killedCount} orphaned process tree(s)`);
|
|
37985
38588
|
}
|
|
37986
38589
|
}
|
|
37987
38590
|
const legacyResult = db.exec(
|
|
@@ -37991,10 +38594,11 @@ async function startServer(options = {}) {
|
|
|
37991
38594
|
if (legacyCount > 0) {
|
|
37992
38595
|
db.run(
|
|
37993
38596
|
`UPDATE command_executions
|
|
37994
|
-
SET exit_code =
|
|
38597
|
+
SET exit_code = ?,
|
|
37995
38598
|
output = COALESCE(output, '') || '
|
|
37996
38599
|
[Cancelled]'
|
|
37997
|
-
WHERE finished_at IS NOT NULL AND exit_code IS NULL
|
|
38600
|
+
WHERE finished_at IS NOT NULL AND exit_code IS NULL`,
|
|
38601
|
+
[CANCELLED_EXIT_CODE]
|
|
37998
38602
|
);
|
|
37999
38603
|
console.log(` Backfilled ${legacyCount} finished command(s) missing an exit code`);
|
|
38000
38604
|
}
|
|
@@ -38017,11 +38621,25 @@ async function startServer(options = {}) {
|
|
|
38017
38621
|
` Auto-closed ${staleSessionResult.closedSessionIds.length} stale active session(s) (threshold 7 days)`
|
|
38018
38622
|
);
|
|
38019
38623
|
}
|
|
38624
|
+
const reconcileCompleted = async () => {
|
|
38625
|
+
try {
|
|
38626
|
+
const closed = await reconcileCompletedSessions(ocrDir);
|
|
38627
|
+
if (closed.length > 0) {
|
|
38628
|
+
console.log(
|
|
38629
|
+
` Auto-finalized ${closed.length} completed-but-open session(s)`
|
|
38630
|
+
);
|
|
38631
|
+
}
|
|
38632
|
+
} catch (err) {
|
|
38633
|
+
console.error("[reconcile] completed-session reconciliation failed:", err);
|
|
38634
|
+
}
|
|
38635
|
+
};
|
|
38636
|
+
await reconcileCompleted();
|
|
38020
38637
|
const SWEEP_INTERVAL_MS = 5 * 60 * 1e3;
|
|
38021
38638
|
const sweepTimer = setInterval(() => {
|
|
38022
38639
|
try {
|
|
38023
38640
|
logAgentSweep(sweepStaleAgentSessions(db, heartbeatSeconds, defaultIsAlive));
|
|
38024
38641
|
sweepStaleSessions(db, STALE_SESSION_THRESHOLD_SECONDS);
|
|
38642
|
+
void reconcileCompleted();
|
|
38025
38643
|
} catch (err) {
|
|
38026
38644
|
console.error("[sweep] periodic sweep failed:", err);
|
|
38027
38645
|
}
|
|
@@ -38057,11 +38675,11 @@ async function startServer(options = {}) {
|
|
|
38057
38675
|
app.use("/api/agent-sessions", createAgentSessionsRouter(db, () => pullSync()));
|
|
38058
38676
|
app.use("/api/sessions", createHandoffRouter(sessionCapture, ocrDir, () => pullSync()));
|
|
38059
38677
|
app.use("/api/team", createTeamRouter(ocrDir));
|
|
38060
|
-
const clientDir =
|
|
38061
|
-
if (process.env.NODE_ENV === "production" &&
|
|
38678
|
+
const clientDir = join19(__dirname3, "client");
|
|
38679
|
+
if (process.env.NODE_ENV === "production" && existsSync17(clientDir)) {
|
|
38062
38680
|
app.use(import_express15.default.static(clientDir, { index: false }));
|
|
38063
|
-
const indexHtmlPath =
|
|
38064
|
-
const rawIndexHtml =
|
|
38681
|
+
const indexHtmlPath = join19(clientDir, "index.html");
|
|
38682
|
+
const rawIndexHtml = existsSync17(indexHtmlPath) ? readFileSync12(indexHtmlPath, "utf-8") : "";
|
|
38065
38683
|
const tokenScript = `<script>window.__OCR_TOKEN__=${JSON.stringify(AUTH_TOKEN)};</script>`;
|
|
38066
38684
|
const injectedIndexHtml = rawIndexHtml.replace(
|
|
38067
38685
|
"</head>",
|
|
@@ -38080,7 +38698,7 @@ async function startServer(options = {}) {
|
|
|
38080
38698
|
registerChatHandlers(io, socket, db, ocrDir, aiCliService);
|
|
38081
38699
|
registerPostHandlers(io, socket, db, ocrDir, aiCliService);
|
|
38082
38700
|
});
|
|
38083
|
-
const dbFilePath =
|
|
38701
|
+
const dbFilePath = join19(ocrDir, "data", "ocr.db");
|
|
38084
38702
|
const dbSyncWatcher = new DbSyncWatcher(
|
|
38085
38703
|
db,
|
|
38086
38704
|
dbFilePath,
|
|
@@ -38096,7 +38714,7 @@ async function startServer(options = {}) {
|
|
|
38096
38714
|
dbSyncWatcher.startWatching();
|
|
38097
38715
|
pullSync = () => dbSyncWatcher.syncFromDisk();
|
|
38098
38716
|
console.log(` Watching DB: ${shortenPath(dbFilePath)}`);
|
|
38099
|
-
const sessionsDir =
|
|
38717
|
+
const sessionsDir = join19(ocrDir, "sessions");
|
|
38100
38718
|
const fsSync = new FilesystemSync(db, sessionsDir, io);
|
|
38101
38719
|
await fsSync.fullScan();
|
|
38102
38720
|
fsSync.startWatching();
|
|
@@ -38135,7 +38753,7 @@ async function startServer(options = {}) {
|
|
|
38135
38753
|
if (actualPort !== port) {
|
|
38136
38754
|
console.log(` Note: using port ${actualPort} (${port} was in use)`);
|
|
38137
38755
|
}
|
|
38138
|
-
|
|
38756
|
+
writeFileSync6(portFilePath, String(actualPort), { mode: 384 });
|
|
38139
38757
|
console.log(` Server: http://localhost:${actualPort}`);
|
|
38140
38758
|
console.log(` OCR directory: ${shortenPath(ocrDir)}`);
|
|
38141
38759
|
console.log();
|
|
@@ -38148,53 +38766,40 @@ async function startServer(options = {}) {
|
|
|
38148
38766
|
} catch {
|
|
38149
38767
|
}
|
|
38150
38768
|
}
|
|
38151
|
-
const shutdown = (signal) => {
|
|
38769
|
+
const shutdown = async (signal) => {
|
|
38152
38770
|
console.log(
|
|
38153
38771
|
`Shutting down dashboard server${signal ? ` (received ${signal})` : ""}...`
|
|
38154
38772
|
);
|
|
38155
38773
|
try {
|
|
38156
|
-
|
|
38774
|
+
unlinkSync5(pidFilePath);
|
|
38157
38775
|
} catch {
|
|
38158
38776
|
}
|
|
38159
38777
|
try {
|
|
38160
|
-
|
|
38161
|
-
} catch {
|
|
38162
|
-
}
|
|
38163
|
-
try {
|
|
38164
|
-
unlinkSync3(join18(dataDir, "dashboard-active-spawn.json"));
|
|
38778
|
+
unlinkSync5(portFilePath);
|
|
38165
38779
|
} catch {
|
|
38166
38780
|
}
|
|
38781
|
+
clearSpawnMarker(ocrDir);
|
|
38167
38782
|
try {
|
|
38168
38783
|
const activeResult = db.exec(
|
|
38169
|
-
"SELECT id, pid
|
|
38784
|
+
"SELECT id, pid FROM command_executions WHERE pid IS NOT NULL AND finished_at IS NULL"
|
|
38170
38785
|
);
|
|
38171
38786
|
if (activeResult.length > 0 && activeResult[0]) {
|
|
38172
38787
|
const { columns, values: activeRows } = activeResult[0];
|
|
38173
38788
|
const colIdx = Object.fromEntries(columns.map((c, i) => [c, i]));
|
|
38174
38789
|
for (const row of activeRows) {
|
|
38175
38790
|
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
|
-
}
|
|
38791
|
+
reapTree(pid, 750);
|
|
38792
|
+
console.log(`Reaping child process tree (PID ${pid})`);
|
|
38190
38793
|
}
|
|
38794
|
+
await new Promise((resolve3) => setTimeout(resolve3, 1e3));
|
|
38191
38795
|
db.run(
|
|
38192
38796
|
`UPDATE command_executions
|
|
38193
|
-
SET exit_code =
|
|
38797
|
+
SET exit_code = ?, finished_at = datetime('now'),
|
|
38194
38798
|
output = COALESCE(output, '') || '
|
|
38195
38799
|
[Cancelled \u2014 server shutdown]',
|
|
38196
38800
|
pid = NULL
|
|
38197
|
-
WHERE pid IS NOT NULL AND finished_at IS NULL
|
|
38801
|
+
WHERE pid IS NOT NULL AND finished_at IS NULL`,
|
|
38802
|
+
[CANCELLED_EXIT_CODE]
|
|
38198
38803
|
);
|
|
38199
38804
|
}
|
|
38200
38805
|
} catch (err) {
|
|
@@ -38217,9 +38822,9 @@ async function startServer(options = {}) {
|
|
|
38217
38822
|
process.exit(1);
|
|
38218
38823
|
}, 2e3).unref();
|
|
38219
38824
|
};
|
|
38220
|
-
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
38221
|
-
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
38222
|
-
process.on("SIGHUP", () => shutdown("SIGHUP"));
|
|
38825
|
+
process.on("SIGINT", () => void shutdown("SIGINT"));
|
|
38826
|
+
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
|
38827
|
+
process.on("SIGHUP", () => void shutdown("SIGHUP"));
|
|
38223
38828
|
process.on("uncaughtException", (err) => {
|
|
38224
38829
|
console.error("[dashboard] uncaughtException:", err);
|
|
38225
38830
|
});
|