@open-code-review/cli 1.11.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +8 -4
  2. package/dist/dashboard/client/assets/{_basePickBy-D8RU9s_y.js → _basePickBy-B3ALyupE.js} +1 -1
  3. package/dist/dashboard/client/assets/{_baseUniq-CjVeYx1J.js → _baseUniq-b2RALAWc.js} +1 -1
  4. package/dist/dashboard/client/assets/{arc-DsFstmf9.js → arc-DcSVvhUd.js} +1 -1
  5. package/dist/dashboard/client/assets/{architectureDiagram-VXUJARFQ-iNJB-g1N.js → architectureDiagram-VXUJARFQ-BNUlmSCS.js} +1 -1
  6. package/dist/dashboard/client/assets/{blockDiagram-VD42YOAC-Zp2Aw0zR.js → blockDiagram-VD42YOAC-BmhiQVwa.js} +1 -1
  7. package/dist/dashboard/client/assets/{c4Diagram-YG6GDRKO-BGppUmwT.js → c4Diagram-YG6GDRKO-jyJ3WOv5.js} +1 -1
  8. package/dist/dashboard/client/assets/channel-D3J8-GF_.js +1 -0
  9. package/dist/dashboard/client/assets/{chunk-4BX2VUAB-CZcRxeE4.js → chunk-4BX2VUAB-x1dQU_s3.js} +1 -1
  10. package/dist/dashboard/client/assets/{chunk-55IACEB6-CVdL59yY.js → chunk-55IACEB6-CwbsE2XQ.js} +1 -1
  11. package/dist/dashboard/client/assets/{chunk-B4BG7PRW-CFPp6g6e.js → chunk-B4BG7PRW-BaE7c-ti.js} +1 -1
  12. package/dist/dashboard/client/assets/{chunk-DI55MBZ5-DH9BzE6I.js → chunk-DI55MBZ5-Bw5PUaMK.js} +1 -1
  13. package/dist/dashboard/client/assets/{chunk-FMBD7UC4-DZ2DTwqS.js → chunk-FMBD7UC4-B7cF6P3s.js} +1 -1
  14. package/dist/dashboard/client/assets/{chunk-QN33PNHL-DODPm0CR.js → chunk-QN33PNHL-OY4evNHd.js} +1 -1
  15. package/dist/dashboard/client/assets/{chunk-QZHKN3VN-CNI_LxUf.js → chunk-QZHKN3VN-BpjQwIWz.js} +1 -1
  16. package/dist/dashboard/client/assets/{chunk-TZMSLE5B-sxZQF02c.js → chunk-TZMSLE5B-D8b_Oq9B.js} +1 -1
  17. package/dist/dashboard/client/assets/classDiagram-2ON5EDUG-tkFUL-1Y.js +1 -0
  18. package/dist/dashboard/client/assets/classDiagram-v2-WZHVMYZB-tkFUL-1Y.js +1 -0
  19. package/dist/dashboard/client/assets/clone-CkY5ajLr.js +1 -0
  20. package/dist/dashboard/client/assets/{cose-bilkent-S5V4N54A-BHa2lABH.js → cose-bilkent-S5V4N54A-C-sfP8PN.js} +1 -1
  21. package/dist/dashboard/client/assets/{dagre-6UL2VRFP-CvCLBtkz.js → dagre-6UL2VRFP-Cqfo0NRg.js} +1 -1
  22. package/dist/dashboard/client/assets/{diagram-PSM6KHXK-Cklwd4YA.js → diagram-PSM6KHXK-BR3ppxqI.js} +1 -1
  23. package/dist/dashboard/client/assets/{diagram-QEK2KX5R-3bDERTbp.js → diagram-QEK2KX5R-Dvcx6x3R.js} +1 -1
  24. package/dist/dashboard/client/assets/{diagram-S2PKOQOG-DbiWlPc6.js → diagram-S2PKOQOG-DoyBLnVN.js} +1 -1
  25. package/dist/dashboard/client/assets/{erDiagram-Q2GNP2WA-BQa_VNbt.js → erDiagram-Q2GNP2WA-hy77l1cL.js} +1 -1
  26. package/dist/dashboard/client/assets/{flowDiagram-NV44I4VS-BDaJyl9N.js → flowDiagram-NV44I4VS-Bz0B1rKM.js} +1 -1
  27. package/dist/dashboard/client/assets/{ganttDiagram-JELNMOA3-DsTnleSr.js → ganttDiagram-JELNMOA3-CLgrZPoC.js} +1 -1
  28. package/dist/dashboard/client/assets/{gitGraphDiagram-V2S2FVAM-BRuBadgn.js → gitGraphDiagram-V2S2FVAM-DwJ-1f-v.js} +1 -1
  29. package/dist/dashboard/client/assets/{graph-CYYqXm9c.js → graph-DDBMM_t2.js} +1 -1
  30. package/dist/dashboard/client/assets/{index-eZMoytob.js → index-Cr9yEo_B.js} +123 -123
  31. package/dist/dashboard/client/assets/{infoDiagram-HS3SLOUP-CHnA8k7H.js → infoDiagram-HS3SLOUP-Bhn1FmAk.js} +1 -1
  32. package/dist/dashboard/client/assets/{journeyDiagram-XKPGCS4Q-CAXR1-Ju.js → journeyDiagram-XKPGCS4Q-CzGbjX1y.js} +1 -1
  33. package/dist/dashboard/client/assets/{kanban-definition-3W4ZIXB7-Clf3HfHz.js → kanban-definition-3W4ZIXB7-Da77-WYk.js} +1 -1
  34. package/dist/dashboard/client/assets/{layout-DQPaNqnO.js → layout-CVwSB-GS.js} +1 -1
  35. package/dist/dashboard/client/assets/{linear-qUnNXvWB.js → linear-CTRAc5Jn.js} +1 -1
  36. package/dist/dashboard/client/assets/{mermaid-renderer-C7Se8vjl.js → mermaid-renderer-Bjo170ax.js} +4 -4
  37. package/dist/dashboard/client/assets/{mindmap-definition-VGOIOE7T-DBIdG0OR.js → mindmap-definition-VGOIOE7T-B55C2odl.js} +1 -1
  38. package/dist/dashboard/client/assets/{pieDiagram-ADFJNKIX-DXAIiG6W.js → pieDiagram-ADFJNKIX-5lrQLrSz.js} +1 -1
  39. package/dist/dashboard/client/assets/{quadrantDiagram-AYHSOK5B-D4yAxif0.js → quadrantDiagram-AYHSOK5B-Bg55gC30.js} +1 -1
  40. package/dist/dashboard/client/assets/{requirementDiagram-UZGBJVZJ-D27ME1VO.js → requirementDiagram-UZGBJVZJ-CyR4YFJY.js} +1 -1
  41. package/dist/dashboard/client/assets/{sankeyDiagram-TZEHDZUN-BeEaA_QM.js → sankeyDiagram-TZEHDZUN-BVWKr9_-.js} +1 -1
  42. package/dist/dashboard/client/assets/{sequenceDiagram-WL72ISMW-GTI12qU0.js → sequenceDiagram-WL72ISMW-D0AJg_tE.js} +1 -1
  43. package/dist/dashboard/client/assets/{stateDiagram-FKZM4ZOC-ClSoeZM0.js → stateDiagram-FKZM4ZOC-BuHpTgim.js} +1 -1
  44. package/dist/dashboard/client/assets/stateDiagram-v2-4FDKWEC3-DwAPhteN.js +1 -0
  45. package/dist/dashboard/client/assets/{timeline-definition-IT6M3QCI-cj5d_Kyh.js → timeline-definition-IT6M3QCI-LDhpAmDd.js} +1 -1
  46. package/dist/dashboard/client/assets/{treemap-GDKQZRPO-BrRT1igb.js → treemap-GDKQZRPO-Dd4gjvUl.js} +1 -1
  47. package/dist/dashboard/client/assets/{xychartDiagram-PRI3JC2R-DlzGitHh.js → xychartDiagram-PRI3JC2R-B9RDod39.js} +1 -1
  48. package/dist/dashboard/client/index.html +1 -1
  49. package/dist/dashboard/server.js +1113 -657
  50. package/dist/index.js +1719 -718
  51. package/dist/lib/db/index.js +638 -101
  52. package/package.json +4 -4
  53. package/dist/dashboard/client/assets/channel-C8plpfdz.js +0 -1
  54. package/dist/dashboard/client/assets/classDiagram-2ON5EDUG-Dqn6u1oQ.js +0 -1
  55. package/dist/dashboard/client/assets/classDiagram-v2-WZHVMYZB-Dqn6u1oQ.js +0 -1
  56. package/dist/dashboard/client/assets/clone-BQ8hOLqM.js +0 -1
  57. package/dist/dashboard/client/assets/stateDiagram-v2-4FDKWEC3-Bim3s-dq.js +0 -1
@@ -18413,7 +18413,7 @@ var require_view = __commonJS({
18413
18413
  var dirname14 = path2.dirname;
18414
18414
  var basename4 = path2.basename;
18415
18415
  var extname = path2.extname;
18416
- var join18 = path2.join;
18416
+ var join19 = path2.join;
18417
18417
  var resolve3 = path2.resolve;
18418
18418
  module.exports = View;
18419
18419
  function View(name, options) {
@@ -18461,12 +18461,12 @@ var require_view = __commonJS({
18461
18461
  };
18462
18462
  View.prototype.resolve = function resolve4(dir, file) {
18463
18463
  var ext = this.ext;
18464
- var path3 = join18(dir, file);
18464
+ var path3 = join19(dir, file);
18465
18465
  var stat = tryStat(path3);
18466
18466
  if (stat && stat.isFile()) {
18467
18467
  return path3;
18468
18468
  }
18469
- path3 = join18(dir, basename4(file, ext), "index" + ext);
18469
+ path3 = join19(dir, basename4(file, ext), "index" + ext);
18470
18470
  stat = tryStat(path3);
18471
18471
  if (stat && stat.isFile()) {
18472
18472
  return path3;
@@ -19099,7 +19099,7 @@ var require_send = __commonJS({
19099
19099
  var Stream = __require("stream");
19100
19100
  var util = __require("util");
19101
19101
  var extname = path2.extname;
19102
- var join18 = path2.join;
19102
+ var join19 = path2.join;
19103
19103
  var normalize = path2.normalize;
19104
19104
  var resolve3 = path2.resolve;
19105
19105
  var sep = path2.sep;
@@ -19318,7 +19318,7 @@ var require_send = __commonJS({
19318
19318
  return res;
19319
19319
  }
19320
19320
  parts = path3.split(sep);
19321
- path3 = normalize(join18(root, path3));
19321
+ path3 = normalize(join19(root, path3));
19322
19322
  } else {
19323
19323
  if (UP_PATH_REGEXP.test(path3)) {
19324
19324
  debug('malicious path "%s"', path3);
@@ -19453,7 +19453,7 @@ var require_send = __commonJS({
19453
19453
  if (err) return self.onStatError(err);
19454
19454
  return self.error(404);
19455
19455
  }
19456
- var p = join18(path3, self._index[i]);
19456
+ var p = join19(path3, self._index[i]);
19457
19457
  debug('stat "%s"', p);
19458
19458
  fs6.stat(p, function(err2, stat) {
19459
19459
  if (err2) return next(err2);
@@ -21876,7 +21876,7 @@ var require_response = __commonJS({
21876
21876
  var encodeUrl = require_encodeurl();
21877
21877
  var escapeHtml = require_escape_html();
21878
21878
  var http = __require("http");
21879
- var isAbsolute2 = require_utils2().isAbsolute;
21879
+ var isAbsolute3 = require_utils2().isAbsolute;
21880
21880
  var onFinished = require_on_finished();
21881
21881
  var path2 = __require("path");
21882
21882
  var statuses = require_statuses();
@@ -22082,7 +22082,7 @@ var require_response = __commonJS({
22082
22082
  done = options;
22083
22083
  opts = {};
22084
22084
  }
22085
- if (!opts.root && !isAbsolute2(path3)) {
22085
+ if (!opts.root && !isAbsolute3(path3)) {
22086
22086
  throw new TypeError("path must be absolute or specify root to res.sendFile");
22087
22087
  }
22088
22088
  var pathname = encodeURI(path3);
@@ -30483,8 +30483,8 @@ var init_open = __esm({
30483
30483
  // src/server/index.ts
30484
30484
  var import_express15 = __toESM(require_express2(), 1);
30485
30485
  import { createServer } from "node:http";
30486
- import { existsSync as existsSync15, readFileSync as readFileSync15, writeFileSync as writeFileSync7, unlinkSync as unlinkSync3, mkdirSync as mkdirSync7 } from "node:fs";
30487
- import { join as join17, dirname as dirname13, resolve as resolve2 } from "node:path";
30486
+ import { existsSync as existsSync15, readFileSync as readFileSync12, writeFileSync as writeFileSync5, unlinkSync as unlinkSync3, mkdirSync as mkdirSync6 } from "node:fs";
30487
+ import { join as join18, dirname as dirname13, resolve as resolve2 } from "node:path";
30488
30488
  import { fileURLToPath as fileURLToPath3 } from "node:url";
30489
30489
  import { randomBytes } from "node:crypto";
30490
30490
  import { Server as SocketIOServer } from "socket.io";
@@ -30509,17 +30509,79 @@ function resolveOcrDir(startDir) {
30509
30509
  }
30510
30510
  }
30511
30511
 
30512
- // src/server/db.ts
30513
- import { existsSync as existsSync4, readFileSync as readFileSync3, renameSync as renameSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "node:fs";
30514
- import { join as join4, dirname as dirname4 } from "node:path";
30515
- import initSqlJs2 from "sql.js";
30516
-
30517
30512
  // ../cli/src/lib/db/index.ts
30518
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "node:fs";
30519
- import { dirname as dirname3, join as join3 } from "node:path";
30520
- import { createRequire } from "node:module";
30521
- import { spawnSync } from "node:child_process";
30522
- import initSqlJs from "sql.js";
30513
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, copyFileSync, statSync } from "node:fs";
30514
+ import { dirname as dirname4, join as join4 } from "node:path";
30515
+
30516
+ // ../cli/src/lib/db/engine.ts
30517
+ import BetterSqlite3 from "better-sqlite3";
30518
+ var BUSY_RETRY_ATTEMPTS = 5;
30519
+ var BUSY_RETRY_BACKOFF_MS = 50;
30520
+ function isBusyError(e) {
30521
+ if (e instanceof BetterSqlite3.SqliteError) {
30522
+ return e.code === "SQLITE_BUSY" || e.code === "SQLITE_BUSY_SNAPSHOT";
30523
+ }
30524
+ const code = e?.code;
30525
+ return code === "SQLITE_BUSY" || code === "SQLITE_BUSY_SNAPSHOT";
30526
+ }
30527
+ function sleepSync(ms) {
30528
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
30529
+ }
30530
+ var BetterSqliteAdapter = class {
30531
+ raw;
30532
+ constructor(db) {
30533
+ this.raw = db;
30534
+ }
30535
+ exec(sql, params) {
30536
+ const stmt = this.raw.prepare(sql);
30537
+ if (!stmt.reader) {
30538
+ stmt.run(...params ?? []);
30539
+ return [];
30540
+ }
30541
+ const columns = stmt.columns().map((c) => c.name);
30542
+ const values = stmt.raw().all(...params ?? []);
30543
+ return values.length > 0 ? [{ columns, values }] : [];
30544
+ }
30545
+ run(sql, params) {
30546
+ if (params !== void 0) {
30547
+ this.raw.prepare(sql).run(...params);
30548
+ return;
30549
+ }
30550
+ this.raw.exec(sql);
30551
+ }
30552
+ prepare(sql) {
30553
+ return this.raw.prepare(sql);
30554
+ }
30555
+ transaction(fn) {
30556
+ const tx = this.raw.transaction(fn);
30557
+ for (let attempt = 0; ; attempt++) {
30558
+ try {
30559
+ return tx.immediate();
30560
+ } catch (e) {
30561
+ if (!isBusyError(e) || attempt >= BUSY_RETRY_ATTEMPTS - 1) throw e;
30562
+ sleepSync(BUSY_RETRY_BACKOFF_MS);
30563
+ }
30564
+ }
30565
+ }
30566
+ pragma(source) {
30567
+ return this.raw.pragma(source);
30568
+ }
30569
+ close() {
30570
+ try {
30571
+ this.raw.pragma("wal_checkpoint(TRUNCATE)");
30572
+ } catch {
30573
+ }
30574
+ this.raw.close();
30575
+ }
30576
+ };
30577
+ function openEngine(dbPath) {
30578
+ const native = new BetterSqlite3(dbPath);
30579
+ native.pragma("journal_mode = WAL");
30580
+ native.pragma("foreign_keys = ON");
30581
+ native.pragma("busy_timeout = 5000");
30582
+ native.pragma("synchronous = NORMAL");
30583
+ return new BetterSqliteAdapter(native);
30584
+ }
30523
30585
 
30524
30586
  // ../cli/src/lib/db/migrations.ts
30525
30587
  var MIGRATIONS = [
@@ -30843,8 +30905,157 @@ var MIGRATIONS = [
30843
30905
  DROP INDEX IF EXISTS idx_agent_sessions_status_heartbeat;
30844
30906
  DROP TABLE IF EXISTS agent_sessions;
30845
30907
  `
30908
+ },
30909
+ {
30910
+ version: 12,
30911
+ description: "Event-sourced lifecycle hardening: event_type taxonomy guard, sweep indexes, session_completeness view",
30912
+ sql: `
30913
+ -- \u2500\u2500 Indexes for the now-periodic stale-session sweep + round derivation \u2500\u2500
30914
+ -- The sweep filters sessions by status and rolls up MAX(created_at) per
30915
+ -- session over the event log; deriveNextRound does MAX(round). Index both.
30916
+ CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
30917
+ CREATE INDEX IF NOT EXISTS idx_events_session_created
30918
+ ON orchestration_events(session_id, created_at);
30919
+
30920
+ -- \u2500\u2500 Event-type taxonomy guard \u2500\u2500
30921
+ -- orchestration_events.event_type is the spine of all lifecycle
30922
+ -- derivation. A typo (e.g. 'round_complete' vs 'round_completed') would
30923
+ -- silently break deriveNextRound and the completeness view. SQLite cannot
30924
+ -- add a CHECK to an existing column without a table rebuild, so enforce
30925
+ -- the closed vocabulary with a BEFORE INSERT trigger instead.
30926
+ CREATE TRIGGER IF NOT EXISTS trg_events_known_type
30927
+ BEFORE INSERT ON orchestration_events
30928
+ WHEN NEW.event_type NOT IN (
30929
+ 'session_created', 'session_resumed', 'round_started', 'phase_transition',
30930
+ 'round_completed', 'map_completed', 'session_closed', 'session_aborted',
30931
+ 'session_auto_closed_stale', 'session_synced', 'session_legacy_import'
30932
+ )
30933
+ BEGIN
30934
+ SELECT RAISE(ABORT, 'unknown orchestration_events.event_type');
30935
+ END;
30936
+
30937
+ -- \u2500\u2500 Close-guard (DB backstop for the completion invariant) \u2500\u2500
30938
+ -- A session cannot transition active \u2192 closed unless its current
30939
+ -- round/run has a terminal artifact event, OR an explicit reason event
30940
+ -- (abort / auto-close-stale / sync / legacy-import) is present. Only a
30941
+ -- *silent* premature close is banned \u2014 every legitimate non-artifact
30942
+ -- close carries a reason event and passes. App-level guards in
30943
+ -- stateClose/finish are the primary check; this makes the illegal state
30944
+ -- unrepresentable even via raw SQL.
30945
+ --
30946
+ -- DEFENCE-IN-DEPTH NOTE (intentional, documented gap): the reason-event
30947
+ -- branch below (event_type IN (...)) is NOT round-scoped \u2014 a reason event
30948
+ -- recorded for an earlier round would also satisfy a later close. The
30949
+ -- app-level guards ARE round-scoped (hasCompletionInvariant checks the
30950
+ -- current round/run), so the precise check lives in the application; this
30951
+ -- trigger is a coarse backstop against a *silent* premature close via raw
30952
+ -- SQL. Tightening it to be round-scoped would require a new migration
30953
+ -- (this v12 trigger is append-only and already shipped); the residual
30954
+ -- risk is a non-artifact close carrying a stale reason event, which is
30955
+ -- still an explicit, audited terminal \u2014 not the failure mode this guards.
30956
+ CREATE TRIGGER IF NOT EXISTS trg_sessions_close_guard
30957
+ BEFORE UPDATE OF status ON sessions
30958
+ WHEN NEW.status = 'closed' AND OLD.status <> 'closed'
30959
+ AND NOT EXISTS (
30960
+ SELECT 1 FROM orchestration_events e
30961
+ WHERE e.session_id = NEW.id
30962
+ AND (
30963
+ (NEW.workflow_type = 'review' AND e.event_type = 'round_completed' AND e.round = NEW.current_round)
30964
+ OR (NEW.workflow_type = 'map' AND e.event_type = 'map_completed' AND e.round = NEW.current_map_run)
30965
+ OR e.event_type IN ('session_aborted','session_auto_closed_stale','session_synced','session_legacy_import')
30966
+ )
30967
+ )
30968
+ BEGIN
30969
+ SELECT RAISE(ABORT, 'cannot close session without a completed round/run or an explicit reason event');
30970
+ END;
30971
+
30972
+ -- \u2500\u2500 session_completeness view \u2500\u2500
30973
+ -- The published contract for "is this session actually complete, and if
30974
+ -- not, what's missing". Completion is DERIVED from the event log, never a
30975
+ -- mutable flag: a session is complete iff it is closed AND a terminal
30976
+ -- artifact event exists for its current round/run. The dashboard's
30977
+ -- outcome derivation and the agent 'status' command read this view, so
30978
+ -- they cannot disagree.
30979
+ --
30980
+ -- completeness_state is an INTENTIONAL HYBRID: it combines the mutable
30981
+ -- status column (marked_closed) with append-only event evidence (the
30982
+ -- terminal artifact event). This is sound precisely because the
30983
+ -- close-guard trigger above makes the status column trustworthy \u2014 a row
30984
+ -- can only reach status='closed' with a completed round/run or an
30985
+ -- explicit reason event \u2014 so reading the column is not a regression to
30986
+ -- the old "mutable flag that could lie" model.
30987
+ --
30988
+ -- completeness_state:
30989
+ -- 'complete' \u2014 closed + terminal artifact for current round/run
30990
+ -- 'closed_without_artifact' \u2014 closed but no terminal artifact (the
30991
+ -- "completed too soon" condition)
30992
+ -- 'in_flight' \u2014 open with a dependent process still running
30993
+ -- 'open_no_artifact' \u2014 open, no in-flight dependents
30994
+ CREATE VIEW IF NOT EXISTS session_completeness AS
30995
+ SELECT
30996
+ s.id AS session_id,
30997
+ s.workflow_type AS workflow_type,
30998
+ s.status AS status,
30999
+ s.current_round AS current_round,
31000
+ s.current_map_run AS current_map_run,
31001
+ CASE WHEN EXISTS (
31002
+ SELECT 1 FROM orchestration_events e
31003
+ WHERE e.session_id = s.id
31004
+ AND (
31005
+ (s.workflow_type = 'review' AND e.event_type = 'round_completed' AND e.round = s.current_round)
31006
+ OR (s.workflow_type = 'map' AND e.event_type = 'map_completed' AND e.round = s.current_map_run)
31007
+ )
31008
+ ) THEN 1 ELSE 0 END AS has_terminal_artifact,
31009
+ CASE WHEN s.status = 'closed' THEN 1 ELSE 0 END AS marked_closed,
31010
+ CASE WHEN NOT EXISTS (
31011
+ SELECT 1 FROM command_executions ce
31012
+ WHERE ce.workflow_id = s.id AND ce.finished_at IS NULL
31013
+ ) THEN 1 ELSE 0 END AS dependents_settled,
31014
+ CASE
31015
+ WHEN s.status = 'closed' AND EXISTS (
31016
+ SELECT 1 FROM orchestration_events e
31017
+ WHERE e.session_id = s.id
31018
+ AND (
31019
+ (s.workflow_type = 'review' AND e.event_type = 'round_completed' AND e.round = s.current_round)
31020
+ OR (s.workflow_type = 'map' AND e.event_type = 'map_completed' AND e.round = s.current_map_run)
31021
+ )
31022
+ ) THEN 'complete'
31023
+ WHEN s.status = 'closed' THEN 'closed_without_artifact'
31024
+ WHEN EXISTS (
31025
+ SELECT 1 FROM command_executions ce
31026
+ WHERE ce.workflow_id = s.id AND ce.finished_at IS NULL
31027
+ ) THEN 'in_flight'
31028
+ ELSE 'open_no_artifact'
31029
+ END AS completeness_state
31030
+ FROM sessions s;
31031
+ `
31032
+ },
31033
+ {
31034
+ version: 13,
31035
+ description: "Retire dead parent_id column on command_executions (never written; row kind is derived from command)",
31036
+ // parent_id was reserved for an AI-instance → dashboard-spawn lineage link
31037
+ // that was never wired (no writer, no reader). A process's KIND (supervisor
31038
+ // / reviewer-instance / utility) is derived from columns that are always
31039
+ // present (command + last_heartbeat_at), so the dead lineage column and its
31040
+ // all-NULL index are removed. Re-add a wired parent_id alongside a real
31041
+ // consumer (e.g. a parent→child tree view) if lineage is ever needed.
31042
+ //
31043
+ // Imperative + guarded so the DROP COLUMN (which SQLite can't express as
31044
+ // IF EXISTS) is idempotent under re-application.
31045
+ run: (db) => {
31046
+ if (!columnExists(db, "command_executions", "parent_id")) return;
31047
+ db.run("DROP INDEX IF EXISTS idx_command_executions_parent;");
31048
+ db.run("ALTER TABLE command_executions DROP COLUMN parent_id;");
31049
+ }
30846
31050
  }
30847
31051
  ];
31052
+ function columnExists(db, table, column) {
31053
+ const result = db.exec(`PRAGMA table_info(${table})`);
31054
+ const first = result[0];
31055
+ if (!first) return false;
31056
+ const nameIdx = first.columns.indexOf("name");
31057
+ return first.values.some((row) => row[nameIdx] === column);
31058
+ }
30848
31059
  function ensureSchemaVersionTable(db) {
30849
31060
  db.run(`
30850
31061
  CREATE TABLE IF NOT EXISTS schema_version (
@@ -30854,6 +31065,10 @@ function ensureSchemaVersionTable(db) {
30854
31065
  );
30855
31066
  `);
30856
31067
  }
31068
+ function getSchemaVersion(db) {
31069
+ ensureSchemaVersionTable(db);
31070
+ return getCurrentVersion(db);
31071
+ }
30857
31072
  function getCurrentVersion(db) {
30858
31073
  const result = db.exec(
30859
31074
  "SELECT MAX(version) as v FROM schema_version"
@@ -30871,9 +31086,10 @@ function runMigrations(db) {
30871
31086
  if (migration.version <= currentVersion) {
30872
31087
  continue;
30873
31088
  }
30874
- db.run("BEGIN TRANSACTION;");
31089
+ db.run("BEGIN IMMEDIATE;");
30875
31090
  try {
30876
- db.run(migration.sql);
31091
+ if (migration.sql) db.run(migration.sql);
31092
+ migration.run?.(db);
30877
31093
  db.run(
30878
31094
  "INSERT INTO schema_version (version, description) VALUES (?, ?);",
30879
31095
  [migration.version, migration.description]
@@ -30886,6 +31102,10 @@ function runMigrations(db) {
30886
31102
  }
30887
31103
  }
30888
31104
 
31105
+ // ../cli/src/lib/db/reconcile.ts
31106
+ import { existsSync as existsSync2 } from "node:fs";
31107
+ import { isAbsolute, join as join2, dirname as dirname2 } from "node:path";
31108
+
30889
31109
  // ../cli/src/lib/db/result-mapper.ts
30890
31110
  function resultToRows(result) {
30891
31111
  if (result.length === 0 || !result[0]) {
@@ -30906,16 +31126,260 @@ function resultToRow(result) {
30906
31126
  }
30907
31127
 
30908
31128
  // ../cli/src/lib/db/queries.ts
31129
+ function insertSession(db, params) {
31130
+ const {
31131
+ id,
31132
+ branch,
31133
+ workflow_type,
31134
+ current_phase = "context",
31135
+ phase_number = 1,
31136
+ current_round = 1,
31137
+ current_map_run = 1,
31138
+ session_dir
31139
+ } = params;
31140
+ db.run(
31141
+ `INSERT INTO sessions (id, branch, workflow_type, current_phase, phase_number, current_round, current_map_run, session_dir)
31142
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
31143
+ [id, branch, workflow_type, current_phase, phase_number, current_round, current_map_run, session_dir]
31144
+ );
31145
+ }
31146
+ function updateSession(db, id, params) {
31147
+ const setClauses = [];
31148
+ const values = [];
31149
+ if (params.status !== void 0) {
31150
+ setClauses.push("status = ?");
31151
+ values.push(params.status);
31152
+ }
31153
+ if (params.current_phase !== void 0) {
31154
+ setClauses.push("current_phase = ?");
31155
+ values.push(params.current_phase);
31156
+ }
31157
+ if (params.phase_number !== void 0) {
31158
+ setClauses.push("phase_number = ?");
31159
+ values.push(params.phase_number);
31160
+ }
31161
+ if (params.current_round !== void 0) {
31162
+ setClauses.push("current_round = ?");
31163
+ values.push(params.current_round);
31164
+ }
31165
+ if (params.current_map_run !== void 0) {
31166
+ setClauses.push("current_map_run = ?");
31167
+ values.push(params.current_map_run);
31168
+ }
31169
+ if (setClauses.length === 0) {
31170
+ return;
31171
+ }
31172
+ setClauses.push("updated_at = datetime('now')");
31173
+ values.push(id);
31174
+ db.run(
31175
+ `UPDATE sessions SET ${setClauses.join(", ")} WHERE id = ?`,
31176
+ values
31177
+ );
31178
+ }
30909
31179
  function getSession(db, id) {
30910
31180
  return resultToRow(
30911
31181
  db.exec("SELECT * FROM sessions WHERE id = ?", [id])
30912
31182
  );
30913
31183
  }
31184
+ function getAllSessions(db) {
31185
+ return resultToRows(
31186
+ db.exec("SELECT * FROM sessions ORDER BY started_at DESC")
31187
+ );
31188
+ }
31189
+ function insertEvent(db, params) {
31190
+ const {
31191
+ session_id,
31192
+ event_type,
31193
+ phase,
31194
+ phase_number,
31195
+ round,
31196
+ metadata
31197
+ } = params;
31198
+ db.run(
31199
+ `INSERT INTO orchestration_events (session_id, event_type, phase, phase_number, round, metadata)
31200
+ VALUES (?, ?, ?, ?, ?, ?)`,
31201
+ [
31202
+ session_id,
31203
+ event_type,
31204
+ phase ?? null,
31205
+ phase_number ?? null,
31206
+ round ?? null,
31207
+ metadata ?? null
31208
+ ]
31209
+ );
31210
+ }
31211
+ function commitReasonClose(db, sessionId, reasonEvent, projectionUpdates) {
31212
+ db.transaction(() => {
31213
+ insertEvent(db, { session_id: sessionId, ...reasonEvent });
31214
+ updateSession(db, sessionId, projectionUpdates);
31215
+ });
31216
+ }
30914
31217
 
30915
- // ../cli/src/lib/db/agent-sessions.ts
30916
- var ORPHAN_EXIT_CODE = -3;
31218
+ // ../cli/src/lib/db/reconcile.ts
31219
+ var DEFAULT_STALE_THRESHOLD_SECONDS = 7 * 24 * 60 * 60;
31220
+ function hasTerminalArtifactEvent(db, sessionId, workflowType, currentRound, currentMapRun) {
31221
+ const eventType = workflowType === "map" ? "map_completed" : "round_completed";
31222
+ const round = workflowType === "map" ? currentMapRun : currentRound;
31223
+ const r = db.exec(
31224
+ `SELECT 1 FROM orchestration_events
31225
+ WHERE session_id = ? AND event_type = ? AND round = ? LIMIT 1`,
31226
+ [sessionId, eventType, round]
31227
+ );
31228
+ return (r[0]?.values.length ?? 0) > 0;
31229
+ }
31230
+ function hasReasonEvent(db, sessionId) {
31231
+ const r = db.exec(
31232
+ `SELECT 1 FROM orchestration_events
31233
+ WHERE session_id = ?
31234
+ AND event_type IN ('session_aborted','session_auto_closed_stale','session_synced','session_legacy_import')
31235
+ LIMIT 1`,
31236
+ [sessionId]
31237
+ );
31238
+ return (r[0]?.values.length ?? 0) > 0;
31239
+ }
31240
+ function lastEventAgeSeconds(db, sessionId) {
31241
+ const r = db.exec(
31242
+ `SELECT (julianday('now') - julianday(MAX(created_at))) * 86400
31243
+ FROM orchestration_events WHERE session_id = ?`,
31244
+ [sessionId]
31245
+ );
31246
+ const v = r[0]?.values[0]?.[0];
31247
+ return typeof v === "number" ? v : null;
31248
+ }
31249
+ function hasInFlightDependents(db, sessionId) {
31250
+ const r = db.exec(
31251
+ `SELECT 1 FROM command_executions
31252
+ WHERE workflow_id = ? AND finished_at IS NULL LIMIT 1`,
31253
+ [sessionId]
31254
+ );
31255
+ return (r[0]?.values.length ?? 0) > 0;
31256
+ }
31257
+ function resolveSessionDir(ocrDir, sessionDir) {
31258
+ if (!sessionDir) return null;
31259
+ if (isAbsolute(sessionDir)) return sessionDir;
31260
+ return join2(dirname2(ocrDir), sessionDir);
31261
+ }
31262
+ function reconcileLegacyState(db, ocrDir, opts = {}) {
31263
+ const dryRun = opts.dryRun ?? false;
31264
+ const threshold = opts.staleThresholdSeconds ?? DEFAULT_STALE_THRESHOLD_SECONDS;
31265
+ const actions = [];
31266
+ for (const s of getAllSessions(db)) {
31267
+ const dir = resolveSessionDir(ocrDir, s.session_dir);
31268
+ if (s.status === "closed") {
31269
+ if (hasTerminalArtifactEvent(db, s.id, s.workflow_type, s.current_round, s.current_map_run) || hasReasonEvent(db, s.id)) {
31270
+ continue;
31271
+ }
31272
+ const reviewFinal = s.workflow_type === "review" && dir ? existsSync2(join2(dir, "rounds", `round-${s.current_round}`, "final.md")) : false;
31273
+ const mapFinal = s.workflow_type === "map" && dir ? existsSync2(join2(dir, "map", "runs", `run-${s.current_map_run}`, "map.md")) : false;
31274
+ if (reviewFinal) {
31275
+ actions.push({
31276
+ sessionId: s.id,
31277
+ kind: "synthesize-round-completed",
31278
+ detail: `final.md present for round ${s.current_round}; synthesizing round_completed`
31279
+ });
31280
+ if (!dryRun) {
31281
+ insertEvent(db, {
31282
+ session_id: s.id,
31283
+ event_type: "round_completed",
31284
+ phase: "synthesis",
31285
+ phase_number: 7,
31286
+ round: s.current_round,
31287
+ metadata: JSON.stringify({ source: "reconciled", synthesized_from: "final.md" })
31288
+ });
31289
+ }
31290
+ } else if (mapFinal) {
31291
+ actions.push({
31292
+ sessionId: s.id,
31293
+ kind: "synthesize-map-completed",
31294
+ detail: `map.md present for run ${s.current_map_run}; synthesizing map_completed`
31295
+ });
31296
+ if (!dryRun) {
31297
+ insertEvent(db, {
31298
+ session_id: s.id,
31299
+ event_type: "map_completed",
31300
+ phase: "synthesis",
31301
+ phase_number: 5,
31302
+ round: s.current_map_run,
31303
+ metadata: JSON.stringify({ source: "reconciled", synthesized_from: "map.md" })
31304
+ });
31305
+ }
31306
+ } else {
31307
+ actions.push({
31308
+ sessionId: s.id,
31309
+ kind: "grandfather",
31310
+ detail: "no provable artifact; recording session_legacy_import"
31311
+ });
31312
+ if (!dryRun) {
31313
+ insertEvent(db, {
31314
+ session_id: s.id,
31315
+ event_type: "session_legacy_import",
31316
+ phase: "complete",
31317
+ metadata: JSON.stringify({ source: "reconciled" })
31318
+ });
31319
+ }
31320
+ }
31321
+ continue;
31322
+ }
31323
+ const age = lastEventAgeSeconds(db, s.id);
31324
+ const stale = (age === null || age > threshold) && !hasInFlightDependents(db, s.id);
31325
+ if (stale) {
31326
+ actions.push({
31327
+ sessionId: s.id,
31328
+ kind: "stale-close",
31329
+ detail: age === null ? "active with no events and no in-flight dependents" : `active, last event ${Math.round(age / 86400)}d ago, no in-flight dependents`
31330
+ });
31331
+ if (!dryRun) {
31332
+ commitReasonClose(
31333
+ db,
31334
+ s.id,
31335
+ {
31336
+ event_type: "session_auto_closed_stale",
31337
+ phase: "complete",
31338
+ metadata: JSON.stringify({ source: "reconciled", threshold_seconds: threshold })
31339
+ },
31340
+ { status: "closed", current_phase: "complete" }
31341
+ );
31342
+ }
31343
+ }
31344
+ }
31345
+ return { dryRun, actions };
31346
+ }
31347
+
31348
+ // ../cli/src/lib/db/liveness.ts
31349
+ var PID_REUSE_GUARD_MS = 24 * 60 * 60 * 1e3;
31350
+ function defaultIsAlive(pid) {
31351
+ try {
31352
+ process.kill(pid, 0);
31353
+ return true;
31354
+ } catch (err) {
31355
+ return !(err instanceof Error && "code" in err && err.code === "ESRCH");
31356
+ }
31357
+ }
31358
+ function sqliteUtcMs(ts) {
31359
+ const sqliteShape = ts.includes(" ");
31360
+ return new Date(sqliteShape ? ts.replace(" ", "T") + "Z" : ts).getTime();
31361
+ }
31362
+
31363
+ // ../cli/src/lib/state/exit-codes.ts
30917
31364
  var CANCELLED_EXIT_CODE = -2;
31365
+ var ORPHAN_EXIT_CODE = -3;
31366
+ var CASCADE_CLOSE_EXIT_CODE = -4;
31367
+
31368
+ // ../cli/src/lib/db/agent-sessions.ts
30918
31369
  var NOTE_ORPHAN_PREFIX = "orphaned by liveness sweep";
31370
+ var INSTANCE_COMMAND = "session-instance";
31371
+ function cascadeTerminateExecutions(db, workflowId, exitCode, note) {
31372
+ db.run(
31373
+ `UPDATE command_executions
31374
+ SET finished_at = datetime('now'),
31375
+ exit_code = ?,
31376
+ pid = NULL,
31377
+ notes = COALESCE(notes || char(10), '') || ?
31378
+ WHERE workflow_id = ?
31379
+ AND finished_at IS NULL`,
31380
+ [exitCode, note, workflowId]
31381
+ );
31382
+ }
30919
31383
  function rowToAgentSession(row) {
30920
31384
  return {
30921
31385
  // The OCR-owned id is the `uid` column. Fall back to the integer
@@ -30930,6 +31394,7 @@ function rowToAgentSession(row) {
30930
31394
  resolved_model: row.resolved_model,
30931
31395
  phase: null,
30932
31396
  status: deriveStatus(row),
31397
+ kind: rowKind(row),
30933
31398
  pid: row.pid,
30934
31399
  started_at: row.started_at,
30935
31400
  last_heartbeat_at: row.last_heartbeat_at ?? row.started_at,
@@ -30943,7 +31408,9 @@ function deriveStatus(row) {
30943
31408
  return "running";
30944
31409
  }
30945
31410
  if (row.exit_code === ORPHAN_EXIT_CODE) return "orphaned";
30946
- if (row.exit_code === CANCELLED_EXIT_CODE) return "cancelled";
31411
+ if (row.exit_code === CANCELLED_EXIT_CODE || row.exit_code === CASCADE_CLOSE_EXIT_CODE) {
31412
+ return "cancelled";
31413
+ }
30947
31414
  if (row.exit_code === 0) return "done";
30948
31415
  return "crashed";
30949
31416
  }
@@ -30988,38 +31455,112 @@ function linkDashboardInvocationToWorkflow(db, dashboardUid, workflowId) {
30988
31455
  [workflowId, dashboardUid]
30989
31456
  );
30990
31457
  }
30991
- function sweepStaleAgentSessions(db, thresholdSeconds) {
30992
- const staleSql = `
30993
- SELECT uid, id FROM command_executions
30994
- WHERE finished_at IS NULL
30995
- AND last_heartbeat_at IS NOT NULL
30996
- AND (julianday('now') - julianday(last_heartbeat_at)) * 86400 > ?
30997
- `;
30998
- const stale = resultToRows(
30999
- db.exec(staleSql, [thresholdSeconds])
31458
+ function sweepStaleAgentSessions(db, thresholdSeconds, isAlive = defaultIsAlive) {
31459
+ const candidates = resultToRows(
31460
+ db.exec(
31461
+ `SELECT uid, id, pid, started_at, workflow_id, command, last_heartbeat_at
31462
+ FROM command_executions
31463
+ WHERE finished_at IS NULL
31464
+ AND pid IS NOT NULL
31465
+ AND last_heartbeat_at IS NOT NULL
31466
+ AND (julianday('now') - julianday(last_heartbeat_at)) * 86400 > ?`,
31467
+ [thresholdSeconds]
31468
+ )
31000
31469
  );
31001
- if (stale.length === 0) {
31002
- return { orphanedIds: [] };
31470
+ if (candidates.length === 0) {
31471
+ return { orphanedIds: [], cascadedWorkflowIds: [] };
31472
+ }
31473
+ const reuseCutoffMs = Date.now() - PID_REUSE_GUARD_MS;
31474
+ const dead = candidates.filter((row) => {
31475
+ if (row.pid === null) return false;
31476
+ if (sqliteUtcMs(row.started_at) < reuseCutoffMs) return false;
31477
+ return !isAlive(row.pid);
31478
+ });
31479
+ if (dead.length === 0) {
31480
+ return { orphanedIds: [], cascadedWorkflowIds: [] };
31003
31481
  }
31004
31482
  const note = `${NOTE_ORPHAN_PREFIX} (threshold ${thresholdSeconds}s)`;
31005
- db.run(
31006
- `UPDATE command_executions
31007
- SET finished_at = datetime('now'),
31008
- exit_code = ?,
31009
- notes = COALESCE(notes || char(10), '') || ?
31010
- WHERE finished_at IS NULL
31011
- AND last_heartbeat_at IS NOT NULL
31012
- AND (julianday('now') - julianday(last_heartbeat_at)) * 86400 > ?`,
31013
- [ORPHAN_EXIT_CODE, note, thresholdSeconds]
31014
- );
31483
+ const placeholders = dead.map(() => "?").join(", ");
31484
+ const cascadedWorkflowIds = [];
31485
+ db.transaction(() => {
31486
+ db.run(
31487
+ `UPDATE command_executions
31488
+ SET finished_at = datetime('now'),
31489
+ exit_code = ?,
31490
+ pid = NULL,
31491
+ notes = COALESCE(notes || char(10), '') || ?
31492
+ WHERE id IN (${placeholders})
31493
+ AND finished_at IS NULL`,
31494
+ [ORPHAN_EXIT_CODE, note, ...dead.map((r) => r.id)]
31495
+ );
31496
+ for (const row of dead) {
31497
+ if (row.workflow_id && rowKind(row) === "supervisor") {
31498
+ cascadeTerminateExecutions(
31499
+ db,
31500
+ row.workflow_id,
31501
+ CASCADE_CLOSE_EXIT_CODE,
31502
+ "cascade-closed: workflow process orphaned by liveness sweep"
31503
+ );
31504
+ cascadedWorkflowIds.push(row.workflow_id);
31505
+ }
31506
+ }
31507
+ });
31015
31508
  return {
31016
- orphanedIds: stale.map((row) => row.uid ?? String(row.id))
31509
+ orphanedIds: dead.map((r) => r.uid ?? String(r.id)),
31510
+ cascadedWorkflowIds
31017
31511
  };
31018
31512
  }
31513
+ function rowKind(row) {
31514
+ if (row.command === INSTANCE_COMMAND || row.command.startsWith(`${INSTANCE_COMMAND}:`)) {
31515
+ return "instance";
31516
+ }
31517
+ return row.last_heartbeat_at == null ? "utility" : "supervisor";
31518
+ }
31519
+ function sweepStaleSessions(db, thresholdSeconds) {
31520
+ const sql = `
31521
+ SELECT s.id
31522
+ FROM sessions s
31523
+ LEFT JOIN (
31524
+ SELECT session_id, MAX(created_at) AS last_event_at
31525
+ FROM orchestration_events
31526
+ GROUP BY session_id
31527
+ ) e ON e.session_id = s.id
31528
+ WHERE s.status = 'active'
31529
+ AND (
31530
+ e.last_event_at IS NULL
31531
+ OR (julianday('now') - julianday(e.last_event_at)) * 86400 > ?
31532
+ )
31533
+ AND NOT EXISTS (
31534
+ SELECT 1 FROM command_executions ce
31535
+ WHERE ce.workflow_id = s.id
31536
+ AND ce.finished_at IS NULL
31537
+ )
31538
+ `;
31539
+ const rows = resultToRows(db.exec(sql, [thresholdSeconds]));
31540
+ if (rows.length === 0) {
31541
+ return { closedSessionIds: [] };
31542
+ }
31543
+ for (const row of rows) {
31544
+ commitReasonClose(
31545
+ db,
31546
+ row.id,
31547
+ {
31548
+ event_type: "session_auto_closed_stale",
31549
+ phase: "complete",
31550
+ metadata: JSON.stringify({
31551
+ reason: "no events past threshold; no in-flight dependents",
31552
+ threshold_seconds: thresholdSeconds
31553
+ })
31554
+ },
31555
+ { status: "closed", current_phase: "complete" }
31556
+ );
31557
+ }
31558
+ return { closedSessionIds: rows.map((r) => r.id) };
31559
+ }
31019
31560
 
31020
31561
  // ../cli/src/lib/db/command-log.ts
31021
- import { appendFileSync, existsSync as existsSync2, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
31022
- import { dirname as dirname2, join as join2 } from "node:path";
31562
+ import { appendFileSync, existsSync as existsSync3, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
31563
+ import { dirname as dirname3, join as join3 } from "node:path";
31023
31564
  import { randomUUID } from "node:crypto";
31024
31565
  var CACHE_DIR = ".cache";
31025
31566
  var FILENAME = "command-history.jsonl";
@@ -31030,16 +31571,16 @@ function generateCommandUid() {
31030
31571
  return randomUUID();
31031
31572
  }
31032
31573
  function cacheDir(ocrDir) {
31033
- return join2(ocrDir, "data", CACHE_DIR);
31574
+ return join3(ocrDir, "data", CACHE_DIR);
31034
31575
  }
31035
31576
  function commandLogPath(ocrDir) {
31036
- return join2(cacheDir(ocrDir), FILENAME);
31577
+ return join3(cacheDir(ocrDir), FILENAME);
31037
31578
  }
31038
31579
  function appendCommandLog(ocrDir, entry) {
31039
31580
  try {
31040
31581
  const filePath = commandLogPath(ocrDir);
31041
- const dir = dirname2(filePath);
31042
- if (!existsSync2(dir)) mkdirSync(dir, { recursive: true });
31582
+ const dir = dirname3(filePath);
31583
+ if (!existsSync3(dir)) mkdirSync(dir, { recursive: true });
31043
31584
  const line = JSON.stringify(entry) + "\n";
31044
31585
  appendFileSync(filePath, line, { encoding: "utf-8" });
31045
31586
  if (approxLineCount >= 0) approxLineCount++;
@@ -31049,7 +31590,7 @@ function appendCommandLog(ocrDir, entry) {
31049
31590
  }
31050
31591
  function readCommandLog(ocrDir) {
31051
31592
  const filePath = commandLogPath(ocrDir);
31052
- if (!existsSync2(filePath)) return [];
31593
+ if (!existsSync3(filePath)) return [];
31053
31594
  const content = readFileSync(filePath, "utf-8");
31054
31595
  const entries = [];
31055
31596
  for (const line of content.split("\n")) {
@@ -31115,99 +31656,137 @@ function rotateIfNeeded(filePath) {
31115
31656
  }
31116
31657
 
31117
31658
  // ../cli/src/lib/db/index.ts
31118
- function locateWasm() {
31119
- const require2 = createRequire(import.meta.url);
31120
- const sqlJsPath = require2.resolve("sql.js");
31121
- return join3(dirname3(sqlJsPath), "sql-wasm.wasm");
31659
+ var V2_SCHEMA_VERSION = 12;
31660
+ function maybeSnapshotBeforeUpgrade(db, dbPath, fromVersion) {
31661
+ if (fromVersion < 1 || fromVersion >= V2_SCHEMA_VERSION) return null;
31662
+ const bakPath = `${dbPath}.bak.v${fromVersion}`;
31663
+ if (existsSync4(bakPath)) return bakPath;
31664
+ try {
31665
+ if (!existsSync4(dbPath) || statSync(dbPath).size === 0) return null;
31666
+ db.pragma("wal_checkpoint(TRUNCATE)");
31667
+ copyFileSync(dbPath, bakPath);
31668
+ return bakPath;
31669
+ } catch {
31670
+ return null;
31671
+ }
31122
31672
  }
31123
- function applyPragmas(db) {
31124
- db.run("PRAGMA foreign_keys = ON;");
31125
- db.run("PRAGMA journal_mode = WAL;");
31126
- db.run("PRAGMA busy_timeout = 5000;");
31673
+ function formatUpgradeNotice(bakPath, reconcile) {
31674
+ const lines = [
31675
+ "Storage upgraded to v2.0 \u2014 durable SQLite engine (WAL), event-sourced lifecycle."
31676
+ ];
31677
+ if (bakPath) {
31678
+ lines.push(` A backup of your previous database was saved to: ${bakPath}`);
31679
+ }
31680
+ const repairs = (reconcile?.actions ?? []).filter((a) => a.kind !== "ok");
31681
+ if (repairs.length > 0) {
31682
+ const n = (kind) => repairs.filter((a) => a.kind === kind).length;
31683
+ const parts = [];
31684
+ const finalized = n("synthesize-round-completed") + n("synthesize-map-completed");
31685
+ if (finalized > 0) parts.push(`${finalized} finalized from artifacts`);
31686
+ if (n("grandfather") > 0) parts.push(`${n("grandfather")} grandfathered`);
31687
+ if (n("stale-close") > 0) parts.push(`${n("stale-close")} stale closed`);
31688
+ lines.push(
31689
+ ` Reconciled ${repairs.length} legacy session(s): ${parts.join(", ")}.`
31690
+ );
31691
+ }
31692
+ lines.push(" Run `ocr doctor` to verify the storage engine.");
31693
+ return lines.map((l) => `[ocr] ${l}`).join("\n");
31127
31694
  }
31128
- function walCheckpointTruncate(dbPath) {
31129
- if (!existsSync3(dbPath)) {
31130
- return "skipped";
31695
+ var connections = /* @__PURE__ */ new Map();
31696
+ async function openDatabase(dbPath) {
31697
+ const cached = connections.get(dbPath);
31698
+ if (cached) {
31699
+ return cached;
31700
+ }
31701
+ const dir = dirname4(dbPath);
31702
+ if (!existsSync4(dir)) {
31703
+ mkdirSync2(dir, { recursive: true });
31131
31704
  }
31705
+ const db = openEngine(dbPath);
31706
+ connections.set(dbPath, db);
31707
+ return db;
31708
+ }
31709
+ async function ensureDatabase(ocrDir) {
31710
+ const dataDir = join4(ocrDir, "data");
31711
+ if (!existsSync4(dataDir)) {
31712
+ mkdirSync2(dataDir, { recursive: true });
31713
+ }
31714
+ const dbPath = join4(dataDir, "ocr.db");
31715
+ const db = await openDatabase(dbPath);
31716
+ let before = 0;
31132
31717
  try {
31133
- const probe = spawnSync("sqlite3", ["-version"], {
31134
- stdio: "ignore",
31135
- timeout: 2e3
31136
- });
31137
- if (probe.status !== 0) {
31138
- return "skipped";
31139
- }
31718
+ before = getSchemaVersion(db);
31140
31719
  } catch {
31720
+ before = 0;
31721
+ }
31722
+ const isLegacyUpgrade = before >= 1 && before < V2_SCHEMA_VERSION;
31723
+ const bakPath = maybeSnapshotBeforeUpgrade(db, dbPath, before);
31724
+ runMigrations(db);
31725
+ let reconcile;
31726
+ if (before < V2_SCHEMA_VERSION) {
31727
+ try {
31728
+ reconcile = reconcileLegacyState(db, ocrDir);
31729
+ } catch (err) {
31730
+ console.error(
31731
+ `[ocr] legacy reconciliation skipped: ${err instanceof Error ? err.message : String(err)}`
31732
+ );
31733
+ }
31734
+ }
31735
+ if (isLegacyUpgrade) {
31736
+ const notice = formatUpgradeNotice(bakPath, reconcile);
31737
+ if (notice) console.error(notice);
31738
+ }
31739
+ return db;
31740
+ }
31741
+ function walCheckpointTruncate(dbPath) {
31742
+ if (!existsSync4(dbPath)) {
31141
31743
  return "skipped";
31142
31744
  }
31745
+ const cached = connections.get(dbPath);
31746
+ if (cached) {
31747
+ try {
31748
+ cached.pragma("wal_checkpoint(TRUNCATE)");
31749
+ return "checkpointed";
31750
+ } catch {
31751
+ return "failed";
31752
+ }
31753
+ }
31754
+ let transient;
31143
31755
  try {
31144
- const result = spawnSync(
31145
- "sqlite3",
31146
- [dbPath, "PRAGMA wal_checkpoint(TRUNCATE);"],
31147
- {
31148
- stdio: "ignore",
31149
- timeout: 5e3
31150
- }
31151
- );
31152
- return result.status === 0 ? "checkpointed" : "failed";
31756
+ transient = openEngine(dbPath);
31757
+ transient.pragma("wal_checkpoint(TRUNCATE)");
31758
+ return "checkpointed";
31153
31759
  } catch {
31154
31760
  return "failed";
31761
+ } finally {
31762
+ try {
31763
+ transient?.raw.close();
31764
+ } catch {
31765
+ }
31766
+ }
31767
+ }
31768
+ function closeDatabase(dbPath) {
31769
+ const db = connections.get(dbPath);
31770
+ if (db) {
31771
+ db.close();
31772
+ connections.delete(dbPath);
31155
31773
  }
31156
31774
  }
31157
31775
 
31158
31776
  // src/server/db.ts
31777
+ import { join as join5 } from "node:path";
31159
31778
  var cachedDb = null;
31160
31779
  var cachedDbPath = null;
31161
- var preSaveHook = null;
31162
- var postSaveHook = null;
31163
- function registerSaveHooks(preSave, postSave) {
31164
- preSaveHook = preSave;
31165
- postSaveHook = postSave;
31166
- }
31167
31780
  async function openDb(ocrDir) {
31168
- const dbPath = join4(ocrDir, "data", "ocr.db");
31169
- if (cachedDb && cachedDbPath === dbPath) {
31170
- return cachedDb;
31171
- }
31172
- const wasmBuffer = readFileSync3(locateWasm());
31173
- const wasmBinary = wasmBuffer.buffer.slice(
31174
- wasmBuffer.byteOffset,
31175
- wasmBuffer.byteOffset + wasmBuffer.byteLength
31176
- );
31177
- const SQL = await initSqlJs2({ wasmBinary });
31178
- let db;
31179
- if (existsSync4(dbPath)) {
31180
- const fileBuffer = readFileSync3(dbPath);
31181
- db = new SQL.Database(fileBuffer);
31182
- } else {
31183
- const dataDir = dirname4(dbPath);
31184
- if (!existsSync4(dataDir)) {
31185
- mkdirSync3(dataDir, { recursive: true });
31186
- }
31187
- db = new SQL.Database();
31188
- }
31189
- applyPragmas(db);
31190
- runMigrations(db);
31781
+ const dbPath = join5(ocrDir, "data", "ocr.db");
31782
+ const db = await ensureDatabase(ocrDir);
31191
31783
  cachedDb = db;
31192
31784
  cachedDbPath = dbPath;
31193
31785
  return db;
31194
31786
  }
31195
- function saveDb(db, ocrDir) {
31196
- preSaveHook?.();
31197
- const dbPath = join4(ocrDir, "data", "ocr.db");
31198
- const data = db.export();
31199
- const dir = dirname4(dbPath);
31200
- if (!existsSync4(dir)) {
31201
- mkdirSync3(dir, { recursive: true });
31202
- }
31203
- const tmpPath = `${dbPath}.${process.pid}.tmp`;
31204
- writeFileSync3(tmpPath, Buffer.from(data));
31205
- renameSync3(tmpPath, dbPath);
31206
- postSaveHook?.();
31207
- }
31208
31787
  function closeDb() {
31209
- if (cachedDb) {
31210
- cachedDb.close();
31788
+ if (cachedDbPath) {
31789
+ closeDatabase(cachedDbPath);
31211
31790
  cachedDb = null;
31212
31791
  cachedDbPath = null;
31213
31792
  }
@@ -31431,7 +32010,11 @@ function deleteNote(db, noteId) {
31431
32010
  function getCommandHistory(db, limit = 50) {
31432
32011
  return resultToRows(
31433
32012
  db.exec(
31434
- "SELECT * FROM command_executions ORDER BY started_at DESC LIMIT ?",
32013
+ `SELECT ce.*, sc.completeness_state AS workflow_completeness
32014
+ FROM command_executions ce
32015
+ LEFT JOIN session_completeness sc ON sc.session_id = ce.workflow_id
32016
+ ORDER BY ce.started_at DESC
32017
+ LIMIT ?`,
31435
32018
  [limit]
31436
32019
  )
31437
32020
  );
@@ -32120,34 +32703,7 @@ var VALID_ROUND_STATUSES = /* @__PURE__ */ new Set([
32120
32703
  "acknowledged",
32121
32704
  "dismissed"
32122
32705
  ]);
32123
- var saveTimer = null;
32124
- var pendingDb = null;
32125
- var pendingOcrDir = null;
32126
- function debouncedSave(db, ocrDir) {
32127
- pendingDb = db;
32128
- pendingOcrDir = ocrDir;
32129
- if (saveTimer) clearTimeout(saveTimer);
32130
- saveTimer = setTimeout(() => {
32131
- if (pendingDb && pendingOcrDir) {
32132
- saveDb(pendingDb, pendingOcrDir);
32133
- }
32134
- saveTimer = null;
32135
- pendingDb = null;
32136
- pendingOcrDir = null;
32137
- }, 500);
32138
- }
32139
- function flushSave() {
32140
- if (saveTimer) {
32141
- clearTimeout(saveTimer);
32142
- saveTimer = null;
32143
- }
32144
- if (pendingDb && pendingOcrDir) {
32145
- saveDb(pendingDb, pendingOcrDir);
32146
- pendingDb = null;
32147
- pendingOcrDir = null;
32148
- }
32149
- }
32150
- function createProgressRouter(db, ocrDir) {
32706
+ function createProgressRouter(db) {
32151
32707
  const router = (0, import_express5.Router)();
32152
32708
  router.patch("/map-files/:id/progress", (req, res) => {
32153
32709
  try {
@@ -32167,7 +32723,6 @@ function createProgressRouter(db, ocrDir) {
32167
32723
  return;
32168
32724
  }
32169
32725
  upsertFileProgress(db, fileId, isReviewed);
32170
- debouncedSave(db, ocrDir);
32171
32726
  const progress = getFileProgress(db, fileId);
32172
32727
  res.json(progress);
32173
32728
  } catch (err) {
@@ -32183,7 +32738,6 @@ function createProgressRouter(db, ocrDir) {
32183
32738
  return;
32184
32739
  }
32185
32740
  deleteFileProgress(db, fileId);
32186
- debouncedSave(db, ocrDir);
32187
32741
  res.status(200).json({ deleted: true });
32188
32742
  } catch (err) {
32189
32743
  console.error("Failed to clear file progress:", err);
@@ -32211,7 +32765,6 @@ function createProgressRouter(db, ocrDir) {
32211
32765
  return;
32212
32766
  }
32213
32767
  upsertFindingProgress(db, findingId, status);
32214
- debouncedSave(db, ocrDir);
32215
32768
  const progress = getFindingProgress(db, findingId);
32216
32769
  res.json(progress);
32217
32770
  } catch (err) {
@@ -32227,7 +32780,6 @@ function createProgressRouter(db, ocrDir) {
32227
32780
  return;
32228
32781
  }
32229
32782
  deleteFindingProgress(db, findingId);
32230
- debouncedSave(db, ocrDir);
32231
32783
  res.status(200).json({ deleted: true });
32232
32784
  } catch (err) {
32233
32785
  console.error("Failed to clear finding progress:", err);
@@ -32255,7 +32807,6 @@ function createProgressRouter(db, ocrDir) {
32255
32807
  return;
32256
32808
  }
32257
32809
  upsertRoundProgress(db, roundId, status);
32258
- debouncedSave(db, ocrDir);
32259
32810
  const progress = getRoundProgress(db, roundId);
32260
32811
  res.json(progress);
32261
32812
  } catch (err) {
@@ -32271,7 +32822,6 @@ function createProgressRouter(db, ocrDir) {
32271
32822
  return;
32272
32823
  }
32273
32824
  deleteRoundProgress(db, roundId);
32274
- debouncedSave(db, ocrDir);
32275
32825
  res.status(200).json({ deleted: true });
32276
32826
  } catch (err) {
32277
32827
  console.error("Failed to clear round progress:", err);
@@ -32291,7 +32841,7 @@ var VALID_TARGET_TYPES = /* @__PURE__ */ new Set([
32291
32841
  "section",
32292
32842
  "file"
32293
32843
  ]);
32294
- function createNotesRouter(db, ocrDir) {
32844
+ function createNotesRouter(db) {
32295
32845
  const router = (0, import_express6.Router)();
32296
32846
  router.get("/", (req, res) => {
32297
32847
  try {
@@ -32330,7 +32880,6 @@ function createNotesRouter(db, ocrDir) {
32330
32880
  return;
32331
32881
  }
32332
32882
  const noteId = insertNote(db, target_type, target_id, content);
32333
- saveDb(db, ocrDir);
32334
32883
  const note = getNote(db, noteId);
32335
32884
  res.status(201).json(note);
32336
32885
  } catch (err) {
@@ -32356,7 +32905,6 @@ function createNotesRouter(db, ocrDir) {
32356
32905
  return;
32357
32906
  }
32358
32907
  updateNote(db, noteId, content);
32359
- saveDb(db, ocrDir);
32360
32908
  const note = getNote(db, noteId);
32361
32909
  res.json(note);
32362
32910
  } catch (err) {
@@ -32377,7 +32925,6 @@ function createNotesRouter(db, ocrDir) {
32377
32925
  return;
32378
32926
  }
32379
32927
  deleteNote(db, noteId);
32380
- saveDb(db, ocrDir);
32381
32928
  res.status(200).json({ deleted: true });
32382
32929
  } catch (err) {
32383
32930
  console.error("Failed to delete note:", err);
@@ -32442,12 +32989,44 @@ function spawnBinary(binary, args, opts) {
32442
32989
  }
32443
32990
 
32444
32991
  // src/server/socket/command-runner.ts
32445
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync4, unlinkSync, mkdirSync as mkdirSync5, existsSync as existsSync7 } from "node:fs";
32446
- import { dirname as dirname6, join as join9 } from "node:path";
32992
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync2, unlinkSync, mkdirSync as mkdirSync4, existsSync as existsSync7 } from "node:fs";
32993
+ import { dirname as dirname6, join as join10 } from "node:path";
32994
+
32995
+ // src/server/services/command-outcome.ts
32996
+ function deriveCommandOutcome(exitCode, completeness) {
32997
+ if (exitCode === null) return null;
32998
+ if (exitCode === CANCELLED_EXIT_CODE || exitCode === CASCADE_CLOSE_EXIT_CODE) {
32999
+ return "cancelled";
33000
+ }
33001
+ if (exitCode !== 0) return "failed";
33002
+ if (completeness === null || completeness === "complete") return "success";
33003
+ return "incomplete";
33004
+ }
33005
+ function deriveCancellationReason(exitCode) {
33006
+ if (exitCode === CANCELLED_EXIT_CODE) return "user";
33007
+ if (exitCode === CASCADE_CLOSE_EXIT_CODE) return "cascade";
33008
+ return null;
33009
+ }
33010
+ function getWorkflowCompletenessForExecution(db, executionId) {
33011
+ const result = db.exec(
33012
+ `SELECT sc.completeness_state
33013
+ FROM command_executions ce
33014
+ LEFT JOIN session_completeness sc ON sc.session_id = ce.workflow_id
33015
+ WHERE ce.id = ?`,
33016
+ [executionId]
33017
+ );
33018
+ const row = result[0]?.values[0];
33019
+ if (!row) return null;
33020
+ const state = row[0];
33021
+ if (state === "complete" || state === "closed_without_artifact" || state === "in_flight" || state === "open_no_artifact") {
33022
+ return state;
33023
+ }
33024
+ return null;
33025
+ }
32447
33026
 
32448
33027
  // src/server/services/ai-cli/index.ts
32449
- import { readFileSync as readFileSync5 } from "node:fs";
32450
- import { join as join7 } from "node:path";
33028
+ import { readFileSync as readFileSync3 } from "node:fs";
33029
+ import { join as join8 } from "node:path";
32451
33030
 
32452
33031
  // src/server/socket/env.ts
32453
33032
  var ENV_ALLOWLIST = [
@@ -32937,19 +33516,19 @@ function extractToolOutput(part) {
32937
33516
  import {
32938
33517
  createWriteStream,
32939
33518
  existsSync as existsSync5,
32940
- mkdirSync as mkdirSync4,
32941
- readFileSync as readFileSync4
33519
+ mkdirSync as mkdirSync3,
33520
+ readFileSync as readFileSync2
32942
33521
  } from "node:fs";
32943
- import { join as join5 } from "node:path";
33522
+ import { join as join6 } from "node:path";
32944
33523
  function eventsDir(ocrDir) {
32945
- const dir = join5(ocrDir, "data", "events");
33524
+ const dir = join6(ocrDir, "data", "events");
32946
33525
  if (!existsSync5(dir)) {
32947
- mkdirSync4(dir, { recursive: true });
33526
+ mkdirSync3(dir, { recursive: true });
32948
33527
  }
32949
33528
  return dir;
32950
33529
  }
32951
33530
  function eventJournalPath(ocrDir, executionId) {
32952
- return join5(eventsDir(ocrDir), `${executionId}.jsonl`);
33531
+ return join6(eventsDir(ocrDir), `${executionId}.jsonl`);
32953
33532
  }
32954
33533
  var EventJournalAppender = class {
32955
33534
  stream;
@@ -32989,7 +33568,7 @@ function readEventJournal(ocrDir, executionId) {
32989
33568
  if (!existsSync5(path2)) return [];
32990
33569
  let raw;
32991
33570
  try {
32992
- raw = readFileSync4(path2, "utf-8");
33571
+ raw = readFileSync2(path2, "utf-8");
32993
33572
  } catch {
32994
33573
  return [];
32995
33574
  }
@@ -33008,7 +33587,7 @@ function readEventJournal(ocrDir, executionId) {
33008
33587
 
33009
33588
  // src/server/services/ai-cli/helpers.ts
33010
33589
  import { tmpdir } from "node:os";
33011
- import { join as join6 } from "node:path";
33590
+ import { join as join7 } from "node:path";
33012
33591
  function formatToolDetail(tool, input) {
33013
33592
  switch (tool) {
33014
33593
  case "Read":
@@ -33032,13 +33611,13 @@ function formatToolDetail(tool, input) {
33032
33611
  return `Using ${tool}`;
33033
33612
  }
33034
33613
  }
33035
- var TEMP_BASE = join6(tmpdir(), "ocr-ai-prompts");
33614
+ var TEMP_BASE = join7(tmpdir(), "ocr-ai-prompts");
33036
33615
 
33037
33616
  // src/server/services/ai-cli/index.ts
33038
33617
  function readAiCliPreference(ocrDir) {
33039
33618
  try {
33040
- const configPath = join7(ocrDir, "config.yaml");
33041
- const content = readFileSync5(configPath, "utf-8");
33619
+ const configPath = join8(ocrDir, "config.yaml");
33620
+ const content = readFileSync3(configPath, "utf-8");
33042
33621
  const match = content.match(/^\s*ai_cli:\s*(\S+)/m);
33043
33622
  const value = match?.[1] ?? "auto";
33044
33623
  if (value === "claude" || value === "opencode" || value === "off") return value;
@@ -33148,19 +33727,19 @@ var AiCliService = class {
33148
33727
 
33149
33728
  // src/server/socket/cli-resolver.ts
33150
33729
  import { existsSync as existsSync6 } from "node:fs";
33151
- import { dirname as dirname5, join as join8 } from "node:path";
33730
+ import { dirname as dirname5, join as join9 } from "node:path";
33152
33731
  import { fileURLToPath } from "node:url";
33153
33732
  var __dirname = dirname5(fileURLToPath(import.meta.url));
33154
33733
  function resolveLocalCli() {
33155
- const parentDir = join8(__dirname, "..");
33156
- const bundledCli = join8(parentDir, "index.js");
33157
- if (existsSync6(bundledCli) && existsSync6(join8(parentDir, "dashboard", "server.js"))) {
33734
+ const parentDir = join9(__dirname, "..");
33735
+ const bundledCli = join9(parentDir, "index.js");
33736
+ if (existsSync6(bundledCli) && existsSync6(join9(parentDir, "dashboard", "server.js"))) {
33158
33737
  return bundledCli;
33159
33738
  }
33160
33739
  let dir = __dirname;
33161
33740
  for (let i = 0; i < 8; i++) {
33162
- if (existsSync6(join8(dir, "nx.json"))) {
33163
- const candidate = join8(dir, "packages", "cli", "dist", "index.js");
33741
+ if (existsSync6(join9(dir, "nx.json"))) {
33742
+ const candidate = join9(dir, "packages", "cli", "dist", "index.js");
33164
33743
  if (existsSync6(candidate)) return candidate;
33165
33744
  break;
33166
33745
  }
@@ -33286,8 +33865,8 @@ function buildPrompt(opts) {
33286
33865
  "",
33287
33866
  "Examples:",
33288
33867
  `- Instead of \`ocr state show\`, run: \`node ${localCli} state show\``,
33289
- `- Instead of \`ocr state init ...\`, run: \`node ${localCli} state init ...\``,
33290
- `- Instead of \`ocr state transition ...\`, run: \`node ${localCli} state transition ...\``,
33868
+ `- Instead of \`ocr state begin ...\`, run: \`node ${localCli} state begin ...\``,
33869
+ `- Instead of \`ocr state advance ...\`, run: \`node ${localCli} state advance ...\``,
33291
33870
  "",
33292
33871
  "This applies to every `ocr` invocation. Do NOT use bare `ocr` commands."
33293
33872
  );
@@ -33297,7 +33876,7 @@ function buildPrompt(opts) {
33297
33876
  "",
33298
33877
  "## Dashboard Linkage (REQUIRED for terminal handoff)",
33299
33878
  "",
33300
- 'You are running inside the OCR dashboard. To enable the "Pick up in terminal" affordance for this review, your first `ocr state init` invocation MUST include this flag:',
33879
+ 'You are running inside the OCR dashboard. To enable the "Pick up in terminal" affordance for this review, your first `ocr state begin` invocation MUST include this flag:',
33301
33880
  "",
33302
33881
  "```",
33303
33882
  `--dashboard-uid ${executionUid}`,
@@ -33306,7 +33885,7 @@ function buildPrompt(opts) {
33306
33885
  "Full example:",
33307
33886
  "",
33308
33887
  "```",
33309
- `node ${localCli} state init --session-id <id> --branch <branch> --workflow-type review --dashboard-uid ${executionUid}`,
33888
+ `node ${localCli} state begin --session-id <id> --branch <branch> --workflow-type review --dashboard-uid ${executionUid}`,
33310
33889
  "```",
33311
33890
  "",
33312
33891
  "Without this flag the dashboard cannot link your review session to its execution row, and the resume command will not be available."
@@ -33353,17 +33932,17 @@ function extractPerInstanceModels(subArgs) {
33353
33932
  var MAX_CONCURRENT = 3;
33354
33933
  var activeCommands = /* @__PURE__ */ new Map();
33355
33934
  function spawnMarkerPath(ocrDir) {
33356
- return join9(ocrDir, "data", "dashboard-active-spawn.json");
33935
+ return join10(ocrDir, "data", "dashboard-active-spawn.json");
33357
33936
  }
33358
33937
  function writeSpawnMarker(ocrDir, executionUid, pid) {
33359
- const dataDir = join9(ocrDir, "data");
33360
- if (!existsSync7(dataDir)) mkdirSync5(dataDir, { recursive: true });
33938
+ const dataDir = join10(ocrDir, "data");
33939
+ if (!existsSync7(dataDir)) mkdirSync4(dataDir, { recursive: true });
33361
33940
  const payload = JSON.stringify({
33362
33941
  execution_uid: executionUid,
33363
33942
  pid,
33364
33943
  started_at: (/* @__PURE__ */ new Date()).toISOString()
33365
33944
  });
33366
- writeFileSync4(spawnMarkerPath(ocrDir), payload, { mode: 384 });
33945
+ writeFileSync2(spawnMarkerPath(ocrDir), payload, { mode: 384 });
33367
33946
  }
33368
33947
  function clearSpawnMarker(ocrDir) {
33369
33948
  try {
@@ -33571,10 +34150,10 @@ function spawnAiCommand(io2, _socket, db, ocrDir, executionId, baseCommand, subA
33571
34150
  io2.emit("command:output", { execution_id: executionId, content: warning });
33572
34151
  }
33573
34152
  }
33574
- const commandMdPath = join9(ocrDir, "commands", `${baseCommand}.md`);
34153
+ const commandMdPath = join10(ocrDir, "commands", `${baseCommand}.md`);
33575
34154
  let commandContent;
33576
34155
  try {
33577
- commandContent = readFileSync6(commandMdPath, "utf-8");
34156
+ commandContent = readFileSync4(commandMdPath, "utf-8");
33578
34157
  } catch {
33579
34158
  const content = `Error: Could not read command file at ${commandMdPath}
33580
34159
  `;
@@ -33805,7 +34384,9 @@ function finishExecution(io2, db, ocrDir, executionId, code, output) {
33805
34384
  WHERE id = ?`,
33806
34385
  [code, finishedAt, output, executionId]
33807
34386
  );
33808
- saveDb(db, ocrDir);
34387
+ const completeness = getWorkflowCompletenessForExecution(db, executionId);
34388
+ const outcome = deriveCommandOutcome(code, completeness);
34389
+ const cancellationReason = deriveCancellationReason(code);
33809
34390
  if (entry?.uid) {
33810
34391
  appendCommandLog(ocrDir, {
33811
34392
  v: 1,
@@ -33824,7 +34405,9 @@ function finishExecution(io2, db, ocrDir, executionId, code, output) {
33824
34405
  io2.emit("command:finished", {
33825
34406
  execution_id: executionId,
33826
34407
  exitCode: code,
33827
- finished_at: finishedAt
34408
+ finished_at: finishedAt,
34409
+ outcome,
34410
+ cancellation_reason: cancellationReason
33828
34411
  });
33829
34412
  activeCommands.delete(executionId);
33830
34413
  }
@@ -33874,10 +34457,21 @@ function createCommandsRouter(db, ocrDir) {
33874
34457
  router.get("/history", (req, res) => {
33875
34458
  try {
33876
34459
  const limit = parseInt(req.query["limit"], 10) || 50;
33877
- const history = getCommandHistory(db, limit).map((row) => ({
33878
- ...row,
33879
- duration_ms: row.finished_at && row.started_at ? new Date(row.finished_at).getTime() - new Date(row.started_at).getTime() : null
33880
- }));
34460
+ const history = getCommandHistory(db, limit).map((row) => {
34461
+ const { workflow_completeness, ...persisted } = row;
34462
+ return {
34463
+ ...persisted,
34464
+ duration_ms: row.finished_at && row.started_at ? new Date(row.finished_at).getTime() - new Date(row.started_at).getTime() : null,
34465
+ // Derived from (exit_code, event-sourced workflow completeness) —
34466
+ // single source of truth shared with the live `command:finished`
34467
+ // socket event.
34468
+ outcome: deriveCommandOutcome(row.exit_code, workflow_completeness),
34469
+ // Orthogonal discriminator within the 'cancelled' bucket so the
34470
+ // client distinguishes a user cancel from a cascade close without
34471
+ // reaching past `outcome` to match a magic exit-code number.
34472
+ cancellation_reason: deriveCancellationReason(row.exit_code)
34473
+ };
34474
+ });
33881
34475
  res.json(history);
33882
34476
  } catch (err) {
33883
34477
  console.error("Failed to fetch command history:", err);
@@ -33903,8 +34497,8 @@ function createCommandsRouter(db, ocrDir) {
33903
34497
 
33904
34498
  // src/server/routes/config.ts
33905
34499
  var import_express9 = __toESM(require_express2(), 1);
33906
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "node:fs";
33907
- import { join as join10, dirname as dirname7, basename } from "node:path";
34500
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
34501
+ import { join as join11, dirname as dirname7, basename } from "node:path";
33908
34502
  var VALID_IDES = ["vscode", "cursor", "windsurf", "jetbrains", "sublime"];
33909
34503
  function detectIde() {
33910
34504
  const termProgram = process.env.TERM_PROGRAM?.toLowerCase() ?? "";
@@ -33921,8 +34515,8 @@ function detectIde() {
33921
34515
  }
33922
34516
  function readIdeFromConfig(ocrDir) {
33923
34517
  try {
33924
- const configPath = join10(ocrDir, "config.yaml");
33925
- const content = readFileSync7(configPath, "utf-8");
34518
+ const configPath = join11(ocrDir, "config.yaml");
34519
+ const content = readFileSync5(configPath, "utf-8");
33926
34520
  const match = content.match(/^\s*ide:\s*(\S+)/m);
33927
34521
  return match?.[1] ?? "auto";
33928
34522
  } catch {
@@ -33968,8 +34562,8 @@ function createConfigRouter(ocrDir, aiCliService) {
33968
34562
  return;
33969
34563
  }
33970
34564
  try {
33971
- const configPath = join10(ocrDir, "config.yaml");
33972
- let content = readFileSync7(configPath, "utf-8");
34565
+ const configPath = join11(ocrDir, "config.yaml");
34566
+ let content = readFileSync5(configPath, "utf-8");
33973
34567
  if (content.match(/^\s*ide:\s*\S+/m)) {
33974
34568
  content = content.replace(/^(\s*ide:\s*)\S+/m, `$1${ide}`);
33975
34569
  } else if (content.includes("dashboard:")) {
@@ -33981,7 +34575,7 @@ dashboard:
33981
34575
  ide: ${ide}
33982
34576
  `;
33983
34577
  }
33984
- writeFileSync5(configPath, content);
34578
+ writeFileSync3(configPath, content);
33985
34579
  res.json({ ide });
33986
34580
  } catch (err) {
33987
34581
  console.error("Failed to update config:", err);
@@ -33993,7 +34587,7 @@ dashboard:
33993
34587
 
33994
34588
  // src/server/routes/chat.ts
33995
34589
  var import_express10 = __toESM(require_express2(), 1);
33996
- function createChatRouter(db, ocrDir) {
34590
+ function createChatRouter(db) {
33997
34591
  const router = (0, import_express10.Router)();
33998
34592
  router.get("/:id/chat", (req, res) => {
33999
34593
  try {
@@ -34046,7 +34640,6 @@ function createChatRouter(db, ocrDir) {
34046
34640
  return;
34047
34641
  }
34048
34642
  deleteConversation(db, conversationId);
34049
- saveDb(db, ocrDir);
34050
34643
  res.status(200).json({ deleted: true });
34051
34644
  } catch (err) {
34052
34645
  console.error("Failed to delete conversation:", err);
@@ -34058,15 +34651,15 @@ function createChatRouter(db, ocrDir) {
34058
34651
 
34059
34652
  // src/server/routes/reviewers.ts
34060
34653
  var import_express11 = __toESM(require_express2(), 1);
34061
- import { readFileSync as readFileSync8, existsSync as existsSync8, watch } from "node:fs";
34062
- import { join as join11 } from "node:path";
34654
+ import { readFileSync as readFileSync6, existsSync as existsSync8, watch } from "node:fs";
34655
+ import { join as join12 } from "node:path";
34063
34656
  function readReviewersMeta(ocrDir) {
34064
- const metaPath = join11(ocrDir, "reviewers-meta.json");
34657
+ const metaPath = join12(ocrDir, "reviewers-meta.json");
34065
34658
  if (!existsSync8(metaPath)) {
34066
34659
  return { reviewers: [], defaults: [] };
34067
34660
  }
34068
34661
  try {
34069
- const raw = readFileSync8(metaPath, "utf-8");
34662
+ const raw = readFileSync6(metaPath, "utf-8");
34070
34663
  const meta = JSON.parse(raw);
34071
34664
  const reviewers = meta.reviewers ?? [];
34072
34665
  const defaults = reviewers.filter((r) => r.is_default).map((r) => r.id);
@@ -34087,13 +34680,13 @@ function createReviewersRouter(ocrDir) {
34087
34680
  res.status(400).json({ error: "Invalid reviewer ID" });
34088
34681
  return;
34089
34682
  }
34090
- const filePath = join11(ocrDir, "skills", "references", "reviewers", `${id}.md`);
34683
+ const filePath = join12(ocrDir, "skills", "references", "reviewers", `${id}.md`);
34091
34684
  if (!existsSync8(filePath)) {
34092
34685
  res.status(404).json({ error: "Reviewer not found", id });
34093
34686
  return;
34094
34687
  }
34095
34688
  try {
34096
- const content = readFileSync8(filePath, "utf-8");
34689
+ const content = readFileSync6(filePath, "utf-8");
34097
34690
  res.json({ id, content });
34098
34691
  } catch {
34099
34692
  res.status(500).json({ error: "Failed to read reviewer file", id });
@@ -34102,7 +34695,7 @@ function createReviewersRouter(ocrDir) {
34102
34695
  return router;
34103
34696
  }
34104
34697
  function watchReviewersMeta(ocrDir, io2) {
34105
- const metaPath = join11(ocrDir, "reviewers-meta.json");
34698
+ const metaPath = join12(ocrDir, "reviewers-meta.json");
34106
34699
  let watcher = null;
34107
34700
  let debounce;
34108
34701
  try {
@@ -34178,18 +34771,18 @@ function createHandoffRouter(sessionCapture, ocrDir, syncFromDisk = () => {
34178
34771
 
34179
34772
  // src/server/routes/team.ts
34180
34773
  var import_express14 = __toESM(require_express2(), 1);
34181
- import { spawnSync as spawnSync2 } from "node:child_process";
34774
+ import { spawnSync } from "node:child_process";
34182
34775
 
34183
34776
  // ../cli/src/lib/team-config.ts
34184
34777
  var import_yaml = __toESM(require_dist(), 1);
34185
- import { existsSync as existsSync9, readFileSync as readFileSync9 } from "node:fs";
34186
- import { join as join12 } from "node:path";
34778
+ import { existsSync as existsSync9, readFileSync as readFileSync7 } from "node:fs";
34779
+ import { join as join13 } from "node:path";
34187
34780
  function loadTeamConfig(ocrDir) {
34188
- const configPath = join12(ocrDir, "config.yaml");
34781
+ const configPath = join13(ocrDir, "config.yaml");
34189
34782
  if (!existsSync9(configPath)) {
34190
34783
  return { team: [], aliases: {}, defaultModel: null };
34191
34784
  }
34192
- const content = readFileSync9(configPath, "utf-8");
34785
+ const content = readFileSync7(configPath, "utf-8");
34193
34786
  return parseTeamConfigYaml(content);
34194
34787
  }
34195
34788
  function parseTeamConfigYaml(content) {
@@ -34470,7 +35063,7 @@ function createTeamRouter(ocrDir) {
34470
35063
  return;
34471
35064
  }
34472
35065
  try {
34473
- const result = spawnSync2("ocr", ["team", "set", "--stdin"], {
35066
+ const result = spawnSync("ocr", ["team", "set", "--stdin"], {
34474
35067
  input: JSON.stringify(body.team),
34475
35068
  encoding: "utf-8",
34476
35069
  cwd: ocrDir.replace(/\/\.ocr$/, ""),
@@ -34623,7 +35216,6 @@ function createSessionCaptureService(deps) {
34623
35216
  return;
34624
35217
  }
34625
35218
  recordVendorSessionIdForExecution(db, executionId, vendorSessionId);
34626
- saveDb(db, ocrDir);
34627
35219
  } catch (err) {
34628
35220
  console.error(
34629
35221
  `[session-capture] recordSessionId failed for execution ${executionId} \u2192 ${vendorSessionId}:`,
@@ -34634,7 +35226,6 @@ function createSessionCaptureService(deps) {
34634
35226
  function linkInvocationToWorkflow(uid, workflowId) {
34635
35227
  try {
34636
35228
  linkDashboardInvocationToWorkflow(db, uid, workflowId);
34637
- saveDb(db, ocrDir);
34638
35229
  } catch (err) {
34639
35230
  console.error("[session-capture] linkInvocationToWorkflow failed:", err);
34640
35231
  }
@@ -34791,8 +35382,8 @@ function buildDiagnostics(input) {
34791
35382
  }
34792
35383
 
34793
35384
  // src/server/services/filesystem-sync.ts
34794
- import { readdirSync, readFileSync as readFileSync10, statSync, existsSync as existsSync10 } from "node:fs";
34795
- import { join as join13, basename as basename2, dirname as dirname9, relative } from "node:path";
35385
+ import { readdirSync, readFileSync as readFileSync8, statSync as statSync2, existsSync as existsSync10 } from "node:fs";
35386
+ import { join as join14, basename as basename2, dirname as dirname9, relative } from "node:path";
34796
35387
  import { watch as watch2 } from "chokidar";
34797
35388
 
34798
35389
  // src/server/services/parsers/reviewer-parser.ts
@@ -34986,15 +35577,13 @@ function queryScalar(db, sql, params = []) {
34986
35577
  return result[0].values[0]?.[0] ?? null;
34987
35578
  }
34988
35579
  var FilesystemSync = class {
34989
- constructor(db, sessionsDir, io2, onSync) {
35580
+ constructor(db, sessionsDir, io2) {
34990
35581
  this.db = db;
34991
35582
  this.sessionsDir = sessionsDir;
34992
35583
  this.io = io2;
34993
- this.onSync = onSync;
34994
35584
  }
34995
35585
  watcher = null;
34996
35586
  debounceTimers = /* @__PURE__ */ new Map();
34997
- onSync;
34998
35587
  // ── 6.1: Full Scan ──
34999
35588
  async fullScan() {
35000
35589
  if (!existsSync10(this.sessionsDir)) return;
@@ -35002,13 +35591,13 @@ var FilesystemSync = class {
35002
35591
  for (const entry of entries) {
35003
35592
  if (!entry.isDirectory()) continue;
35004
35593
  const sessionId = entry.name;
35005
- const sessionDir = join13(this.sessionsDir, sessionId);
35594
+ const sessionDir = join14(this.sessionsDir, sessionId);
35006
35595
  this.syncSession(sessionId, sessionDir);
35007
35596
  }
35008
35597
  }
35009
35598
  syncSession(sessionId, sessionDir) {
35010
35599
  this.ensureSessionRow(sessionId, sessionDir);
35011
- const roundsDir = join13(sessionDir, "rounds");
35600
+ const roundsDir = join14(sessionDir, "rounds");
35012
35601
  if (existsSync10(roundsDir)) {
35013
35602
  const rounds = readdirSync(roundsDir, { withFileTypes: true });
35014
35603
  for (const roundEntry of rounds) {
@@ -35016,34 +35605,34 @@ var FilesystemSync = class {
35016
35605
  const roundMatch = roundEntry.name.match(/^round-(\d+)$/);
35017
35606
  if (!roundMatch) continue;
35018
35607
  const roundNumber = parseInt(roundMatch[1] ?? "0", 10);
35019
- const roundDir = join13(roundsDir, roundEntry.name);
35020
- const reviewsDir = join13(roundDir, "reviews");
35608
+ const roundDir = join14(roundsDir, roundEntry.name);
35609
+ const reviewsDir = join14(roundDir, "reviews");
35021
35610
  if (existsSync10(reviewsDir)) {
35022
35611
  const reviewFiles = readdirSync(reviewsDir).filter((f) => f.endsWith(".md"));
35023
35612
  for (const reviewFile of reviewFiles) {
35024
- const filePath = join13(reviewsDir, reviewFile);
35613
+ const filePath = join14(reviewsDir, reviewFile);
35025
35614
  this.processReviewerOutput(sessionId, roundNumber, filePath, reviewFile);
35026
35615
  }
35027
35616
  }
35028
- const roundMetaPath = join13(roundDir, "round-meta.json");
35617
+ const roundMetaPath = join14(roundDir, "round-meta.json");
35029
35618
  if (existsSync10(roundMetaPath)) {
35030
35619
  this.processRoundMeta(sessionId, roundNumber, roundMetaPath);
35031
35620
  }
35032
- const finalPath = join13(roundDir, "final.md");
35621
+ const finalPath = join14(roundDir, "final.md");
35033
35622
  if (existsSync10(finalPath)) {
35034
35623
  this.processFinalMd(sessionId, roundNumber, finalPath);
35035
35624
  }
35036
- const finalHumanPath = join13(roundDir, "final-human.md");
35625
+ const finalHumanPath = join14(roundDir, "final-human.md");
35037
35626
  if (existsSync10(finalHumanPath)) {
35038
35627
  this.processGenericArtifact(sessionId, "final-human", finalHumanPath, roundNumber);
35039
35628
  }
35040
- const discoursePath = join13(roundDir, "discourse.md");
35629
+ const discoursePath = join14(roundDir, "discourse.md");
35041
35630
  if (existsSync10(discoursePath)) {
35042
35631
  this.processGenericArtifact(sessionId, "discourse", discoursePath, roundNumber);
35043
35632
  }
35044
35633
  }
35045
35634
  }
35046
- const mapDir = join13(sessionDir, "map", "runs");
35635
+ const mapDir = join14(sessionDir, "map", "runs");
35047
35636
  if (existsSync10(mapDir)) {
35048
35637
  const runs = readdirSync(mapDir, { withFileTypes: true });
35049
35638
  for (const runEntry of runs) {
@@ -35051,12 +35640,12 @@ var FilesystemSync = class {
35051
35640
  const runMatch = runEntry.name.match(/^run-(\d+)$/);
35052
35641
  if (!runMatch) continue;
35053
35642
  const runNumber = parseInt(runMatch[1] ?? "0", 10);
35054
- const runDir = join13(mapDir, runEntry.name);
35055
- const mapMetaPath = join13(runDir, "map-meta.json");
35643
+ const runDir = join14(mapDir, runEntry.name);
35644
+ const mapMetaPath = join14(runDir, "map-meta.json");
35056
35645
  if (existsSync10(mapMetaPath)) {
35057
35646
  this.processMapMeta(sessionId, runNumber, mapMetaPath);
35058
35647
  }
35059
- const mapPath = join13(runDir, "map.md");
35648
+ const mapPath = join14(runDir, "map.md");
35060
35649
  if (existsSync10(mapPath)) {
35061
35650
  this.processMapMd(sessionId, runNumber, mapPath);
35062
35651
  }
@@ -35066,7 +35655,7 @@ var FilesystemSync = class {
35066
35655
  ["requirements-mapping.md", "requirements-mapping"]
35067
35656
  ];
35068
35657
  for (const [fileName, artifactType] of mapArtifacts) {
35069
- const filePath = join13(runDir, fileName);
35658
+ const filePath = join14(runDir, fileName);
35070
35659
  if (existsSync10(filePath)) {
35071
35660
  this.processGenericArtifact(sessionId, artifactType, filePath, void 0, runNumber);
35072
35661
  }
@@ -35078,7 +35667,7 @@ var FilesystemSync = class {
35078
35667
  ["discovered-standards.md", "discovered-standards"]
35079
35668
  ];
35080
35669
  for (const [fileName, artifactType] of sessionArtifacts) {
35081
- const filePath = join13(sessionDir, fileName);
35670
+ const filePath = join14(sessionDir, fileName);
35082
35671
  if (existsSync10(filePath)) {
35083
35672
  this.processGenericArtifact(sessionId, artifactType, filePath);
35084
35673
  }
@@ -35088,16 +35677,16 @@ var FilesystemSync = class {
35088
35677
  ensureSessionRow(sessionId, sessionDir) {
35089
35678
  const branchMatch = sessionId.match(/^\d{4}-\d{2}-\d{2}-(.+)$/);
35090
35679
  const branch = branchMatch?.[1] ?? "unknown";
35091
- const hasRoundsDir = existsSync10(join13(sessionDir, "rounds"));
35092
- const hasMapDir = existsSync10(join13(sessionDir, "map"));
35680
+ const hasRoundsDir = existsSync10(join14(sessionDir, "rounds"));
35681
+ const hasMapDir = existsSync10(join14(sessionDir, "map"));
35093
35682
  const workflowType = hasMapDir && !hasRoundsDir ? "map" : "review";
35094
35683
  let currentRound = 1;
35095
35684
  if (hasRoundsDir) {
35096
- const roundDirs = readdirSync(join13(sessionDir, "rounds")).filter((d) => d.match(/^round-\d+$/));
35685
+ const roundDirs = readdirSync(join14(sessionDir, "rounds")).filter((d) => d.match(/^round-\d+$/));
35097
35686
  currentRound = Math.max(1, roundDirs.length);
35098
35687
  }
35099
35688
  let currentMapRun = 1;
35100
- const mapRunsDir = join13(sessionDir, "map", "runs");
35689
+ const mapRunsDir = join14(sessionDir, "map", "runs");
35101
35690
  if (existsSync10(mapRunsDir)) {
35102
35691
  const runDirs = readdirSync(mapRunsDir).filter((d) => d.match(/^run-\d+$/));
35103
35692
  currentMapRun = Math.max(1, runDirs.length);
@@ -35106,40 +35695,40 @@ var FilesystemSync = class {
35106
35695
  let phaseNumber = 1;
35107
35696
  let status = "closed";
35108
35697
  if (workflowType === "review" && hasRoundsDir) {
35109
- const roundDir = join13(sessionDir, "rounds", `round-${currentRound}`);
35110
- if (existsSync10(join13(roundDir, "final.md"))) {
35698
+ const roundDir = join14(sessionDir, "rounds", `round-${currentRound}`);
35699
+ if (existsSync10(join14(roundDir, "final.md"))) {
35111
35700
  phase = "complete";
35112
35701
  phaseNumber = 8;
35113
35702
  status = "closed";
35114
- } else if (existsSync10(join13(roundDir, "discourse.md"))) {
35703
+ } else if (existsSync10(join14(roundDir, "discourse.md"))) {
35115
35704
  phase = "synthesis";
35116
35705
  phaseNumber = 7;
35117
- } else if (existsSync10(join13(roundDir, "reviews")) && readdirSync(join13(roundDir, "reviews")).filter((f) => f.endsWith(".md")).length > 0) {
35706
+ } else if (existsSync10(join14(roundDir, "reviews")) && readdirSync(join14(roundDir, "reviews")).filter((f) => f.endsWith(".md")).length > 0) {
35118
35707
  phase = "reviews";
35119
35708
  phaseNumber = 4;
35120
- } else if (existsSync10(join13(sessionDir, "context.md"))) {
35709
+ } else if (existsSync10(join14(sessionDir, "context.md"))) {
35121
35710
  phase = "analysis";
35122
35711
  phaseNumber = 3;
35123
- } else if (existsSync10(join13(sessionDir, "discovered-standards.md"))) {
35712
+ } else if (existsSync10(join14(sessionDir, "discovered-standards.md"))) {
35124
35713
  phase = "change-context";
35125
35714
  phaseNumber = 2;
35126
35715
  }
35127
35716
  } else if (workflowType === "map" && hasMapDir) {
35128
- const runDir = join13(mapRunsDir, `run-${currentMapRun}`);
35129
- if (existsSync10(join13(runDir, "map.md"))) {
35717
+ const runDir = join14(mapRunsDir, `run-${currentMapRun}`);
35718
+ if (existsSync10(join14(runDir, "map.md"))) {
35130
35719
  phase = "complete";
35131
35720
  phaseNumber = 6;
35132
35721
  status = "closed";
35133
- } else if (existsSync10(join13(runDir, "requirements-mapping.md"))) {
35722
+ } else if (existsSync10(join14(runDir, "requirements-mapping.md"))) {
35134
35723
  phase = "synthesis";
35135
35724
  phaseNumber = 5;
35136
- } else if (existsSync10(join13(runDir, "flow-analysis.md"))) {
35725
+ } else if (existsSync10(join14(runDir, "flow-analysis.md"))) {
35137
35726
  phase = "requirements-mapping";
35138
35727
  phaseNumber = 4;
35139
- } else if (existsSync10(join13(runDir, "topology.md"))) {
35728
+ } else if (existsSync10(join14(runDir, "topology.md"))) {
35140
35729
  phase = "flow-analysis";
35141
35730
  phaseNumber = 3;
35142
- } else if (existsSync10(join13(sessionDir, "discovered-standards.md"))) {
35731
+ } else if (existsSync10(join14(sessionDir, "discovered-standards.md"))) {
35143
35732
  phase = "topology";
35144
35733
  phaseNumber = 2;
35145
35734
  }
@@ -35153,14 +35742,40 @@ var FilesystemSync = class {
35153
35742
  );
35154
35743
  } else {
35155
35744
  if (!this.hasArtifacts(sessionDir)) return;
35156
- this.db.run(
35157
- `INSERT INTO sessions (id, branch, workflow_type, current_phase, phase_number, current_round, current_map_run, session_dir, status)
35158
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
35159
- [sessionId, branch, workflowType, phase, phaseNumber, currentRound, currentMapRun, sessionDir, status]
35160
- );
35745
+ this.db.transaction(() => {
35746
+ insertSession(this.db, {
35747
+ id: sessionId,
35748
+ branch,
35749
+ workflow_type: workflowType,
35750
+ current_phase: phase,
35751
+ phase_number: phaseNumber,
35752
+ current_round: currentRound,
35753
+ current_map_run: currentMapRun,
35754
+ session_dir: sessionDir
35755
+ });
35756
+ insertEvent(this.db, {
35757
+ session_id: sessionId,
35758
+ event_type: "session_created",
35759
+ phase,
35760
+ phase_number: 1,
35761
+ round: 1
35762
+ });
35763
+ });
35764
+ if (status === "closed") {
35765
+ commitReasonClose(
35766
+ this.db,
35767
+ sessionId,
35768
+ {
35769
+ event_type: "session_synced",
35770
+ phase,
35771
+ phase_number: phaseNumber,
35772
+ metadata: JSON.stringify({ source: "filesystem_backfill" })
35773
+ },
35774
+ { status: "closed", current_phase: phase, phase_number: phaseNumber }
35775
+ );
35776
+ }
35161
35777
  this.io?.emit("session:created", { id: sessionId, branch, workflow_type: workflowType, status, current_phase: phase });
35162
35778
  }
35163
- this.onSync?.();
35164
35779
  }
35165
35780
  // ── Artifact Check ──
35166
35781
  /** Returns true if the directory contains at least one .md or .json file (recursively). */
@@ -35168,7 +35783,7 @@ var FilesystemSync = class {
35168
35783
  try {
35169
35784
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
35170
35785
  if (entry.isDirectory()) {
35171
- if (this.hasArtifacts(join13(dir, entry.name))) return true;
35786
+ if (this.hasArtifacts(join14(dir, entry.name))) return true;
35172
35787
  } else if (/\.(md|json)$/.test(entry.name)) {
35173
35788
  return true;
35174
35789
  }
@@ -35181,7 +35796,7 @@ var FilesystemSync = class {
35181
35796
  shouldSkip(filePath, existingParsedAt) {
35182
35797
  if (!existingParsedAt) return false;
35183
35798
  try {
35184
- const mtime = statSync(filePath).mtime;
35799
+ const mtime = statSync2(filePath).mtime;
35185
35800
  const parsedAt = new Date(existingParsedAt);
35186
35801
  return mtime <= parsedAt;
35187
35802
  } catch {
@@ -35218,12 +35833,12 @@ var FilesystemSync = class {
35218
35833
  );
35219
35834
  if (existingRun && this.shouldSkip(filePath, existingRun["parsed_at"] ?? null)) return;
35220
35835
  if (existingRun?.["source"] === "orchestrator") {
35221
- const content2 = readFileSync10(filePath, "utf-8");
35836
+ const content2 = readFileSync8(filePath, "utf-8");
35222
35837
  const action2 = this.upsertMarkdownArtifact(sessionId, "map", filePath, content2, void 0);
35223
35838
  this.emitArtifactEvent(action2, { sessionId, artifactType: "map", filePath });
35224
35839
  return;
35225
35840
  }
35226
- const content = readFileSync10(filePath, "utf-8");
35841
+ const content = readFileSync8(filePath, "utf-8");
35227
35842
  const parsed = parseMapMd(content);
35228
35843
  this.db.run(
35229
35844
  `INSERT OR REPLACE INTO map_runs (session_id, run_number, file_count, map_md_path, parsed_at, source)
@@ -35309,10 +35924,16 @@ var FilesystemSync = class {
35309
35924
  [sessionId]
35310
35925
  );
35311
35926
  if (session && session["workflow_type"] === "map" && (session["current_phase"] !== "complete" || session["phase_number"] < 6)) {
35312
- this.db.run(
35313
- `UPDATE sessions SET current_phase = 'complete', phase_number = 6, status = 'closed', updated_at = datetime('now')
35314
- WHERE id = ?`,
35315
- [sessionId]
35927
+ commitReasonClose(
35928
+ this.db,
35929
+ sessionId,
35930
+ {
35931
+ event_type: "session_synced",
35932
+ phase: "complete",
35933
+ phase_number: 6,
35934
+ metadata: JSON.stringify({ source: "filesystem_backfill" })
35935
+ },
35936
+ { status: "closed", current_phase: "complete", phase_number: 6 }
35316
35937
  );
35317
35938
  this.io?.emit("session:updated", {
35318
35939
  id: sessionId,
@@ -35343,7 +35964,7 @@ var FilesystemSync = class {
35343
35964
  const roundId = roundRow?.["id"];
35344
35965
  if (!roundId) return;
35345
35966
  if (roundRow?.["source"] === "orchestrator") {
35346
- const content2 = readFileSync10(filePath, "utf-8");
35967
+ const content2 = readFileSync8(filePath, "utf-8");
35347
35968
  const action2 = this.upsertMarkdownArtifact(sessionId, "reviewer-output", filePath, content2, roundNumber);
35348
35969
  this.emitArtifactEvent(action2, {
35349
35970
  sessionId,
@@ -35362,7 +35983,7 @@ var FilesystemSync = class {
35362
35983
  [roundId, reviewerType, instanceNumber]
35363
35984
  );
35364
35985
  if (existingOutput && this.shouldSkip(filePath, existingOutput["parsed_at"] ?? null)) return;
35365
- const content = readFileSync10(filePath, "utf-8");
35986
+ const content = readFileSync8(filePath, "utf-8");
35366
35987
  const parsed = parseReviewerOutput(content);
35367
35988
  this.db.run(
35368
35989
  `INSERT OR REPLACE INTO reviewer_outputs (round_id, reviewer_type, instance_number, file_path, finding_count, parsed_at)
@@ -35450,7 +36071,7 @@ var FilesystemSync = class {
35450
36071
  if (existingRound?.["source"] === "orchestrator" && this.shouldSkip(filePath, existingRound["parsed_at"] ?? null)) return;
35451
36072
  let raw;
35452
36073
  try {
35453
- raw = JSON.parse(readFileSync10(filePath, "utf-8"));
36074
+ raw = JSON.parse(readFileSync8(filePath, "utf-8"));
35454
36075
  } catch {
35455
36076
  console.error(`[FilesystemSync] Failed to parse ${filePath}`);
35456
36077
  return;
@@ -35501,7 +36122,7 @@ var FilesystemSync = class {
35501
36122
  const reviewerType = reviewer.type ?? "unknown";
35502
36123
  const instanceNumber = reviewer.instance ?? 1;
35503
36124
  const findings = reviewer.findings ?? [];
35504
- const reviewerMdPath = join13(roundDir, "reviews", `${reviewerType}-${instanceNumber}.md`);
36125
+ const reviewerMdPath = join14(roundDir, "reviews", `${reviewerType}-${instanceNumber}.md`);
35505
36126
  this.db.run(
35506
36127
  `INSERT OR REPLACE INTO reviewer_outputs (round_id, reviewer_type, instance_number, file_path, finding_count, parsed_at)
35507
36128
  VALUES (?, ?, ?, ?, ?, ?)`,
@@ -35599,7 +36220,7 @@ var FilesystemSync = class {
35599
36220
  if (existingRun?.["source"] === "orchestrator" && this.shouldSkip(filePath, existingRun["parsed_at"] ?? null)) return;
35600
36221
  let raw;
35601
36222
  try {
35602
- raw = JSON.parse(readFileSync10(filePath, "utf-8"));
36223
+ raw = JSON.parse(readFileSync8(filePath, "utf-8"));
35603
36224
  } catch {
35604
36225
  console.error(`[FilesystemSync] Failed to parse ${filePath}`);
35605
36226
  return;
@@ -35727,7 +36348,7 @@ var FilesystemSync = class {
35727
36348
  );
35728
36349
  const isOrchestratorSource = existingRound?.["source"] === "orchestrator";
35729
36350
  if (!isOrchestratorSource && existingRound && this.shouldSkip(filePath, existingRound["parsed_at"] ?? null)) return;
35730
- const content = readFileSync10(filePath, "utf-8");
36351
+ const content = readFileSync8(filePath, "utf-8");
35731
36352
  if (isOrchestratorSource) {
35732
36353
  this.db.run(
35733
36354
  `UPDATE review_rounds SET final_md_path = ?, parsed_at = ?
@@ -35771,10 +36392,16 @@ var FilesystemSync = class {
35771
36392
  [sessionId]
35772
36393
  );
35773
36394
  if (session && (session["current_phase"] !== "complete" || session["phase_number"] < 8)) {
35774
- this.db.run(
35775
- `UPDATE sessions SET current_phase = 'complete', phase_number = 8, status = 'closed', updated_at = datetime('now')
35776
- WHERE id = ?`,
35777
- [sessionId]
36395
+ commitReasonClose(
36396
+ this.db,
36397
+ sessionId,
36398
+ {
36399
+ event_type: "session_synced",
36400
+ phase: "complete",
36401
+ phase_number: 8,
36402
+ metadata: JSON.stringify({ source: "filesystem_backfill" })
36403
+ },
36404
+ { status: "closed", current_phase: "complete", phase_number: 8 }
35778
36405
  );
35779
36406
  this.io?.emit("session:updated", {
35780
36407
  id: sessionId,
@@ -35800,7 +36427,7 @@ var FilesystemSync = class {
35800
36427
  [sessionId, artifactType, relPath]
35801
36428
  );
35802
36429
  if (existing && this.shouldSkip(filePath, existing["parsed_at"] ?? null)) return;
35803
- const content = readFileSync10(filePath, "utf-8");
36430
+ const content = readFileSync8(filePath, "utf-8");
35804
36431
  const action = this.upsertMarkdownArtifact(sessionId, artifactType, filePath, content, roundNumber);
35805
36432
  this.emitArtifactEvent(action, {
35806
36433
  sessionId,
@@ -35847,7 +36474,6 @@ var FilesystemSync = class {
35847
36474
  this.debounceTimers.delete(filePath);
35848
36475
  try {
35849
36476
  this.processChangedFile(filePath);
35850
- this.onSync?.();
35851
36477
  } catch (err) {
35852
36478
  console.error(`[FilesystemSync] Error processing ${filePath}:`, err);
35853
36479
  }
@@ -35859,7 +36485,7 @@ var FilesystemSync = class {
35859
36485
  const parts = relFromSessions.split("/");
35860
36486
  const sessionId = parts[0];
35861
36487
  if (!sessionId) return;
35862
- const sessionDir = join13(this.sessionsDir, sessionId);
36488
+ const sessionDir = join14(this.sessionsDir, sessionId);
35863
36489
  this.ensureSessionRow(sessionId, sessionDir);
35864
36490
  const fileName = basename2(filePath);
35865
36491
  const reviewerMatch = relFromSessions.match(/rounds\/round-(\d+)\/reviews\/(.+\.md)$/);
@@ -35931,53 +36557,52 @@ var FilesystemSync = class {
35931
36557
  };
35932
36558
 
35933
36559
  // src/server/services/db-sync-watcher.ts
35934
- import { existsSync as existsSync11, readFileSync as readFileSync11, statSync as statSync2 } from "node:fs";
36560
+ import { existsSync as existsSync11 } from "node:fs";
35935
36561
  import { dirname as dirname10, basename as basename3 } from "node:path";
35936
36562
  import { watch as watch3 } from "chokidar";
35937
- import initSqlJs3 from "sql.js";
35938
36563
  function col(row, key) {
35939
36564
  return row[key] ?? null;
35940
36565
  }
35941
- var SQLITE_MAGIC = Buffer.from("SQLite format 3\0", "utf-8");
35942
- function isValidSqliteHeader(buf) {
35943
- if (buf.length < SQLITE_MAGIC.length) return false;
35944
- return buf.subarray(0, SQLITE_MAGIC.length).equals(SQLITE_MAGIC);
35945
- }
35946
36566
  var DbSyncWatcher = class {
35947
- constructor(db, dbFilePath, io2, onSync, onSessionInserted) {
36567
+ constructor(db, dbFilePath, io2, onSessionInserted) {
35948
36568
  this.db = db;
35949
36569
  this.dbFilePath = dbFilePath;
35950
36570
  this.io = io2;
35951
- this.onSync = onSync;
35952
36571
  this.onSessionInserted = onSessionInserted;
35953
36572
  }
35954
36573
  watcher = null;
35955
36574
  debounceTimer = null;
35956
- lastMtime = 0;
35957
- wasmBinary = null;
35958
- SQL = null;
36575
+ // Snapshots of last-emitted state, so we only emit on genuine changes.
36576
+ seenSessions = /* @__PURE__ */ new Map();
36577
+ maxEventId = 0;
36578
+ seenCommandRows = /* @__PURE__ */ new Map();
35959
36579
  /**
35960
- * Initialize the WASM runtime (called once at startup).
36580
+ * Prime snapshots from the current database so existing state is not
36581
+ * re-emitted on first watch tick. (No WASM runtime to initialise under
36582
+ * the native engine.)
35961
36583
  */
35962
36584
  async init() {
35963
- const wasmBuffer = readFileSync11(locateWasm());
35964
- this.wasmBinary = wasmBuffer.buffer.slice(
35965
- wasmBuffer.byteOffset,
35966
- wasmBuffer.byteOffset + wasmBuffer.byteLength
35967
- );
35968
- this.SQL = await initSqlJs3({ wasmBinary: this.wasmBinary });
36585
+ this.primeSnapshots();
35969
36586
  }
35970
- /**
35971
- * Start watching the DB file for external changes.
35972
- */
36587
+ primeSnapshots() {
36588
+ for (const row of this.readSessions()) {
36589
+ const id = col(row, "id");
36590
+ if (id) this.seenSessions.set(id, sessionFingerprint(row));
36591
+ }
36592
+ const maxResult = this.db.exec("SELECT MAX(id) FROM orchestration_events");
36593
+ const maxVal = maxResult[0]?.values[0]?.[0];
36594
+ this.maxEventId = typeof maxVal === "number" ? maxVal : 0;
36595
+ for (const row of this.readCommandRows()) {
36596
+ const id = col(row, "id");
36597
+ if (id != null) this.seenCommandRows.set(id, commandFingerprint(row));
36598
+ }
36599
+ }
36600
+ /** Start watching the DB file (and its WAL sidecar) for external writes. */
35973
36601
  startWatching() {
35974
36602
  if (!existsSync11(this.dbFilePath)) return;
35975
- try {
35976
- this.lastMtime = statSync2(this.dbFilePath).mtimeMs;
35977
- } catch {
35978
- }
35979
36603
  const watchDir = dirname10(this.dbFilePath);
35980
- const watchedFile = basename3(this.dbFilePath);
36604
+ const dbFile = basename3(this.dbFilePath);
36605
+ const walFile = `${dbFile}-wal`;
35981
36606
  this.watcher = watch3(watchDir, {
35982
36607
  persistent: true,
35983
36608
  ignoreInitial: true,
@@ -35986,15 +36611,13 @@ var DbSyncWatcher = class {
35986
36611
  interval: 200
35987
36612
  });
35988
36613
  const onAnyEvent = (path2) => {
35989
- if (basename3(path2) === watchedFile) this.debouncedSync();
36614
+ const name = basename3(path2);
36615
+ if (name === dbFile || name === walFile) this.debouncedSync();
35990
36616
  };
35991
36617
  this.watcher.on("change", onAnyEvent);
35992
36618
  this.watcher.on("add", onAnyEvent);
35993
36619
  this.watcher.on("unlink", onAnyEvent);
35994
36620
  }
35995
- /**
35996
- * Stop watching.
35997
- */
35998
36621
  stopWatching() {
35999
36622
  if (this.debounceTimer) {
36000
36623
  clearTimeout(this.debounceTimer);
@@ -36005,9 +36628,6 @@ var DbSyncWatcher = class {
36005
36628
  this.watcher = null;
36006
36629
  }
36007
36630
  }
36008
- /**
36009
- * Debounce sync to avoid rapid reloads during multi-statement writes.
36010
- */
36011
36631
  debouncedSync() {
36012
36632
  if (this.debounceTimer) clearTimeout(this.debounceTimer);
36013
36633
  this.debounceTimer = setTimeout(() => {
@@ -36015,85 +36635,39 @@ var DbSyncWatcher = class {
36015
36635
  }, 300);
36016
36636
  }
36017
36637
  /**
36018
- * Read the on-disk DB and sync CLI-owned tables into the in-memory DB.
36019
- *
36020
- * Only syncs `sessions` and `orchestration_events` these are the tables
36021
- * the CLI writes to via `ocr state` commands. All other tables are
36022
- * dashboard-owned and untouched.
36023
- *
36024
- * Public for direct use; also called automatically via registered save hooks
36025
- * to avoid overwriting CLI changes.
36026
- *
36027
- * Resilience:
36028
- * 1. Validates the SQLite magic header before handing the buffer to
36029
- * sql.js. If the header is missing — e.g. we caught the file
36030
- * mid-atomic-rename and read the temp file or a zero-byte stub —
36031
- * we skip the load and wait for the next watch event. Without this
36032
- * guard, a torn read corrupts sql.js and surfaces as an opaque WASM
36033
- * `memory access out of bounds` exception.
36034
- * 2. Only advances `lastMtime` AFTER a successful load. A failed load
36035
- * leaves the watermark untouched so the next change event retries.
36638
+ * Rescan the live database and emit notifications for anything that
36639
+ * changed since the last scan. Named `syncFromDisk` for call-site
36640
+ * compatibility; under the native engine it performs no merge writes —
36641
+ * it diffs the live connection against cached snapshots and emits.
36036
36642
  */
36037
36643
  syncFromDisk() {
36038
- if (!this.SQL || !existsSync11(this.dbFilePath)) return;
36039
- let currentMtime;
36040
36644
  try {
36041
- currentMtime = statSync2(this.dbFilePath).mtimeMs;
36042
- } catch {
36043
- return;
36044
- }
36045
- if (currentMtime <= this.lastMtime) return;
36046
- let diskDb = null;
36047
- try {
36048
- const fileBuffer = readFileSync11(this.dbFilePath);
36049
- if (!isValidSqliteHeader(fileBuffer)) {
36050
- return;
36051
- }
36052
- diskDb = new this.SQL.Database(fileBuffer);
36053
- this.syncSessions(diskDb);
36054
- this.syncEvents(diskDb);
36055
- this.syncAgentSessions(diskDb);
36056
- this.lastMtime = currentMtime;
36057
- this.onSync?.();
36645
+ this.detectSessionChanges();
36646
+ this.detectNewEvents();
36647
+ this.detectCommandChanges();
36058
36648
  } catch (err) {
36059
- console.error("[DbSyncWatcher] Error syncing from disk:", err);
36060
- } finally {
36061
- diskDb?.close();
36649
+ console.error("[DbSyncWatcher] Error scanning for changes:", err);
36062
36650
  }
36063
36651
  }
36064
- /**
36065
- * Sync the `sessions` table from disk → in-memory.
36066
- * The CLI is authoritative for: current_phase, phase_number, status,
36067
- * current_round, current_map_run, workflow_type, updated_at.
36068
- */
36069
- syncSessions(diskDb) {
36070
- const diskSessions = resultToRows(diskDb.exec("SELECT * FROM sessions"));
36071
- for (const row of diskSessions) {
36652
+ readSessions() {
36653
+ return resultToRows(this.db.exec("SELECT * FROM sessions"));
36654
+ }
36655
+ readCommandRows() {
36656
+ return resultToRows(
36657
+ this.db.exec(
36658
+ `SELECT id, last_heartbeat_at, finished_at, exit_code, workflow_id, vendor_session_id
36659
+ FROM command_executions`
36660
+ )
36661
+ );
36662
+ }
36663
+ detectSessionChanges() {
36664
+ for (const row of this.readSessions()) {
36072
36665
  const id = col(row, "id");
36073
36666
  if (!id) continue;
36074
- const memResult = this.db.exec(
36075
- "SELECT current_phase, phase_number, status, updated_at FROM sessions WHERE id = ?",
36076
- [id]
36077
- );
36078
- const memRows = resultToRows(memResult);
36079
- if (memRows.length === 0) {
36080
- this.db.run(
36081
- `INSERT INTO sessions (id, branch, status, workflow_type, current_phase, phase_number, current_round, current_map_run, started_at, updated_at, session_dir)
36082
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
36083
- [
36084
- col(row, "id"),
36085
- col(row, "branch"),
36086
- col(row, "status"),
36087
- col(row, "workflow_type"),
36088
- col(row, "current_phase"),
36089
- col(row, "phase_number"),
36090
- col(row, "current_round"),
36091
- col(row, "current_map_run"),
36092
- col(row, "started_at"),
36093
- col(row, "updated_at"),
36094
- col(row, "session_dir")
36095
- ]
36096
- );
36667
+ const fp = sessionFingerprint(row);
36668
+ const prev = this.seenSessions.get(id);
36669
+ if (prev === void 0) {
36670
+ this.seenSessions.set(id, fp);
36097
36671
  this.io.emit("session:created", {
36098
36672
  id,
36099
36673
  branch: col(row, "branch"),
@@ -36110,81 +36684,33 @@ var DbSyncWatcher = class {
36110
36684
  } catch (err) {
36111
36685
  console.error("[DbSyncWatcher] onSessionInserted hook failed:", err);
36112
36686
  }
36113
- } else {
36114
- const mem = memRows[0];
36115
- const diskPhase = col(row, "phase_number");
36116
- const memPhase = col(mem, "phase_number");
36117
- const diskStatus = col(row, "status");
36118
- const memStatus = col(mem, "status");
36119
- const diskCurrent = col(row, "current_phase");
36120
- const memCurrent = col(mem, "current_phase");
36121
- if (diskPhase !== memPhase || diskStatus !== memStatus || diskCurrent !== memCurrent) {
36122
- this.db.run(
36123
- `UPDATE sessions
36124
- SET current_phase = ?, phase_number = ?, status = ?,
36125
- current_round = ?, current_map_run = ?, workflow_type = ?,
36126
- updated_at = ?
36127
- WHERE id = ?`,
36128
- [
36129
- col(row, "current_phase"),
36130
- col(row, "phase_number"),
36131
- col(row, "status"),
36132
- col(row, "current_round"),
36133
- col(row, "current_map_run"),
36134
- col(row, "workflow_type"),
36135
- col(row, "updated_at"),
36136
- id
36137
- ]
36138
- );
36139
- this.io.emit("session:updated", {
36140
- id,
36141
- status: col(row, "status"),
36142
- current_phase: col(row, "current_phase"),
36143
- phase_number: col(row, "phase_number")
36144
- });
36145
- }
36687
+ } else if (prev !== fp) {
36688
+ this.seenSessions.set(id, fp);
36689
+ this.io.emit("session:updated", {
36690
+ id,
36691
+ status: col(row, "status"),
36692
+ current_phase: col(row, "current_phase"),
36693
+ phase_number: col(row, "phase_number")
36694
+ });
36146
36695
  }
36147
36696
  }
36148
36697
  }
36149
- /**
36150
- * Sync the `orchestration_events` table from disk → in-memory.
36151
- * Events are append-only, so we INSERT any that don't exist yet.
36152
- */
36153
- syncEvents(diskDb) {
36154
- const diskEvents = resultToRows(
36155
- diskDb.exec("SELECT * FROM orchestration_events ORDER BY id ASC")
36698
+ detectNewEvents() {
36699
+ const rows = resultToRows(
36700
+ this.db.exec(
36701
+ "SELECT * FROM orchestration_events WHERE id > ? ORDER BY id ASC",
36702
+ [this.maxEventId]
36703
+ )
36156
36704
  );
36157
- const newEvents = [];
36705
+ if (rows.length === 0) return;
36158
36706
  const affectedSessions = /* @__PURE__ */ new Set();
36159
- for (const row of diskEvents) {
36707
+ for (const row of rows) {
36160
36708
  const eventId = col(row, "id");
36161
36709
  const sessionId = col(row, "session_id");
36162
- const existing = this.db.exec(
36163
- "SELECT id FROM orchestration_events WHERE id = ?",
36164
- [eventId]
36165
- );
36166
- if (existing.length > 0 && existing[0]?.values.length !== 0) continue;
36167
- this.db.run(
36168
- `INSERT OR IGNORE INTO orchestration_events (id, session_id, event_type, phase, phase_number, round, metadata, created_at)
36169
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
36170
- [
36171
- eventId,
36172
- sessionId,
36173
- col(row, "event_type"),
36174
- col(row, "phase"),
36175
- col(row, "phase_number"),
36176
- col(row, "round"),
36177
- col(row, "metadata"),
36178
- col(row, "created_at")
36179
- ]
36180
- );
36181
- newEvents.push(row);
36182
- affectedSessions.add(sessionId);
36183
- }
36184
- for (const row of newEvents) {
36185
36710
  const eventType = col(row, "event_type");
36186
- const sessionId = col(row, "session_id");
36187
36711
  const metadataStr = col(row, "metadata");
36712
+ if (eventId > this.maxEventId) this.maxEventId = eventId;
36713
+ if (sessionId) affectedSessions.add(sessionId);
36188
36714
  if (eventType === "round_completed") {
36189
36715
  const roundNumber = col(row, "round");
36190
36716
  if (sessionId && roundNumber && metadataStr) {
@@ -36197,98 +36723,33 @@ var DbSyncWatcher = class {
36197
36723
  }
36198
36724
  }
36199
36725
  }
36200
- if (newEvents.length > 0) {
36201
- for (const sessionId of affectedSessions) {
36202
- this.io.to(`session:${sessionId}`).emit("session:events", { session_id: sessionId });
36203
- }
36726
+ for (const sessionId of affectedSessions) {
36727
+ this.io.to(`session:${sessionId}`).emit("session:events", { session_id: sessionId });
36204
36728
  }
36205
36729
  }
36206
- /**
36207
- * Sync the `command_executions` table from disk → in-memory.
36208
- *
36209
- * Migration v11 unified `agent_sessions` into `command_executions`. The
36210
- * journal fields (`vendor`, `vendor_session_id`, `persona`, `resolved_model`,
36211
- * `last_heartbeat_at`, `notes`, …) live on the same table. Rows the CLI
36212
- * writes via `ocr session start-instance` / `bind-vendor-id` / `beat` /
36213
- * `end-instance` flow through atomic file rename, so the disk file is the
36214
- * source of truth.
36215
- *
36216
- * Strategy: full mirror via `INSERT OR REPLACE` keyed on the integer
36217
- * primary key. The table is small enough that copying every row each
36218
- * sync is cheap. Emits `agent_session:updated` with affected workflow ids
36219
- * so the dashboard's query cache invalidates selectively.
36220
- */
36221
- syncAgentSessions(diskDb) {
36222
- const diskRows = resultToRows(
36223
- diskDb.exec("SELECT * FROM command_executions")
36224
- );
36225
- if (diskRows.length === 0) return;
36730
+ detectCommandChanges() {
36226
36731
  const affectedWorkflows = /* @__PURE__ */ new Set();
36227
- let changed = 0;
36228
- for (const row of diskRows) {
36732
+ for (const row of this.readCommandRows()) {
36229
36733
  const id = col(row, "id");
36734
+ if (id == null) continue;
36735
+ const fp = commandFingerprint(row);
36736
+ if (this.seenCommandRows.get(id) === fp) continue;
36737
+ this.seenCommandRows.set(id, fp);
36230
36738
  const workflowId = col(row, "workflow_id");
36231
- const vendorSessionId = col(row, "vendor_session_id");
36232
36739
  const heartbeat = col(row, "last_heartbeat_at");
36233
- const finishedAt = col(row, "finished_at");
36234
- const exitCode = col(row, "exit_code");
36235
- const existing = this.db.exec(
36236
- `SELECT last_heartbeat_at, finished_at, exit_code,
36237
- workflow_id, vendor_session_id
36238
- FROM command_executions WHERE id = ?`,
36239
- [id]
36240
- );
36241
- const existingRow = existing[0]?.values?.[0];
36242
- const sameHeartbeat = (existingRow?.[0] ?? null) === heartbeat;
36243
- const sameFinished = (existingRow?.[1] ?? null) === finishedAt;
36244
- const sameExit = (existingRow?.[2] ?? null) === exitCode;
36245
- const sameWorkflow = (existingRow?.[3] ?? null) === workflowId;
36246
- const sameVendorSession = (existingRow?.[4] ?? null) === vendorSessionId;
36247
- if (existingRow && sameHeartbeat && sameFinished && sameExit && sameWorkflow && sameVendorSession) continue;
36248
- this.db.run(
36249
- `INSERT OR REPLACE INTO command_executions
36250
- (id, uid, command, args, exit_code, started_at, finished_at, output,
36251
- pid, is_detached, workflow_id, parent_id, vendor, vendor_session_id,
36252
- persona, instance_index, name, resolved_model, last_heartbeat_at, notes)
36253
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
36254
- [
36255
- id,
36256
- col(row, "uid"),
36257
- col(row, "command"),
36258
- col(row, "args"),
36259
- exitCode,
36260
- col(row, "started_at"),
36261
- finishedAt,
36262
- col(row, "output"),
36263
- col(row, "pid"),
36264
- col(row, "is_detached"),
36265
- workflowId,
36266
- col(row, "parent_id"),
36267
- col(row, "vendor"),
36268
- col(row, "vendor_session_id"),
36269
- col(row, "persona"),
36270
- col(row, "instance_index"),
36271
- col(row, "name"),
36272
- col(row, "resolved_model"),
36273
- heartbeat,
36274
- col(row, "notes")
36275
- ]
36276
- );
36277
36740
  if (heartbeat !== null && workflowId) {
36278
36741
  affectedWorkflows.add(workflowId);
36279
36742
  }
36280
- changed++;
36281
36743
  }
36282
- if (changed > 0 && affectedWorkflows.size > 0) {
36744
+ if (affectedWorkflows.size > 0) {
36283
36745
  this.io.emit("agent_session:updated", {
36284
36746
  workflow_ids: Array.from(affectedWorkflows)
36285
36747
  });
36286
36748
  }
36287
36749
  }
36288
36750
  /**
36289
- * Process a `round_completed` orchestration event.
36290
- * Upserts review_rounds with orchestrator data for real-time dashboard updates.
36291
- * Idempotent — skips if round already has source='orchestrator'.
36751
+ * Project a `round_completed` event into `review_rounds` (orchestrator
36752
+ * source latch) and emit a room-scoped `round:updated`. Idempotent.
36292
36753
  */
36293
36754
  processRoundCompletedEvent(sessionId, roundNumber, metadataStr) {
36294
36755
  let metadata;
@@ -36306,8 +36767,7 @@ var DbSyncWatcher = class {
36306
36767
  return;
36307
36768
  }
36308
36769
  this.db.run(
36309
- `INSERT OR IGNORE INTO review_rounds (session_id, round_number)
36310
- VALUES (?, ?)`,
36770
+ `INSERT OR IGNORE INTO review_rounds (session_id, round_number) VALUES (?, ?)`,
36311
36771
  [sessionId, roundNumber]
36312
36772
  );
36313
36773
  this.db.run(
@@ -36338,9 +36798,8 @@ var DbSyncWatcher = class {
36338
36798
  });
36339
36799
  }
36340
36800
  /**
36341
- * Process a `map_completed` orchestration event.
36342
- * Upserts map_runs with orchestrator data for real-time dashboard updates.
36343
- * Idempotent — skips if run already has source='orchestrator'.
36801
+ * Project a `map_completed` event into `map_runs` and emit a room-scoped
36802
+ * `map:updated`. Idempotent.
36344
36803
  */
36345
36804
  processMapCompletedEvent(sessionId, runNumber, metadataStr) {
36346
36805
  let metadata;
@@ -36358,8 +36817,7 @@ var DbSyncWatcher = class {
36358
36817
  return;
36359
36818
  }
36360
36819
  this.db.run(
36361
- `INSERT OR IGNORE INTO map_runs (session_id, run_number)
36362
- VALUES (?, ?)`,
36820
+ `INSERT OR IGNORE INTO map_runs (session_id, run_number) VALUES (?, ?)`,
36363
36821
  [sessionId, runNumber]
36364
36822
  );
36365
36823
  this.db.run(
@@ -36382,33 +36840,42 @@ var DbSyncWatcher = class {
36382
36840
  source: "orchestrator"
36383
36841
  });
36384
36842
  }
36385
- /**
36386
- * Record current mtime after the dashboard writes to disk.
36387
- * Called automatically via registered save hooks after saveDb().
36388
- */
36389
- markOwnWrite() {
36390
- try {
36391
- this.lastMtime = statSync2(this.dbFilePath).mtimeMs;
36392
- } catch {
36393
- }
36394
- }
36395
36843
  };
36844
+ function sessionFingerprint(row) {
36845
+ return [
36846
+ col(row, "status"),
36847
+ col(row, "current_phase"),
36848
+ col(row, "phase_number"),
36849
+ col(row, "current_round"),
36850
+ col(row, "current_map_run"),
36851
+ col(row, "updated_at")
36852
+ ].join("|");
36853
+ }
36854
+ function commandFingerprint(row) {
36855
+ return [
36856
+ col(row, "last_heartbeat_at"),
36857
+ col(row, "finished_at"),
36858
+ col(row, "exit_code"),
36859
+ col(row, "workflow_id"),
36860
+ col(row, "vendor_session_id")
36861
+ ].join("|");
36862
+ }
36396
36863
 
36397
36864
  // src/server/socket/chat-handler.ts
36398
36865
  import { dirname as dirname11 } from "node:path";
36399
36866
 
36400
36867
  // src/server/services/chat-context.ts
36401
- import { readFileSync as readFileSync12, readdirSync as readdirSync2, existsSync as existsSync12 } from "node:fs";
36402
- import { join as join14 } from "node:path";
36868
+ import { readFileSync as readFileSync9, readdirSync as readdirSync2, existsSync as existsSync12 } from "node:fs";
36869
+ import { join as join15 } from "node:path";
36403
36870
  function buildChatContext(ocrDir, target) {
36404
- const sessionsDir = join14(ocrDir, "sessions");
36871
+ const sessionsDir = join15(ocrDir, "sessions");
36405
36872
  if (target.type === "map_run") {
36406
36873
  return buildMapRunContext(sessionsDir, target.sessionId, target.runNumber);
36407
36874
  }
36408
36875
  return buildReviewRoundContext(sessionsDir, target.sessionId, target.roundNumber);
36409
36876
  }
36410
36877
  function buildMapRunContext(sessionsDir, sessionId, runNumber) {
36411
- const mapPath = join14(
36878
+ const mapPath = join15(
36412
36879
  sessionsDir,
36413
36880
  sessionId,
36414
36881
  "map",
@@ -36423,7 +36890,7 @@ function buildMapRunContext(sessionsDir, sessionId, runNumber) {
36423
36890
  `Below is the Code Review Map that organizes the changeset into reviewable sections:`
36424
36891
  ];
36425
36892
  if (existsSync12(mapPath)) {
36426
- const content = readFileSync12(mapPath, "utf-8");
36893
+ const content = readFileSync9(mapPath, "utf-8");
36427
36894
  parts.push("");
36428
36895
  parts.push("<map>");
36429
36896
  parts.push(content);
@@ -36435,9 +36902,9 @@ function buildMapRunContext(sessionsDir, sessionId, runNumber) {
36435
36902
  return parts.join("\n");
36436
36903
  }
36437
36904
  function buildReviewRoundContext(sessionsDir, sessionId, roundNumber) {
36438
- const roundDir = join14(sessionsDir, sessionId, "rounds", `round-${roundNumber}`);
36439
- const finalPath = join14(roundDir, "final.md");
36440
- const reviewersDir = join14(roundDir, "reviews");
36905
+ const roundDir = join15(sessionsDir, sessionId, "rounds", `round-${roundNumber}`);
36906
+ const finalPath = join15(roundDir, "final.md");
36907
+ const reviewersDir = join15(roundDir, "reviews");
36441
36908
  const parts = [
36442
36909
  `You are an expert code reviewer assisting with a code review session.`,
36443
36910
  `You are looking at review round #${roundNumber} for session "${sessionId}".`,
@@ -36445,7 +36912,7 @@ function buildReviewRoundContext(sessionsDir, sessionId, roundNumber) {
36445
36912
  `Below are the review artifacts for this round:`
36446
36913
  ];
36447
36914
  if (existsSync12(finalPath)) {
36448
- const content = readFileSync12(finalPath, "utf-8");
36915
+ const content = readFileSync9(finalPath, "utf-8");
36449
36916
  parts.push("");
36450
36917
  parts.push("<final-synthesis>");
36451
36918
  parts.push(content);
@@ -36454,7 +36921,7 @@ function buildReviewRoundContext(sessionsDir, sessionId, roundNumber) {
36454
36921
  if (existsSync12(reviewersDir)) {
36455
36922
  const files = readdirSync2(reviewersDir).filter((f) => f.endsWith(".md")).sort();
36456
36923
  for (const file of files) {
36457
- const content = readFileSync12(join14(reviewersDir, file), "utf-8");
36924
+ const content = readFileSync9(join15(reviewersDir, file), "utf-8");
36458
36925
  const reviewerName = file.replace(/\.md$/, "");
36459
36926
  parts.push("");
36460
36927
  parts.push(`<reviewer name="${reviewerName}">`);
@@ -36521,7 +36988,6 @@ function startTrackedExecution(io2, db, ocrDir, command, args = []) {
36521
36988
  WHERE id = ?`,
36522
36989
  [exitCode, finishedAt, outputBuffer, executionId]
36523
36990
  );
36524
- saveDb(db, ocrDir);
36525
36991
  appendCommandLog(ocrDir, {
36526
36992
  ...baseLogEntry,
36527
36993
  is_detached: trackedIsDetached,
@@ -36551,13 +37017,12 @@ function cleanupChat(conversationId) {
36551
37017
  activeChats.delete(conversationId);
36552
37018
  }
36553
37019
  }
36554
- function resetIdleTimer(conversationId, db, ocrDir) {
37020
+ function resetIdleTimer(conversationId, db) {
36555
37021
  const chat = activeChats.get(conversationId);
36556
37022
  if (chat) {
36557
37023
  clearTimeout(chat.timer);
36558
37024
  chat.timer = setTimeout(() => {
36559
37025
  updateConversationStatus(db, conversationId, "expired");
36560
- saveDb(db, ocrDir);
36561
37026
  cleanupChat(conversationId);
36562
37027
  }, IDLE_TIMEOUT_MS);
36563
37028
  }
@@ -36581,9 +37046,7 @@ function registerChatHandlers(io2, socket, db, ocrDir, aiCliService) {
36581
37046
  return;
36582
37047
  }
36583
37048
  upsertConversation(db, conversationId, sessionId, targetType, targetId);
36584
- saveDb(db, ocrDir);
36585
37049
  insertMessage(db, conversationId, "user", message);
36586
- saveDb(db, ocrDir);
36587
37050
  const conversation = getConversation(db, conversationId);
36588
37051
  const claudeSessionId = conversation?.claude_session_id ?? null;
36589
37052
  let prompt;
@@ -36624,7 +37087,6 @@ User: ${message}`;
36624
37087
  const proc = spawnResult.process;
36625
37088
  const timer = setTimeout(() => {
36626
37089
  updateConversationStatus(db, conversationId, "expired");
36627
- saveDb(db, ocrDir);
36628
37090
  cleanupChat(conversationId);
36629
37091
  }, IDLE_TIMEOUT_MS);
36630
37092
  activeChats.set(conversationId, { process: proc, conversationId, timer });
@@ -36714,7 +37176,6 @@ User: ${message}`;
36714
37176
  if (assistantText.trim()) {
36715
37177
  insertMessage(db, conversationId, "assistant", assistantText.trim());
36716
37178
  }
36717
- saveDb(db, ocrDir);
36718
37179
  if (code === 0) {
36719
37180
  tracker.appendOutput("\n\u2713 Response complete\n");
36720
37181
  tracker.finish(0);
@@ -36730,7 +37191,7 @@ User: ${message}`;
36730
37191
  error: errMsg
36731
37192
  });
36732
37193
  }
36733
- resetIdleTimer(conversationId, db, ocrDir);
37194
+ resetIdleTimer(conversationId, db);
36734
37195
  const chat = activeChats.get(conversationId);
36735
37196
  if (chat) {
36736
37197
  chat.process = null;
@@ -36790,13 +37251,13 @@ function cleanupAllChats() {
36790
37251
  }
36791
37252
 
36792
37253
  // src/server/socket/post-handler.ts
36793
- import { existsSync as existsSync13, mkdirSync as mkdirSync6, readFileSync as readFileSync13, unlinkSync as unlinkSync2, writeFileSync as writeFileSync6 } from "node:fs";
37254
+ import { existsSync as existsSync13, mkdirSync as mkdirSync5, readFileSync as readFileSync10, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "node:fs";
36794
37255
  import { tmpdir as tmpdir2 } from "node:os";
36795
- import { join as join15, dirname as dirname12, isAbsolute } from "node:path";
37256
+ import { join as join16, dirname as dirname12, isAbsolute as isAbsolute2 } from "node:path";
36796
37257
  import { randomUUID as randomUUID2 } from "node:crypto";
36797
- function resolveSessionDir(sessionDir, ocrDir) {
36798
- if (isAbsolute(sessionDir)) return sessionDir;
36799
- return join15(dirname12(ocrDir), sessionDir);
37258
+ function resolveSessionDir2(sessionDir, ocrDir) {
37259
+ if (isAbsolute2(sessionDir)) return sessionDir;
37260
+ return join16(dirname12(ocrDir), sessionDir);
36800
37261
  }
36801
37262
  var BRANCH_PREFIXES = [
36802
37263
  "feat",
@@ -36966,19 +37427,19 @@ function registerPostHandlers(io2, socket, db, ocrDir, aiCliService) {
36966
37427
  socket.emit("post:error", { error: "Session not found" });
36967
37428
  return;
36968
37429
  }
36969
- const sessionDir = session.session_dir ? resolveSessionDir(session.session_dir, ocrDir) : join15(ocrDir, "sessions", sessionId);
36970
- const roundDir = join15(sessionDir, "rounds", `round-${roundNumber}`);
36971
- const finalPath = join15(roundDir, "final.md");
37430
+ const sessionDir = session.session_dir ? resolveSessionDir2(session.session_dir, ocrDir) : join16(ocrDir, "sessions", sessionId);
37431
+ const roundDir = join16(sessionDir, "rounds", `round-${roundNumber}`);
37432
+ const finalPath = join16(roundDir, "final.md");
36972
37433
  if (!existsSync13(finalPath)) {
36973
37434
  socket.emit("post:error", { error: "final.md not found for this round" });
36974
37435
  return;
36975
37436
  }
36976
- const humanReviewPath = join15(roundDir, "final-human.md");
37437
+ const humanReviewPath = join16(roundDir, "final-human.md");
36977
37438
  const repoRoot = dirname12(ocrDir);
36978
- const commandMdPath = join15(ocrDir, "commands", "translate-review-to-single-human.md");
37439
+ const commandMdPath = join16(ocrDir, "commands", "translate-review-to-single-human.md");
36979
37440
  let commandContent;
36980
37441
  try {
36981
- commandContent = readFileSync13(commandMdPath, "utf-8");
37442
+ commandContent = readFileSync10(commandMdPath, "utf-8");
36982
37443
  } catch {
36983
37444
  socket.emit("post:error", {
36984
37445
  error: `Command file not found: ${commandMdPath}. Run \`ocr init\` to set up.`
@@ -37006,8 +37467,8 @@ function registerPostHandlers(io2, socket, db, ocrDir, aiCliService) {
37006
37467
  "",
37007
37468
  "Examples:",
37008
37469
  `- Instead of \`ocr state show\`, run: \`node ${localCli} state show\``,
37009
- `- Instead of \`ocr state init ...\`, run: \`node ${localCli} state init ...\``,
37010
- `- Instead of \`ocr state transition ...\`, run: \`node ${localCli} state transition ...\``,
37470
+ `- Instead of \`ocr state begin ...\`, run: \`node ${localCli} state begin ...\``,
37471
+ `- Instead of \`ocr state advance ...\`, run: \`node ${localCli} state advance ...\``,
37011
37472
  "",
37012
37473
  "This applies to every `ocr` invocation. Do NOT use bare `ocr` commands."
37013
37474
  );
@@ -37060,7 +37521,7 @@ function registerPostHandlers(io2, socket, db, ocrDir, aiCliService) {
37060
37521
  let generatedContent = "";
37061
37522
  if (existsSync13(humanReviewPath)) {
37062
37523
  try {
37063
- generatedContent = readFileSync13(humanReviewPath, "utf-8").trim();
37524
+ generatedContent = readFileSync10(humanReviewPath, "utf-8").trim();
37064
37525
  } catch {
37065
37526
  }
37066
37527
  }
@@ -37135,12 +37596,11 @@ function registerPostHandlers(io2, socket, db, ocrDir, aiCliService) {
37135
37596
  socket.emit("post:save-result", { success: false, error: "Session not found" });
37136
37597
  return;
37137
37598
  }
37138
- const sessionDir = session.session_dir ? resolveSessionDir(session.session_dir, ocrDir) : join15(ocrDir, "sessions", sessionId);
37139
- const roundDir = join15(sessionDir, "rounds", `round-${roundNumber}`);
37140
- mkdirSync6(roundDir, { recursive: true });
37141
- const filePath = join15(roundDir, "final-human.md");
37142
- writeFileSync6(filePath, content, { mode: 420 });
37143
- saveDb(db, ocrDir);
37599
+ const sessionDir = session.session_dir ? resolveSessionDir2(session.session_dir, ocrDir) : join16(ocrDir, "sessions", sessionId);
37600
+ const roundDir = join16(sessionDir, "rounds", `round-${roundNumber}`);
37601
+ mkdirSync5(roundDir, { recursive: true });
37602
+ const filePath = join16(roundDir, "final-human.md");
37603
+ writeFileSync4(filePath, content, { mode: 420 });
37144
37604
  socket.emit("post:save-result", { success: true });
37145
37605
  } catch (err) {
37146
37606
  console.error("Error in post:save handler:", err);
@@ -37166,13 +37626,13 @@ function registerPostHandlers(io2, socket, db, ocrDir, aiCliService) {
37166
37626
  );
37167
37627
  tracker.appendOutput(`\u25B8 Posting review to PR #${prNumber}...
37168
37628
  `);
37169
- const tmpDir = join15(tmpdir2(), "ocr-post-comments");
37629
+ const tmpDir = join16(tmpdir2(), "ocr-post-comments");
37170
37630
  try {
37171
- mkdirSync6(tmpDir, { recursive: true, mode: 448 });
37631
+ mkdirSync5(tmpDir, { recursive: true, mode: 448 });
37172
37632
  } catch {
37173
37633
  }
37174
- const tmpFile = join15(tmpDir, `${randomUUID2()}.md`);
37175
- writeFileSync6(tmpFile, content, { mode: 384 });
37634
+ const tmpFile = join16(tmpDir, `${randomUUID2()}.md`);
37635
+ writeFileSync4(tmpFile, content, { mode: 384 });
37176
37636
  const repoRoot = dirname12(ocrDir);
37177
37637
  try {
37178
37638
  const { stdout } = await execBinaryAsync(
@@ -37217,17 +37677,17 @@ function cleanupAllPostGenerations() {
37217
37677
  }
37218
37678
 
37219
37679
  // ../cli/src/lib/runtime-config.ts
37220
- import { existsSync as existsSync14, readFileSync as readFileSync14 } from "node:fs";
37221
- import { join as join16 } from "node:path";
37680
+ import { existsSync as existsSync14, readFileSync as readFileSync11 } from "node:fs";
37681
+ import { join as join17 } from "node:path";
37222
37682
  var DEFAULT_AGENT_HEARTBEAT_SECONDS = 60;
37223
37683
  function getAgentHeartbeatSeconds(ocrDir) {
37224
- const configPath = join16(ocrDir, "config.yaml");
37684
+ const configPath = join17(ocrDir, "config.yaml");
37225
37685
  if (!existsSync14(configPath)) {
37226
37686
  return DEFAULT_AGENT_HEARTBEAT_SECONDS;
37227
37687
  }
37228
37688
  let content;
37229
37689
  try {
37230
- content = readFileSync14(configPath, "utf-8");
37690
+ content = readFileSync11(configPath, "utf-8");
37231
37691
  } catch {
37232
37692
  return DEFAULT_AGENT_HEARTBEAT_SECONDS;
37233
37693
  }
@@ -37322,23 +37782,23 @@ async function startServer(options = {}) {
37322
37782
  const port = options.port ?? parseInt(process.env.PORT ?? "4173", 10);
37323
37783
  const ocrDir = resolveOcrDir();
37324
37784
  const aiCliService = new AiCliService(ocrDir);
37325
- const dbPathForCheckpoint = join17(ocrDir, "data", "ocr.db");
37785
+ const dbPathForCheckpoint = join18(ocrDir, "data", "ocr.db");
37326
37786
  const walResult = walCheckpointTruncate(dbPathForCheckpoint);
37327
37787
  if (walResult === "checkpointed") {
37328
37788
  console.log(" WAL checkpoint: truncated stale write-ahead-log file");
37329
37789
  }
37330
37790
  const db = await openDb(ocrDir);
37331
- const dataDir = join17(ocrDir, "data");
37332
- const pidFilePath = join17(dataDir, "dashboard.pid");
37333
- const portFilePath = join17(dataDir, "server-port");
37334
- mkdirSync7(dataDir, { recursive: true });
37791
+ const dataDir = join18(ocrDir, "data");
37792
+ const pidFilePath = join18(dataDir, "dashboard.pid");
37793
+ const portFilePath = join18(dataDir, "server-port");
37794
+ mkdirSync6(dataDir, { recursive: true });
37335
37795
  try {
37336
37796
  unlinkSync3(portFilePath);
37337
37797
  } catch {
37338
37798
  }
37339
37799
  if (existsSync15(pidFilePath)) {
37340
37800
  try {
37341
- const oldPid = parseInt(readFileSync15(pidFilePath, "utf-8").trim(), 10);
37801
+ const oldPid = parseInt(readFileSync12(pidFilePath, "utf-8").trim(), 10);
37342
37802
  if (!isNaN(oldPid)) {
37343
37803
  try {
37344
37804
  process.kill(oldPid, 0);
@@ -37351,13 +37811,12 @@ async function startServer(options = {}) {
37351
37811
  } catch {
37352
37812
  }
37353
37813
  }
37354
- writeFileSync7(pidFilePath, String(process.pid), { mode: 384 });
37814
+ writeFileSync5(pidFilePath, String(process.pid), { mode: 384 });
37355
37815
  const cmdCountResult = db.exec("SELECT COUNT(*) as c FROM command_executions");
37356
37816
  const totalCmds = cmdCountResult[0]?.values[0]?.[0] ?? 0;
37357
37817
  if (totalCmds === 0) {
37358
37818
  const recovered = replayCommandLog(db, ocrDir);
37359
37819
  if (recovered > 0) {
37360
- saveDb(db, ocrDir);
37361
37820
  console.log(` Recovered ${recovered} command(s) from JSONL backup`);
37362
37821
  }
37363
37822
  }
@@ -37368,15 +37827,14 @@ async function startServer(options = {}) {
37368
37827
  if (orphanResult.length > 0 && orphanResult[0]) {
37369
37828
  const { columns, values: orphanRows } = orphanResult[0];
37370
37829
  const colIdx = Object.fromEntries(columns.map((c, i) => [c, i]));
37371
- const cutoff = Date.now() - 24 * 60 * 60 * 1e3;
37830
+ const cutoff = Date.now() - PID_REUSE_GUARD_MS;
37372
37831
  let killedCount = 0;
37373
37832
  for (const row of orphanRows) {
37374
37833
  const pid = row[colIdx["pid"]];
37375
37834
  const isDetached = row[colIdx["is_detached"]] === 1;
37376
37835
  const startedAt = row[colIdx["started_at"]];
37377
- if (new Date(startedAt).getTime() < cutoff) continue;
37378
- try {
37379
- process.kill(pid, 0);
37836
+ if (sqliteUtcMs(startedAt) < cutoff) continue;
37837
+ if (defaultIsAlive(pid)) {
37380
37838
  if (isDetached) {
37381
37839
  try {
37382
37840
  process.kill(-pid, "SIGTERM");
@@ -37400,37 +37858,55 @@ async function startServer(options = {}) {
37400
37858
  } catch {
37401
37859
  }
37402
37860
  }, 2e3);
37403
- } catch {
37404
37861
  }
37405
37862
  }
37406
37863
  if (killedCount > 0) {
37407
37864
  console.log(` Cleaned up ${killedCount} orphaned process(es)`);
37408
37865
  }
37409
37866
  }
37410
- const staleResult = db.exec(
37411
- "SELECT COUNT(*) as c FROM command_executions WHERE finished_at IS NULL OR exit_code IS NULL"
37867
+ const legacyResult = db.exec(
37868
+ "SELECT COUNT(*) as c FROM command_executions WHERE finished_at IS NOT NULL AND exit_code IS NULL"
37412
37869
  );
37413
- const staleCount = staleResult[0]?.values[0]?.[0] ?? 0;
37414
- if (staleCount > 0) {
37870
+ const legacyCount = legacyResult[0]?.values[0]?.[0] ?? 0;
37871
+ if (legacyCount > 0) {
37415
37872
  db.run(
37416
37873
  `UPDATE command_executions
37417
- SET exit_code = -2, finished_at = COALESCE(finished_at, datetime('now')),
37874
+ SET exit_code = -2,
37418
37875
  output = COALESCE(output, '') || '
37419
- [Cancelled]',
37420
- pid = NULL
37421
- WHERE finished_at IS NULL OR exit_code IS NULL`
37876
+ [Cancelled]'
37877
+ WHERE finished_at IS NOT NULL AND exit_code IS NULL`
37422
37878
  );
37423
- saveDb(db, ocrDir);
37424
- console.log(` Cleaned up ${staleCount} stale command(s)`);
37879
+ console.log(` Backfilled ${legacyCount} finished command(s) missing an exit code`);
37425
37880
  }
37426
37881
  const heartbeatSeconds = getAgentHeartbeatSeconds(ocrDir);
37427
- const sweepResult = sweepStaleAgentSessions(db, heartbeatSeconds);
37428
- if (sweepResult.orphanedIds.length > 0) {
37429
- saveDb(db, ocrDir);
37882
+ const logAgentSweep = (result) => {
37883
+ if (result.orphanedIds.length === 0) return;
37884
+ const cascaded = result.cascadedWorkflowIds.length;
37430
37885
  console.log(
37431
- ` Cleaned up ${sweepResult.orphanedIds.length} stale agent session(s) (heartbeat threshold ${heartbeatSeconds}s)`
37886
+ ` Cleaned up ${result.orphanedIds.length} stale agent session(s) (heartbeat threshold ${heartbeatSeconds}s)` + (cascaded > 0 ? `; cascade-closed dependents of ${cascaded} workflow(s)` : "")
37887
+ );
37888
+ };
37889
+ logAgentSweep(sweepStaleAgentSessions(db, heartbeatSeconds, defaultIsAlive));
37890
+ const STALE_SESSION_THRESHOLD_SECONDS = 7 * 24 * 60 * 60;
37891
+ const staleSessionResult = sweepStaleSessions(
37892
+ db,
37893
+ STALE_SESSION_THRESHOLD_SECONDS
37894
+ );
37895
+ if (staleSessionResult.closedSessionIds.length > 0) {
37896
+ console.log(
37897
+ ` Auto-closed ${staleSessionResult.closedSessionIds.length} stale active session(s) (threshold 7 days)`
37432
37898
  );
37433
37899
  }
37900
+ const SWEEP_INTERVAL_MS = 5 * 60 * 1e3;
37901
+ const sweepTimer = setInterval(() => {
37902
+ try {
37903
+ logAgentSweep(sweepStaleAgentSessions(db, heartbeatSeconds, defaultIsAlive));
37904
+ sweepStaleSessions(db, STALE_SESSION_THRESHOLD_SECONDS);
37905
+ } catch (err) {
37906
+ console.error("[sweep] periodic sweep failed:", err);
37907
+ }
37908
+ }, SWEEP_INTERVAL_MS);
37909
+ sweepTimer.unref();
37434
37910
  app.get("/api/reviews", (_req, res) => {
37435
37911
  try {
37436
37912
  const rounds = getAllRounds(db).map((r) => ({
@@ -37448,12 +37924,12 @@ async function startServer(options = {}) {
37448
37924
  app.use("/api/sessions", createReviewsRouter(db));
37449
37925
  app.use("/api/sessions", createMapsRouter(db));
37450
37926
  app.use("/api/sessions", createArtifactsRouter(db));
37451
- app.use("/api", createProgressRouter(db, ocrDir));
37452
- app.use("/api/notes", createNotesRouter(db, ocrDir));
37927
+ app.use("/api", createProgressRouter(db));
37928
+ app.use("/api/notes", createNotesRouter(db));
37453
37929
  app.use("/api/stats", createStatsRouter(db));
37454
37930
  app.use("/api/commands", createCommandsRouter(db, ocrDir));
37455
37931
  app.use("/api/config", createConfigRouter(ocrDir, aiCliService));
37456
- app.use("/api/sessions", createChatRouter(db, ocrDir));
37932
+ app.use("/api/sessions", createChatRouter(db));
37457
37933
  app.use("/api/reviewers", createReviewersRouter(ocrDir));
37458
37934
  let pullSync = () => {
37459
37935
  };
@@ -37461,11 +37937,11 @@ async function startServer(options = {}) {
37461
37937
  app.use("/api/agent-sessions", createAgentSessionsRouter(db, () => pullSync()));
37462
37938
  app.use("/api/sessions", createHandoffRouter(sessionCapture, ocrDir, () => pullSync()));
37463
37939
  app.use("/api/team", createTeamRouter(ocrDir));
37464
- const clientDir = join17(__dirname3, "client");
37940
+ const clientDir = join18(__dirname3, "client");
37465
37941
  if (process.env.NODE_ENV === "production" && existsSync15(clientDir)) {
37466
37942
  app.use(import_express15.default.static(clientDir, { index: false }));
37467
- const indexHtmlPath = join17(clientDir, "index.html");
37468
- const rawIndexHtml = existsSync15(indexHtmlPath) ? readFileSync15(indexHtmlPath, "utf-8") : "";
37943
+ const indexHtmlPath = join18(clientDir, "index.html");
37944
+ const rawIndexHtml = existsSync15(indexHtmlPath) ? readFileSync12(indexHtmlPath, "utf-8") : "";
37469
37945
  const tokenScript = `<script>window.__OCR_TOKEN__=${JSON.stringify(AUTH_TOKEN)};</script>`;
37470
37946
  const injectedIndexHtml = rawIndexHtml.replace(
37471
37947
  "</head>",
@@ -37484,16 +37960,13 @@ async function startServer(options = {}) {
37484
37960
  registerChatHandlers(io, socket, db, ocrDir, aiCliService);
37485
37961
  registerPostHandlers(io, socket, db, ocrDir, aiCliService);
37486
37962
  });
37487
- const dbFilePath = join17(ocrDir, "data", "ocr.db");
37963
+ const dbFilePath = join18(ocrDir, "data", "ocr.db");
37488
37964
  const dbSyncWatcher = new DbSyncWatcher(
37489
37965
  db,
37490
37966
  dbFilePath,
37491
37967
  io,
37492
- () => {
37493
- saveDb(db, ocrDir);
37494
- },
37495
37968
  // Auto-link the dashboard's parent execution row when the AI
37496
- // creates a new session via `ocr state init`. Eliminates the
37969
+ // creates a new session via `ocr state begin`. Eliminates the
37497
37970
  // dependency on env-var/flag propagation through the AI's shell.
37498
37971
  (session) => {
37499
37972
  sessionCapture.autoLinkPendingDashboardExecution(session.id);
@@ -37503,14 +37976,9 @@ async function startServer(options = {}) {
37503
37976
  dbSyncWatcher.startWatching();
37504
37977
  pullSync = () => dbSyncWatcher.syncFromDisk();
37505
37978
  console.log(` Watching DB: ${shortenPath(dbFilePath)}`);
37506
- registerSaveHooks(
37507
- () => dbSyncWatcher.syncFromDisk(),
37508
- () => dbSyncWatcher.markOwnWrite()
37509
- );
37510
- const sessionsDir = join17(ocrDir, "sessions");
37511
- const fsSync = new FilesystemSync(db, sessionsDir, io, () => saveDb(db, ocrDir));
37979
+ const sessionsDir = join18(ocrDir, "sessions");
37980
+ const fsSync = new FilesystemSync(db, sessionsDir, io);
37512
37981
  await fsSync.fullScan();
37513
- saveDb(db, ocrDir);
37514
37982
  fsSync.startWatching();
37515
37983
  console.log(` Watching sessions: ${shortenPath(sessionsDir)}`);
37516
37984
  const stopReviewersWatch = watchReviewersMeta(ocrDir, io);
@@ -37547,7 +38015,7 @@ async function startServer(options = {}) {
37547
38015
  if (actualPort !== port) {
37548
38016
  console.log(` Note: using port ${actualPort} (${port} was in use)`);
37549
38017
  }
37550
- writeFileSync7(portFilePath, String(actualPort), { mode: 384 });
38018
+ writeFileSync5(portFilePath, String(actualPort), { mode: 384 });
37551
38019
  console.log(` Server: http://localhost:${actualPort}`);
37552
38020
  console.log(` OCR directory: ${shortenPath(ocrDir)}`);
37553
38021
  console.log();
@@ -37573,7 +38041,7 @@ async function startServer(options = {}) {
37573
38041
  } catch {
37574
38042
  }
37575
38043
  try {
37576
- unlinkSync3(join17(dataDir, "dashboard-active-spawn.json"));
38044
+ unlinkSync3(join18(dataDir, "dashboard-active-spawn.json"));
37577
38045
  } catch {
37578
38046
  }
37579
38047
  try {
@@ -37614,24 +38082,12 @@ async function startServer(options = {}) {
37614
38082
  }
37615
38083
  cleanupAllChats();
37616
38084
  cleanupAllPostGenerations();
37617
- try {
37618
- flushSave();
37619
- } catch {
37620
- }
37621
- try {
37622
- saveDb(db, ocrDir);
37623
- } catch {
37624
- }
37625
38085
  dbSyncWatcher.stopWatching();
37626
38086
  fsSync.stopWatching();
37627
38087
  stopReviewersWatch();
37628
38088
  io.close();
37629
38089
  httpServer.closeAllConnections();
37630
38090
  httpServer.close(() => {
37631
- try {
37632
- saveDb(db, ocrDir);
37633
- } catch {
37634
- }
37635
38091
  closeDb();
37636
38092
  console.log("Server stopped.");
37637
38093
  process.exit(0);