@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.
- package/README.md +8 -4
- package/dist/dashboard/client/assets/{_basePickBy-D8RU9s_y.js → _basePickBy-B3ALyupE.js} +1 -1
- package/dist/dashboard/client/assets/{_baseUniq-CjVeYx1J.js → _baseUniq-b2RALAWc.js} +1 -1
- package/dist/dashboard/client/assets/{arc-DsFstmf9.js → arc-DcSVvhUd.js} +1 -1
- package/dist/dashboard/client/assets/{architectureDiagram-VXUJARFQ-iNJB-g1N.js → architectureDiagram-VXUJARFQ-BNUlmSCS.js} +1 -1
- package/dist/dashboard/client/assets/{blockDiagram-VD42YOAC-Zp2Aw0zR.js → blockDiagram-VD42YOAC-BmhiQVwa.js} +1 -1
- package/dist/dashboard/client/assets/{c4Diagram-YG6GDRKO-BGppUmwT.js → c4Diagram-YG6GDRKO-jyJ3WOv5.js} +1 -1
- package/dist/dashboard/client/assets/channel-D3J8-GF_.js +1 -0
- package/dist/dashboard/client/assets/{chunk-4BX2VUAB-CZcRxeE4.js → chunk-4BX2VUAB-x1dQU_s3.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-55IACEB6-CVdL59yY.js → chunk-55IACEB6-CwbsE2XQ.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-B4BG7PRW-CFPp6g6e.js → chunk-B4BG7PRW-BaE7c-ti.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-DI55MBZ5-DH9BzE6I.js → chunk-DI55MBZ5-Bw5PUaMK.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-FMBD7UC4-DZ2DTwqS.js → chunk-FMBD7UC4-B7cF6P3s.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-QN33PNHL-DODPm0CR.js → chunk-QN33PNHL-OY4evNHd.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-QZHKN3VN-CNI_LxUf.js → chunk-QZHKN3VN-BpjQwIWz.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-TZMSLE5B-sxZQF02c.js → chunk-TZMSLE5B-D8b_Oq9B.js} +1 -1
- package/dist/dashboard/client/assets/classDiagram-2ON5EDUG-tkFUL-1Y.js +1 -0
- package/dist/dashboard/client/assets/classDiagram-v2-WZHVMYZB-tkFUL-1Y.js +1 -0
- package/dist/dashboard/client/assets/clone-CkY5ajLr.js +1 -0
- package/dist/dashboard/client/assets/{cose-bilkent-S5V4N54A-BHa2lABH.js → cose-bilkent-S5V4N54A-C-sfP8PN.js} +1 -1
- package/dist/dashboard/client/assets/{dagre-6UL2VRFP-CvCLBtkz.js → dagre-6UL2VRFP-Cqfo0NRg.js} +1 -1
- package/dist/dashboard/client/assets/{diagram-PSM6KHXK-Cklwd4YA.js → diagram-PSM6KHXK-BR3ppxqI.js} +1 -1
- package/dist/dashboard/client/assets/{diagram-QEK2KX5R-3bDERTbp.js → diagram-QEK2KX5R-Dvcx6x3R.js} +1 -1
- package/dist/dashboard/client/assets/{diagram-S2PKOQOG-DbiWlPc6.js → diagram-S2PKOQOG-DoyBLnVN.js} +1 -1
- package/dist/dashboard/client/assets/{erDiagram-Q2GNP2WA-BQa_VNbt.js → erDiagram-Q2GNP2WA-hy77l1cL.js} +1 -1
- package/dist/dashboard/client/assets/{flowDiagram-NV44I4VS-BDaJyl9N.js → flowDiagram-NV44I4VS-Bz0B1rKM.js} +1 -1
- package/dist/dashboard/client/assets/{ganttDiagram-JELNMOA3-DsTnleSr.js → ganttDiagram-JELNMOA3-CLgrZPoC.js} +1 -1
- package/dist/dashboard/client/assets/{gitGraphDiagram-V2S2FVAM-BRuBadgn.js → gitGraphDiagram-V2S2FVAM-DwJ-1f-v.js} +1 -1
- package/dist/dashboard/client/assets/{graph-CYYqXm9c.js → graph-DDBMM_t2.js} +1 -1
- package/dist/dashboard/client/assets/{index-eZMoytob.js → index-Cr9yEo_B.js} +123 -123
- package/dist/dashboard/client/assets/{infoDiagram-HS3SLOUP-CHnA8k7H.js → infoDiagram-HS3SLOUP-Bhn1FmAk.js} +1 -1
- package/dist/dashboard/client/assets/{journeyDiagram-XKPGCS4Q-CAXR1-Ju.js → journeyDiagram-XKPGCS4Q-CzGbjX1y.js} +1 -1
- package/dist/dashboard/client/assets/{kanban-definition-3W4ZIXB7-Clf3HfHz.js → kanban-definition-3W4ZIXB7-Da77-WYk.js} +1 -1
- package/dist/dashboard/client/assets/{layout-DQPaNqnO.js → layout-CVwSB-GS.js} +1 -1
- package/dist/dashboard/client/assets/{linear-qUnNXvWB.js → linear-CTRAc5Jn.js} +1 -1
- package/dist/dashboard/client/assets/{mermaid-renderer-C7Se8vjl.js → mermaid-renderer-Bjo170ax.js} +4 -4
- package/dist/dashboard/client/assets/{mindmap-definition-VGOIOE7T-DBIdG0OR.js → mindmap-definition-VGOIOE7T-B55C2odl.js} +1 -1
- package/dist/dashboard/client/assets/{pieDiagram-ADFJNKIX-DXAIiG6W.js → pieDiagram-ADFJNKIX-5lrQLrSz.js} +1 -1
- package/dist/dashboard/client/assets/{quadrantDiagram-AYHSOK5B-D4yAxif0.js → quadrantDiagram-AYHSOK5B-Bg55gC30.js} +1 -1
- package/dist/dashboard/client/assets/{requirementDiagram-UZGBJVZJ-D27ME1VO.js → requirementDiagram-UZGBJVZJ-CyR4YFJY.js} +1 -1
- package/dist/dashboard/client/assets/{sankeyDiagram-TZEHDZUN-BeEaA_QM.js → sankeyDiagram-TZEHDZUN-BVWKr9_-.js} +1 -1
- package/dist/dashboard/client/assets/{sequenceDiagram-WL72ISMW-GTI12qU0.js → sequenceDiagram-WL72ISMW-D0AJg_tE.js} +1 -1
- package/dist/dashboard/client/assets/{stateDiagram-FKZM4ZOC-ClSoeZM0.js → stateDiagram-FKZM4ZOC-BuHpTgim.js} +1 -1
- package/dist/dashboard/client/assets/stateDiagram-v2-4FDKWEC3-DwAPhteN.js +1 -0
- package/dist/dashboard/client/assets/{timeline-definition-IT6M3QCI-cj5d_Kyh.js → timeline-definition-IT6M3QCI-LDhpAmDd.js} +1 -1
- package/dist/dashboard/client/assets/{treemap-GDKQZRPO-BrRT1igb.js → treemap-GDKQZRPO-Dd4gjvUl.js} +1 -1
- package/dist/dashboard/client/assets/{xychartDiagram-PRI3JC2R-DlzGitHh.js → xychartDiagram-PRI3JC2R-B9RDod39.js} +1 -1
- package/dist/dashboard/client/index.html +1 -1
- package/dist/dashboard/server.js +1113 -657
- package/dist/index.js +1719 -718
- package/dist/lib/db/index.js +638 -101
- package/package.json +4 -4
- package/dist/dashboard/client/assets/channel-C8plpfdz.js +0 -1
- package/dist/dashboard/client/assets/classDiagram-2ON5EDUG-Dqn6u1oQ.js +0 -1
- package/dist/dashboard/client/assets/classDiagram-v2-WZHVMYZB-Dqn6u1oQ.js +0 -1
- package/dist/dashboard/client/assets/clone-BQ8hOLqM.js +0 -1
- package/dist/dashboard/client/assets/stateDiagram-v2-4FDKWEC3-Bim3s-dq.js +0 -1
package/dist/dashboard/server.js
CHANGED
|
@@ -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
|
|
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 =
|
|
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 =
|
|
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
|
|
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(
|
|
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 =
|
|
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
|
|
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 && !
|
|
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
|
|
30487
|
-
import { join as
|
|
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
|
|
30519
|
-
import { dirname as
|
|
30520
|
-
|
|
30521
|
-
|
|
30522
|
-
import
|
|
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
|
|
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/
|
|
30916
|
-
var
|
|
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)
|
|
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
|
|
30993
|
-
|
|
30994
|
-
|
|
30995
|
-
|
|
30996
|
-
|
|
30997
|
-
|
|
30998
|
-
|
|
30999
|
-
|
|
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 (
|
|
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
|
-
|
|
31006
|
-
|
|
31007
|
-
|
|
31008
|
-
|
|
31009
|
-
|
|
31010
|
-
|
|
31011
|
-
|
|
31012
|
-
|
|
31013
|
-
|
|
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:
|
|
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
|
|
31022
|
-
import { dirname as
|
|
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
|
|
31574
|
+
return join3(ocrDir, "data", CACHE_DIR);
|
|
31034
31575
|
}
|
|
31035
31576
|
function commandLogPath(ocrDir) {
|
|
31036
|
-
return
|
|
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 =
|
|
31042
|
-
if (!
|
|
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 (!
|
|
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
|
-
|
|
31119
|
-
|
|
31120
|
-
|
|
31121
|
-
|
|
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
|
|
31124
|
-
|
|
31125
|
-
|
|
31126
|
-
|
|
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
|
-
|
|
31129
|
-
|
|
31130
|
-
|
|
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
|
-
|
|
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
|
-
|
|
31145
|
-
|
|
31146
|
-
|
|
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 =
|
|
31169
|
-
|
|
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 (
|
|
31210
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
32446
|
-
import { dirname as dirname6, join as
|
|
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
|
|
32450
|
-
import { join as
|
|
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
|
|
32941
|
-
readFileSync as
|
|
33519
|
+
mkdirSync as mkdirSync3,
|
|
33520
|
+
readFileSync as readFileSync2
|
|
32942
33521
|
} from "node:fs";
|
|
32943
|
-
import { join as
|
|
33522
|
+
import { join as join6 } from "node:path";
|
|
32944
33523
|
function eventsDir(ocrDir) {
|
|
32945
|
-
const dir =
|
|
33524
|
+
const dir = join6(ocrDir, "data", "events");
|
|
32946
33525
|
if (!existsSync5(dir)) {
|
|
32947
|
-
|
|
33526
|
+
mkdirSync3(dir, { recursive: true });
|
|
32948
33527
|
}
|
|
32949
33528
|
return dir;
|
|
32950
33529
|
}
|
|
32951
33530
|
function eventJournalPath(ocrDir, executionId) {
|
|
32952
|
-
return
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
33041
|
-
const content =
|
|
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
|
|
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 =
|
|
33156
|
-
const bundledCli =
|
|
33157
|
-
if (existsSync6(bundledCli) && existsSync6(
|
|
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(
|
|
33163
|
-
const candidate =
|
|
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
|
|
33290
|
-
`- Instead of \`ocr state
|
|
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
|
|
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
|
|
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
|
|
33935
|
+
return join10(ocrDir, "data", "dashboard-active-spawn.json");
|
|
33357
33936
|
}
|
|
33358
33937
|
function writeSpawnMarker(ocrDir, executionUid, pid) {
|
|
33359
|
-
const dataDir =
|
|
33360
|
-
if (!existsSync7(dataDir))
|
|
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
|
-
|
|
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 =
|
|
34153
|
+
const commandMdPath = join10(ocrDir, "commands", `${baseCommand}.md`);
|
|
33575
34154
|
let commandContent;
|
|
33576
34155
|
try {
|
|
33577
|
-
commandContent =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
33907
|
-
import { join as
|
|
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 =
|
|
33925
|
-
const content =
|
|
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 =
|
|
33972
|
-
let content =
|
|
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
|
-
|
|
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
|
|
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
|
|
34062
|
-
import { join as
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
34186
|
-
import { join as
|
|
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 =
|
|
34781
|
+
const configPath = join13(ocrDir, "config.yaml");
|
|
34189
34782
|
if (!existsSync9(configPath)) {
|
|
34190
34783
|
return { team: [], aliases: {}, defaultModel: null };
|
|
34191
34784
|
}
|
|
34192
|
-
const content =
|
|
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 =
|
|
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
|
|
34795
|
-
import { join as
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
35020
|
-
const reviewsDir =
|
|
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 =
|
|
35613
|
+
const filePath = join14(reviewsDir, reviewFile);
|
|
35025
35614
|
this.processReviewerOutput(sessionId, roundNumber, filePath, reviewFile);
|
|
35026
35615
|
}
|
|
35027
35616
|
}
|
|
35028
|
-
const roundMetaPath =
|
|
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 =
|
|
35621
|
+
const finalPath = join14(roundDir, "final.md");
|
|
35033
35622
|
if (existsSync10(finalPath)) {
|
|
35034
35623
|
this.processFinalMd(sessionId, roundNumber, finalPath);
|
|
35035
35624
|
}
|
|
35036
|
-
const finalHumanPath =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
35055
|
-
const mapMetaPath =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
35092
|
-
const hasMapDir = existsSync10(
|
|
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(
|
|
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 =
|
|
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 =
|
|
35110
|
-
if (existsSync10(
|
|
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(
|
|
35703
|
+
} else if (existsSync10(join14(roundDir, "discourse.md"))) {
|
|
35115
35704
|
phase = "synthesis";
|
|
35116
35705
|
phaseNumber = 7;
|
|
35117
|
-
} else if (existsSync10(
|
|
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(
|
|
35709
|
+
} else if (existsSync10(join14(sessionDir, "context.md"))) {
|
|
35121
35710
|
phase = "analysis";
|
|
35122
35711
|
phaseNumber = 3;
|
|
35123
|
-
} else if (existsSync10(
|
|
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 =
|
|
35129
|
-
if (existsSync10(
|
|
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(
|
|
35722
|
+
} else if (existsSync10(join14(runDir, "requirements-mapping.md"))) {
|
|
35134
35723
|
phase = "synthesis";
|
|
35135
35724
|
phaseNumber = 5;
|
|
35136
|
-
} else if (existsSync10(
|
|
35725
|
+
} else if (existsSync10(join14(runDir, "flow-analysis.md"))) {
|
|
35137
35726
|
phase = "requirements-mapping";
|
|
35138
35727
|
phaseNumber = 4;
|
|
35139
|
-
} else if (existsSync10(
|
|
35728
|
+
} else if (existsSync10(join14(runDir, "topology.md"))) {
|
|
35140
35729
|
phase = "flow-analysis";
|
|
35141
35730
|
phaseNumber = 3;
|
|
35142
|
-
} else if (existsSync10(
|
|
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.
|
|
35157
|
-
|
|
35158
|
-
|
|
35159
|
-
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
35313
|
-
|
|
35314
|
-
|
|
35315
|
-
|
|
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 =
|
|
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 =
|
|
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(
|
|
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 =
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
35775
|
-
|
|
35776
|
-
|
|
35777
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
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,
|
|
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
|
-
|
|
35957
|
-
|
|
35958
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
36019
|
-
*
|
|
36020
|
-
*
|
|
36021
|
-
* the
|
|
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
|
-
|
|
36042
|
-
|
|
36043
|
-
|
|
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
|
|
36060
|
-
} finally {
|
|
36061
|
-
diskDb?.close();
|
|
36649
|
+
console.error("[DbSyncWatcher] Error scanning for changes:", err);
|
|
36062
36650
|
}
|
|
36063
36651
|
}
|
|
36064
|
-
|
|
36065
|
-
|
|
36066
|
-
|
|
36067
|
-
|
|
36068
|
-
|
|
36069
|
-
|
|
36070
|
-
|
|
36071
|
-
|
|
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
|
|
36075
|
-
|
|
36076
|
-
|
|
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
|
-
|
|
36115
|
-
|
|
36116
|
-
|
|
36117
|
-
|
|
36118
|
-
|
|
36119
|
-
|
|
36120
|
-
|
|
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
|
-
|
|
36151
|
-
|
|
36152
|
-
|
|
36153
|
-
|
|
36154
|
-
|
|
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
|
-
|
|
36705
|
+
if (rows.length === 0) return;
|
|
36158
36706
|
const affectedSessions = /* @__PURE__ */ new Set();
|
|
36159
|
-
for (const row of
|
|
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
|
-
|
|
36201
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
*
|
|
36290
|
-
*
|
|
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
|
-
*
|
|
36342
|
-
*
|
|
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
|
|
36402
|
-
import { join as
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
36439
|
-
const finalPath =
|
|
36440
|
-
const reviewersDir =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
36798
|
-
if (
|
|
36799
|
-
return
|
|
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 ?
|
|
36970
|
-
const roundDir =
|
|
36971
|
-
const finalPath =
|
|
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 =
|
|
37437
|
+
const humanReviewPath = join16(roundDir, "final-human.md");
|
|
36977
37438
|
const repoRoot = dirname12(ocrDir);
|
|
36978
|
-
const commandMdPath =
|
|
37439
|
+
const commandMdPath = join16(ocrDir, "commands", "translate-review-to-single-human.md");
|
|
36979
37440
|
let commandContent;
|
|
36980
37441
|
try {
|
|
36981
|
-
commandContent =
|
|
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
|
|
37010
|
-
`- Instead of \`ocr state
|
|
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 =
|
|
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 ?
|
|
37139
|
-
const roundDir =
|
|
37140
|
-
|
|
37141
|
-
const filePath =
|
|
37142
|
-
|
|
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 =
|
|
37629
|
+
const tmpDir = join16(tmpdir2(), "ocr-post-comments");
|
|
37170
37630
|
try {
|
|
37171
|
-
|
|
37631
|
+
mkdirSync5(tmpDir, { recursive: true, mode: 448 });
|
|
37172
37632
|
} catch {
|
|
37173
37633
|
}
|
|
37174
|
-
const tmpFile =
|
|
37175
|
-
|
|
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
|
|
37221
|
-
import { join as
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
37332
|
-
const pidFilePath =
|
|
37333
|
-
const portFilePath =
|
|
37334
|
-
|
|
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(
|
|
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
|
-
|
|
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() -
|
|
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 (
|
|
37378
|
-
|
|
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
|
|
37411
|
-
"SELECT COUNT(*) as c FROM command_executions WHERE finished_at 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
|
|
37414
|
-
if (
|
|
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,
|
|
37874
|
+
SET exit_code = -2,
|
|
37418
37875
|
output = COALESCE(output, '') || '
|
|
37419
|
-
[Cancelled]'
|
|
37420
|
-
|
|
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
|
-
|
|
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
|
|
37428
|
-
|
|
37429
|
-
|
|
37882
|
+
const logAgentSweep = (result) => {
|
|
37883
|
+
if (result.orphanedIds.length === 0) return;
|
|
37884
|
+
const cascaded = result.cascadedWorkflowIds.length;
|
|
37430
37885
|
console.log(
|
|
37431
|
-
` Cleaned up ${
|
|
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
|
|
37452
|
-
app.use("/api/notes", createNotesRouter(db
|
|
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
|
|
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 =
|
|
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 =
|
|
37468
|
-
const rawIndexHtml = existsSync15(indexHtmlPath) ?
|
|
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 =
|
|
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
|
|
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
|
-
|
|
37507
|
-
|
|
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
|
-
|
|
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(
|
|
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);
|