@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/lib/db/index.js
CHANGED
|
@@ -1,9 +1,88 @@
|
|
|
1
1
|
// src/lib/db/index.ts
|
|
2
|
-
import { existsSync as
|
|
3
|
-
import { dirname as
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import
|
|
2
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, copyFileSync, statSync } from "node:fs";
|
|
3
|
+
import { dirname as dirname3, join as join3 } from "node:path";
|
|
4
|
+
|
|
5
|
+
// src/lib/db/engine.ts
|
|
6
|
+
import BetterSqlite3 from "better-sqlite3";
|
|
7
|
+
var BUSY_RETRY_ATTEMPTS = 5;
|
|
8
|
+
var BUSY_RETRY_BACKOFF_MS = 50;
|
|
9
|
+
function isBusyError(e) {
|
|
10
|
+
if (e instanceof BetterSqlite3.SqliteError) {
|
|
11
|
+
return e.code === "SQLITE_BUSY" || e.code === "SQLITE_BUSY_SNAPSHOT";
|
|
12
|
+
}
|
|
13
|
+
const code = e?.code;
|
|
14
|
+
return code === "SQLITE_BUSY" || code === "SQLITE_BUSY_SNAPSHOT";
|
|
15
|
+
}
|
|
16
|
+
function sleepSync(ms) {
|
|
17
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
18
|
+
}
|
|
19
|
+
var BetterSqliteAdapter = class {
|
|
20
|
+
raw;
|
|
21
|
+
constructor(db) {
|
|
22
|
+
this.raw = db;
|
|
23
|
+
}
|
|
24
|
+
exec(sql, params) {
|
|
25
|
+
const stmt = this.raw.prepare(sql);
|
|
26
|
+
if (!stmt.reader) {
|
|
27
|
+
stmt.run(...params ?? []);
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
const columns = stmt.columns().map((c) => c.name);
|
|
31
|
+
const values = stmt.raw().all(...params ?? []);
|
|
32
|
+
return values.length > 0 ? [{ columns, values }] : [];
|
|
33
|
+
}
|
|
34
|
+
run(sql, params) {
|
|
35
|
+
if (params !== void 0) {
|
|
36
|
+
this.raw.prepare(sql).run(...params);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
this.raw.exec(sql);
|
|
40
|
+
}
|
|
41
|
+
prepare(sql) {
|
|
42
|
+
return this.raw.prepare(sql);
|
|
43
|
+
}
|
|
44
|
+
transaction(fn) {
|
|
45
|
+
const tx = this.raw.transaction(fn);
|
|
46
|
+
for (let attempt = 0; ; attempt++) {
|
|
47
|
+
try {
|
|
48
|
+
return tx.immediate();
|
|
49
|
+
} catch (e) {
|
|
50
|
+
if (!isBusyError(e) || attempt >= BUSY_RETRY_ATTEMPTS - 1) throw e;
|
|
51
|
+
sleepSync(BUSY_RETRY_BACKOFF_MS);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
pragma(source) {
|
|
56
|
+
return this.raw.pragma(source);
|
|
57
|
+
}
|
|
58
|
+
close() {
|
|
59
|
+
try {
|
|
60
|
+
this.raw.pragma("wal_checkpoint(TRUNCATE)");
|
|
61
|
+
} catch {
|
|
62
|
+
}
|
|
63
|
+
this.raw.close();
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
function probeEngine() {
|
|
67
|
+
try {
|
|
68
|
+
const db = new BetterSqlite3(":memory:");
|
|
69
|
+
db.pragma("journal_mode = WAL");
|
|
70
|
+
db.exec("CREATE TABLE _probe(x); INSERT INTO _probe VALUES (1);");
|
|
71
|
+
const row = db.prepare("SELECT sqlite_version() AS v").get();
|
|
72
|
+
db.close();
|
|
73
|
+
return { ok: true, version: row.v };
|
|
74
|
+
} catch (e) {
|
|
75
|
+
return { ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function openEngine(dbPath) {
|
|
79
|
+
const native = new BetterSqlite3(dbPath);
|
|
80
|
+
native.pragma("journal_mode = WAL");
|
|
81
|
+
native.pragma("foreign_keys = ON");
|
|
82
|
+
native.pragma("busy_timeout = 5000");
|
|
83
|
+
native.pragma("synchronous = NORMAL");
|
|
84
|
+
return new BetterSqliteAdapter(native);
|
|
85
|
+
}
|
|
7
86
|
|
|
8
87
|
// src/lib/db/migrations.ts
|
|
9
88
|
var MIGRATIONS = [
|
|
@@ -327,8 +406,157 @@ var MIGRATIONS = [
|
|
|
327
406
|
DROP INDEX IF EXISTS idx_agent_sessions_status_heartbeat;
|
|
328
407
|
DROP TABLE IF EXISTS agent_sessions;
|
|
329
408
|
`
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
version: 12,
|
|
412
|
+
description: "Event-sourced lifecycle hardening: event_type taxonomy guard, sweep indexes, session_completeness view",
|
|
413
|
+
sql: `
|
|
414
|
+
-- \u2500\u2500 Indexes for the now-periodic stale-session sweep + round derivation \u2500\u2500
|
|
415
|
+
-- The sweep filters sessions by status and rolls up MAX(created_at) per
|
|
416
|
+
-- session over the event log; deriveNextRound does MAX(round). Index both.
|
|
417
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
|
|
418
|
+
CREATE INDEX IF NOT EXISTS idx_events_session_created
|
|
419
|
+
ON orchestration_events(session_id, created_at);
|
|
420
|
+
|
|
421
|
+
-- \u2500\u2500 Event-type taxonomy guard \u2500\u2500
|
|
422
|
+
-- orchestration_events.event_type is the spine of all lifecycle
|
|
423
|
+
-- derivation. A typo (e.g. 'round_complete' vs 'round_completed') would
|
|
424
|
+
-- silently break deriveNextRound and the completeness view. SQLite cannot
|
|
425
|
+
-- add a CHECK to an existing column without a table rebuild, so enforce
|
|
426
|
+
-- the closed vocabulary with a BEFORE INSERT trigger instead.
|
|
427
|
+
CREATE TRIGGER IF NOT EXISTS trg_events_known_type
|
|
428
|
+
BEFORE INSERT ON orchestration_events
|
|
429
|
+
WHEN NEW.event_type NOT IN (
|
|
430
|
+
'session_created', 'session_resumed', 'round_started', 'phase_transition',
|
|
431
|
+
'round_completed', 'map_completed', 'session_closed', 'session_aborted',
|
|
432
|
+
'session_auto_closed_stale', 'session_synced', 'session_legacy_import'
|
|
433
|
+
)
|
|
434
|
+
BEGIN
|
|
435
|
+
SELECT RAISE(ABORT, 'unknown orchestration_events.event_type');
|
|
436
|
+
END;
|
|
437
|
+
|
|
438
|
+
-- \u2500\u2500 Close-guard (DB backstop for the completion invariant) \u2500\u2500
|
|
439
|
+
-- A session cannot transition active \u2192 closed unless its current
|
|
440
|
+
-- round/run has a terminal artifact event, OR an explicit reason event
|
|
441
|
+
-- (abort / auto-close-stale / sync / legacy-import) is present. Only a
|
|
442
|
+
-- *silent* premature close is banned \u2014 every legitimate non-artifact
|
|
443
|
+
-- close carries a reason event and passes. App-level guards in
|
|
444
|
+
-- stateClose/finish are the primary check; this makes the illegal state
|
|
445
|
+
-- unrepresentable even via raw SQL.
|
|
446
|
+
--
|
|
447
|
+
-- DEFENCE-IN-DEPTH NOTE (intentional, documented gap): the reason-event
|
|
448
|
+
-- branch below (event_type IN (...)) is NOT round-scoped \u2014 a reason event
|
|
449
|
+
-- recorded for an earlier round would also satisfy a later close. The
|
|
450
|
+
-- app-level guards ARE round-scoped (hasCompletionInvariant checks the
|
|
451
|
+
-- current round/run), so the precise check lives in the application; this
|
|
452
|
+
-- trigger is a coarse backstop against a *silent* premature close via raw
|
|
453
|
+
-- SQL. Tightening it to be round-scoped would require a new migration
|
|
454
|
+
-- (this v12 trigger is append-only and already shipped); the residual
|
|
455
|
+
-- risk is a non-artifact close carrying a stale reason event, which is
|
|
456
|
+
-- still an explicit, audited terminal \u2014 not the failure mode this guards.
|
|
457
|
+
CREATE TRIGGER IF NOT EXISTS trg_sessions_close_guard
|
|
458
|
+
BEFORE UPDATE OF status ON sessions
|
|
459
|
+
WHEN NEW.status = 'closed' AND OLD.status <> 'closed'
|
|
460
|
+
AND NOT EXISTS (
|
|
461
|
+
SELECT 1 FROM orchestration_events e
|
|
462
|
+
WHERE e.session_id = NEW.id
|
|
463
|
+
AND (
|
|
464
|
+
(NEW.workflow_type = 'review' AND e.event_type = 'round_completed' AND e.round = NEW.current_round)
|
|
465
|
+
OR (NEW.workflow_type = 'map' AND e.event_type = 'map_completed' AND e.round = NEW.current_map_run)
|
|
466
|
+
OR e.event_type IN ('session_aborted','session_auto_closed_stale','session_synced','session_legacy_import')
|
|
467
|
+
)
|
|
468
|
+
)
|
|
469
|
+
BEGIN
|
|
470
|
+
SELECT RAISE(ABORT, 'cannot close session without a completed round/run or an explicit reason event');
|
|
471
|
+
END;
|
|
472
|
+
|
|
473
|
+
-- \u2500\u2500 session_completeness view \u2500\u2500
|
|
474
|
+
-- The published contract for "is this session actually complete, and if
|
|
475
|
+
-- not, what's missing". Completion is DERIVED from the event log, never a
|
|
476
|
+
-- mutable flag: a session is complete iff it is closed AND a terminal
|
|
477
|
+
-- artifact event exists for its current round/run. The dashboard's
|
|
478
|
+
-- outcome derivation and the agent 'status' command read this view, so
|
|
479
|
+
-- they cannot disagree.
|
|
480
|
+
--
|
|
481
|
+
-- completeness_state is an INTENTIONAL HYBRID: it combines the mutable
|
|
482
|
+
-- status column (marked_closed) with append-only event evidence (the
|
|
483
|
+
-- terminal artifact event). This is sound precisely because the
|
|
484
|
+
-- close-guard trigger above makes the status column trustworthy \u2014 a row
|
|
485
|
+
-- can only reach status='closed' with a completed round/run or an
|
|
486
|
+
-- explicit reason event \u2014 so reading the column is not a regression to
|
|
487
|
+
-- the old "mutable flag that could lie" model.
|
|
488
|
+
--
|
|
489
|
+
-- completeness_state:
|
|
490
|
+
-- 'complete' \u2014 closed + terminal artifact for current round/run
|
|
491
|
+
-- 'closed_without_artifact' \u2014 closed but no terminal artifact (the
|
|
492
|
+
-- "completed too soon" condition)
|
|
493
|
+
-- 'in_flight' \u2014 open with a dependent process still running
|
|
494
|
+
-- 'open_no_artifact' \u2014 open, no in-flight dependents
|
|
495
|
+
CREATE VIEW IF NOT EXISTS session_completeness AS
|
|
496
|
+
SELECT
|
|
497
|
+
s.id AS session_id,
|
|
498
|
+
s.workflow_type AS workflow_type,
|
|
499
|
+
s.status AS status,
|
|
500
|
+
s.current_round AS current_round,
|
|
501
|
+
s.current_map_run AS current_map_run,
|
|
502
|
+
CASE WHEN EXISTS (
|
|
503
|
+
SELECT 1 FROM orchestration_events e
|
|
504
|
+
WHERE e.session_id = s.id
|
|
505
|
+
AND (
|
|
506
|
+
(s.workflow_type = 'review' AND e.event_type = 'round_completed' AND e.round = s.current_round)
|
|
507
|
+
OR (s.workflow_type = 'map' AND e.event_type = 'map_completed' AND e.round = s.current_map_run)
|
|
508
|
+
)
|
|
509
|
+
) THEN 1 ELSE 0 END AS has_terminal_artifact,
|
|
510
|
+
CASE WHEN s.status = 'closed' THEN 1 ELSE 0 END AS marked_closed,
|
|
511
|
+
CASE WHEN NOT EXISTS (
|
|
512
|
+
SELECT 1 FROM command_executions ce
|
|
513
|
+
WHERE ce.workflow_id = s.id AND ce.finished_at IS NULL
|
|
514
|
+
) THEN 1 ELSE 0 END AS dependents_settled,
|
|
515
|
+
CASE
|
|
516
|
+
WHEN s.status = 'closed' AND EXISTS (
|
|
517
|
+
SELECT 1 FROM orchestration_events e
|
|
518
|
+
WHERE e.session_id = s.id
|
|
519
|
+
AND (
|
|
520
|
+
(s.workflow_type = 'review' AND e.event_type = 'round_completed' AND e.round = s.current_round)
|
|
521
|
+
OR (s.workflow_type = 'map' AND e.event_type = 'map_completed' AND e.round = s.current_map_run)
|
|
522
|
+
)
|
|
523
|
+
) THEN 'complete'
|
|
524
|
+
WHEN s.status = 'closed' THEN 'closed_without_artifact'
|
|
525
|
+
WHEN EXISTS (
|
|
526
|
+
SELECT 1 FROM command_executions ce
|
|
527
|
+
WHERE ce.workflow_id = s.id AND ce.finished_at IS NULL
|
|
528
|
+
) THEN 'in_flight'
|
|
529
|
+
ELSE 'open_no_artifact'
|
|
530
|
+
END AS completeness_state
|
|
531
|
+
FROM sessions s;
|
|
532
|
+
`
|
|
533
|
+
},
|
|
534
|
+
{
|
|
535
|
+
version: 13,
|
|
536
|
+
description: "Retire dead parent_id column on command_executions (never written; row kind is derived from command)",
|
|
537
|
+
// parent_id was reserved for an AI-instance → dashboard-spawn lineage link
|
|
538
|
+
// that was never wired (no writer, no reader). A process's KIND (supervisor
|
|
539
|
+
// / reviewer-instance / utility) is derived from columns that are always
|
|
540
|
+
// present (command + last_heartbeat_at), so the dead lineage column and its
|
|
541
|
+
// all-NULL index are removed. Re-add a wired parent_id alongside a real
|
|
542
|
+
// consumer (e.g. a parent→child tree view) if lineage is ever needed.
|
|
543
|
+
//
|
|
544
|
+
// Imperative + guarded so the DROP COLUMN (which SQLite can't express as
|
|
545
|
+
// IF EXISTS) is idempotent under re-application.
|
|
546
|
+
run: (db) => {
|
|
547
|
+
if (!columnExists(db, "command_executions", "parent_id")) return;
|
|
548
|
+
db.run("DROP INDEX IF EXISTS idx_command_executions_parent;");
|
|
549
|
+
db.run("ALTER TABLE command_executions DROP COLUMN parent_id;");
|
|
550
|
+
}
|
|
330
551
|
}
|
|
331
552
|
];
|
|
553
|
+
function columnExists(db, table, column) {
|
|
554
|
+
const result = db.exec(`PRAGMA table_info(${table})`);
|
|
555
|
+
const first = result[0];
|
|
556
|
+
if (!first) return false;
|
|
557
|
+
const nameIdx = first.columns.indexOf("name");
|
|
558
|
+
return first.values.some((row) => row[nameIdx] === column);
|
|
559
|
+
}
|
|
332
560
|
function ensureSchemaVersionTable(db) {
|
|
333
561
|
db.run(`
|
|
334
562
|
CREATE TABLE IF NOT EXISTS schema_version (
|
|
@@ -338,6 +566,10 @@ function ensureSchemaVersionTable(db) {
|
|
|
338
566
|
);
|
|
339
567
|
`);
|
|
340
568
|
}
|
|
569
|
+
function getSchemaVersion(db) {
|
|
570
|
+
ensureSchemaVersionTable(db);
|
|
571
|
+
return getCurrentVersion(db);
|
|
572
|
+
}
|
|
341
573
|
function getCurrentVersion(db) {
|
|
342
574
|
const result = db.exec(
|
|
343
575
|
"SELECT MAX(version) as v FROM schema_version"
|
|
@@ -355,9 +587,10 @@ function runMigrations(db) {
|
|
|
355
587
|
if (migration.version <= currentVersion) {
|
|
356
588
|
continue;
|
|
357
589
|
}
|
|
358
|
-
db.run("BEGIN
|
|
590
|
+
db.run("BEGIN IMMEDIATE;");
|
|
359
591
|
try {
|
|
360
|
-
db.run(migration.sql);
|
|
592
|
+
if (migration.sql) db.run(migration.sql);
|
|
593
|
+
migration.run?.(db);
|
|
361
594
|
db.run(
|
|
362
595
|
"INSERT INTO schema_version (version, description) VALUES (?, ?);",
|
|
363
596
|
[migration.version, migration.description]
|
|
@@ -370,6 +603,10 @@ function runMigrations(db) {
|
|
|
370
603
|
}
|
|
371
604
|
}
|
|
372
605
|
|
|
606
|
+
// src/lib/db/reconcile.ts
|
|
607
|
+
import { existsSync } from "node:fs";
|
|
608
|
+
import { isAbsolute, join, dirname } from "node:path";
|
|
609
|
+
|
|
373
610
|
// src/lib/db/result-mapper.ts
|
|
374
611
|
function resultToRows(result) {
|
|
375
612
|
if (result.length === 0 || !result[0]) {
|
|
@@ -497,11 +734,196 @@ function getLatestEventId(db) {
|
|
|
497
734
|
const val = result[0]?.values[0]?.[0];
|
|
498
735
|
return typeof val === "number" ? val : 0;
|
|
499
736
|
}
|
|
737
|
+
function commitReasonClose(db, sessionId, reasonEvent, projectionUpdates) {
|
|
738
|
+
db.transaction(() => {
|
|
739
|
+
insertEvent(db, { session_id: sessionId, ...reasonEvent });
|
|
740
|
+
updateSession(db, sessionId, projectionUpdates);
|
|
741
|
+
});
|
|
742
|
+
}
|
|
500
743
|
|
|
501
|
-
// src/lib/db/
|
|
502
|
-
var
|
|
744
|
+
// src/lib/db/reconcile.ts
|
|
745
|
+
var DEFAULT_STALE_THRESHOLD_SECONDS = 7 * 24 * 60 * 60;
|
|
746
|
+
function hasTerminalArtifactEvent(db, sessionId, workflowType, currentRound, currentMapRun) {
|
|
747
|
+
const eventType = workflowType === "map" ? "map_completed" : "round_completed";
|
|
748
|
+
const round = workflowType === "map" ? currentMapRun : currentRound;
|
|
749
|
+
const r = db.exec(
|
|
750
|
+
`SELECT 1 FROM orchestration_events
|
|
751
|
+
WHERE session_id = ? AND event_type = ? AND round = ? LIMIT 1`,
|
|
752
|
+
[sessionId, eventType, round]
|
|
753
|
+
);
|
|
754
|
+
return (r[0]?.values.length ?? 0) > 0;
|
|
755
|
+
}
|
|
756
|
+
function hasReasonEvent(db, sessionId) {
|
|
757
|
+
const r = db.exec(
|
|
758
|
+
`SELECT 1 FROM orchestration_events
|
|
759
|
+
WHERE session_id = ?
|
|
760
|
+
AND event_type IN ('session_aborted','session_auto_closed_stale','session_synced','session_legacy_import')
|
|
761
|
+
LIMIT 1`,
|
|
762
|
+
[sessionId]
|
|
763
|
+
);
|
|
764
|
+
return (r[0]?.values.length ?? 0) > 0;
|
|
765
|
+
}
|
|
766
|
+
function lastEventAgeSeconds(db, sessionId) {
|
|
767
|
+
const r = db.exec(
|
|
768
|
+
`SELECT (julianday('now') - julianday(MAX(created_at))) * 86400
|
|
769
|
+
FROM orchestration_events WHERE session_id = ?`,
|
|
770
|
+
[sessionId]
|
|
771
|
+
);
|
|
772
|
+
const v = r[0]?.values[0]?.[0];
|
|
773
|
+
return typeof v === "number" ? v : null;
|
|
774
|
+
}
|
|
775
|
+
function hasInFlightDependents(db, sessionId) {
|
|
776
|
+
const r = db.exec(
|
|
777
|
+
`SELECT 1 FROM command_executions
|
|
778
|
+
WHERE workflow_id = ? AND finished_at IS NULL LIMIT 1`,
|
|
779
|
+
[sessionId]
|
|
780
|
+
);
|
|
781
|
+
return (r[0]?.values.length ?? 0) > 0;
|
|
782
|
+
}
|
|
783
|
+
function resolveSessionDir(ocrDir, sessionDir) {
|
|
784
|
+
if (!sessionDir) return null;
|
|
785
|
+
if (isAbsolute(sessionDir)) return sessionDir;
|
|
786
|
+
return join(dirname(ocrDir), sessionDir);
|
|
787
|
+
}
|
|
788
|
+
function reconcileLegacyState(db, ocrDir, opts = {}) {
|
|
789
|
+
const dryRun = opts.dryRun ?? false;
|
|
790
|
+
const threshold = opts.staleThresholdSeconds ?? DEFAULT_STALE_THRESHOLD_SECONDS;
|
|
791
|
+
const actions = [];
|
|
792
|
+
for (const s of getAllSessions(db)) {
|
|
793
|
+
const dir = resolveSessionDir(ocrDir, s.session_dir);
|
|
794
|
+
if (s.status === "closed") {
|
|
795
|
+
if (hasTerminalArtifactEvent(db, s.id, s.workflow_type, s.current_round, s.current_map_run) || hasReasonEvent(db, s.id)) {
|
|
796
|
+
continue;
|
|
797
|
+
}
|
|
798
|
+
const reviewFinal = s.workflow_type === "review" && dir ? existsSync(join(dir, "rounds", `round-${s.current_round}`, "final.md")) : false;
|
|
799
|
+
const mapFinal = s.workflow_type === "map" && dir ? existsSync(join(dir, "map", "runs", `run-${s.current_map_run}`, "map.md")) : false;
|
|
800
|
+
if (reviewFinal) {
|
|
801
|
+
actions.push({
|
|
802
|
+
sessionId: s.id,
|
|
803
|
+
kind: "synthesize-round-completed",
|
|
804
|
+
detail: `final.md present for round ${s.current_round}; synthesizing round_completed`
|
|
805
|
+
});
|
|
806
|
+
if (!dryRun) {
|
|
807
|
+
insertEvent(db, {
|
|
808
|
+
session_id: s.id,
|
|
809
|
+
event_type: "round_completed",
|
|
810
|
+
phase: "synthesis",
|
|
811
|
+
phase_number: 7,
|
|
812
|
+
round: s.current_round,
|
|
813
|
+
metadata: JSON.stringify({ source: "reconciled", synthesized_from: "final.md" })
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
} else if (mapFinal) {
|
|
817
|
+
actions.push({
|
|
818
|
+
sessionId: s.id,
|
|
819
|
+
kind: "synthesize-map-completed",
|
|
820
|
+
detail: `map.md present for run ${s.current_map_run}; synthesizing map_completed`
|
|
821
|
+
});
|
|
822
|
+
if (!dryRun) {
|
|
823
|
+
insertEvent(db, {
|
|
824
|
+
session_id: s.id,
|
|
825
|
+
event_type: "map_completed",
|
|
826
|
+
phase: "synthesis",
|
|
827
|
+
phase_number: 5,
|
|
828
|
+
round: s.current_map_run,
|
|
829
|
+
metadata: JSON.stringify({ source: "reconciled", synthesized_from: "map.md" })
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
} else {
|
|
833
|
+
actions.push({
|
|
834
|
+
sessionId: s.id,
|
|
835
|
+
kind: "grandfather",
|
|
836
|
+
detail: "no provable artifact; recording session_legacy_import"
|
|
837
|
+
});
|
|
838
|
+
if (!dryRun) {
|
|
839
|
+
insertEvent(db, {
|
|
840
|
+
session_id: s.id,
|
|
841
|
+
event_type: "session_legacy_import",
|
|
842
|
+
phase: "complete",
|
|
843
|
+
metadata: JSON.stringify({ source: "reconciled" })
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
continue;
|
|
848
|
+
}
|
|
849
|
+
const age = lastEventAgeSeconds(db, s.id);
|
|
850
|
+
const stale = (age === null || age > threshold) && !hasInFlightDependents(db, s.id);
|
|
851
|
+
if (stale) {
|
|
852
|
+
actions.push({
|
|
853
|
+
sessionId: s.id,
|
|
854
|
+
kind: "stale-close",
|
|
855
|
+
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`
|
|
856
|
+
});
|
|
857
|
+
if (!dryRun) {
|
|
858
|
+
commitReasonClose(
|
|
859
|
+
db,
|
|
860
|
+
s.id,
|
|
861
|
+
{
|
|
862
|
+
event_type: "session_auto_closed_stale",
|
|
863
|
+
phase: "complete",
|
|
864
|
+
metadata: JSON.stringify({ source: "reconciled", threshold_seconds: threshold })
|
|
865
|
+
},
|
|
866
|
+
{ status: "closed", current_phase: "complete" }
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
return { dryRun, actions };
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// src/lib/db/liveness.ts
|
|
875
|
+
var PID_REUSE_GUARD_MS = 24 * 60 * 60 * 1e3;
|
|
876
|
+
function defaultIsAlive(pid) {
|
|
877
|
+
try {
|
|
878
|
+
process.kill(pid, 0);
|
|
879
|
+
return true;
|
|
880
|
+
} catch (err) {
|
|
881
|
+
return !(err instanceof Error && "code" in err && err.code === "ESRCH");
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
function sqliteUtcMs(ts) {
|
|
885
|
+
const sqliteShape = ts.includes(" ");
|
|
886
|
+
return new Date(sqliteShape ? ts.replace(" ", "T") + "Z" : ts).getTime();
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// src/lib/state/exit-codes.ts
|
|
890
|
+
var STATE_EXIT = {
|
|
891
|
+
OK: 0,
|
|
892
|
+
USAGE: 2,
|
|
893
|
+
AMBIGUOUS: 3,
|
|
894
|
+
NOT_FOUND: 4,
|
|
895
|
+
ILLEGAL_TRANSITION: 5,
|
|
896
|
+
INVARIANT_UNMET: 6,
|
|
897
|
+
SCHEMA_INVALID: 7,
|
|
898
|
+
/** Database was locked past the bounded retry budget (SQLITE_BUSY). */
|
|
899
|
+
BUSY: 8
|
|
900
|
+
};
|
|
901
|
+
var StateError = class extends Error {
|
|
902
|
+
constructor(code, message) {
|
|
903
|
+
super(message);
|
|
904
|
+
this.code = code;
|
|
905
|
+
this.name = "StateError";
|
|
906
|
+
}
|
|
907
|
+
};
|
|
503
908
|
var CANCELLED_EXIT_CODE = -2;
|
|
909
|
+
var ORPHAN_EXIT_CODE = -3;
|
|
910
|
+
var CASCADE_CLOSE_EXIT_CODE = -4;
|
|
911
|
+
|
|
912
|
+
// src/lib/db/agent-sessions.ts
|
|
504
913
|
var NOTE_ORPHAN_PREFIX = "orphaned by liveness sweep";
|
|
914
|
+
var INSTANCE_COMMAND = "session-instance";
|
|
915
|
+
function cascadeTerminateExecutions(db, workflowId, exitCode, note) {
|
|
916
|
+
db.run(
|
|
917
|
+
`UPDATE command_executions
|
|
918
|
+
SET finished_at = datetime('now'),
|
|
919
|
+
exit_code = ?,
|
|
920
|
+
pid = NULL,
|
|
921
|
+
notes = COALESCE(notes || char(10), '') || ?
|
|
922
|
+
WHERE workflow_id = ?
|
|
923
|
+
AND finished_at IS NULL`,
|
|
924
|
+
[exitCode, note, workflowId]
|
|
925
|
+
);
|
|
926
|
+
}
|
|
505
927
|
function rowToAgentSession(row) {
|
|
506
928
|
return {
|
|
507
929
|
// The OCR-owned id is the `uid` column. Fall back to the integer
|
|
@@ -516,6 +938,7 @@ function rowToAgentSession(row) {
|
|
|
516
938
|
resolved_model: row.resolved_model,
|
|
517
939
|
phase: null,
|
|
518
940
|
status: deriveStatus(row),
|
|
941
|
+
kind: rowKind(row),
|
|
519
942
|
pid: row.pid,
|
|
520
943
|
started_at: row.started_at,
|
|
521
944
|
last_heartbeat_at: row.last_heartbeat_at ?? row.started_at,
|
|
@@ -529,7 +952,9 @@ function deriveStatus(row) {
|
|
|
529
952
|
return "running";
|
|
530
953
|
}
|
|
531
954
|
if (row.exit_code === ORPHAN_EXIT_CODE) return "orphaned";
|
|
532
|
-
if (row.exit_code === CANCELLED_EXIT_CODE)
|
|
955
|
+
if (row.exit_code === CANCELLED_EXIT_CODE || row.exit_code === CASCADE_CLOSE_EXIT_CODE) {
|
|
956
|
+
return "cancelled";
|
|
957
|
+
}
|
|
533
958
|
if (row.exit_code === 0) return "done";
|
|
534
959
|
return "crashed";
|
|
535
960
|
}
|
|
@@ -545,7 +970,7 @@ function insertAgentSession(db, params) {
|
|
|
545
970
|
pid = null,
|
|
546
971
|
notes = null
|
|
547
972
|
} = params;
|
|
548
|
-
const command = persona && instance_index !== null ?
|
|
973
|
+
const command = persona && instance_index !== null ? `${INSTANCE_COMMAND}:${persona}-${instance_index}` : INSTANCE_COMMAND;
|
|
549
974
|
db.run(
|
|
550
975
|
`INSERT INTO command_executions
|
|
551
976
|
(uid, command, args, workflow_id, vendor, persona, instance_index, name,
|
|
@@ -750,38 +1175,112 @@ function updateAgentSession(db, id, params) {
|
|
|
750
1175
|
values
|
|
751
1176
|
);
|
|
752
1177
|
}
|
|
753
|
-
function sweepStaleAgentSessions(db, thresholdSeconds) {
|
|
754
|
-
const
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
1178
|
+
function sweepStaleAgentSessions(db, thresholdSeconds, isAlive = defaultIsAlive) {
|
|
1179
|
+
const candidates = resultToRows(
|
|
1180
|
+
db.exec(
|
|
1181
|
+
`SELECT uid, id, pid, started_at, workflow_id, command, last_heartbeat_at
|
|
1182
|
+
FROM command_executions
|
|
1183
|
+
WHERE finished_at IS NULL
|
|
1184
|
+
AND pid IS NOT NULL
|
|
1185
|
+
AND last_heartbeat_at IS NOT NULL
|
|
1186
|
+
AND (julianday('now') - julianday(last_heartbeat_at)) * 86400 > ?`,
|
|
1187
|
+
[thresholdSeconds]
|
|
1188
|
+
)
|
|
762
1189
|
);
|
|
763
|
-
if (
|
|
764
|
-
return { orphanedIds: [] };
|
|
1190
|
+
if (candidates.length === 0) {
|
|
1191
|
+
return { orphanedIds: [], cascadedWorkflowIds: [] };
|
|
1192
|
+
}
|
|
1193
|
+
const reuseCutoffMs = Date.now() - PID_REUSE_GUARD_MS;
|
|
1194
|
+
const dead = candidates.filter((row) => {
|
|
1195
|
+
if (row.pid === null) return false;
|
|
1196
|
+
if (sqliteUtcMs(row.started_at) < reuseCutoffMs) return false;
|
|
1197
|
+
return !isAlive(row.pid);
|
|
1198
|
+
});
|
|
1199
|
+
if (dead.length === 0) {
|
|
1200
|
+
return { orphanedIds: [], cascadedWorkflowIds: [] };
|
|
765
1201
|
}
|
|
766
1202
|
const note = `${NOTE_ORPHAN_PREFIX} (threshold ${thresholdSeconds}s)`;
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
1203
|
+
const placeholders = dead.map(() => "?").join(", ");
|
|
1204
|
+
const cascadedWorkflowIds = [];
|
|
1205
|
+
db.transaction(() => {
|
|
1206
|
+
db.run(
|
|
1207
|
+
`UPDATE command_executions
|
|
1208
|
+
SET finished_at = datetime('now'),
|
|
1209
|
+
exit_code = ?,
|
|
1210
|
+
pid = NULL,
|
|
1211
|
+
notes = COALESCE(notes || char(10), '') || ?
|
|
1212
|
+
WHERE id IN (${placeholders})
|
|
1213
|
+
AND finished_at IS NULL`,
|
|
1214
|
+
[ORPHAN_EXIT_CODE, note, ...dead.map((r) => r.id)]
|
|
1215
|
+
);
|
|
1216
|
+
for (const row of dead) {
|
|
1217
|
+
if (row.workflow_id && rowKind(row) === "supervisor") {
|
|
1218
|
+
cascadeTerminateExecutions(
|
|
1219
|
+
db,
|
|
1220
|
+
row.workflow_id,
|
|
1221
|
+
CASCADE_CLOSE_EXIT_CODE,
|
|
1222
|
+
"cascade-closed: workflow process orphaned by liveness sweep"
|
|
1223
|
+
);
|
|
1224
|
+
cascadedWorkflowIds.push(row.workflow_id);
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
});
|
|
777
1228
|
return {
|
|
778
|
-
orphanedIds:
|
|
1229
|
+
orphanedIds: dead.map((r) => r.uid ?? String(r.id)),
|
|
1230
|
+
cascadedWorkflowIds
|
|
779
1231
|
};
|
|
780
1232
|
}
|
|
1233
|
+
function rowKind(row) {
|
|
1234
|
+
if (row.command === INSTANCE_COMMAND || row.command.startsWith(`${INSTANCE_COMMAND}:`)) {
|
|
1235
|
+
return "instance";
|
|
1236
|
+
}
|
|
1237
|
+
return row.last_heartbeat_at == null ? "utility" : "supervisor";
|
|
1238
|
+
}
|
|
1239
|
+
function sweepStaleSessions(db, thresholdSeconds) {
|
|
1240
|
+
const sql = `
|
|
1241
|
+
SELECT s.id
|
|
1242
|
+
FROM sessions s
|
|
1243
|
+
LEFT JOIN (
|
|
1244
|
+
SELECT session_id, MAX(created_at) AS last_event_at
|
|
1245
|
+
FROM orchestration_events
|
|
1246
|
+
GROUP BY session_id
|
|
1247
|
+
) e ON e.session_id = s.id
|
|
1248
|
+
WHERE s.status = 'active'
|
|
1249
|
+
AND (
|
|
1250
|
+
e.last_event_at IS NULL
|
|
1251
|
+
OR (julianday('now') - julianday(e.last_event_at)) * 86400 > ?
|
|
1252
|
+
)
|
|
1253
|
+
AND NOT EXISTS (
|
|
1254
|
+
SELECT 1 FROM command_executions ce
|
|
1255
|
+
WHERE ce.workflow_id = s.id
|
|
1256
|
+
AND ce.finished_at IS NULL
|
|
1257
|
+
)
|
|
1258
|
+
`;
|
|
1259
|
+
const rows = resultToRows(db.exec(sql, [thresholdSeconds]));
|
|
1260
|
+
if (rows.length === 0) {
|
|
1261
|
+
return { closedSessionIds: [] };
|
|
1262
|
+
}
|
|
1263
|
+
for (const row of rows) {
|
|
1264
|
+
commitReasonClose(
|
|
1265
|
+
db,
|
|
1266
|
+
row.id,
|
|
1267
|
+
{
|
|
1268
|
+
event_type: "session_auto_closed_stale",
|
|
1269
|
+
phase: "complete",
|
|
1270
|
+
metadata: JSON.stringify({
|
|
1271
|
+
reason: "no events past threshold; no in-flight dependents",
|
|
1272
|
+
threshold_seconds: thresholdSeconds
|
|
1273
|
+
})
|
|
1274
|
+
},
|
|
1275
|
+
{ status: "closed", current_phase: "complete" }
|
|
1276
|
+
);
|
|
1277
|
+
}
|
|
1278
|
+
return { closedSessionIds: rows.map((r) => r.id) };
|
|
1279
|
+
}
|
|
781
1280
|
|
|
782
1281
|
// src/lib/db/command-log.ts
|
|
783
|
-
import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
784
|
-
import { dirname, join } from "node:path";
|
|
1282
|
+
import { appendFileSync, existsSync as existsSync2, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
1283
|
+
import { dirname as dirname2, join as join2 } from "node:path";
|
|
785
1284
|
import { randomUUID } from "node:crypto";
|
|
786
1285
|
var CACHE_DIR = ".cache";
|
|
787
1286
|
var FILENAME = "command-history.jsonl";
|
|
@@ -792,16 +1291,16 @@ function generateCommandUid() {
|
|
|
792
1291
|
return randomUUID();
|
|
793
1292
|
}
|
|
794
1293
|
function cacheDir(ocrDir) {
|
|
795
|
-
return
|
|
1294
|
+
return join2(ocrDir, "data", CACHE_DIR);
|
|
796
1295
|
}
|
|
797
1296
|
function commandLogPath(ocrDir) {
|
|
798
|
-
return
|
|
1297
|
+
return join2(cacheDir(ocrDir), FILENAME);
|
|
799
1298
|
}
|
|
800
1299
|
function appendCommandLog(ocrDir, entry) {
|
|
801
1300
|
try {
|
|
802
1301
|
const filePath = commandLogPath(ocrDir);
|
|
803
|
-
const dir =
|
|
804
|
-
if (!
|
|
1302
|
+
const dir = dirname2(filePath);
|
|
1303
|
+
if (!existsSync2(dir)) mkdirSync(dir, { recursive: true });
|
|
805
1304
|
const line = JSON.stringify(entry) + "\n";
|
|
806
1305
|
appendFileSync(filePath, line, { encoding: "utf-8" });
|
|
807
1306
|
if (approxLineCount >= 0) approxLineCount++;
|
|
@@ -811,7 +1310,7 @@ function appendCommandLog(ocrDir, entry) {
|
|
|
811
1310
|
}
|
|
812
1311
|
function readCommandLog(ocrDir) {
|
|
813
1312
|
const filePath = commandLogPath(ocrDir);
|
|
814
|
-
if (!
|
|
1313
|
+
if (!existsSync2(filePath)) return [];
|
|
815
1314
|
const content = readFileSync(filePath, "utf-8");
|
|
816
1315
|
const entries = [];
|
|
817
1316
|
for (const line of content.split("\n")) {
|
|
@@ -877,93 +1376,117 @@ function rotateIfNeeded(filePath) {
|
|
|
877
1376
|
}
|
|
878
1377
|
|
|
879
1378
|
// src/lib/db/index.ts
|
|
880
|
-
var
|
|
881
|
-
function
|
|
882
|
-
|
|
883
|
-
const
|
|
884
|
-
|
|
1379
|
+
var V2_SCHEMA_VERSION = 12;
|
|
1380
|
+
function maybeSnapshotBeforeUpgrade(db, dbPath, fromVersion) {
|
|
1381
|
+
if (fromVersion < 1 || fromVersion >= V2_SCHEMA_VERSION) return null;
|
|
1382
|
+
const bakPath = `${dbPath}.bak.v${fromVersion}`;
|
|
1383
|
+
if (existsSync3(bakPath)) return bakPath;
|
|
1384
|
+
try {
|
|
1385
|
+
if (!existsSync3(dbPath) || statSync(dbPath).size === 0) return null;
|
|
1386
|
+
db.pragma("wal_checkpoint(TRUNCATE)");
|
|
1387
|
+
copyFileSync(dbPath, bakPath);
|
|
1388
|
+
return bakPath;
|
|
1389
|
+
} catch {
|
|
1390
|
+
return null;
|
|
1391
|
+
}
|
|
885
1392
|
}
|
|
886
|
-
function
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
1393
|
+
function formatUpgradeNotice(bakPath, reconcile) {
|
|
1394
|
+
const lines = [
|
|
1395
|
+
"Storage upgraded to v2.0 \u2014 durable SQLite engine (WAL), event-sourced lifecycle."
|
|
1396
|
+
];
|
|
1397
|
+
if (bakPath) {
|
|
1398
|
+
lines.push(` A backup of your previous database was saved to: ${bakPath}`);
|
|
1399
|
+
}
|
|
1400
|
+
const repairs = (reconcile?.actions ?? []).filter((a) => a.kind !== "ok");
|
|
1401
|
+
if (repairs.length > 0) {
|
|
1402
|
+
const n = (kind) => repairs.filter((a) => a.kind === kind).length;
|
|
1403
|
+
const parts = [];
|
|
1404
|
+
const finalized = n("synthesize-round-completed") + n("synthesize-map-completed");
|
|
1405
|
+
if (finalized > 0) parts.push(`${finalized} finalized from artifacts`);
|
|
1406
|
+
if (n("grandfather") > 0) parts.push(`${n("grandfather")} grandfathered`);
|
|
1407
|
+
if (n("stale-close") > 0) parts.push(`${n("stale-close")} stale closed`);
|
|
1408
|
+
lines.push(
|
|
1409
|
+
` Reconciled ${repairs.length} legacy session(s): ${parts.join(", ")}.`
|
|
1410
|
+
);
|
|
1411
|
+
}
|
|
1412
|
+
lines.push(" Run `ocr doctor` to verify the storage engine.");
|
|
1413
|
+
return lines.map((l) => `[ocr] ${l}`).join("\n");
|
|
890
1414
|
}
|
|
1415
|
+
var connections = /* @__PURE__ */ new Map();
|
|
891
1416
|
async function openDatabase(dbPath) {
|
|
892
1417
|
const cached = connections.get(dbPath);
|
|
893
1418
|
if (cached) {
|
|
894
1419
|
return cached;
|
|
895
1420
|
}
|
|
896
|
-
const
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
wasmBuffer.byteOffset + wasmBuffer.byteLength
|
|
900
|
-
);
|
|
901
|
-
const SQL = await initSqlJs({
|
|
902
|
-
wasmBinary
|
|
903
|
-
});
|
|
904
|
-
let db;
|
|
905
|
-
if (existsSync2(dbPath)) {
|
|
906
|
-
const fileBuffer = readFileSync2(dbPath);
|
|
907
|
-
db = new SQL.Database(fileBuffer);
|
|
908
|
-
} else {
|
|
909
|
-
db = new SQL.Database();
|
|
1421
|
+
const dir = dirname3(dbPath);
|
|
1422
|
+
if (!existsSync3(dir)) {
|
|
1423
|
+
mkdirSync2(dir, { recursive: true });
|
|
910
1424
|
}
|
|
911
|
-
|
|
1425
|
+
const db = openEngine(dbPath);
|
|
912
1426
|
connections.set(dbPath, db);
|
|
913
1427
|
return db;
|
|
914
1428
|
}
|
|
915
|
-
function saveDatabase(db, dbPath) {
|
|
916
|
-
const data = db.export();
|
|
917
|
-
const dir = dirname2(dbPath);
|
|
918
|
-
if (!existsSync2(dir)) {
|
|
919
|
-
mkdirSync2(dir, { recursive: true });
|
|
920
|
-
}
|
|
921
|
-
const tmpPath = `${dbPath}.${process.pid}.tmp`;
|
|
922
|
-
writeFileSync2(tmpPath, Buffer.from(data));
|
|
923
|
-
renameSync2(tmpPath, dbPath);
|
|
924
|
-
}
|
|
925
1429
|
async function getDb(ocrDir) {
|
|
926
|
-
const dbPath =
|
|
1430
|
+
const dbPath = join3(ocrDir, "data", "ocr.db");
|
|
927
1431
|
return openDatabase(dbPath);
|
|
928
1432
|
}
|
|
929
1433
|
async function ensureDatabase(ocrDir) {
|
|
930
|
-
const dataDir =
|
|
931
|
-
if (!
|
|
1434
|
+
const dataDir = join3(ocrDir, "data");
|
|
1435
|
+
if (!existsSync3(dataDir)) {
|
|
932
1436
|
mkdirSync2(dataDir, { recursive: true });
|
|
933
1437
|
}
|
|
934
|
-
const dbPath =
|
|
1438
|
+
const dbPath = join3(dataDir, "ocr.db");
|
|
935
1439
|
const db = await openDatabase(dbPath);
|
|
1440
|
+
let before = 0;
|
|
1441
|
+
try {
|
|
1442
|
+
before = getSchemaVersion(db);
|
|
1443
|
+
} catch {
|
|
1444
|
+
before = 0;
|
|
1445
|
+
}
|
|
1446
|
+
const isLegacyUpgrade = before >= 1 && before < V2_SCHEMA_VERSION;
|
|
1447
|
+
const bakPath = maybeSnapshotBeforeUpgrade(db, dbPath, before);
|
|
936
1448
|
runMigrations(db);
|
|
937
|
-
|
|
1449
|
+
let reconcile;
|
|
1450
|
+
if (before < V2_SCHEMA_VERSION) {
|
|
1451
|
+
try {
|
|
1452
|
+
reconcile = reconcileLegacyState(db, ocrDir);
|
|
1453
|
+
} catch (err) {
|
|
1454
|
+
console.error(
|
|
1455
|
+
`[ocr] legacy reconciliation skipped: ${err instanceof Error ? err.message : String(err)}`
|
|
1456
|
+
);
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
if (isLegacyUpgrade) {
|
|
1460
|
+
const notice = formatUpgradeNotice(bakPath, reconcile);
|
|
1461
|
+
if (notice) console.error(notice);
|
|
1462
|
+
}
|
|
938
1463
|
return db;
|
|
939
1464
|
}
|
|
940
1465
|
function walCheckpointTruncate(dbPath) {
|
|
941
|
-
if (!
|
|
1466
|
+
if (!existsSync3(dbPath)) {
|
|
942
1467
|
return "skipped";
|
|
943
1468
|
}
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
return "
|
|
1469
|
+
const cached = connections.get(dbPath);
|
|
1470
|
+
if (cached) {
|
|
1471
|
+
try {
|
|
1472
|
+
cached.pragma("wal_checkpoint(TRUNCATE)");
|
|
1473
|
+
return "checkpointed";
|
|
1474
|
+
} catch {
|
|
1475
|
+
return "failed";
|
|
951
1476
|
}
|
|
952
|
-
} catch {
|
|
953
|
-
return "skipped";
|
|
954
1477
|
}
|
|
1478
|
+
let transient;
|
|
955
1479
|
try {
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
{
|
|
960
|
-
stdio: "ignore",
|
|
961
|
-
timeout: 5e3
|
|
962
|
-
}
|
|
963
|
-
);
|
|
964
|
-
return result.status === 0 ? "checkpointed" : "failed";
|
|
1480
|
+
transient = openEngine(dbPath);
|
|
1481
|
+
transient.pragma("wal_checkpoint(TRUNCATE)");
|
|
1482
|
+
return "checkpointed";
|
|
965
1483
|
} catch {
|
|
966
1484
|
return "failed";
|
|
1485
|
+
} finally {
|
|
1486
|
+
try {
|
|
1487
|
+
transient?.raw.close();
|
|
1488
|
+
} catch {
|
|
1489
|
+
}
|
|
967
1490
|
}
|
|
968
1491
|
}
|
|
969
1492
|
function closeDatabase(dbPath) {
|
|
@@ -980,16 +1503,25 @@ function closeAllDatabases() {
|
|
|
980
1503
|
}
|
|
981
1504
|
}
|
|
982
1505
|
export {
|
|
1506
|
+
CANCELLED_EXIT_CODE,
|
|
1507
|
+
CASCADE_CLOSE_EXIT_CODE,
|
|
983
1508
|
MIGRATIONS,
|
|
1509
|
+
ORPHAN_EXIT_CODE,
|
|
1510
|
+
PID_REUSE_GUARD_MS,
|
|
1511
|
+
STATE_EXIT,
|
|
1512
|
+
StateError,
|
|
984
1513
|
appendCommandLog,
|
|
985
|
-
applyPragmas,
|
|
986
1514
|
bindVendorSessionIdOpportunistically,
|
|
987
1515
|
bumpAgentSessionHeartbeat,
|
|
988
1516
|
cacheDir,
|
|
1517
|
+
cascadeTerminateExecutions,
|
|
989
1518
|
closeAllDatabases,
|
|
990
1519
|
closeDatabase,
|
|
991
1520
|
commandLogPath,
|
|
1521
|
+
commitReasonClose,
|
|
1522
|
+
defaultIsAlive,
|
|
992
1523
|
ensureDatabase,
|
|
1524
|
+
formatUpgradeNotice,
|
|
993
1525
|
generateCommandUid,
|
|
994
1526
|
getAgentSession,
|
|
995
1527
|
getAllSessions,
|
|
@@ -998,24 +1530,29 @@ export {
|
|
|
998
1530
|
getLatestActiveSession,
|
|
999
1531
|
getLatestAgentSessionWithVendorId,
|
|
1000
1532
|
getLatestEventId,
|
|
1533
|
+
getSchemaVersion,
|
|
1001
1534
|
getSession,
|
|
1002
1535
|
insertAgentSession,
|
|
1003
1536
|
insertEvent,
|
|
1004
1537
|
insertSession,
|
|
1538
|
+
isBusyError,
|
|
1005
1539
|
linkDashboardInvocationToWorkflow,
|
|
1006
1540
|
listAgentSessionsForWorkflow,
|
|
1007
|
-
locateWasm,
|
|
1008
1541
|
openDatabase,
|
|
1542
|
+
probeEngine,
|
|
1009
1543
|
readCommandLog,
|
|
1544
|
+
reconcileLegacyState,
|
|
1010
1545
|
recordVendorSessionIdForExecution,
|
|
1011
1546
|
replayCommandLog,
|
|
1012
1547
|
resultToRow,
|
|
1013
1548
|
resultToRows,
|
|
1549
|
+
rowKind,
|
|
1014
1550
|
runMigrations,
|
|
1015
|
-
saveDatabase,
|
|
1016
1551
|
setAgentSessionStatus,
|
|
1017
1552
|
setAgentSessionVendorId,
|
|
1553
|
+
sqliteUtcMs,
|
|
1018
1554
|
sweepStaleAgentSessions,
|
|
1555
|
+
sweepStaleSessions,
|
|
1019
1556
|
updateAgentSession,
|
|
1020
1557
|
updateSession,
|
|
1021
1558
|
walCheckpointTruncate
|