@open-code-review/cli 2.1.0 → 2.2.1

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