@open-code-review/cli 2.0.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +2 -0
  2. package/dist/dashboard/client/assets/{_basePickBy-B3ALyupE.js → _basePickBy-BBPb8BJA.js} +1 -1
  3. package/dist/dashboard/client/assets/{_baseUniq-b2RALAWc.js → _baseUniq-CFHdos6T.js} +1 -1
  4. package/dist/dashboard/client/assets/{arc-DcSVvhUd.js → arc-BKGGWA2F.js} +1 -1
  5. package/dist/dashboard/client/assets/{architectureDiagram-VXUJARFQ-BNUlmSCS.js → architectureDiagram-VXUJARFQ-B_ovNjX1.js} +1 -1
  6. package/dist/dashboard/client/assets/{blockDiagram-VD42YOAC-BmhiQVwa.js → blockDiagram-VD42YOAC-C2M-avVp.js} +1 -1
  7. package/dist/dashboard/client/assets/{c4Diagram-YG6GDRKO-jyJ3WOv5.js → c4Diagram-YG6GDRKO-BtOBpAzH.js} +1 -1
  8. package/dist/dashboard/client/assets/channel-rgw7C1e7.js +1 -0
  9. package/dist/dashboard/client/assets/{chunk-4BX2VUAB-x1dQU_s3.js → chunk-4BX2VUAB-Cz2EbHPl.js} +1 -1
  10. package/dist/dashboard/client/assets/{chunk-55IACEB6-CwbsE2XQ.js → chunk-55IACEB6-C8xpXw9G.js} +1 -1
  11. package/dist/dashboard/client/assets/{chunk-B4BG7PRW-BaE7c-ti.js → chunk-B4BG7PRW-BSRfOovX.js} +1 -1
  12. package/dist/dashboard/client/assets/{chunk-DI55MBZ5-Bw5PUaMK.js → chunk-DI55MBZ5-CEUbYQWn.js} +1 -1
  13. package/dist/dashboard/client/assets/{chunk-FMBD7UC4-B7cF6P3s.js → chunk-FMBD7UC4-5xWP6GRj.js} +1 -1
  14. package/dist/dashboard/client/assets/{chunk-QN33PNHL-OY4evNHd.js → chunk-QN33PNHL-DfNCVcy8.js} +1 -1
  15. package/dist/dashboard/client/assets/{chunk-QZHKN3VN-BpjQwIWz.js → chunk-QZHKN3VN--OdToKKu.js} +1 -1
  16. package/dist/dashboard/client/assets/{chunk-TZMSLE5B-D8b_Oq9B.js → chunk-TZMSLE5B-B_0K0Qso.js} +1 -1
  17. package/dist/dashboard/client/assets/classDiagram-2ON5EDUG-DTGi7d9X.js +1 -0
  18. package/dist/dashboard/client/assets/classDiagram-v2-WZHVMYZB-DTGi7d9X.js +1 -0
  19. package/dist/dashboard/client/assets/clone-Cz7hswqi.js +1 -0
  20. package/dist/dashboard/client/assets/{cose-bilkent-S5V4N54A-C-sfP8PN.js → cose-bilkent-S5V4N54A-Cc_Dmnxz.js} +1 -1
  21. package/dist/dashboard/client/assets/{dagre-6UL2VRFP-Cqfo0NRg.js → dagre-6UL2VRFP-DaAfvUXU.js} +1 -1
  22. package/dist/dashboard/client/assets/{diagram-PSM6KHXK-BR3ppxqI.js → diagram-PSM6KHXK-7idwN0rC.js} +1 -1
  23. package/dist/dashboard/client/assets/{diagram-QEK2KX5R-Dvcx6x3R.js → diagram-QEK2KX5R-D9j9H13n.js} +1 -1
  24. package/dist/dashboard/client/assets/{diagram-S2PKOQOG-DoyBLnVN.js → diagram-S2PKOQOG-SMF5SB0K.js} +1 -1
  25. package/dist/dashboard/client/assets/{erDiagram-Q2GNP2WA-hy77l1cL.js → erDiagram-Q2GNP2WA-EVJ4Qa2F.js} +1 -1
  26. package/dist/dashboard/client/assets/{flowDiagram-NV44I4VS-Bz0B1rKM.js → flowDiagram-NV44I4VS-tZ7SFE77.js} +1 -1
  27. package/dist/dashboard/client/assets/{ganttDiagram-JELNMOA3-CLgrZPoC.js → ganttDiagram-JELNMOA3-DFSqguY7.js} +1 -1
  28. package/dist/dashboard/client/assets/{gitGraphDiagram-V2S2FVAM-DwJ-1f-v.js → gitGraphDiagram-V2S2FVAM-CqHdP3HE.js} +1 -1
  29. package/dist/dashboard/client/assets/{graph-DDBMM_t2.js → graph-C0XnkNkk.js} +1 -1
  30. package/dist/dashboard/client/assets/{index-Cr9yEo_B.js → index-C3NEq704.js} +133 -138
  31. package/dist/dashboard/client/assets/index-CzxeSSaQ.css +1 -0
  32. package/dist/dashboard/client/assets/{infoDiagram-HS3SLOUP-Bhn1FmAk.js → infoDiagram-HS3SLOUP-DlXZo9U2.js} +1 -1
  33. package/dist/dashboard/client/assets/{journeyDiagram-XKPGCS4Q-CzGbjX1y.js → journeyDiagram-XKPGCS4Q-CgC8_7eN.js} +1 -1
  34. package/dist/dashboard/client/assets/{kanban-definition-3W4ZIXB7-Da77-WYk.js → kanban-definition-3W4ZIXB7-BMAw_jNp.js} +1 -1
  35. package/dist/dashboard/client/assets/{layout-CVwSB-GS.js → layout-XjM3Q-ka.js} +1 -1
  36. package/dist/dashboard/client/assets/{linear-CTRAc5Jn.js → linear-CMUrrr1X.js} +1 -1
  37. package/dist/dashboard/client/assets/{mermaid-renderer-Bjo170ax.js → mermaid-renderer-D2jYNs7K.js} +4 -4
  38. package/dist/dashboard/client/assets/{mindmap-definition-VGOIOE7T-B55C2odl.js → mindmap-definition-VGOIOE7T-CL4hv-vg.js} +1 -1
  39. package/dist/dashboard/client/assets/{pieDiagram-ADFJNKIX-5lrQLrSz.js → pieDiagram-ADFJNKIX-DTqv-1h1.js} +1 -1
  40. package/dist/dashboard/client/assets/{quadrantDiagram-AYHSOK5B-Bg55gC30.js → quadrantDiagram-AYHSOK5B-BpFlSW9N.js} +1 -1
  41. package/dist/dashboard/client/assets/{requirementDiagram-UZGBJVZJ-CyR4YFJY.js → requirementDiagram-UZGBJVZJ-BqYqqXL4.js} +1 -1
  42. package/dist/dashboard/client/assets/{sankeyDiagram-TZEHDZUN-BVWKr9_-.js → sankeyDiagram-TZEHDZUN-kEI9kntR.js} +1 -1
  43. package/dist/dashboard/client/assets/{sequenceDiagram-WL72ISMW-D0AJg_tE.js → sequenceDiagram-WL72ISMW-Cnu_1j-N.js} +1 -1
  44. package/dist/dashboard/client/assets/{stateDiagram-FKZM4ZOC-BuHpTgim.js → stateDiagram-FKZM4ZOC-BoC-rqoG.js} +1 -1
  45. package/dist/dashboard/client/assets/stateDiagram-v2-4FDKWEC3-COR3QD3v.js +1 -0
  46. package/dist/dashboard/client/assets/{timeline-definition-IT6M3QCI-LDhpAmDd.js → timeline-definition-IT6M3QCI-CXMWuzDL.js} +1 -1
  47. package/dist/dashboard/client/assets/{treemap-GDKQZRPO-Dd4gjvUl.js → treemap-GDKQZRPO-o9ZFgpbJ.js} +1 -1
  48. package/dist/dashboard/client/assets/{xychartDiagram-PRI3JC2R-B9RDod39.js → xychartDiagram-PRI3JC2R-CfIuUpeA.js} +1 -1
  49. package/dist/dashboard/client/index.html +2 -2
  50. package/dist/dashboard/server.js +1175 -450
  51. package/dist/index.js +1489 -312
  52. package/dist/lib/db/index.js +666 -48
  53. package/dist/lib/runtime-config.js +29 -13
  54. package/dist/lib/state/index.js +2196 -0
  55. package/package.json +9 -5
  56. package/dist/dashboard/client/assets/channel-D3J8-GF_.js +0 -1
  57. package/dist/dashboard/client/assets/classDiagram-2ON5EDUG-tkFUL-1Y.js +0 -1
  58. package/dist/dashboard/client/assets/classDiagram-v2-WZHVMYZB-tkFUL-1Y.js +0 -1
  59. package/dist/dashboard/client/assets/clone-CkY5ajLr.js +0 -1
  60. package/dist/dashboard/client/assets/index-Z1pPudAt.css +0 -1
  61. package/dist/dashboard/client/assets/stateDiagram-v2-4FDKWEC3-DwAPhteN.js +0 -1
@@ -1579,14 +1579,14 @@ var require_internal = __commonJS({
1579
1579
  }
1580
1580
  InternalCodec.prototype.encoder = InternalEncoder;
1581
1581
  InternalCodec.prototype.decoder = InternalDecoder;
1582
- var StringDecoder = __require("string_decoder").StringDecoder;
1583
- if (!StringDecoder.prototype.end)
1584
- StringDecoder.prototype.end = function() {
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
- StringDecoder.call(this, codec.enc);
1587
+ StringDecoder2.call(this, codec.enc);
1588
1588
  }
1589
- InternalDecoder.prototype = StringDecoder.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 dirname14 = path2.dirname;
18414
- var basename4 = path2.basename;
18413
+ var dirname15 = path2.dirname;
18414
+ var basename5 = path2.basename;
18415
18415
  var extname = path2.extname;
18416
- var join19 = path2.join;
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 = dirname14(loc);
18453
- var file = basename4(loc);
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 = join19(dir, file);
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 = join19(dir, basename4(file, ext), "index" + ext);
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 basename4 = __require("path").basename;
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 = basename4(filename);
18586
+ var name = basename5(filename);
18587
18587
  var isQuotedString = TEXT_REGEXP.test(name);
18588
- var fallbackName = typeof fallback !== "string" ? fallback && getlatin1(name) : basename4(fallback);
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 join19 = path2.join;
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(join19(root, path3));
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 = join19(path3, self._index[i]);
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 existsSync15, readFileSync as readFileSync12, writeFileSync as writeFileSync5, unlinkSync as unlinkSync3, mkdirSync as mkdirSync6 } from "node:fs";
30487
- import { join as join18, dirname as dirname13, resolve as resolve2 } from "node:path";
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
@@ -30510,37 +30652,98 @@ function resolveOcrDir(startDir) {
30510
30652
  }
30511
30653
 
30512
30654
  // ../cli/src/lib/db/index.ts
30513
- import { existsSync as existsSync4, mkdirSync as mkdirSync2, copyFileSync, statSync } from "node:fs";
30514
- import { dirname as dirname4, join as join4 } from "node:path";
30655
+ import {
30656
+ existsSync as existsSync5,
30657
+ mkdirSync as mkdirSync2,
30658
+ copyFileSync as copyFileSync2,
30659
+ statSync as statSync2,
30660
+ mkdtempSync,
30661
+ rmSync
30662
+ } from "node:fs";
30663
+ import { dirname as dirname5, join as join5 } from "node:path";
30664
+
30665
+ // ../cli/src/lib/db/engine.ts
30666
+ import { createRequire } from "node:module";
30667
+
30668
+ // ../cli/src/lib/runtime-checks.ts
30669
+ var NODE_FLOOR = { major: 22, minor: 5 };
30670
+ function isSupportedNode(version) {
30671
+ const [major = 0, minor = 0] = version.split(".").map((n) => Number.parseInt(n, 10) || 0);
30672
+ return major > NODE_FLOOR.major || major === NODE_FLOOR.major && minor >= NODE_FLOOR.minor;
30673
+ }
30674
+ function nodeVersionGuardMessage(version) {
30675
+ return `
30676
+ Open Code Review requires Node.js >= ${NODE_FLOOR.major}.${NODE_FLOOR.minor} (it uses Node's built-in SQLite, \`node:sqlite\`).
30677
+ You have Node ${version}. Upgrade Node (e.g. \`nvm install 22 && nvm use 22\`) and re-run.
30678
+
30679
+ `;
30680
+ }
30681
+ function isSuppressibleSqliteWarning(warning) {
30682
+ const message = typeof warning === "string" ? warning : warning?.message;
30683
+ return typeof message === "string" && message.includes("SQLite is an experimental feature");
30684
+ }
30515
30685
 
30516
30686
  // ../cli/src/lib/db/engine.ts
30517
- import BetterSqlite3 from "better-sqlite3";
30687
+ var SQLITE_BUSY = 5;
30688
+ var SQLITE_BUSY_SNAPSHOT = 261;
30518
30689
  var BUSY_RETRY_ATTEMPTS = 5;
30519
30690
  var BUSY_RETRY_BACKOFF_MS = 50;
30520
- function isBusyError(e) {
30521
- if (e instanceof BetterSqlite3.SqliteError) {
30522
- return e.code === "SQLITE_BUSY" || e.code === "SQLITE_BUSY_SNAPSHOT";
30691
+ var savepointName = (depth) => `ocr_sp_${depth}`;
30692
+ var nodeRequire = createRequire(import.meta.url);
30693
+ var _preconditionsApplied = false;
30694
+ function applyEnginePreconditions() {
30695
+ if (_preconditionsApplied) return;
30696
+ _preconditionsApplied = true;
30697
+ const originalEmitWarning = process.emitWarning.bind(process);
30698
+ process.emitWarning = (warning, ...args) => {
30699
+ if (isSuppressibleSqliteWarning(warning)) return;
30700
+ originalEmitWarning(warning, ...args);
30701
+ };
30702
+ }
30703
+ var _DatabaseSyncCtor;
30704
+ function newDatabase(path2) {
30705
+ if (!_DatabaseSyncCtor) {
30706
+ applyEnginePreconditions();
30707
+ try {
30708
+ _DatabaseSyncCtor = nodeRequire("node:sqlite").DatabaseSync;
30709
+ } catch (e) {
30710
+ if (!isSupportedNode(process.versions.node)) {
30711
+ throw new Error(nodeVersionGuardMessage(process.versions.node).trim());
30712
+ }
30713
+ throw e;
30714
+ }
30523
30715
  }
30524
- const code = e?.code;
30525
- return code === "SQLITE_BUSY" || code === "SQLITE_BUSY_SNAPSHOT";
30716
+ return new _DatabaseSyncCtor(path2);
30717
+ }
30718
+ function isBusyError(e) {
30719
+ const errcode = e?.errcode;
30720
+ return errcode === SQLITE_BUSY || errcode === SQLITE_BUSY_SNAPSHOT;
30526
30721
  }
30722
+ var SLEEP_BUF = new Int32Array(new SharedArrayBuffer(4));
30527
30723
  function sleepSync(ms) {
30528
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
30724
+ Atomics.wait(SLEEP_BUF, 0, 0, ms);
30529
30725
  }
30530
- var BetterSqliteAdapter = class {
30726
+ var NodeSqliteAdapter = class {
30531
30727
  raw;
30728
+ /**
30729
+ * Transaction nesting depth. `node:sqlite` has no transaction helper, so we
30730
+ * drive `BEGIN IMMEDIATE` ourselves and use SAVEPOINTs for nested calls
30731
+ * (better-sqlite3 did this automatically). 0 = no transaction open.
30732
+ */
30733
+ txnDepth = 0;
30532
30734
  constructor(db) {
30533
30735
  this.raw = db;
30534
30736
  }
30535
30737
  exec(sql, params) {
30536
30738
  const stmt = this.raw.prepare(sql);
30537
- if (!stmt.reader) {
30739
+ const cols = stmt.columns();
30740
+ if (cols.length === 0) {
30538
30741
  stmt.run(...params ?? []);
30539
30742
  return [];
30540
30743
  }
30541
- const columns = stmt.columns().map((c) => c.name);
30542
- const values = stmt.raw().all(...params ?? []);
30543
- return values.length > 0 ? [{ columns, values }] : [];
30744
+ stmt.setReturnArrays(true);
30745
+ const values = stmt.all(...params ?? []);
30746
+ return values.length > 0 ? [{ columns: cols.map((c) => c.name), values }] : [];
30544
30747
  }
30545
30748
  run(sql, params) {
30546
30749
  if (params !== void 0) {
@@ -30553,34 +30756,93 @@ var BetterSqliteAdapter = class {
30553
30756
  return this.raw.prepare(sql);
30554
30757
  }
30555
30758
  transaction(fn) {
30556
- const tx = this.raw.transaction(fn);
30557
- for (let attempt = 0; ; attempt++) {
30759
+ return this.txnDepth > 0 ? this.runNested(fn) : this.runOuter(fn);
30760
+ }
30761
+ /**
30762
+ * Nested call: a SAVEPOINT within the outer transaction's write lock. No
30763
+ * busy-retry — the outer transaction already holds the lock. The savepoint
30764
+ * lets the inner block roll back independently while the outer continues.
30765
+ */
30766
+ runNested(fn) {
30767
+ const name = savepointName(this.txnDepth);
30768
+ this.raw.exec(`SAVEPOINT ${name}`);
30769
+ this.txnDepth++;
30770
+ try {
30771
+ const result = fn();
30772
+ this.raw.exec(`RELEASE ${name}`);
30773
+ return result;
30774
+ } catch (e) {
30558
30775
  try {
30559
- return tx.immediate();
30776
+ this.raw.exec(`ROLLBACK TO ${name}`);
30777
+ this.raw.exec(`RELEASE ${name}`);
30778
+ } catch {
30779
+ }
30780
+ throw e;
30781
+ } finally {
30782
+ this.txnDepth--;
30783
+ }
30784
+ }
30785
+ /**
30786
+ * Outer transaction: `BEGIN IMMEDIATE` acquires the write lock up front so
30787
+ * cross-process writers serialize cleanly under WAL instead of failing late
30788
+ * on upgrade. `busy_timeout` covers most contention; a bounded synchronous
30789
+ * retry absorbs the residual SQLITE_BUSY (another connection holds the lock
30790
+ * past the timeout, or BUSY_SNAPSHOT). Non-busy errors and the final attempt
30791
+ * re-throw so genuine failures propagate.
30792
+ */
30793
+ runOuter(fn) {
30794
+ for (let attempt = 0; attempt < BUSY_RETRY_ATTEMPTS; attempt++) {
30795
+ try {
30796
+ return this.runOnce(fn);
30560
30797
  } catch (e) {
30561
- if (!isBusyError(e) || attempt >= BUSY_RETRY_ATTEMPTS - 1) throw e;
30798
+ if (!isBusyError(e) || attempt === BUSY_RETRY_ATTEMPTS - 1) throw e;
30562
30799
  sleepSync(BUSY_RETRY_BACKOFF_MS);
30563
30800
  }
30564
30801
  }
30802
+ throw new Error("transaction retry budget exhausted");
30803
+ }
30804
+ /** One `BEGIN IMMEDIATE` / `COMMIT` / `ROLLBACK` lifecycle. */
30805
+ runOnce(fn) {
30806
+ this.raw.exec("BEGIN IMMEDIATE");
30807
+ this.txnDepth = 1;
30808
+ try {
30809
+ const result = fn();
30810
+ this.raw.exec("COMMIT");
30811
+ return result;
30812
+ } catch (e) {
30813
+ try {
30814
+ this.raw.exec("ROLLBACK");
30815
+ } catch {
30816
+ }
30817
+ throw e;
30818
+ } finally {
30819
+ this.txnDepth = 0;
30820
+ }
30565
30821
  }
30566
30822
  pragma(source) {
30567
- return this.raw.pragma(source);
30823
+ this.raw.exec(`PRAGMA ${source}`);
30824
+ return void 0;
30568
30825
  }
30569
30826
  close() {
30570
30827
  try {
30571
- this.raw.pragma("wal_checkpoint(TRUNCATE)");
30828
+ this.raw.exec("PRAGMA wal_checkpoint(TRUNCATE)");
30572
30829
  } catch {
30573
30830
  }
30574
- this.raw.close();
30831
+ try {
30832
+ this.raw.close();
30833
+ } catch (e) {
30834
+ const message = e?.message ?? "";
30835
+ if (!/database is not open/i.test(message)) throw e;
30836
+ }
30575
30837
  }
30576
30838
  };
30577
30839
  function openEngine(dbPath) {
30578
- const native = new BetterSqlite3(dbPath);
30579
- native.pragma("journal_mode = WAL");
30580
- native.pragma("foreign_keys = ON");
30581
- native.pragma("busy_timeout = 5000");
30582
- native.pragma("synchronous = NORMAL");
30583
- return new BetterSqliteAdapter(native);
30840
+ const native = newDatabase(dbPath);
30841
+ native.exec("PRAGMA journal_mode = WAL");
30842
+ native.exec("PRAGMA foreign_keys = ON");
30843
+ native.exec("PRAGMA busy_timeout = 5000");
30844
+ native.exec("PRAGMA synchronous = NORMAL");
30845
+ return new NodeSqliteAdapter(native);
30584
30846
  }
30585
30847
 
30586
30848
  // ../cli/src/lib/db/migrations.ts
@@ -31047,6 +31309,35 @@ var MIGRATIONS = [
31047
31309
  db.run("DROP INDEX IF EXISTS idx_command_executions_parent;");
31048
31310
  db.run("ALTER TABLE command_executions DROP COLUMN parent_id;");
31049
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
+ }
31050
31341
  }
31051
31342
  ];
31052
31343
  function columnExists(db, table, column) {
@@ -31361,9 +31652,28 @@ function sqliteUtcMs(ts) {
31361
31652
  }
31362
31653
 
31363
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
+ };
31364
31673
  var CANCELLED_EXIT_CODE = -2;
31365
31674
  var ORPHAN_EXIT_CODE = -3;
31366
31675
  var CASCADE_CLOSE_EXIT_CODE = -4;
31676
+ var WATCHDOG_DEADLINE_EXIT_CODE = -5;
31367
31677
 
31368
31678
  // ../cli/src/lib/db/agent-sessions.ts
31369
31679
  var NOTE_ORPHAN_PREFIX = "orphaned by liveness sweep";
@@ -31558,9 +31868,84 @@ function sweepStaleSessions(db, thresholdSeconds) {
31558
31868
  return { closedSessionIds: rows.map((r) => r.id) };
31559
31869
  }
31560
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
+
31561
31946
  // ../cli/src/lib/db/command-log.ts
31562
- import { appendFileSync, existsSync as existsSync3, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
31563
- import { dirname as dirname3, join as join3 } from "node:path";
31947
+ import { appendFileSync, existsSync as existsSync4, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
31948
+ import { dirname as dirname4, join as join4 } from "node:path";
31564
31949
  import { randomUUID } from "node:crypto";
31565
31950
  var CACHE_DIR = ".cache";
31566
31951
  var FILENAME = "command-history.jsonl";
@@ -31571,16 +31956,16 @@ function generateCommandUid() {
31571
31956
  return randomUUID();
31572
31957
  }
31573
31958
  function cacheDir(ocrDir) {
31574
- return join3(ocrDir, "data", CACHE_DIR);
31959
+ return join4(ocrDir, "data", CACHE_DIR);
31575
31960
  }
31576
31961
  function commandLogPath(ocrDir) {
31577
- return join3(cacheDir(ocrDir), FILENAME);
31962
+ return join4(cacheDir(ocrDir), FILENAME);
31578
31963
  }
31579
31964
  function appendCommandLog(ocrDir, entry) {
31580
31965
  try {
31581
31966
  const filePath = commandLogPath(ocrDir);
31582
- const dir = dirname3(filePath);
31583
- if (!existsSync3(dir)) mkdirSync(dir, { recursive: true });
31967
+ const dir = dirname4(filePath);
31968
+ if (!existsSync4(dir)) mkdirSync(dir, { recursive: true });
31584
31969
  const line = JSON.stringify(entry) + "\n";
31585
31970
  appendFileSync(filePath, line, { encoding: "utf-8" });
31586
31971
  if (approxLineCount >= 0) approxLineCount++;
@@ -31590,7 +31975,7 @@ function appendCommandLog(ocrDir, entry) {
31590
31975
  }
31591
31976
  function readCommandLog(ocrDir) {
31592
31977
  const filePath = commandLogPath(ocrDir);
31593
- if (!existsSync3(filePath)) return [];
31978
+ if (!existsSync4(filePath)) return [];
31594
31979
  const content = readFileSync(filePath, "utf-8");
31595
31980
  const entries = [];
31596
31981
  for (const line of content.split("\n")) {
@@ -31660,11 +32045,11 @@ var V2_SCHEMA_VERSION = 12;
31660
32045
  function maybeSnapshotBeforeUpgrade(db, dbPath, fromVersion) {
31661
32046
  if (fromVersion < 1 || fromVersion >= V2_SCHEMA_VERSION) return null;
31662
32047
  const bakPath = `${dbPath}.bak.v${fromVersion}`;
31663
- if (existsSync4(bakPath)) return bakPath;
32048
+ if (existsSync5(bakPath)) return bakPath;
31664
32049
  try {
31665
- if (!existsSync4(dbPath) || statSync(dbPath).size === 0) return null;
32050
+ if (!existsSync5(dbPath) || statSync2(dbPath).size === 0) return null;
31666
32051
  db.pragma("wal_checkpoint(TRUNCATE)");
31667
- copyFileSync(dbPath, bakPath);
32052
+ copyFileSync2(dbPath, bakPath);
31668
32053
  return bakPath;
31669
32054
  } catch {
31670
32055
  return null;
@@ -31698,8 +32083,8 @@ async function openDatabase(dbPath) {
31698
32083
  if (cached) {
31699
32084
  return cached;
31700
32085
  }
31701
- const dir = dirname4(dbPath);
31702
- if (!existsSync4(dir)) {
32086
+ const dir = dirname5(dbPath);
32087
+ if (!existsSync5(dir)) {
31703
32088
  mkdirSync2(dir, { recursive: true });
31704
32089
  }
31705
32090
  const db = openEngine(dbPath);
@@ -31707,11 +32092,11 @@ async function openDatabase(dbPath) {
31707
32092
  return db;
31708
32093
  }
31709
32094
  async function ensureDatabase(ocrDir) {
31710
- const dataDir = join4(ocrDir, "data");
31711
- if (!existsSync4(dataDir)) {
32095
+ const dataDir = join5(ocrDir, "data");
32096
+ if (!existsSync5(dataDir)) {
31712
32097
  mkdirSync2(dataDir, { recursive: true });
31713
32098
  }
31714
- const dbPath = join4(dataDir, "ocr.db");
32099
+ const dbPath = join5(dataDir, "ocr.db");
31715
32100
  const db = await openDatabase(dbPath);
31716
32101
  let before = 0;
31717
32102
  try {
@@ -31739,7 +32124,7 @@ async function ensureDatabase(ocrDir) {
31739
32124
  return db;
31740
32125
  }
31741
32126
  function walCheckpointTruncate(dbPath) {
31742
- if (!existsSync4(dbPath)) {
32127
+ if (!existsSync5(dbPath)) {
31743
32128
  return "skipped";
31744
32129
  }
31745
32130
  const cached = connections.get(dbPath);
@@ -31760,7 +32145,7 @@ function walCheckpointTruncate(dbPath) {
31760
32145
  return "failed";
31761
32146
  } finally {
31762
32147
  try {
31763
- transient?.raw.close();
32148
+ transient?.close();
31764
32149
  } catch {
31765
32150
  }
31766
32151
  }
@@ -31774,11 +32159,11 @@ function closeDatabase(dbPath) {
31774
32159
  }
31775
32160
 
31776
32161
  // src/server/db.ts
31777
- import { join as join5 } from "node:path";
32162
+ import { join as join6 } from "node:path";
31778
32163
  var cachedDb = null;
31779
32164
  var cachedDbPath = null;
31780
32165
  async function openDb(ocrDir) {
31781
- const dbPath = join5(ocrDir, "data", "ocr.db");
32166
+ const dbPath = join6(ocrDir, "data", "ocr.db");
31782
32167
  const db = await ensureDatabase(ocrDir);
31783
32168
  cachedDb = db;
31784
32169
  cachedDbPath = dbPath;
@@ -32960,37 +33345,9 @@ function createStatsRouter(db) {
32960
33345
  // src/server/routes/commands.ts
32961
33346
  var import_express8 = __toESM(require_express2(), 1);
32962
33347
 
32963
- // ../shared/platform/src/index.ts
32964
- import {
32965
- execFile,
32966
- execFileSync,
32967
- spawn
32968
- } from "node:child_process";
32969
- import { promisify } from "node:util";
32970
- var execFilePromise = promisify(execFile);
32971
- var isWindows = process.platform === "win32";
32972
- function execBinary(binary, args, opts) {
32973
- return execFileSync(binary, args, {
32974
- ...opts,
32975
- shell: isWindows
32976
- });
32977
- }
32978
- async function execBinaryAsync(binary, args, opts) {
32979
- return execFilePromise(binary, args, {
32980
- ...opts,
32981
- shell: isWindows
32982
- });
32983
- }
32984
- function spawnBinary(binary, args, opts) {
32985
- return spawn(binary, args, {
32986
- ...opts,
32987
- ...isWindows && { shell: true, windowsHide: true }
32988
- });
32989
- }
32990
-
32991
33348
  // src/server/socket/command-runner.ts
32992
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync2, unlinkSync, mkdirSync as mkdirSync4, existsSync as existsSync7 } from "node:fs";
32993
- import { dirname as dirname6, join as join10 } from "node:path";
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";
32994
33351
 
32995
33352
  // src/server/services/command-outcome.ts
32996
33353
  function deriveCommandOutcome(exitCode, completeness) {
@@ -32998,6 +33355,7 @@ function deriveCommandOutcome(exitCode, completeness) {
32998
33355
  if (exitCode === CANCELLED_EXIT_CODE || exitCode === CASCADE_CLOSE_EXIT_CODE) {
32999
33356
  return "cancelled";
33000
33357
  }
33358
+ if (exitCode === WATCHDOG_DEADLINE_EXIT_CODE) return "failed";
33001
33359
  if (exitCode !== 0) return "failed";
33002
33360
  if (completeness === null || completeness === "complete") return "success";
33003
33361
  return "incomplete";
@@ -33026,7 +33384,50 @@ function getWorkflowCompletenessForExecution(db, executionId) {
33026
33384
 
33027
33385
  // src/server/services/ai-cli/index.ts
33028
33386
  import { readFileSync as readFileSync3 } from "node:fs";
33029
- import { join as join8 } from "node:path";
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");
33030
33431
 
33031
33432
  // src/server/socket/env.ts
33032
33433
  var ENV_ALLOWLIST = [
@@ -33099,6 +33500,8 @@ var ClaudeCodeAdapter = class {
33099
33500
  // Claude Code subagent definitions support per-subagent model frontmatter,
33100
33501
  // so per-task model overrides are honored at the host level.
33101
33502
  supportsPerTaskModel = true;
33503
+ // Claude Code can spawn reviewer sub-agents via its Task tool.
33504
+ supportsSubagentSpawn = true;
33102
33505
  buildResumeArgs(vendorSessionId) {
33103
33506
  return buildResumeArgs("claude", vendorSessionId);
33104
33507
  }
@@ -33139,15 +33542,25 @@ var ClaudeCodeAdapter = class {
33139
33542
  if (opts.model) {
33140
33543
  flags.push("--model", opts.model);
33141
33544
  }
33545
+ const { stdio, logFd, logPath } = buildFileStdio(
33546
+ "pipe",
33547
+ isWorkflow ? opts.logFile : void 0
33548
+ );
33142
33549
  const proc = spawnBinary("claude", flags, {
33143
33550
  cwd: opts.cwd,
33144
33551
  env: { ...cleanEnv(), ...opts.env ?? {} },
33145
33552
  detached: isWorkflow,
33146
- stdio: ["pipe", "pipe", "pipe"]
33553
+ stdio
33147
33554
  });
33555
+ closeFileStdio(logFd);
33556
+ if (isWorkflow) proc.unref();
33148
33557
  proc.stdin?.write(opts.prompt);
33149
33558
  proc.stdin?.end();
33150
- return { process: proc, detached: isWorkflow };
33559
+ return {
33560
+ process: proc,
33561
+ detached: isWorkflow,
33562
+ ...logPath ? { logPath } : {}
33563
+ };
33151
33564
  }
33152
33565
  async listModels() {
33153
33566
  try {
@@ -33306,6 +33719,13 @@ var ClaudeLineParser = class {
33306
33719
  const message = typeof parsed["message"] === "string" ? parsed["message"] : "Agent error";
33307
33720
  events.push({ type: "error", source: "agent", message });
33308
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
+ }
33309
33729
  return events;
33310
33730
  }
33311
33731
  };
@@ -33341,6 +33761,9 @@ var OpenCodeAdapter = class {
33341
33761
  // until OpenCode adds per-task model support; OCR surfaces a warning to
33342
33762
  // the user when this happens.
33343
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;
33344
33767
  buildResumeArgs(vendorSessionId) {
33345
33768
  return buildResumeArgs("opencode", vendorSessionId);
33346
33769
  }
@@ -33378,13 +33801,23 @@ var OpenCodeAdapter = class {
33378
33801
  if (opts.model) {
33379
33802
  args.push("--model", opts.model);
33380
33803
  }
33804
+ const { stdio, logFd, logPath } = buildFileStdio(
33805
+ "ignore",
33806
+ isWorkflow ? opts.logFile : void 0
33807
+ );
33381
33808
  const proc = spawnBinary("opencode", args, {
33382
33809
  cwd: opts.cwd,
33383
33810
  env: { ...cleanEnv(), ...opts.env ?? {} },
33384
33811
  detached: isWorkflow,
33385
- stdio: ["ignore", "pipe", "pipe"]
33812
+ stdio
33386
33813
  });
33387
- return { process: proc, detached: isWorkflow };
33814
+ closeFileStdio(logFd);
33815
+ if (isWorkflow) proc.unref();
33816
+ return {
33817
+ process: proc,
33818
+ detached: isWorkflow,
33819
+ ...logPath ? { logPath } : {}
33820
+ };
33388
33821
  }
33389
33822
  async listModels() {
33390
33823
  try {
@@ -33515,20 +33948,20 @@ function extractToolOutput(part) {
33515
33948
  // src/server/services/event-journal.ts
33516
33949
  import {
33517
33950
  createWriteStream,
33518
- existsSync as existsSync5,
33519
- mkdirSync as mkdirSync3,
33951
+ existsSync as existsSync6,
33952
+ mkdirSync as mkdirSync4,
33520
33953
  readFileSync as readFileSync2
33521
33954
  } from "node:fs";
33522
- import { join as join6 } from "node:path";
33955
+ import { join as join8 } from "node:path";
33523
33956
  function eventsDir(ocrDir) {
33524
- const dir = join6(ocrDir, "data", "events");
33525
- if (!existsSync5(dir)) {
33526
- mkdirSync3(dir, { recursive: true });
33957
+ const dir = join8(ocrDir, "data", "events");
33958
+ if (!existsSync6(dir)) {
33959
+ mkdirSync4(dir, { recursive: true });
33527
33960
  }
33528
33961
  return dir;
33529
33962
  }
33530
33963
  function eventJournalPath(ocrDir, executionId) {
33531
- return join6(eventsDir(ocrDir), `${executionId}.jsonl`);
33964
+ return join8(eventsDir(ocrDir), `${executionId}.jsonl`);
33532
33965
  }
33533
33966
  var EventJournalAppender = class {
33534
33967
  stream;
@@ -33565,7 +33998,7 @@ var EventJournalAppender = class {
33565
33998
  };
33566
33999
  function readEventJournal(ocrDir, executionId) {
33567
34000
  const path2 = eventJournalPath(ocrDir, executionId);
33568
- if (!existsSync5(path2)) return [];
34001
+ if (!existsSync6(path2)) return [];
33569
34002
  let raw;
33570
34003
  try {
33571
34004
  raw = readFileSync2(path2, "utf-8");
@@ -33585,38 +34018,10 @@ function readEventJournal(ocrDir, executionId) {
33585
34018
  return events;
33586
34019
  }
33587
34020
 
33588
- // src/server/services/ai-cli/helpers.ts
33589
- import { tmpdir } from "node:os";
33590
- import { join as join7 } from "node:path";
33591
- function formatToolDetail(tool, input) {
33592
- switch (tool) {
33593
- case "Read":
33594
- return `Reading ${input["file_path"] ?? "file"}`;
33595
- case "Write":
33596
- return `Writing ${input["file_path"] ?? "file"}`;
33597
- case "Edit":
33598
- return `Editing ${input["file_path"] ?? "file"}`;
33599
- case "Grep":
33600
- return `Searching for "${input["pattern"] ?? "..."}"`;
33601
- case "Glob":
33602
- return `Finding files matching ${input["pattern"] ?? "..."}`;
33603
- case "Bash": {
33604
- let cmd = input["command"] ?? "...";
33605
- cmd = cmd.replace(/^cd\s+\S+\s*&&\s*/, "");
33606
- return `Running: ${cmd.slice(0, 120)}`;
33607
- }
33608
- case "Agent":
33609
- return `Spawning agent: ${input["description"] ?? "..."}`;
33610
- default:
33611
- return `Using ${tool}`;
33612
- }
33613
- }
33614
- var TEMP_BASE = join7(tmpdir(), "ocr-ai-prompts");
33615
-
33616
34021
  // src/server/services/ai-cli/index.ts
33617
34022
  function readAiCliPreference(ocrDir) {
33618
34023
  try {
33619
- const configPath = join8(ocrDir, "config.yaml");
34024
+ const configPath = join9(ocrDir, "config.yaml");
33620
34025
  const content = readFileSync3(configPath, "utf-8");
33621
34026
  const match = content.match(/^\s*ai_cli:\s*(\S+)/m);
33622
34027
  const value = match?.[1] ?? "auto";
@@ -33725,31 +34130,245 @@ var AiCliService = class {
33725
34130
  }
33726
34131
  };
33727
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
+
33728
34208
  // src/server/socket/cli-resolver.ts
33729
- import { existsSync as existsSync6 } from "node:fs";
33730
- import { dirname as dirname5, join as join9 } from "node:path";
34209
+ import { existsSync as existsSync8 } from "node:fs";
34210
+ import { dirname as dirname6, join as join10 } from "node:path";
33731
34211
  import { fileURLToPath } from "node:url";
33732
- var __dirname = dirname5(fileURLToPath(import.meta.url));
34212
+ var __dirname = dirname6(fileURLToPath(import.meta.url));
33733
34213
  function resolveLocalCli() {
33734
- const parentDir = join9(__dirname, "..");
33735
- const bundledCli = join9(parentDir, "index.js");
33736
- if (existsSync6(bundledCli) && existsSync6(join9(parentDir, "dashboard", "server.js"))) {
34214
+ const parentDir = join10(__dirname, "..");
34215
+ const bundledCli = join10(parentDir, "index.js");
34216
+ if (existsSync8(bundledCli) && existsSync8(join10(parentDir, "dashboard", "server.js"))) {
33737
34217
  return bundledCli;
33738
34218
  }
33739
34219
  let dir = __dirname;
33740
34220
  for (let i = 0; i < 8; i++) {
33741
- if (existsSync6(join9(dir, "nx.json"))) {
33742
- const candidate = join9(dir, "packages", "cli", "dist", "index.js");
33743
- if (existsSync6(candidate)) return candidate;
34221
+ if (existsSync8(join10(dir, "nx.json"))) {
34222
+ const candidate = join10(dir, "packages", "cli", "dist", "index.js");
34223
+ if (existsSync8(candidate)) return candidate;
33744
34224
  break;
33745
34225
  }
33746
- const parent = dirname5(dir);
34226
+ const parent = dirname6(dir);
33747
34227
  if (parent === dir) break;
33748
34228
  dir = parent;
33749
34229
  }
33750
34230
  return null;
33751
34231
  }
33752
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
+
33753
34372
  // src/server/socket/command-runner.ts
33754
34373
  function shellSplit(str) {
33755
34374
  const tokens = [];
@@ -33783,7 +34402,7 @@ var ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
33783
34402
  ]);
33784
34403
  var AI_COMMANDS = /* @__PURE__ */ new Set(["map", "review", "translate-review-to-single-human", "address", "create-reviewer", "sync-reviewers"]);
33785
34404
  function escapeUserHeaders(value) {
33786
- 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");
33787
34406
  }
33788
34407
  function buildPrompt(opts) {
33789
34408
  const { baseCommand, subArgs, commandContent, executionUid, localCli } = opts;
@@ -33930,23 +34549,45 @@ function extractPerInstanceModels(subArgs) {
33930
34549
  return [...models];
33931
34550
  }
33932
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
+ }
33933
34574
  var activeCommands = /* @__PURE__ */ new Map();
33934
34575
  function spawnMarkerPath(ocrDir) {
33935
- return join10(ocrDir, "data", "dashboard-active-spawn.json");
34576
+ return join12(ocrDir, "data", "dashboard-active-spawn.json");
33936
34577
  }
33937
34578
  function writeSpawnMarker(ocrDir, executionUid, pid) {
33938
- const dataDir = join10(ocrDir, "data");
33939
- if (!existsSync7(dataDir)) mkdirSync4(dataDir, { recursive: true });
34579
+ const dataDir = join12(ocrDir, "data");
34580
+ if (!existsSync10(dataDir)) mkdirSync5(dataDir, { recursive: true });
33940
34581
  const payload = JSON.stringify({
33941
34582
  execution_uid: executionUid,
33942
34583
  pid,
33943
34584
  started_at: (/* @__PURE__ */ new Date()).toISOString()
33944
34585
  });
33945
- writeFileSync2(spawnMarkerPath(ocrDir), payload, { mode: 384 });
34586
+ writeFileSync3(spawnMarkerPath(ocrDir), payload, { mode: 384 });
33946
34587
  }
33947
34588
  function clearSpawnMarker(ocrDir) {
33948
34589
  try {
33949
- unlinkSync(spawnMarkerPath(ocrDir));
34590
+ unlinkSync3(spawnMarkerPath(ocrDir));
33950
34591
  } catch {
33951
34592
  }
33952
34593
  }
@@ -34073,25 +34714,14 @@ function registerCommandHandlers(io2, socket, db, ocrDir, aiCliService, sessionC
34073
34714
  if (!proc) return;
34074
34715
  const pid = proc.pid;
34075
34716
  if (entry.detached && pid) {
34076
- try {
34077
- process.kill(-pid, "SIGTERM");
34078
- } catch {
34079
- proc.kill("SIGTERM");
34080
- }
34717
+ reapTree(pid);
34081
34718
  } else {
34082
34719
  proc.kill("SIGTERM");
34720
+ const killTimer = setTimeout(() => {
34721
+ if (activeCommands.has(targetId)) proc.kill("SIGKILL");
34722
+ }, 5e3);
34723
+ proc.once("close", () => clearTimeout(killTimer));
34083
34724
  }
34084
- const killTimer = setTimeout(() => {
34085
- if (!activeCommands.has(targetId)) return;
34086
- if (entry.detached && pid) {
34087
- try {
34088
- process.kill(-pid, "SIGKILL");
34089
- } catch {
34090
- }
34091
- }
34092
- proc.kill("SIGKILL");
34093
- }, 5e3);
34094
- proc.once("close", () => clearTimeout(killTimer));
34095
34725
  } catch (err) {
34096
34726
  console.error("Error in command:cancel handler:", err);
34097
34727
  socket.emit("error", { message: "Internal error" });
@@ -34100,7 +34730,7 @@ function registerCommandHandlers(io2, socket, db, ocrDir, aiCliService, sessionC
34100
34730
  }
34101
34731
  function spawnCliCommand(io2, db, ocrDir, executionId, baseCommand, subArgs, entry) {
34102
34732
  const localCli = resolveLocalCli();
34103
- const repoRoot = dirname6(ocrDir);
34733
+ const repoRoot = dirname7(ocrDir);
34104
34734
  const proc = localCli ? spawnBinary("node", [localCli, baseCommand, ...subArgs], {
34105
34735
  cwd: repoRoot,
34106
34736
  env: cleanEnv()
@@ -34126,8 +34756,7 @@ function spawnCliCommand(io2, db, ocrDir, executionId, baseCommand, subArgs, ent
34126
34756
  io2.emit("command:output", { execution_id: executionId, content: chunk });
34127
34757
  });
34128
34758
  proc.on("close", (code) => {
34129
- const finalCode = code ?? (entry.cancelled ? -2 : -1);
34130
- finishExecution(io2, db, ocrDir, executionId, finalCode, entry.outputBuffer);
34759
+ finishExecution(io2, db, ocrDir, executionId, code ?? -1, entry.outputBuffer);
34131
34760
  });
34132
34761
  proc.on("error", (err) => {
34133
34762
  entry.outputBuffer += `Process error: ${err.message}`;
@@ -34150,10 +34779,10 @@ function spawnAiCommand(io2, _socket, db, ocrDir, executionId, baseCommand, subA
34150
34779
  io2.emit("command:output", { execution_id: executionId, content: warning });
34151
34780
  }
34152
34781
  }
34153
- const commandMdPath = join10(ocrDir, "commands", `${baseCommand}.md`);
34782
+ const commandMdPath = join12(ocrDir, "commands", `${baseCommand}.md`);
34154
34783
  let commandContent;
34155
34784
  try {
34156
- commandContent = readFileSync4(commandMdPath, "utf-8");
34785
+ commandContent = readFileSync5(commandMdPath, "utf-8");
34157
34786
  } catch {
34158
34787
  const content = `Error: Could not read command file at ${commandMdPath}
34159
34788
  `;
@@ -34197,7 +34826,20 @@ function spawnAiCommand(io2, _socket, db, ocrDir, executionId, baseCommand, subA
34197
34826
  console.error("Failed to resolve resume context:", err);
34198
34827
  }
34199
34828
  }
34200
- const repoRoot = dirname6(ocrDir);
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
+ }
34201
34843
  const spawnOpts = {
34202
34844
  mode: "workflow",
34203
34845
  prompt,
@@ -34207,7 +34849,10 @@ function spawnAiCommand(io2, _socket, db, ocrDir, executionId, baseCommand, subA
34207
34849
  if (resumeSessionId) {
34208
34850
  spawnOpts.resumeSessionId = resumeSessionId;
34209
34851
  }
34210
- const { process: proc, detached } = adapter.spawn(spawnOpts);
34852
+ if (logFile) {
34853
+ spawnOpts.logFile = logFile;
34854
+ }
34855
+ const { process: proc, detached, logPath } = adapter.spawn(spawnOpts);
34211
34856
  entry.process = proc;
34212
34857
  entry.detached = detached;
34213
34858
  if (proc.pid) {
@@ -34243,6 +34888,61 @@ function spawnAiCommand(io2, _socket, db, ocrDir, executionId, baseCommand, subA
34243
34888
  }
34244
34889
  }, POLL_INTERVAL_MS);
34245
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();
34246
34946
  io2.emit("command:output", {
34247
34947
  execution_id: executionId,
34248
34948
  content: `\u25B8 Starting OCR ${baseCommand} workflow...
@@ -34307,11 +35007,16 @@ function spawnAiCommand(io2, _socket, db, ocrDir, executionId, baseCommand, subA
34307
35007
  emitStreamEvent(evt);
34308
35008
  break;
34309
35009
  }
35010
+ case "result": {
35011
+ entry.resultSeenAt = Date.now();
35012
+ entry.resultIsError = evt.isError;
35013
+ emitStreamEvent(evt);
35014
+ break;
35015
+ }
34310
35016
  }
34311
35017
  }
34312
- proc.stdout?.setEncoding("utf-8");
34313
- proc.stderr?.setEncoding("utf-8");
34314
- proc.stdout?.on("data", (chunk) => {
35018
+ function onOutputChunk(chunk) {
35019
+ bumpHeartbeat();
34315
35020
  lineBuffer += chunk;
34316
35021
  const lines = lineBuffer.split("\n");
34317
35022
  lineBuffer = lines.pop() ?? "";
@@ -34326,17 +35031,30 @@ function spawnAiCommand(io2, _socket, db, ocrDir, executionId, baseCommand, subA
34326
35031
  handleEvent(evt);
34327
35032
  }
34328
35033
  }
34329
- });
35034
+ }
34330
35035
  let stderrBuffer = "";
34331
- proc.stderr?.on("data", (chunk) => {
34332
- stderrBuffer += chunk;
34333
- });
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
+ }
34334
35048
  proc.on("close", (code) => {
34335
35049
  if (entry.linkPoll) {
34336
35050
  clearInterval(entry.linkPoll);
34337
35051
  entry.linkPoll = void 0;
34338
35052
  }
34339
35053
  clearSpawnMarker(ocrDir);
35054
+ if (entry.tailer) {
35055
+ entry.tailer.stop();
35056
+ entry.tailer = void 0;
35057
+ }
34340
35058
  if (lineBuffer.trim()) {
34341
35059
  const events = parser.parseLine(lineBuffer);
34342
35060
  for (const evt of events) {
@@ -34360,8 +35078,7 @@ ${stderrBuffer}`;
34360
35078
  journal.close().catch((err) => {
34361
35079
  console.error("[event-journal] close failed:", err);
34362
35080
  });
34363
- const finalCode = code ?? (entry.cancelled ? -2 : -1);
34364
- finishExecution(io2, db, ocrDir, executionId, finalCode, entry.outputBuffer);
35081
+ finishExecution(io2, db, ocrDir, executionId, code ?? -1, entry.outputBuffer);
34365
35082
  });
34366
35083
  proc.on("error", (err) => {
34367
35084
  if (entry.linkPoll) {
@@ -34375,15 +35092,28 @@ ${stderrBuffer}`;
34375
35092
  finishExecution(io2, db, ocrDir, executionId, -1, entry.outputBuffer);
34376
35093
  });
34377
35094
  }
34378
- function finishExecution(io2, db, ocrDir, executionId, code, output) {
35095
+ function finishExecution(io2, db, ocrDir, executionId, rawCode, output) {
34379
35096
  const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
34380
35097
  const entry = activeCommands.get(executionId);
34381
- db.run(
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(
34382
35112
  `UPDATE command_executions
34383
- SET exit_code = ?, finished_at = ?, output = ?, pid = NULL
34384
- WHERE id = ?`,
34385
- [code, finishedAt, output, executionId]
34386
- );
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;
34387
35117
  const completeness = getWorkflowCompletenessForExecution(db, executionId);
34388
35118
  const outcome = deriveCommandOutcome(code, completeness);
34389
35119
  const cancellationReason = deriveCancellationReason(code);
@@ -34398,7 +35128,7 @@ function finishExecution(io2, db, ocrDir, executionId, code, output) {
34398
35128
  started_at: entry.startedAt,
34399
35129
  finished_at: finishedAt,
34400
35130
  is_detached: entry.detached ? 1 : 0,
34401
- event: code === -2 ? "cancel" : "finish",
35131
+ event: code === CANCELLED_EXIT_CODE ? "cancel" : "finish",
34402
35132
  writer: "dashboard"
34403
35133
  });
34404
35134
  }
@@ -34410,6 +35140,27 @@ function finishExecution(io2, db, ocrDir, executionId, code, output) {
34410
35140
  cancellation_reason: cancellationReason
34411
35141
  });
34412
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
+ }
34413
35164
  }
34414
35165
 
34415
35166
  // src/server/routes/commands.ts
@@ -34497,8 +35248,8 @@ function createCommandsRouter(db, ocrDir) {
34497
35248
 
34498
35249
  // src/server/routes/config.ts
34499
35250
  var import_express9 = __toESM(require_express2(), 1);
34500
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
34501
- import { join as join11, dirname as dirname7, basename } from "node:path";
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";
34502
35253
  var VALID_IDES = ["vscode", "cursor", "windsurf", "jetbrains", "sublime"];
34503
35254
  function detectIde() {
34504
35255
  const termProgram = process.env.TERM_PROGRAM?.toLowerCase() ?? "";
@@ -34515,8 +35266,8 @@ function detectIde() {
34515
35266
  }
34516
35267
  function readIdeFromConfig(ocrDir) {
34517
35268
  try {
34518
- const configPath = join11(ocrDir, "config.yaml");
34519
- const content = readFileSync5(configPath, "utf-8");
35269
+ const configPath = join13(ocrDir, "config.yaml");
35270
+ const content = readFileSync6(configPath, "utf-8");
34520
35271
  const match = content.match(/^\s*ide:\s*(\S+)/m);
34521
35272
  return match?.[1] ?? "auto";
34522
35273
  } catch {
@@ -34543,8 +35294,8 @@ function detectGitBranch(cwd) {
34543
35294
  }
34544
35295
  function createConfigRouter(ocrDir, aiCliService) {
34545
35296
  const router = (0, import_express9.Router)();
34546
- const projectRoot = dirname7(ocrDir);
34547
- const workspaceName = basename(projectRoot);
35297
+ const projectRoot = dirname8(ocrDir);
35298
+ const workspaceName = basename2(projectRoot);
34548
35299
  const gitBranch = detectGitBranch(projectRoot);
34549
35300
  router.get("/", (_req, res) => {
34550
35301
  res.json({
@@ -34562,8 +35313,8 @@ function createConfigRouter(ocrDir, aiCliService) {
34562
35313
  return;
34563
35314
  }
34564
35315
  try {
34565
- const configPath = join11(ocrDir, "config.yaml");
34566
- let content = readFileSync5(configPath, "utf-8");
35316
+ const configPath = join13(ocrDir, "config.yaml");
35317
+ let content = readFileSync6(configPath, "utf-8");
34567
35318
  if (content.match(/^\s*ide:\s*\S+/m)) {
34568
35319
  content = content.replace(/^(\s*ide:\s*)\S+/m, `$1${ide}`);
34569
35320
  } else if (content.includes("dashboard:")) {
@@ -34575,7 +35326,7 @@ dashboard:
34575
35326
  ide: ${ide}
34576
35327
  `;
34577
35328
  }
34578
- writeFileSync3(configPath, content);
35329
+ writeFileSync4(configPath, content);
34579
35330
  res.json({ ide });
34580
35331
  } catch (err) {
34581
35332
  console.error("Failed to update config:", err);
@@ -34651,17 +35402,20 @@ function createChatRouter(db) {
34651
35402
 
34652
35403
  // src/server/routes/reviewers.ts
34653
35404
  var import_express11 = __toESM(require_express2(), 1);
34654
- import { readFileSync as readFileSync6, existsSync as existsSync8, watch } from "node:fs";
34655
- import { join as join12 } from "node:path";
35405
+ import { readFileSync as readFileSync7, existsSync as existsSync11, watch } from "node:fs";
35406
+ import { join as join14 } from "node:path";
34656
35407
  function readReviewersMeta(ocrDir) {
34657
- const metaPath = join12(ocrDir, "reviewers-meta.json");
34658
- if (!existsSync8(metaPath)) {
35408
+ const metaPath = join14(ocrDir, "reviewers-meta.json");
35409
+ if (!existsSync11(metaPath)) {
34659
35410
  return { reviewers: [], defaults: [] };
34660
35411
  }
34661
35412
  try {
34662
- const raw = readFileSync6(metaPath, "utf-8");
35413
+ const raw = readFileSync7(metaPath, "utf-8");
34663
35414
  const meta = JSON.parse(raw);
34664
- const reviewers = meta.reviewers ?? [];
35415
+ const reviewers = (meta.reviewers ?? []).map((r) => ({
35416
+ ...r,
35417
+ icon: r.icon || defaultIconFor(r.id, r.tier)
35418
+ }));
34665
35419
  const defaults = reviewers.filter((r) => r.is_default).map((r) => r.id);
34666
35420
  return { reviewers, defaults };
34667
35421
  } catch {
@@ -34680,13 +35434,13 @@ function createReviewersRouter(ocrDir) {
34680
35434
  res.status(400).json({ error: "Invalid reviewer ID" });
34681
35435
  return;
34682
35436
  }
34683
- const filePath = join12(ocrDir, "skills", "references", "reviewers", `${id}.md`);
34684
- if (!existsSync8(filePath)) {
35437
+ const filePath = join14(ocrDir, "skills", "references", "reviewers", `${id}.md`);
35438
+ if (!existsSync11(filePath)) {
34685
35439
  res.status(404).json({ error: "Reviewer not found", id });
34686
35440
  return;
34687
35441
  }
34688
35442
  try {
34689
- const content = readFileSync6(filePath, "utf-8");
35443
+ const content = readFileSync7(filePath, "utf-8");
34690
35444
  res.json({ id, content });
34691
35445
  } catch {
34692
35446
  res.status(500).json({ error: "Failed to read reviewer file", id });
@@ -34695,7 +35449,7 @@ function createReviewersRouter(ocrDir) {
34695
35449
  return router;
34696
35450
  }
34697
35451
  function watchReviewersMeta(ocrDir, io2) {
34698
- const metaPath = join12(ocrDir, "reviewers-meta.json");
35452
+ const metaPath = join14(ocrDir, "reviewers-meta.json");
34699
35453
  let watcher = null;
34700
35454
  let debounce;
34701
35455
  try {
@@ -34741,11 +35495,11 @@ function createAgentSessionsRouter(db, syncFromDisk = () => {
34741
35495
 
34742
35496
  // src/server/routes/handoff.ts
34743
35497
  var import_express13 = __toESM(require_express2(), 1);
34744
- import { dirname as dirname8 } from "node:path";
35498
+ import { dirname as dirname9 } from "node:path";
34745
35499
  function createHandoffRouter(sessionCapture, ocrDir, syncFromDisk = () => {
34746
35500
  }) {
34747
35501
  const router = (0, import_express13.Router)();
34748
- const projectDir = dirname8(ocrDir);
35502
+ const projectDir = dirname9(ocrDir);
34749
35503
  router.get("/:id/handoff", (req, res) => {
34750
35504
  const workflowId = req.params["id"];
34751
35505
  if (!workflowId) {
@@ -34775,14 +35529,14 @@ import { spawnSync } from "node:child_process";
34775
35529
 
34776
35530
  // ../cli/src/lib/team-config.ts
34777
35531
  var import_yaml = __toESM(require_dist(), 1);
34778
- import { existsSync as existsSync9, readFileSync as readFileSync7 } from "node:fs";
34779
- import { join as join13 } from "node:path";
35532
+ import { existsSync as existsSync12, readFileSync as readFileSync8 } from "node:fs";
35533
+ import { join as join15 } from "node:path";
34780
35534
  function loadTeamConfig(ocrDir) {
34781
- const configPath = join13(ocrDir, "config.yaml");
34782
- if (!existsSync9(configPath)) {
35535
+ const configPath = join15(ocrDir, "config.yaml");
35536
+ if (!existsSync12(configPath)) {
34783
35537
  return { team: [], aliases: {}, defaultModel: null };
34784
35538
  }
34785
- const content = readFileSync7(configPath, "utf-8");
35539
+ const content = readFileSync8(configPath, "utf-8");
34786
35540
  return parseTeamConfigYaml(content);
34787
35541
  }
34788
35542
  function parseTeamConfigYaml(content) {
@@ -35382,8 +36136,8 @@ function buildDiagnostics(input) {
35382
36136
  }
35383
36137
 
35384
36138
  // src/server/services/filesystem-sync.ts
35385
- import { readdirSync, readFileSync as readFileSync8, statSync as statSync2, existsSync as existsSync10 } from "node:fs";
35386
- import { join as join14, basename as basename2, dirname as dirname9, relative } from "node:path";
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";
35387
36141
  import { watch as watch2 } from "chokidar";
35388
36142
 
35389
36143
  // src/server/services/parsers/reviewer-parser.ts
@@ -35586,67 +36340,67 @@ var FilesystemSync = class {
35586
36340
  debounceTimers = /* @__PURE__ */ new Map();
35587
36341
  // ── 6.1: Full Scan ──
35588
36342
  async fullScan() {
35589
- if (!existsSync10(this.sessionsDir)) return;
35590
- const entries = readdirSync(this.sessionsDir, { withFileTypes: true });
36343
+ if (!existsSync13(this.sessionsDir)) return;
36344
+ const entries = readdirSync2(this.sessionsDir, { withFileTypes: true });
35591
36345
  for (const entry of entries) {
35592
36346
  if (!entry.isDirectory()) continue;
35593
36347
  const sessionId = entry.name;
35594
- const sessionDir = join14(this.sessionsDir, sessionId);
36348
+ const sessionDir = join16(this.sessionsDir, sessionId);
35595
36349
  this.syncSession(sessionId, sessionDir);
35596
36350
  }
35597
36351
  }
35598
36352
  syncSession(sessionId, sessionDir) {
35599
36353
  this.ensureSessionRow(sessionId, sessionDir);
35600
- const roundsDir = join14(sessionDir, "rounds");
35601
- if (existsSync10(roundsDir)) {
35602
- const rounds = readdirSync(roundsDir, { withFileTypes: true });
36354
+ const roundsDir = join16(sessionDir, "rounds");
36355
+ if (existsSync13(roundsDir)) {
36356
+ const rounds = readdirSync2(roundsDir, { withFileTypes: true });
35603
36357
  for (const roundEntry of rounds) {
35604
36358
  if (!roundEntry.isDirectory()) continue;
35605
36359
  const roundMatch = roundEntry.name.match(/^round-(\d+)$/);
35606
36360
  if (!roundMatch) continue;
35607
36361
  const roundNumber = parseInt(roundMatch[1] ?? "0", 10);
35608
- const roundDir = join14(roundsDir, roundEntry.name);
35609
- const reviewsDir = join14(roundDir, "reviews");
35610
- if (existsSync10(reviewsDir)) {
35611
- const reviewFiles = readdirSync(reviewsDir).filter((f) => f.endsWith(".md"));
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"));
35612
36366
  for (const reviewFile of reviewFiles) {
35613
- const filePath = join14(reviewsDir, reviewFile);
36367
+ const filePath = join16(reviewsDir, reviewFile);
35614
36368
  this.processReviewerOutput(sessionId, roundNumber, filePath, reviewFile);
35615
36369
  }
35616
36370
  }
35617
- const roundMetaPath = join14(roundDir, "round-meta.json");
35618
- if (existsSync10(roundMetaPath)) {
36371
+ const roundMetaPath = join16(roundDir, "round-meta.json");
36372
+ if (existsSync13(roundMetaPath)) {
35619
36373
  this.processRoundMeta(sessionId, roundNumber, roundMetaPath);
35620
36374
  }
35621
- const finalPath = join14(roundDir, "final.md");
35622
- if (existsSync10(finalPath)) {
36375
+ const finalPath = join16(roundDir, "final.md");
36376
+ if (existsSync13(finalPath)) {
35623
36377
  this.processFinalMd(sessionId, roundNumber, finalPath);
35624
36378
  }
35625
- const finalHumanPath = join14(roundDir, "final-human.md");
35626
- if (existsSync10(finalHumanPath)) {
36379
+ const finalHumanPath = join16(roundDir, "final-human.md");
36380
+ if (existsSync13(finalHumanPath)) {
35627
36381
  this.processGenericArtifact(sessionId, "final-human", finalHumanPath, roundNumber);
35628
36382
  }
35629
- const discoursePath = join14(roundDir, "discourse.md");
35630
- if (existsSync10(discoursePath)) {
36383
+ const discoursePath = join16(roundDir, "discourse.md");
36384
+ if (existsSync13(discoursePath)) {
35631
36385
  this.processGenericArtifact(sessionId, "discourse", discoursePath, roundNumber);
35632
36386
  }
35633
36387
  }
35634
36388
  }
35635
- const mapDir = join14(sessionDir, "map", "runs");
35636
- if (existsSync10(mapDir)) {
35637
- const runs = readdirSync(mapDir, { withFileTypes: true });
36389
+ const mapDir = join16(sessionDir, "map", "runs");
36390
+ if (existsSync13(mapDir)) {
36391
+ const runs = readdirSync2(mapDir, { withFileTypes: true });
35638
36392
  for (const runEntry of runs) {
35639
36393
  if (!runEntry.isDirectory()) continue;
35640
36394
  const runMatch = runEntry.name.match(/^run-(\d+)$/);
35641
36395
  if (!runMatch) continue;
35642
36396
  const runNumber = parseInt(runMatch[1] ?? "0", 10);
35643
- const runDir = join14(mapDir, runEntry.name);
35644
- const mapMetaPath = join14(runDir, "map-meta.json");
35645
- if (existsSync10(mapMetaPath)) {
36397
+ const runDir = join16(mapDir, runEntry.name);
36398
+ const mapMetaPath = join16(runDir, "map-meta.json");
36399
+ if (existsSync13(mapMetaPath)) {
35646
36400
  this.processMapMeta(sessionId, runNumber, mapMetaPath);
35647
36401
  }
35648
- const mapPath = join14(runDir, "map.md");
35649
- if (existsSync10(mapPath)) {
36402
+ const mapPath = join16(runDir, "map.md");
36403
+ if (existsSync13(mapPath)) {
35650
36404
  this.processMapMd(sessionId, runNumber, mapPath);
35651
36405
  }
35652
36406
  const mapArtifacts = [
@@ -35655,8 +36409,8 @@ var FilesystemSync = class {
35655
36409
  ["requirements-mapping.md", "requirements-mapping"]
35656
36410
  ];
35657
36411
  for (const [fileName, artifactType] of mapArtifacts) {
35658
- const filePath = join14(runDir, fileName);
35659
- if (existsSync10(filePath)) {
36412
+ const filePath = join16(runDir, fileName);
36413
+ if (existsSync13(filePath)) {
35660
36414
  this.processGenericArtifact(sessionId, artifactType, filePath, void 0, runNumber);
35661
36415
  }
35662
36416
  }
@@ -35667,8 +36421,8 @@ var FilesystemSync = class {
35667
36421
  ["discovered-standards.md", "discovered-standards"]
35668
36422
  ];
35669
36423
  for (const [fileName, artifactType] of sessionArtifacts) {
35670
- const filePath = join14(sessionDir, fileName);
35671
- if (existsSync10(filePath)) {
36424
+ const filePath = join16(sessionDir, fileName);
36425
+ if (existsSync13(filePath)) {
35672
36426
  this.processGenericArtifact(sessionId, artifactType, filePath);
35673
36427
  }
35674
36428
  }
@@ -35677,58 +36431,58 @@ var FilesystemSync = class {
35677
36431
  ensureSessionRow(sessionId, sessionDir) {
35678
36432
  const branchMatch = sessionId.match(/^\d{4}-\d{2}-\d{2}-(.+)$/);
35679
36433
  const branch = branchMatch?.[1] ?? "unknown";
35680
- const hasRoundsDir = existsSync10(join14(sessionDir, "rounds"));
35681
- const hasMapDir = existsSync10(join14(sessionDir, "map"));
36434
+ const hasRoundsDir = existsSync13(join16(sessionDir, "rounds"));
36435
+ const hasMapDir = existsSync13(join16(sessionDir, "map"));
35682
36436
  const workflowType = hasMapDir && !hasRoundsDir ? "map" : "review";
35683
36437
  let currentRound = 1;
35684
36438
  if (hasRoundsDir) {
35685
- const roundDirs = readdirSync(join14(sessionDir, "rounds")).filter((d) => d.match(/^round-\d+$/));
36439
+ const roundDirs = readdirSync2(join16(sessionDir, "rounds")).filter((d) => d.match(/^round-\d+$/));
35686
36440
  currentRound = Math.max(1, roundDirs.length);
35687
36441
  }
35688
36442
  let currentMapRun = 1;
35689
- const mapRunsDir = join14(sessionDir, "map", "runs");
35690
- if (existsSync10(mapRunsDir)) {
35691
- const runDirs = readdirSync(mapRunsDir).filter((d) => d.match(/^run-\d+$/));
36443
+ const mapRunsDir = join16(sessionDir, "map", "runs");
36444
+ if (existsSync13(mapRunsDir)) {
36445
+ const runDirs = readdirSync2(mapRunsDir).filter((d) => d.match(/^run-\d+$/));
35692
36446
  currentMapRun = Math.max(1, runDirs.length);
35693
36447
  }
35694
36448
  let phase = "context";
35695
36449
  let phaseNumber = 1;
35696
36450
  let status = "closed";
35697
36451
  if (workflowType === "review" && hasRoundsDir) {
35698
- const roundDir = join14(sessionDir, "rounds", `round-${currentRound}`);
35699
- if (existsSync10(join14(roundDir, "final.md"))) {
36452
+ const roundDir = join16(sessionDir, "rounds", `round-${currentRound}`);
36453
+ if (existsSync13(join16(roundDir, "final.md"))) {
35700
36454
  phase = "complete";
35701
36455
  phaseNumber = 8;
35702
36456
  status = "closed";
35703
- } else if (existsSync10(join14(roundDir, "discourse.md"))) {
36457
+ } else if (existsSync13(join16(roundDir, "discourse.md"))) {
35704
36458
  phase = "synthesis";
35705
36459
  phaseNumber = 7;
35706
- } else if (existsSync10(join14(roundDir, "reviews")) && readdirSync(join14(roundDir, "reviews")).filter((f) => f.endsWith(".md")).length > 0) {
36460
+ } else if (existsSync13(join16(roundDir, "reviews")) && readdirSync2(join16(roundDir, "reviews")).filter((f) => f.endsWith(".md")).length > 0) {
35707
36461
  phase = "reviews";
35708
36462
  phaseNumber = 4;
35709
- } else if (existsSync10(join14(sessionDir, "context.md"))) {
36463
+ } else if (existsSync13(join16(sessionDir, "context.md"))) {
35710
36464
  phase = "analysis";
35711
36465
  phaseNumber = 3;
35712
- } else if (existsSync10(join14(sessionDir, "discovered-standards.md"))) {
36466
+ } else if (existsSync13(join16(sessionDir, "discovered-standards.md"))) {
35713
36467
  phase = "change-context";
35714
36468
  phaseNumber = 2;
35715
36469
  }
35716
36470
  } else if (workflowType === "map" && hasMapDir) {
35717
- const runDir = join14(mapRunsDir, `run-${currentMapRun}`);
35718
- if (existsSync10(join14(runDir, "map.md"))) {
36471
+ const runDir = join16(mapRunsDir, `run-${currentMapRun}`);
36472
+ if (existsSync13(join16(runDir, "map.md"))) {
35719
36473
  phase = "complete";
35720
36474
  phaseNumber = 6;
35721
36475
  status = "closed";
35722
- } else if (existsSync10(join14(runDir, "requirements-mapping.md"))) {
36476
+ } else if (existsSync13(join16(runDir, "requirements-mapping.md"))) {
35723
36477
  phase = "synthesis";
35724
36478
  phaseNumber = 5;
35725
- } else if (existsSync10(join14(runDir, "flow-analysis.md"))) {
36479
+ } else if (existsSync13(join16(runDir, "flow-analysis.md"))) {
35726
36480
  phase = "requirements-mapping";
35727
36481
  phaseNumber = 4;
35728
- } else if (existsSync10(join14(runDir, "topology.md"))) {
36482
+ } else if (existsSync13(join16(runDir, "topology.md"))) {
35729
36483
  phase = "flow-analysis";
35730
36484
  phaseNumber = 3;
35731
- } else if (existsSync10(join14(sessionDir, "discovered-standards.md"))) {
36485
+ } else if (existsSync13(join16(sessionDir, "discovered-standards.md"))) {
35732
36486
  phase = "topology";
35733
36487
  phaseNumber = 2;
35734
36488
  }
@@ -35781,9 +36535,9 @@ var FilesystemSync = class {
35781
36535
  /** Returns true if the directory contains at least one .md or .json file (recursively). */
35782
36536
  hasArtifacts(dir) {
35783
36537
  try {
35784
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
36538
+ for (const entry of readdirSync2(dir, { withFileTypes: true })) {
35785
36539
  if (entry.isDirectory()) {
35786
- if (this.hasArtifacts(join14(dir, entry.name))) return true;
36540
+ if (this.hasArtifacts(join16(dir, entry.name))) return true;
35787
36541
  } else if (/\.(md|json)$/.test(entry.name)) {
35788
36542
  return true;
35789
36543
  }
@@ -35796,7 +36550,7 @@ var FilesystemSync = class {
35796
36550
  shouldSkip(filePath, existingParsedAt) {
35797
36551
  if (!existingParsedAt) return false;
35798
36552
  try {
35799
- const mtime = statSync2(filePath).mtime;
36553
+ const mtime = statSync3(filePath).mtime;
35800
36554
  const parsedAt = new Date(existingParsedAt);
35801
36555
  return mtime <= parsedAt;
35802
36556
  } catch {
@@ -35811,13 +36565,19 @@ var FilesystemSync = class {
35811
36565
  "SELECT id FROM markdown_artifacts WHERE session_id = ? AND artifact_type = ? AND round_number IS ? AND file_path = ?",
35812
36566
  [sessionId, artifactType, roundNumber ?? null, relPath]
35813
36567
  );
35814
- const isUpdate = existing !== null;
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
+ }
35815
36575
  this.db.run(
35816
- `INSERT OR REPLACE INTO markdown_artifacts (session_id, artifact_type, round_number, file_path, content, parsed_at)
36576
+ `INSERT INTO markdown_artifacts (session_id, artifact_type, round_number, file_path, content, parsed_at)
35817
36577
  VALUES (?, ?, ?, ?, ?, datetime('now'))`,
35818
36578
  [sessionId, artifactType, roundNumber ?? null, relPath, content]
35819
36579
  );
35820
- return isUpdate ? "updated" : "created";
36580
+ return "created";
35821
36581
  }
35822
36582
  // ── 6.7: Socket.IO Emission ──
35823
36583
  emitArtifactEvent(action, event) {
@@ -35833,12 +36593,12 @@ var FilesystemSync = class {
35833
36593
  );
35834
36594
  if (existingRun && this.shouldSkip(filePath, existingRun["parsed_at"] ?? null)) return;
35835
36595
  if (existingRun?.["source"] === "orchestrator") {
35836
- const content2 = readFileSync8(filePath, "utf-8");
36596
+ const content2 = readFileSync9(filePath, "utf-8");
35837
36597
  const action2 = this.upsertMarkdownArtifact(sessionId, "map", filePath, content2, void 0);
35838
36598
  this.emitArtifactEvent(action2, { sessionId, artifactType: "map", filePath });
35839
36599
  return;
35840
36600
  }
35841
- const content = readFileSync8(filePath, "utf-8");
36601
+ const content = readFileSync9(filePath, "utf-8");
35842
36602
  const parsed = parseMapMd(content);
35843
36603
  this.db.run(
35844
36604
  `INSERT OR REPLACE INTO map_runs (session_id, run_number, file_count, map_md_path, parsed_at, source)
@@ -35964,7 +36724,7 @@ var FilesystemSync = class {
35964
36724
  const roundId = roundRow?.["id"];
35965
36725
  if (!roundId) return;
35966
36726
  if (roundRow?.["source"] === "orchestrator") {
35967
- const content2 = readFileSync8(filePath, "utf-8");
36727
+ const content2 = readFileSync9(filePath, "utf-8");
35968
36728
  const action2 = this.upsertMarkdownArtifact(sessionId, "reviewer-output", filePath, content2, roundNumber);
35969
36729
  this.emitArtifactEvent(action2, {
35970
36730
  sessionId,
@@ -35983,7 +36743,7 @@ var FilesystemSync = class {
35983
36743
  [roundId, reviewerType, instanceNumber]
35984
36744
  );
35985
36745
  if (existingOutput && this.shouldSkip(filePath, existingOutput["parsed_at"] ?? null)) return;
35986
- const content = readFileSync8(filePath, "utf-8");
36746
+ const content = readFileSync9(filePath, "utf-8");
35987
36747
  const parsed = parseReviewerOutput(content);
35988
36748
  this.db.run(
35989
36749
  `INSERT OR REPLACE INTO reviewer_outputs (round_id, reviewer_type, instance_number, file_path, finding_count, parsed_at)
@@ -36071,7 +36831,7 @@ var FilesystemSync = class {
36071
36831
  if (existingRound?.["source"] === "orchestrator" && this.shouldSkip(filePath, existingRound["parsed_at"] ?? null)) return;
36072
36832
  let raw;
36073
36833
  try {
36074
- raw = JSON.parse(readFileSync8(filePath, "utf-8"));
36834
+ raw = JSON.parse(readFileSync9(filePath, "utf-8"));
36075
36835
  } catch {
36076
36836
  console.error(`[FilesystemSync] Failed to parse ${filePath}`);
36077
36837
  return;
@@ -36117,12 +36877,12 @@ var FilesystemSync = class {
36117
36877
  this.db.run("COMMIT");
36118
36878
  return;
36119
36879
  }
36120
- const roundDir = dirname9(filePath);
36880
+ const roundDir = dirname10(filePath);
36121
36881
  for (const reviewer of meta.reviewers) {
36122
36882
  const reviewerType = reviewer.type ?? "unknown";
36123
36883
  const instanceNumber = reviewer.instance ?? 1;
36124
36884
  const findings = reviewer.findings ?? [];
36125
- const reviewerMdPath = join14(roundDir, "reviews", `${reviewerType}-${instanceNumber}.md`);
36885
+ const reviewerMdPath = join16(roundDir, "reviews", `${reviewerType}-${instanceNumber}.md`);
36126
36886
  this.db.run(
36127
36887
  `INSERT OR REPLACE INTO reviewer_outputs (round_id, reviewer_type, instance_number, file_path, finding_count, parsed_at)
36128
36888
  VALUES (?, ?, ?, ?, ?, ?)`,
@@ -36220,7 +36980,7 @@ var FilesystemSync = class {
36220
36980
  if (existingRun?.["source"] === "orchestrator" && this.shouldSkip(filePath, existingRun["parsed_at"] ?? null)) return;
36221
36981
  let raw;
36222
36982
  try {
36223
- raw = JSON.parse(readFileSync8(filePath, "utf-8"));
36983
+ raw = JSON.parse(readFileSync9(filePath, "utf-8"));
36224
36984
  } catch {
36225
36985
  console.error(`[FilesystemSync] Failed to parse ${filePath}`);
36226
36986
  return;
@@ -36348,7 +37108,7 @@ var FilesystemSync = class {
36348
37108
  );
36349
37109
  const isOrchestratorSource = existingRound?.["source"] === "orchestrator";
36350
37110
  if (!isOrchestratorSource && existingRound && this.shouldSkip(filePath, existingRound["parsed_at"] ?? null)) return;
36351
- const content = readFileSync8(filePath, "utf-8");
37111
+ const content = readFileSync9(filePath, "utf-8");
36352
37112
  if (isOrchestratorSource) {
36353
37113
  this.db.run(
36354
37114
  `UPDATE review_rounds SET final_md_path = ?, parsed_at = ?
@@ -36427,7 +37187,7 @@ var FilesystemSync = class {
36427
37187
  [sessionId, artifactType, relPath]
36428
37188
  );
36429
37189
  if (existing && this.shouldSkip(filePath, existing["parsed_at"] ?? null)) return;
36430
- const content = readFileSync8(filePath, "utf-8");
37190
+ const content = readFileSync9(filePath, "utf-8");
36431
37191
  const action = this.upsertMarkdownArtifact(sessionId, artifactType, filePath, content, roundNumber);
36432
37192
  this.emitArtifactEvent(action, {
36433
37193
  sessionId,
@@ -36446,7 +37206,7 @@ var FilesystemSync = class {
36446
37206
  ignored: [
36447
37207
  // Only ignore entries whose own name starts with a dot — the old regex
36448
37208
  // /(^|[/\\])\../ matched `.ocr` in the parent path, silencing ALL events.
36449
- (filePath) => basename2(filePath).startsWith("."),
37209
+ (filePath) => basename3(filePath).startsWith("."),
36450
37210
  /node_modules/,
36451
37211
  /\.db$/
36452
37212
  ]
@@ -36485,9 +37245,9 @@ var FilesystemSync = class {
36485
37245
  const parts = relFromSessions.split("/");
36486
37246
  const sessionId = parts[0];
36487
37247
  if (!sessionId) return;
36488
- const sessionDir = join14(this.sessionsDir, sessionId);
37248
+ const sessionDir = join16(this.sessionsDir, sessionId);
36489
37249
  this.ensureSessionRow(sessionId, sessionDir);
36490
- const fileName = basename2(filePath);
37250
+ const fileName = basename3(filePath);
36491
37251
  const reviewerMatch = relFromSessions.match(/rounds\/round-(\d+)\/reviews\/(.+\.md)$/);
36492
37252
  if (reviewerMatch) {
36493
37253
  const roundNumber = parseInt(reviewerMatch[1] ?? "0", 10);
@@ -36557,8 +37317,8 @@ var FilesystemSync = class {
36557
37317
  };
36558
37318
 
36559
37319
  // src/server/services/db-sync-watcher.ts
36560
- import { existsSync as existsSync11 } from "node:fs";
36561
- import { dirname as dirname10, basename as basename3 } from "node:path";
37320
+ import { existsSync as existsSync14 } from "node:fs";
37321
+ import { dirname as dirname11, basename as basename4 } from "node:path";
36562
37322
  import { watch as watch3 } from "chokidar";
36563
37323
  function col(row, key) {
36564
37324
  return row[key] ?? null;
@@ -36599,9 +37359,9 @@ var DbSyncWatcher = class {
36599
37359
  }
36600
37360
  /** Start watching the DB file (and its WAL sidecar) for external writes. */
36601
37361
  startWatching() {
36602
- if (!existsSync11(this.dbFilePath)) return;
36603
- const watchDir = dirname10(this.dbFilePath);
36604
- const dbFile = basename3(this.dbFilePath);
37362
+ if (!existsSync14(this.dbFilePath)) return;
37363
+ const watchDir = dirname11(this.dbFilePath);
37364
+ const dbFile = basename4(this.dbFilePath);
36605
37365
  const walFile = `${dbFile}-wal`;
36606
37366
  this.watcher = watch3(watchDir, {
36607
37367
  persistent: true,
@@ -36611,7 +37371,7 @@ var DbSyncWatcher = class {
36611
37371
  interval: 200
36612
37372
  });
36613
37373
  const onAnyEvent = (path2) => {
36614
- const name = basename3(path2);
37374
+ const name = basename4(path2);
36615
37375
  if (name === dbFile || name === walFile) this.debouncedSync();
36616
37376
  };
36617
37377
  this.watcher.on("change", onAnyEvent);
@@ -36862,20 +37622,20 @@ function commandFingerprint(row) {
36862
37622
  }
36863
37623
 
36864
37624
  // src/server/socket/chat-handler.ts
36865
- import { dirname as dirname11 } from "node:path";
37625
+ import { dirname as dirname12 } from "node:path";
36866
37626
 
36867
37627
  // src/server/services/chat-context.ts
36868
- import { readFileSync as readFileSync9, readdirSync as readdirSync2, existsSync as existsSync12 } from "node:fs";
36869
- import { join as join15 } from "node:path";
37628
+ import { readFileSync as readFileSync10, readdirSync as readdirSync3, existsSync as existsSync15 } from "node:fs";
37629
+ import { join as join17 } from "node:path";
36870
37630
  function buildChatContext(ocrDir, target) {
36871
- const sessionsDir = join15(ocrDir, "sessions");
37631
+ const sessionsDir = join17(ocrDir, "sessions");
36872
37632
  if (target.type === "map_run") {
36873
37633
  return buildMapRunContext(sessionsDir, target.sessionId, target.runNumber);
36874
37634
  }
36875
37635
  return buildReviewRoundContext(sessionsDir, target.sessionId, target.roundNumber);
36876
37636
  }
36877
37637
  function buildMapRunContext(sessionsDir, sessionId, runNumber) {
36878
- const mapPath = join15(
37638
+ const mapPath = join17(
36879
37639
  sessionsDir,
36880
37640
  sessionId,
36881
37641
  "map",
@@ -36889,8 +37649,8 @@ function buildMapRunContext(sessionsDir, sessionId, runNumber) {
36889
37649
  "",
36890
37650
  `Below is the Code Review Map that organizes the changeset into reviewable sections:`
36891
37651
  ];
36892
- if (existsSync12(mapPath)) {
36893
- const content = readFileSync9(mapPath, "utf-8");
37652
+ if (existsSync15(mapPath)) {
37653
+ const content = readFileSync10(mapPath, "utf-8");
36894
37654
  parts.push("");
36895
37655
  parts.push("<map>");
36896
37656
  parts.push(content);
@@ -36902,26 +37662,26 @@ function buildMapRunContext(sessionsDir, sessionId, runNumber) {
36902
37662
  return parts.join("\n");
36903
37663
  }
36904
37664
  function buildReviewRoundContext(sessionsDir, sessionId, roundNumber) {
36905
- const roundDir = join15(sessionsDir, sessionId, "rounds", `round-${roundNumber}`);
36906
- const finalPath = join15(roundDir, "final.md");
36907
- const reviewersDir = join15(roundDir, "reviews");
37665
+ const roundDir = join17(sessionsDir, sessionId, "rounds", `round-${roundNumber}`);
37666
+ const finalPath = join17(roundDir, "final.md");
37667
+ const reviewersDir = join17(roundDir, "reviews");
36908
37668
  const parts = [
36909
37669
  `You are an expert code reviewer assisting with a code review session.`,
36910
37670
  `You are looking at review round #${roundNumber} for session "${sessionId}".`,
36911
37671
  "",
36912
37672
  `Below are the review artifacts for this round:`
36913
37673
  ];
36914
- if (existsSync12(finalPath)) {
36915
- const content = readFileSync9(finalPath, "utf-8");
37674
+ if (existsSync15(finalPath)) {
37675
+ const content = readFileSync10(finalPath, "utf-8");
36916
37676
  parts.push("");
36917
37677
  parts.push("<final-synthesis>");
36918
37678
  parts.push(content);
36919
37679
  parts.push("</final-synthesis>");
36920
37680
  }
36921
- if (existsSync12(reviewersDir)) {
36922
- const files = readdirSync2(reviewersDir).filter((f) => f.endsWith(".md")).sort();
37681
+ if (existsSync15(reviewersDir)) {
37682
+ const files = readdirSync3(reviewersDir).filter((f) => f.endsWith(".md")).sort();
36923
37683
  for (const file of files) {
36924
- const content = readFileSync9(join15(reviewersDir, file), "utf-8");
37684
+ const content = readFileSync10(join17(reviewersDir, file), "utf-8");
36925
37685
  const reviewerName = file.replace(/\.md$/, "");
36926
37686
  parts.push("");
36927
37687
  parts.push(`<reviewer name="${reviewerName}">`);
@@ -36929,7 +37689,7 @@ function buildReviewRoundContext(sessionsDir, sessionId, roundNumber) {
36929
37689
  parts.push("</reviewer>");
36930
37690
  }
36931
37691
  }
36932
- if (!existsSync12(finalPath) && !existsSync12(reviewersDir)) {
37692
+ if (!existsSync15(finalPath) && !existsSync15(reviewersDir)) {
36933
37693
  parts.push("");
36934
37694
  parts.push("(No review artifacts found on disk for this round.)");
36935
37695
  }
@@ -37075,7 +37835,7 @@ User: ${message}`;
37075
37835
  });
37076
37836
  return;
37077
37837
  }
37078
- const repoRoot = dirname11(ocrDir);
37838
+ const repoRoot = dirname12(ocrDir);
37079
37839
  const spawnResult = adapter.spawn({
37080
37840
  prompt,
37081
37841
  cwd: repoRoot,
@@ -37251,13 +38011,13 @@ function cleanupAllChats() {
37251
38011
  }
37252
38012
 
37253
38013
  // src/server/socket/post-handler.ts
37254
- import { existsSync as existsSync13, mkdirSync as mkdirSync5, readFileSync as readFileSync10, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "node:fs";
38014
+ import { existsSync as existsSync16, mkdirSync as mkdirSync6, readFileSync as readFileSync11, unlinkSync as unlinkSync4, writeFileSync as writeFileSync5 } from "node:fs";
37255
38015
  import { tmpdir as tmpdir2 } from "node:os";
37256
- import { join as join16, dirname as dirname12, isAbsolute as isAbsolute2 } from "node:path";
38016
+ import { join as join18, dirname as dirname13, isAbsolute as isAbsolute2 } from "node:path";
37257
38017
  import { randomUUID as randomUUID2 } from "node:crypto";
37258
38018
  function resolveSessionDir2(sessionDir, ocrDir) {
37259
38019
  if (isAbsolute2(sessionDir)) return sessionDir;
37260
- return join16(dirname12(ocrDir), sessionDir);
38020
+ return join18(dirname13(ocrDir), sessionDir);
37261
38021
  }
37262
38022
  var BRANCH_PREFIXES = [
37263
38023
  "feat",
@@ -37326,7 +38086,7 @@ function registerPostHandlers(io2, socket, db, ocrDir, aiCliService) {
37326
38086
  return;
37327
38087
  }
37328
38088
  const branch = session.branch;
37329
- const repoRoot = dirname12(ocrDir);
38089
+ const repoRoot = dirname13(ocrDir);
37330
38090
  try {
37331
38091
  await execBinaryAsync("gh", ["auth", "status"], { env: cleanEnv(), cwd: repoRoot, encoding: "utf-8" });
37332
38092
  } catch {
@@ -37427,19 +38187,19 @@ function registerPostHandlers(io2, socket, db, ocrDir, aiCliService) {
37427
38187
  socket.emit("post:error", { error: "Session not found" });
37428
38188
  return;
37429
38189
  }
37430
- const sessionDir = session.session_dir ? resolveSessionDir2(session.session_dir, ocrDir) : join16(ocrDir, "sessions", sessionId);
37431
- const roundDir = join16(sessionDir, "rounds", `round-${roundNumber}`);
37432
- const finalPath = join16(roundDir, "final.md");
37433
- if (!existsSync13(finalPath)) {
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)) {
37434
38194
  socket.emit("post:error", { error: "final.md not found for this round" });
37435
38195
  return;
37436
38196
  }
37437
- const humanReviewPath = join16(roundDir, "final-human.md");
37438
- const repoRoot = dirname12(ocrDir);
37439
- const commandMdPath = join16(ocrDir, "commands", "translate-review-to-single-human.md");
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");
37440
38200
  let commandContent;
37441
38201
  try {
37442
- commandContent = readFileSync10(commandMdPath, "utf-8");
38202
+ commandContent = readFileSync11(commandMdPath, "utf-8");
37443
38203
  } catch {
37444
38204
  socket.emit("post:error", {
37445
38205
  error: `Command file not found: ${commandMdPath}. Run \`ocr init\` to set up.`
@@ -37519,9 +38279,9 @@ function registerPostHandlers(io2, socket, db, ocrDir, aiCliService) {
37519
38279
  }
37520
38280
  }
37521
38281
  let generatedContent = "";
37522
- if (existsSync13(humanReviewPath)) {
38282
+ if (existsSync16(humanReviewPath)) {
37523
38283
  try {
37524
- generatedContent = readFileSync10(humanReviewPath, "utf-8").trim();
38284
+ generatedContent = readFileSync11(humanReviewPath, "utf-8").trim();
37525
38285
  } catch {
37526
38286
  }
37527
38287
  }
@@ -37596,11 +38356,11 @@ function registerPostHandlers(io2, socket, db, ocrDir, aiCliService) {
37596
38356
  socket.emit("post:save-result", { success: false, error: "Session not found" });
37597
38357
  return;
37598
38358
  }
37599
- const sessionDir = session.session_dir ? resolveSessionDir2(session.session_dir, ocrDir) : join16(ocrDir, "sessions", sessionId);
37600
- const roundDir = join16(sessionDir, "rounds", `round-${roundNumber}`);
37601
- mkdirSync5(roundDir, { recursive: true });
37602
- const filePath = join16(roundDir, "final-human.md");
37603
- writeFileSync4(filePath, content, { mode: 420 });
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 });
37604
38364
  socket.emit("post:save-result", { success: true });
37605
38365
  } catch (err) {
37606
38366
  console.error("Error in post:save handler:", err);
@@ -37626,14 +38386,14 @@ function registerPostHandlers(io2, socket, db, ocrDir, aiCliService) {
37626
38386
  );
37627
38387
  tracker.appendOutput(`\u25B8 Posting review to PR #${prNumber}...
37628
38388
  `);
37629
- const tmpDir = join16(tmpdir2(), "ocr-post-comments");
38389
+ const tmpDir = join18(tmpdir2(), "ocr-post-comments");
37630
38390
  try {
37631
- mkdirSync5(tmpDir, { recursive: true, mode: 448 });
38391
+ mkdirSync6(tmpDir, { recursive: true, mode: 448 });
37632
38392
  } catch {
37633
38393
  }
37634
- const tmpFile = join16(tmpDir, `${randomUUID2()}.md`);
37635
- writeFileSync4(tmpFile, content, { mode: 384 });
37636
- const repoRoot = dirname12(ocrDir);
38394
+ const tmpFile = join18(tmpDir, `${randomUUID2()}.md`);
38395
+ writeFileSync5(tmpFile, content, { mode: 384 });
38396
+ const repoRoot = dirname13(ocrDir);
37637
38397
  try {
37638
38398
  const { stdout } = await execBinaryAsync(
37639
38399
  "gh",
@@ -37656,7 +38416,7 @@ function registerPostHandlers(io2, socket, db, ocrDir, aiCliService) {
37656
38416
  });
37657
38417
  } finally {
37658
38418
  try {
37659
- unlinkSync2(tmpFile);
38419
+ unlinkSync4(tmpFile);
37660
38420
  } catch {
37661
38421
  }
37662
38422
  }
@@ -37676,45 +38436,9 @@ function cleanupAllPostGenerations() {
37676
38436
  }
37677
38437
  }
37678
38438
 
37679
- // ../cli/src/lib/runtime-config.ts
37680
- import { existsSync as existsSync14, readFileSync as readFileSync11 } from "node:fs";
37681
- import { join as join17 } from "node:path";
37682
- var DEFAULT_AGENT_HEARTBEAT_SECONDS = 60;
37683
- function getAgentHeartbeatSeconds(ocrDir) {
37684
- const configPath = join17(ocrDir, "config.yaml");
37685
- if (!existsSync14(configPath)) {
37686
- return DEFAULT_AGENT_HEARTBEAT_SECONDS;
37687
- }
37688
- let content;
37689
- try {
37690
- content = readFileSync11(configPath, "utf-8");
37691
- } catch {
37692
- return DEFAULT_AGENT_HEARTBEAT_SECONDS;
37693
- }
37694
- const blockMatch = content.match(
37695
- /^runtime:\s*\n(?:\s+[^\n]*\n)*?\s+agent_heartbeat_seconds:\s*([^\s#\n]+)/m
37696
- );
37697
- const inlineMatch = content.match(
37698
- /^runtime:\s*\{[^}]*\bagent_heartbeat_seconds:\s*([^\s,}]+)/m
37699
- );
37700
- const raw = blockMatch?.[1] ?? inlineMatch?.[1];
37701
- if (!raw) {
37702
- return DEFAULT_AGENT_HEARTBEAT_SECONDS;
37703
- }
37704
- const parsed = Number(raw);
37705
- if (!Number.isFinite(parsed) || parsed <= 0 || !Number.isInteger(parsed)) {
37706
- process.stderr.write(
37707
- `[ocr] runtime.agent_heartbeat_seconds is not a positive integer (got "${raw}"); falling back to ${DEFAULT_AGENT_HEARTBEAT_SECONDS}s.
37708
- `
37709
- );
37710
- return DEFAULT_AGENT_HEARTBEAT_SECONDS;
37711
- }
37712
- return parsed;
37713
- }
37714
-
37715
38439
  // src/server/index.ts
37716
38440
  import { homedir } from "node:os";
37717
- var __dirname3 = dirname13(fileURLToPath3(import.meta.url));
38441
+ var __dirname3 = dirname14(fileURLToPath3(import.meta.url));
37718
38442
  function shortenPath(p) {
37719
38443
  const home = homedir();
37720
38444
  return p.startsWith(home) ? "~" + p.slice(home.length) : p;
@@ -37778,40 +38502,61 @@ if (process.env.NODE_ENV !== "production") {
37778
38502
  res.json({ token: AUTH_TOKEN });
37779
38503
  });
37780
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
+ }
37781
38518
  async function startServer(options = {}) {
37782
38519
  const port = options.port ?? parseInt(process.env.PORT ?? "4173", 10);
38520
+ process.title = "ocr-dashboard";
37783
38521
  const ocrDir = resolveOcrDir();
37784
38522
  const aiCliService = new AiCliService(ocrDir);
37785
- const dbPathForCheckpoint = join18(ocrDir, "data", "ocr.db");
38523
+ const dbPathForCheckpoint = join19(ocrDir, "data", "ocr.db");
37786
38524
  const walResult = walCheckpointTruncate(dbPathForCheckpoint);
37787
38525
  if (walResult === "checkpointed") {
37788
38526
  console.log(" WAL checkpoint: truncated stale write-ahead-log file");
37789
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
+ }
37790
38535
  const db = await openDb(ocrDir);
37791
- const dataDir = join18(ocrDir, "data");
37792
- const pidFilePath = join18(dataDir, "dashboard.pid");
37793
- const portFilePath = join18(dataDir, "server-port");
37794
- mkdirSync6(dataDir, { recursive: true });
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 });
37795
38540
  try {
37796
- unlinkSync3(portFilePath);
38541
+ unlinkSync5(portFilePath);
37797
38542
  } catch {
37798
38543
  }
37799
- if (existsSync15(pidFilePath)) {
38544
+ if (existsSync17(pidFilePath)) {
37800
38545
  try {
37801
38546
  const oldPid = parseInt(readFileSync12(pidFilePath, "utf-8").trim(), 10);
37802
- if (!isNaN(oldPid)) {
37803
- try {
37804
- process.kill(oldPid, 0);
37805
- console.warn(
37806
- `Warning: another dashboard server (PID ${oldPid}) appears to be running. If this is stale, delete ${pidFilePath} and restart.`
37807
- );
37808
- } 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));
37809
38553
  }
38554
+ walCheckpointTruncate(dbPathForCheckpoint);
37810
38555
  }
37811
38556
  } catch {
37812
38557
  }
37813
38558
  }
37814
- writeFileSync5(pidFilePath, String(process.pid), { mode: 384 });
38559
+ writeFileSync6(pidFilePath, String(process.pid), { mode: 384 });
37815
38560
  const cmdCountResult = db.exec("SELECT COUNT(*) as c FROM command_executions");
37816
38561
  const totalCmds = cmdCountResult[0]?.values[0]?.[0] ?? 0;
37817
38562
  if (totalCmds === 0) {
@@ -37821,7 +38566,7 @@ async function startServer(options = {}) {
37821
38566
  }
37822
38567
  }
37823
38568
  const orphanResult = db.exec(
37824
- `SELECT id, pid, is_detached, started_at FROM command_executions
38569
+ `SELECT id, pid, started_at FROM command_executions
37825
38570
  WHERE pid IS NOT NULL AND finished_at IS NULL`
37826
38571
  );
37827
38572
  if (orphanResult.length > 0 && orphanResult[0]) {
@@ -37831,37 +38576,15 @@ async function startServer(options = {}) {
37831
38576
  let killedCount = 0;
37832
38577
  for (const row of orphanRows) {
37833
38578
  const pid = row[colIdx["pid"]];
37834
- const isDetached = row[colIdx["is_detached"]] === 1;
37835
38579
  const startedAt = row[colIdx["started_at"]];
37836
38580
  if (sqliteUtcMs(startedAt) < cutoff) continue;
37837
38581
  if (defaultIsAlive(pid)) {
37838
- if (isDetached) {
37839
- try {
37840
- process.kill(-pid, "SIGTERM");
37841
- } catch {
37842
- process.kill(pid, "SIGTERM");
37843
- }
37844
- } else {
37845
- process.kill(pid, "SIGTERM");
37846
- }
38582
+ reapTree(pid);
37847
38583
  killedCount++;
37848
- setTimeout(() => {
37849
- try {
37850
- process.kill(pid, 0);
37851
- if (isDetached) {
37852
- try {
37853
- process.kill(-pid, "SIGKILL");
37854
- } catch {
37855
- }
37856
- }
37857
- process.kill(pid, "SIGKILL");
37858
- } catch {
37859
- }
37860
- }, 2e3);
37861
38584
  }
37862
38585
  }
37863
38586
  if (killedCount > 0) {
37864
- console.log(` Cleaned up ${killedCount} orphaned process(es)`);
38587
+ console.log(` Reaped ${killedCount} orphaned process tree(s)`);
37865
38588
  }
37866
38589
  }
37867
38590
  const legacyResult = db.exec(
@@ -37871,10 +38594,11 @@ async function startServer(options = {}) {
37871
38594
  if (legacyCount > 0) {
37872
38595
  db.run(
37873
38596
  `UPDATE command_executions
37874
- SET exit_code = -2,
38597
+ SET exit_code = ?,
37875
38598
  output = COALESCE(output, '') || '
37876
38599
  [Cancelled]'
37877
- 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]
37878
38602
  );
37879
38603
  console.log(` Backfilled ${legacyCount} finished command(s) missing an exit code`);
37880
38604
  }
@@ -37897,11 +38621,25 @@ async function startServer(options = {}) {
37897
38621
  ` Auto-closed ${staleSessionResult.closedSessionIds.length} stale active session(s) (threshold 7 days)`
37898
38622
  );
37899
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();
37900
38637
  const SWEEP_INTERVAL_MS = 5 * 60 * 1e3;
37901
38638
  const sweepTimer = setInterval(() => {
37902
38639
  try {
37903
38640
  logAgentSweep(sweepStaleAgentSessions(db, heartbeatSeconds, defaultIsAlive));
37904
38641
  sweepStaleSessions(db, STALE_SESSION_THRESHOLD_SECONDS);
38642
+ void reconcileCompleted();
37905
38643
  } catch (err) {
37906
38644
  console.error("[sweep] periodic sweep failed:", err);
37907
38645
  }
@@ -37937,11 +38675,11 @@ async function startServer(options = {}) {
37937
38675
  app.use("/api/agent-sessions", createAgentSessionsRouter(db, () => pullSync()));
37938
38676
  app.use("/api/sessions", createHandoffRouter(sessionCapture, ocrDir, () => pullSync()));
37939
38677
  app.use("/api/team", createTeamRouter(ocrDir));
37940
- const clientDir = join18(__dirname3, "client");
37941
- if (process.env.NODE_ENV === "production" && existsSync15(clientDir)) {
38678
+ const clientDir = join19(__dirname3, "client");
38679
+ if (process.env.NODE_ENV === "production" && existsSync17(clientDir)) {
37942
38680
  app.use(import_express15.default.static(clientDir, { index: false }));
37943
- const indexHtmlPath = join18(clientDir, "index.html");
37944
- const rawIndexHtml = existsSync15(indexHtmlPath) ? readFileSync12(indexHtmlPath, "utf-8") : "";
38681
+ const indexHtmlPath = join19(clientDir, "index.html");
38682
+ const rawIndexHtml = existsSync17(indexHtmlPath) ? readFileSync12(indexHtmlPath, "utf-8") : "";
37945
38683
  const tokenScript = `<script>window.__OCR_TOKEN__=${JSON.stringify(AUTH_TOKEN)};</script>`;
37946
38684
  const injectedIndexHtml = rawIndexHtml.replace(
37947
38685
  "</head>",
@@ -37960,7 +38698,7 @@ async function startServer(options = {}) {
37960
38698
  registerChatHandlers(io, socket, db, ocrDir, aiCliService);
37961
38699
  registerPostHandlers(io, socket, db, ocrDir, aiCliService);
37962
38700
  });
37963
- const dbFilePath = join18(ocrDir, "data", "ocr.db");
38701
+ const dbFilePath = join19(ocrDir, "data", "ocr.db");
37964
38702
  const dbSyncWatcher = new DbSyncWatcher(
37965
38703
  db,
37966
38704
  dbFilePath,
@@ -37976,7 +38714,7 @@ async function startServer(options = {}) {
37976
38714
  dbSyncWatcher.startWatching();
37977
38715
  pullSync = () => dbSyncWatcher.syncFromDisk();
37978
38716
  console.log(` Watching DB: ${shortenPath(dbFilePath)}`);
37979
- const sessionsDir = join18(ocrDir, "sessions");
38717
+ const sessionsDir = join19(ocrDir, "sessions");
37980
38718
  const fsSync = new FilesystemSync(db, sessionsDir, io);
37981
38719
  await fsSync.fullScan();
37982
38720
  fsSync.startWatching();
@@ -38015,7 +38753,7 @@ async function startServer(options = {}) {
38015
38753
  if (actualPort !== port) {
38016
38754
  console.log(` Note: using port ${actualPort} (${port} was in use)`);
38017
38755
  }
38018
- writeFileSync5(portFilePath, String(actualPort), { mode: 384 });
38756
+ writeFileSync6(portFilePath, String(actualPort), { mode: 384 });
38019
38757
  console.log(` Server: http://localhost:${actualPort}`);
38020
38758
  console.log(` OCR directory: ${shortenPath(ocrDir)}`);
38021
38759
  console.log();
@@ -38028,53 +38766,40 @@ async function startServer(options = {}) {
38028
38766
  } catch {
38029
38767
  }
38030
38768
  }
38031
- const shutdown = (signal) => {
38769
+ const shutdown = async (signal) => {
38032
38770
  console.log(
38033
38771
  `Shutting down dashboard server${signal ? ` (received ${signal})` : ""}...`
38034
38772
  );
38035
38773
  try {
38036
- unlinkSync3(pidFilePath);
38037
- } catch {
38038
- }
38039
- try {
38040
- unlinkSync3(portFilePath);
38774
+ unlinkSync5(pidFilePath);
38041
38775
  } catch {
38042
38776
  }
38043
38777
  try {
38044
- unlinkSync3(join18(dataDir, "dashboard-active-spawn.json"));
38778
+ unlinkSync5(portFilePath);
38045
38779
  } catch {
38046
38780
  }
38781
+ clearSpawnMarker(ocrDir);
38047
38782
  try {
38048
38783
  const activeResult = db.exec(
38049
- "SELECT id, pid, is_detached FROM command_executions WHERE pid IS NOT NULL AND finished_at IS NULL"
38784
+ "SELECT id, pid FROM command_executions WHERE pid IS NOT NULL AND finished_at IS NULL"
38050
38785
  );
38051
38786
  if (activeResult.length > 0 && activeResult[0]) {
38052
38787
  const { columns, values: activeRows } = activeResult[0];
38053
38788
  const colIdx = Object.fromEntries(columns.map((c, i) => [c, i]));
38054
38789
  for (const row of activeRows) {
38055
38790
  const pid = row[colIdx["pid"]];
38056
- const isDetached = row[colIdx["is_detached"]] === 1;
38057
- try {
38058
- if (isDetached) {
38059
- try {
38060
- process.kill(-pid, "SIGTERM");
38061
- } catch {
38062
- process.kill(pid, "SIGTERM");
38063
- }
38064
- } else {
38065
- process.kill(pid, "SIGTERM");
38066
- }
38067
- console.log(`Sent SIGTERM to child process (PID ${pid})`);
38068
- } catch {
38069
- }
38791
+ reapTree(pid, 750);
38792
+ console.log(`Reaping child process tree (PID ${pid})`);
38070
38793
  }
38794
+ await new Promise((resolve3) => setTimeout(resolve3, 1e3));
38071
38795
  db.run(
38072
38796
  `UPDATE command_executions
38073
- SET exit_code = -2, finished_at = datetime('now'),
38797
+ SET exit_code = ?, finished_at = datetime('now'),
38074
38798
  output = COALESCE(output, '') || '
38075
38799
  [Cancelled \u2014 server shutdown]',
38076
38800
  pid = NULL
38077
- 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]
38078
38803
  );
38079
38804
  }
38080
38805
  } catch (err) {
@@ -38097,9 +38822,9 @@ async function startServer(options = {}) {
38097
38822
  process.exit(1);
38098
38823
  }, 2e3).unref();
38099
38824
  };
38100
- process.on("SIGINT", () => shutdown("SIGINT"));
38101
- process.on("SIGTERM", () => shutdown("SIGTERM"));
38102
- 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"));
38103
38828
  process.on("uncaughtException", (err) => {
38104
38829
  console.error("[dashboard] uncaughtException:", err);
38105
38830
  });