@open-code-review/cli 1.10.4 → 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 +31 -5
- package/dist/dashboard/client/assets/{_basePickBy-DbLJVCA4.js → _basePickBy-B3ALyupE.js} +1 -1
- package/dist/dashboard/client/assets/{_baseUniq-IXEG0cJJ.js → _baseUniq-b2RALAWc.js} +1 -1
- package/dist/dashboard/client/assets/{arc-lsKxmOJY.js → arc-DcSVvhUd.js} +1 -1
- package/dist/dashboard/client/assets/{architectureDiagram-VXUJARFQ-DfMlzFJX.js → architectureDiagram-VXUJARFQ-BNUlmSCS.js} +1 -1
- package/dist/dashboard/client/assets/{blockDiagram-VD42YOAC-bSpnd26J.js → blockDiagram-VD42YOAC-BmhiQVwa.js} +1 -1
- package/dist/dashboard/client/assets/{c4Diagram-YG6GDRKO-DPYmVhCZ.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-CI9zC4lV.js → chunk-4BX2VUAB-x1dQU_s3.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-55IACEB6-BqUdJdx5.js → chunk-55IACEB6-CwbsE2XQ.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-B4BG7PRW-DymQrTp-.js → chunk-B4BG7PRW-BaE7c-ti.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-DI55MBZ5-lZ_9LKGJ.js → chunk-DI55MBZ5-Bw5PUaMK.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-FMBD7UC4-DC5rgLNm.js → chunk-FMBD7UC4-B7cF6P3s.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-QN33PNHL-BrygpHrX.js → chunk-QN33PNHL-OY4evNHd.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-QZHKN3VN-CWJqBdNg.js → chunk-QZHKN3VN-BpjQwIWz.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-TZMSLE5B-BACgM5pG.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-BYvGIfo0.js → cose-bilkent-S5V4N54A-C-sfP8PN.js} +1 -1
- package/dist/dashboard/client/assets/{dagre-6UL2VRFP-B1rZyiLJ.js → dagre-6UL2VRFP-Cqfo0NRg.js} +1 -1
- package/dist/dashboard/client/assets/{diagram-PSM6KHXK-Dvl5dQMd.js → diagram-PSM6KHXK-BR3ppxqI.js} +1 -1
- package/dist/dashboard/client/assets/{diagram-QEK2KX5R-Cmntmhht.js → diagram-QEK2KX5R-Dvcx6x3R.js} +1 -1
- package/dist/dashboard/client/assets/{diagram-S2PKOQOG-BqZcpG85.js → diagram-S2PKOQOG-DoyBLnVN.js} +1 -1
- package/dist/dashboard/client/assets/{erDiagram-Q2GNP2WA-Cw7BALso.js → erDiagram-Q2GNP2WA-hy77l1cL.js} +1 -1
- package/dist/dashboard/client/assets/{flowDiagram-NV44I4VS-B_amTHzQ.js → flowDiagram-NV44I4VS-Bz0B1rKM.js} +1 -1
- package/dist/dashboard/client/assets/{ganttDiagram-JELNMOA3-B1j2-sTo.js → ganttDiagram-JELNMOA3-CLgrZPoC.js} +1 -1
- package/dist/dashboard/client/assets/{gitGraphDiagram-V2S2FVAM-D5BkfAMt.js → gitGraphDiagram-V2S2FVAM-DwJ-1f-v.js} +1 -1
- package/dist/dashboard/client/assets/{graph-B_v15DHv.js → graph-DDBMM_t2.js} +1 -1
- package/dist/dashboard/client/assets/index-Cr9yEo_B.js +576 -0
- package/dist/dashboard/client/assets/index-Z1pPudAt.css +1 -0
- package/dist/dashboard/client/assets/{infoDiagram-HS3SLOUP-C4dtIkj3.js → infoDiagram-HS3SLOUP-Bhn1FmAk.js} +1 -1
- package/dist/dashboard/client/assets/{journeyDiagram-XKPGCS4Q-hha4Am8v.js → journeyDiagram-XKPGCS4Q-CzGbjX1y.js} +1 -1
- package/dist/dashboard/client/assets/{kanban-definition-3W4ZIXB7-1EY8l7Ng.js → kanban-definition-3W4ZIXB7-Da77-WYk.js} +1 -1
- package/dist/dashboard/client/assets/{layout-7SmAbjFT.js → layout-CVwSB-GS.js} +1 -1
- package/dist/dashboard/client/assets/{linear-BfjSBezh.js → linear-CTRAc5Jn.js} +1 -1
- package/dist/dashboard/client/assets/{mermaid-renderer-PPIt-kY4.js → mermaid-renderer-Bjo170ax.js} +4 -4
- package/dist/dashboard/client/assets/{mindmap-definition-VGOIOE7T-BFpjN9LY.js → mindmap-definition-VGOIOE7T-B55C2odl.js} +1 -1
- package/dist/dashboard/client/assets/{pieDiagram-ADFJNKIX-GBbQtDBQ.js → pieDiagram-ADFJNKIX-5lrQLrSz.js} +1 -1
- package/dist/dashboard/client/assets/{quadrantDiagram-AYHSOK5B-Dm0vOhOw.js → quadrantDiagram-AYHSOK5B-Bg55gC30.js} +1 -1
- package/dist/dashboard/client/assets/{requirementDiagram-UZGBJVZJ-BrKONIV8.js → requirementDiagram-UZGBJVZJ-CyR4YFJY.js} +1 -1
- package/dist/dashboard/client/assets/{sankeyDiagram-TZEHDZUN-IOobtmDc.js → sankeyDiagram-TZEHDZUN-BVWKr9_-.js} +1 -1
- package/dist/dashboard/client/assets/{sequenceDiagram-WL72ISMW-Dnb0bOW5.js → sequenceDiagram-WL72ISMW-D0AJg_tE.js} +1 -1
- package/dist/dashboard/client/assets/{stateDiagram-FKZM4ZOC-C9-bf7bn.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-tJogDEHB.js → timeline-definition-IT6M3QCI-LDhpAmDd.js} +1 -1
- package/dist/dashboard/client/assets/{treemap-GDKQZRPO-DQY6HADq.js → treemap-GDKQZRPO-Dd4gjvUl.js} +1 -1
- package/dist/dashboard/client/assets/{xychartDiagram-PRI3JC2R-DfxeQmTO.js → xychartDiagram-PRI3JC2R-B9RDod39.js} +1 -1
- package/dist/dashboard/client/index.html +2 -2
- package/dist/dashboard/server.js +10297 -878
- package/dist/index.js +10441 -913
- package/dist/lib/db/index.js +984 -58
- package/dist/lib/models.js +85 -0
- package/dist/lib/runtime-config.js +39 -0
- package/dist/lib/team-config.js +175 -0
- package/dist/lib/vendor-resume.js +31 -0
- package/package.json +29 -4
- package/dist/dashboard/client/assets/channel-C--wY_Wd.js +0 -1
- package/dist/dashboard/client/assets/classDiagram-2ON5EDUG-DoxmMlnf.js +0 -1
- package/dist/dashboard/client/assets/classDiagram-v2-WZHVMYZB-DoxmMlnf.js +0 -1
- package/dist/dashboard/client/assets/clone-BgvweD4v.js +0 -1
- package/dist/dashboard/client/assets/index-UkJZZdYD.js +0 -548
- package/dist/dashboard/client/assets/index-Zl---B_3.css +0 -1
- package/dist/dashboard/client/assets/stateDiagram-v2-4FDKWEC3-C8Gr4khP.js +0 -1
- package/dist/package.json +0 -61
package/dist/lib/db/index.js
CHANGED
|
@@ -1,10 +1,88 @@
|
|
|
1
|
-
import { createRequire as _cjsReq } from "module"; const require = _cjsReq(import.meta.url);
|
|
2
|
-
|
|
3
1
|
// src/lib/db/index.ts
|
|
4
|
-
import { existsSync as
|
|
5
|
-
import { dirname as
|
|
6
|
-
|
|
7
|
-
|
|
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
|
+
}
|
|
8
86
|
|
|
9
87
|
// src/lib/db/migrations.ts
|
|
10
88
|
var MIGRATIONS = [
|
|
@@ -261,8 +339,224 @@ var MIGRATIONS = [
|
|
|
261
339
|
ALTER TABLE command_executions ADD COLUMN uid TEXT;
|
|
262
340
|
CREATE UNIQUE INDEX idx_command_executions_uid ON command_executions(uid);
|
|
263
341
|
`
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
version: 10,
|
|
345
|
+
description: "Add agent_sessions journal for per-instance lifecycle tracking",
|
|
346
|
+
sql: `
|
|
347
|
+
CREATE TABLE agent_sessions (
|
|
348
|
+
id TEXT PRIMARY KEY,
|
|
349
|
+
workflow_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE RESTRICT,
|
|
350
|
+
vendor TEXT NOT NULL,
|
|
351
|
+
vendor_session_id TEXT,
|
|
352
|
+
persona TEXT,
|
|
353
|
+
instance_index INTEGER,
|
|
354
|
+
name TEXT,
|
|
355
|
+
resolved_model TEXT,
|
|
356
|
+
phase TEXT,
|
|
357
|
+
status TEXT NOT NULL CHECK(status IN ('spawning', 'running', 'done', 'crashed', 'cancelled', 'orphaned')),
|
|
358
|
+
pid INTEGER,
|
|
359
|
+
started_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
360
|
+
last_heartbeat_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
361
|
+
ended_at TEXT,
|
|
362
|
+
exit_code INTEGER,
|
|
363
|
+
notes TEXT
|
|
364
|
+
);
|
|
365
|
+
CREATE INDEX idx_agent_sessions_workflow ON agent_sessions(workflow_id);
|
|
366
|
+
CREATE INDEX idx_agent_sessions_status_heartbeat ON agent_sessions(status, last_heartbeat_at);
|
|
367
|
+
`
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
version: 11,
|
|
371
|
+
description: "Unify agent_sessions into command_executions \u2014 every spawned process is one execution row",
|
|
372
|
+
sql: `
|
|
373
|
+
-- Extend command_executions with the journaling fields previously on agent_sessions.
|
|
374
|
+
-- A NULL workflow_id is allowed because some commands (e.g. sync-reviewers,
|
|
375
|
+
-- create-reviewer) don't tie to a review workflow. Existing rows get NULL by default.
|
|
376
|
+
ALTER TABLE command_executions ADD COLUMN workflow_id TEXT REFERENCES sessions(id) ON DELETE RESTRICT;
|
|
377
|
+
-- parent_id = the dashboard-spawn that's the "Tech Lead" parent of an AI-spawned
|
|
378
|
+
-- session-instance row. NULL for top-level dashboard spawns.
|
|
379
|
+
ALTER TABLE command_executions ADD COLUMN parent_id INTEGER REFERENCES command_executions(id);
|
|
380
|
+
-- Vendor metadata (claude | opencode | gemini | \u2026). NULL for non-AI commands.
|
|
381
|
+
ALTER TABLE command_executions ADD COLUMN vendor TEXT;
|
|
382
|
+
-- The underlying CLI's own session id, captured from stream events.
|
|
383
|
+
-- Used for resume / handoff. Hidden from users (ocr exposes its own id only).
|
|
384
|
+
ALTER TABLE command_executions ADD COLUMN vendor_session_id TEXT;
|
|
385
|
+
-- Persona/instance metadata for AI sub-agents (set when the AI calls
|
|
386
|
+
-- ocr session start-instance). NULL for the parent dashboard spawn.
|
|
387
|
+
ALTER TABLE command_executions ADD COLUMN persona TEXT;
|
|
388
|
+
ALTER TABLE command_executions ADD COLUMN instance_index INTEGER;
|
|
389
|
+
ALTER TABLE command_executions ADD COLUMN name TEXT;
|
|
390
|
+
-- Resolved model string passed to --model post-alias-expansion.
|
|
391
|
+
ALTER TABLE command_executions ADD COLUMN resolved_model TEXT;
|
|
392
|
+
-- Liveness heartbeat. Bumped on every state event the AI emits.
|
|
393
|
+
-- Stale rows past the threshold are reclassified to orphaned (exit_code=-3).
|
|
394
|
+
ALTER TABLE command_executions ADD COLUMN last_heartbeat_at TEXT;
|
|
395
|
+
-- Free-form annotations (sweep notes, host-CLI capability warnings, etc).
|
|
396
|
+
ALTER TABLE command_executions ADD COLUMN notes TEXT;
|
|
397
|
+
CREATE INDEX idx_command_executions_workflow ON command_executions(workflow_id);
|
|
398
|
+
CREATE INDEX idx_command_executions_parent ON command_executions(parent_id);
|
|
399
|
+
CREATE INDEX idx_command_executions_heartbeat ON command_executions(last_heartbeat_at);
|
|
400
|
+
|
|
401
|
+
-- The agent_sessions table is retired. Phase 1 was a parallel journal that
|
|
402
|
+
-- this migration consolidates. We drop the table outright \u2014 the only existing
|
|
403
|
+
-- consumers are the cli helpers and tests, which are updated alongside this
|
|
404
|
+
-- migration. No production deployments have agent_sessions data worth migrating.
|
|
405
|
+
DROP INDEX IF EXISTS idx_agent_sessions_workflow;
|
|
406
|
+
DROP INDEX IF EXISTS idx_agent_sessions_status_heartbeat;
|
|
407
|
+
DROP TABLE IF EXISTS agent_sessions;
|
|
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
|
+
}
|
|
264
551
|
}
|
|
265
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
|
+
}
|
|
266
560
|
function ensureSchemaVersionTable(db) {
|
|
267
561
|
db.run(`
|
|
268
562
|
CREATE TABLE IF NOT EXISTS schema_version (
|
|
@@ -272,6 +566,10 @@ function ensureSchemaVersionTable(db) {
|
|
|
272
566
|
);
|
|
273
567
|
`);
|
|
274
568
|
}
|
|
569
|
+
function getSchemaVersion(db) {
|
|
570
|
+
ensureSchemaVersionTable(db);
|
|
571
|
+
return getCurrentVersion(db);
|
|
572
|
+
}
|
|
275
573
|
function getCurrentVersion(db) {
|
|
276
574
|
const result = db.exec(
|
|
277
575
|
"SELECT MAX(version) as v FROM schema_version"
|
|
@@ -289,9 +587,10 @@ function runMigrations(db) {
|
|
|
289
587
|
if (migration.version <= currentVersion) {
|
|
290
588
|
continue;
|
|
291
589
|
}
|
|
292
|
-
db.run("BEGIN
|
|
590
|
+
db.run("BEGIN IMMEDIATE;");
|
|
293
591
|
try {
|
|
294
|
-
db.run(migration.sql);
|
|
592
|
+
if (migration.sql) db.run(migration.sql);
|
|
593
|
+
migration.run?.(db);
|
|
295
594
|
db.run(
|
|
296
595
|
"INSERT INTO schema_version (version, description) VALUES (?, ?);",
|
|
297
596
|
[migration.version, migration.description]
|
|
@@ -304,6 +603,10 @@ function runMigrations(db) {
|
|
|
304
603
|
}
|
|
305
604
|
}
|
|
306
605
|
|
|
606
|
+
// src/lib/db/reconcile.ts
|
|
607
|
+
import { existsSync } from "node:fs";
|
|
608
|
+
import { isAbsolute, join, dirname } from "node:path";
|
|
609
|
+
|
|
307
610
|
// src/lib/db/result-mapper.ts
|
|
308
611
|
function resultToRows(result) {
|
|
309
612
|
if (result.length === 0 || !result[0]) {
|
|
@@ -431,10 +734,553 @@ function getLatestEventId(db) {
|
|
|
431
734
|
const val = result[0]?.values[0]?.[0];
|
|
432
735
|
return typeof val === "number" ? val : 0;
|
|
433
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
|
+
}
|
|
743
|
+
|
|
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
|
+
};
|
|
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
|
|
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
|
+
}
|
|
927
|
+
function rowToAgentSession(row) {
|
|
928
|
+
return {
|
|
929
|
+
// The OCR-owned id is the `uid` column. Fall back to the integer
|
|
930
|
+
// primary key for legacy command_executions rows without a uid.
|
|
931
|
+
id: row.uid ?? String(row.id),
|
|
932
|
+
workflow_id: row.workflow_id ?? "",
|
|
933
|
+
vendor: row.vendor ?? "",
|
|
934
|
+
vendor_session_id: row.vendor_session_id,
|
|
935
|
+
persona: row.persona,
|
|
936
|
+
instance_index: row.instance_index,
|
|
937
|
+
name: row.name,
|
|
938
|
+
resolved_model: row.resolved_model,
|
|
939
|
+
phase: null,
|
|
940
|
+
status: deriveStatus(row),
|
|
941
|
+
kind: rowKind(row),
|
|
942
|
+
pid: row.pid,
|
|
943
|
+
started_at: row.started_at,
|
|
944
|
+
last_heartbeat_at: row.last_heartbeat_at ?? row.started_at,
|
|
945
|
+
ended_at: row.finished_at,
|
|
946
|
+
exit_code: row.exit_code,
|
|
947
|
+
notes: row.notes
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
function deriveStatus(row) {
|
|
951
|
+
if (row.finished_at === null) {
|
|
952
|
+
return "running";
|
|
953
|
+
}
|
|
954
|
+
if (row.exit_code === ORPHAN_EXIT_CODE) return "orphaned";
|
|
955
|
+
if (row.exit_code === CANCELLED_EXIT_CODE || row.exit_code === CASCADE_CLOSE_EXIT_CODE) {
|
|
956
|
+
return "cancelled";
|
|
957
|
+
}
|
|
958
|
+
if (row.exit_code === 0) return "done";
|
|
959
|
+
return "crashed";
|
|
960
|
+
}
|
|
961
|
+
function insertAgentSession(db, params) {
|
|
962
|
+
const {
|
|
963
|
+
id,
|
|
964
|
+
workflow_id,
|
|
965
|
+
vendor,
|
|
966
|
+
persona = null,
|
|
967
|
+
instance_index = null,
|
|
968
|
+
name = null,
|
|
969
|
+
resolved_model = null,
|
|
970
|
+
pid = null,
|
|
971
|
+
notes = null
|
|
972
|
+
} = params;
|
|
973
|
+
const command = persona && instance_index !== null ? `${INSTANCE_COMMAND}:${persona}-${instance_index}` : INSTANCE_COMMAND;
|
|
974
|
+
db.run(
|
|
975
|
+
`INSERT INTO command_executions
|
|
976
|
+
(uid, command, args, workflow_id, vendor, persona, instance_index, name,
|
|
977
|
+
resolved_model, pid, notes, last_heartbeat_at)
|
|
978
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))`,
|
|
979
|
+
[
|
|
980
|
+
id,
|
|
981
|
+
command,
|
|
982
|
+
null,
|
|
983
|
+
workflow_id,
|
|
984
|
+
vendor,
|
|
985
|
+
persona,
|
|
986
|
+
instance_index,
|
|
987
|
+
name,
|
|
988
|
+
resolved_model,
|
|
989
|
+
pid,
|
|
990
|
+
notes
|
|
991
|
+
]
|
|
992
|
+
);
|
|
993
|
+
}
|
|
994
|
+
function getAgentSession(db, id) {
|
|
995
|
+
const row = resultToRow(
|
|
996
|
+
db.exec(
|
|
997
|
+
`SELECT * FROM command_executions WHERE uid = ? AND last_heartbeat_at IS NOT NULL`,
|
|
998
|
+
[id]
|
|
999
|
+
)
|
|
1000
|
+
);
|
|
1001
|
+
return row ? rowToAgentSession(row) : void 0;
|
|
1002
|
+
}
|
|
1003
|
+
function listAgentSessionsForWorkflow(db, workflowId) {
|
|
1004
|
+
const rows = resultToRows(
|
|
1005
|
+
db.exec(
|
|
1006
|
+
`SELECT * FROM command_executions
|
|
1007
|
+
WHERE workflow_id = ? AND last_heartbeat_at IS NOT NULL
|
|
1008
|
+
ORDER BY started_at ASC, id ASC`,
|
|
1009
|
+
[workflowId]
|
|
1010
|
+
)
|
|
1011
|
+
);
|
|
1012
|
+
return rows.map(rowToAgentSession);
|
|
1013
|
+
}
|
|
1014
|
+
function getLatestAgentSessionWithVendorId(db, workflowId) {
|
|
1015
|
+
const row = resultToRow(
|
|
1016
|
+
db.exec(
|
|
1017
|
+
`SELECT * FROM command_executions
|
|
1018
|
+
WHERE workflow_id = ? AND vendor_session_id IS NOT NULL
|
|
1019
|
+
ORDER BY started_at DESC, id DESC
|
|
1020
|
+
LIMIT 1`,
|
|
1021
|
+
[workflowId]
|
|
1022
|
+
)
|
|
1023
|
+
);
|
|
1024
|
+
return row ? rowToAgentSession(row) : void 0;
|
|
1025
|
+
}
|
|
1026
|
+
function bumpAgentSessionHeartbeat(db, id) {
|
|
1027
|
+
db.run(
|
|
1028
|
+
`UPDATE command_executions
|
|
1029
|
+
SET last_heartbeat_at = datetime('now')
|
|
1030
|
+
WHERE uid = ?`,
|
|
1031
|
+
[id]
|
|
1032
|
+
);
|
|
1033
|
+
}
|
|
1034
|
+
function setAgentSessionVendorId(db, id, vendorSessionId) {
|
|
1035
|
+
const existing = getAgentSession(db, id);
|
|
1036
|
+
if (!existing) {
|
|
1037
|
+
throw new Error(`Agent session not found: ${id}`);
|
|
1038
|
+
}
|
|
1039
|
+
if (existing.vendor_session_id && existing.vendor_session_id !== vendorSessionId) {
|
|
1040
|
+
throw new Error(
|
|
1041
|
+
`Agent session ${id} already bound to vendor session ${existing.vendor_session_id}; refusing to rebind to ${vendorSessionId}`
|
|
1042
|
+
);
|
|
1043
|
+
}
|
|
1044
|
+
db.run(
|
|
1045
|
+
`UPDATE command_executions
|
|
1046
|
+
SET vendor_session_id = ?,
|
|
1047
|
+
last_heartbeat_at = datetime('now')
|
|
1048
|
+
WHERE uid = ?`,
|
|
1049
|
+
[vendorSessionId, id]
|
|
1050
|
+
);
|
|
1051
|
+
}
|
|
1052
|
+
function bindVendorSessionIdOpportunistically(db, vendorSessionId) {
|
|
1053
|
+
const alreadyBound = resultToRow(
|
|
1054
|
+
db.exec(
|
|
1055
|
+
`SELECT c.uid FROM command_executions c
|
|
1056
|
+
INNER JOIN sessions s ON s.id = c.workflow_id
|
|
1057
|
+
WHERE c.vendor_session_id = ?
|
|
1058
|
+
LIMIT 1`,
|
|
1059
|
+
[vendorSessionId]
|
|
1060
|
+
)
|
|
1061
|
+
);
|
|
1062
|
+
if (alreadyBound?.uid) return alreadyBound.uid;
|
|
1063
|
+
const candidate = resultToRow(
|
|
1064
|
+
db.exec(
|
|
1065
|
+
`SELECT c.uid, c.id FROM command_executions c
|
|
1066
|
+
INNER JOIN sessions s ON s.id = c.workflow_id
|
|
1067
|
+
WHERE c.finished_at IS NULL
|
|
1068
|
+
AND c.vendor_session_id IS NULL
|
|
1069
|
+
AND c.last_heartbeat_at IS NOT NULL
|
|
1070
|
+
AND s.status = 'active'
|
|
1071
|
+
ORDER BY c.started_at DESC, c.id DESC
|
|
1072
|
+
LIMIT 1`
|
|
1073
|
+
)
|
|
1074
|
+
);
|
|
1075
|
+
if (!candidate) return null;
|
|
1076
|
+
db.run(
|
|
1077
|
+
`UPDATE command_executions
|
|
1078
|
+
SET vendor_session_id = ?,
|
|
1079
|
+
last_heartbeat_at = datetime('now')
|
|
1080
|
+
WHERE id = ?`,
|
|
1081
|
+
[vendorSessionId, candidate.id]
|
|
1082
|
+
);
|
|
1083
|
+
return candidate.uid ?? String(candidate.id);
|
|
1084
|
+
}
|
|
1085
|
+
function recordVendorSessionIdForExecution(db, executionId, vendorSessionId) {
|
|
1086
|
+
db.run(
|
|
1087
|
+
`UPDATE command_executions
|
|
1088
|
+
SET vendor_session_id = COALESCE(vendor_session_id, ?),
|
|
1089
|
+
last_heartbeat_at = datetime('now')
|
|
1090
|
+
WHERE id = ?`,
|
|
1091
|
+
[vendorSessionId, executionId]
|
|
1092
|
+
);
|
|
1093
|
+
}
|
|
1094
|
+
function linkDashboardInvocationToWorkflow(db, dashboardUid, workflowId) {
|
|
1095
|
+
db.run(
|
|
1096
|
+
`UPDATE command_executions
|
|
1097
|
+
SET workflow_id = COALESCE(workflow_id, ?),
|
|
1098
|
+
last_heartbeat_at = COALESCE(last_heartbeat_at, datetime('now'))
|
|
1099
|
+
WHERE uid = ?`,
|
|
1100
|
+
[workflowId, dashboardUid]
|
|
1101
|
+
);
|
|
1102
|
+
}
|
|
1103
|
+
function setAgentSessionStatus(db, id, status, options = {}) {
|
|
1104
|
+
const { exitCode, note, setEndedAt } = options;
|
|
1105
|
+
const isTerminal = status === "done" || status === "crashed" || status === "cancelled" || status === "orphaned";
|
|
1106
|
+
const stampEnded = setEndedAt ?? isTerminal;
|
|
1107
|
+
let resolvedExit;
|
|
1108
|
+
if (exitCode !== void 0) {
|
|
1109
|
+
resolvedExit = exitCode;
|
|
1110
|
+
} else if (status === "done") {
|
|
1111
|
+
resolvedExit = 0;
|
|
1112
|
+
} else if (status === "cancelled") {
|
|
1113
|
+
resolvedExit = CANCELLED_EXIT_CODE;
|
|
1114
|
+
} else if (status === "orphaned") {
|
|
1115
|
+
resolvedExit = ORPHAN_EXIT_CODE;
|
|
1116
|
+
} else if (status === "crashed") {
|
|
1117
|
+
resolvedExit = 1;
|
|
1118
|
+
} else {
|
|
1119
|
+
resolvedExit = null;
|
|
1120
|
+
}
|
|
1121
|
+
const finishedClause = stampEnded ? ", finished_at = datetime('now')" : "";
|
|
1122
|
+
if (note !== void 0) {
|
|
1123
|
+
db.run(
|
|
1124
|
+
`UPDATE command_executions
|
|
1125
|
+
SET exit_code = ?,
|
|
1126
|
+
notes = COALESCE(notes || char(10), '') || ?
|
|
1127
|
+
${finishedClause}
|
|
1128
|
+
WHERE uid = ?`,
|
|
1129
|
+
[resolvedExit, note, id]
|
|
1130
|
+
);
|
|
1131
|
+
} else {
|
|
1132
|
+
db.run(
|
|
1133
|
+
`UPDATE command_executions
|
|
1134
|
+
SET exit_code = ?
|
|
1135
|
+
${finishedClause}
|
|
1136
|
+
WHERE uid = ?`,
|
|
1137
|
+
[resolvedExit, id]
|
|
1138
|
+
);
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
function updateAgentSession(db, id, params) {
|
|
1142
|
+
const setClauses = [];
|
|
1143
|
+
const values = [];
|
|
1144
|
+
if (params.vendor_session_id !== void 0) {
|
|
1145
|
+
setClauses.push("vendor_session_id = ?");
|
|
1146
|
+
values.push(params.vendor_session_id);
|
|
1147
|
+
}
|
|
1148
|
+
if (params.status !== void 0) {
|
|
1149
|
+
setAgentSessionStatus(db, id, params.status, {
|
|
1150
|
+
exitCode: params.exit_code ?? void 0,
|
|
1151
|
+
note: params.notes ?? void 0
|
|
1152
|
+
});
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
if (params.pid !== void 0) {
|
|
1156
|
+
setClauses.push("pid = ?");
|
|
1157
|
+
values.push(params.pid);
|
|
1158
|
+
}
|
|
1159
|
+
if (params.ended_at !== void 0) {
|
|
1160
|
+
setClauses.push("finished_at = ?");
|
|
1161
|
+
values.push(params.ended_at);
|
|
1162
|
+
}
|
|
1163
|
+
if (params.exit_code !== void 0) {
|
|
1164
|
+
setClauses.push("exit_code = ?");
|
|
1165
|
+
values.push(params.exit_code);
|
|
1166
|
+
}
|
|
1167
|
+
if (params.notes !== void 0) {
|
|
1168
|
+
setClauses.push("notes = ?");
|
|
1169
|
+
values.push(params.notes);
|
|
1170
|
+
}
|
|
1171
|
+
if (setClauses.length === 0) return;
|
|
1172
|
+
values.push(id);
|
|
1173
|
+
db.run(
|
|
1174
|
+
`UPDATE command_executions SET ${setClauses.join(", ")} WHERE uid = ?`,
|
|
1175
|
+
values
|
|
1176
|
+
);
|
|
1177
|
+
}
|
|
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
|
+
)
|
|
1189
|
+
);
|
|
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: [] };
|
|
1201
|
+
}
|
|
1202
|
+
const note = `${NOTE_ORPHAN_PREFIX} (threshold ${thresholdSeconds}s)`;
|
|
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
|
+
});
|
|
1228
|
+
return {
|
|
1229
|
+
orphanedIds: dead.map((r) => r.uid ?? String(r.id)),
|
|
1230
|
+
cascadedWorkflowIds
|
|
1231
|
+
};
|
|
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
|
+
}
|
|
434
1280
|
|
|
435
1281
|
// src/lib/db/command-log.ts
|
|
436
|
-
import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
437
|
-
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";
|
|
438
1284
|
import { randomUUID } from "node:crypto";
|
|
439
1285
|
var CACHE_DIR = ".cache";
|
|
440
1286
|
var FILENAME = "command-history.jsonl";
|
|
@@ -445,16 +1291,16 @@ function generateCommandUid() {
|
|
|
445
1291
|
return randomUUID();
|
|
446
1292
|
}
|
|
447
1293
|
function cacheDir(ocrDir) {
|
|
448
|
-
return
|
|
1294
|
+
return join2(ocrDir, "data", CACHE_DIR);
|
|
449
1295
|
}
|
|
450
1296
|
function commandLogPath(ocrDir) {
|
|
451
|
-
return
|
|
1297
|
+
return join2(cacheDir(ocrDir), FILENAME);
|
|
452
1298
|
}
|
|
453
1299
|
function appendCommandLog(ocrDir, entry) {
|
|
454
1300
|
try {
|
|
455
1301
|
const filePath = commandLogPath(ocrDir);
|
|
456
|
-
const dir =
|
|
457
|
-
if (!
|
|
1302
|
+
const dir = dirname2(filePath);
|
|
1303
|
+
if (!existsSync2(dir)) mkdirSync(dir, { recursive: true });
|
|
458
1304
|
const line = JSON.stringify(entry) + "\n";
|
|
459
1305
|
appendFileSync(filePath, line, { encoding: "utf-8" });
|
|
460
1306
|
if (approxLineCount >= 0) approxLineCount++;
|
|
@@ -464,7 +1310,7 @@ function appendCommandLog(ocrDir, entry) {
|
|
|
464
1310
|
}
|
|
465
1311
|
function readCommandLog(ocrDir) {
|
|
466
1312
|
const filePath = commandLogPath(ocrDir);
|
|
467
|
-
if (!
|
|
1313
|
+
if (!existsSync2(filePath)) return [];
|
|
468
1314
|
const content = readFileSync(filePath, "utf-8");
|
|
469
1315
|
const entries = [];
|
|
470
1316
|
for (const line of content.split("\n")) {
|
|
@@ -530,66 +1376,119 @@ function rotateIfNeeded(filePath) {
|
|
|
530
1376
|
}
|
|
531
1377
|
|
|
532
1378
|
// src/lib/db/index.ts
|
|
533
|
-
var
|
|
534
|
-
function
|
|
535
|
-
|
|
536
|
-
const
|
|
537
|
-
|
|
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
|
+
}
|
|
538
1392
|
}
|
|
539
|
-
function
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
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");
|
|
543
1414
|
}
|
|
1415
|
+
var connections = /* @__PURE__ */ new Map();
|
|
544
1416
|
async function openDatabase(dbPath) {
|
|
545
1417
|
const cached = connections.get(dbPath);
|
|
546
1418
|
if (cached) {
|
|
547
1419
|
return cached;
|
|
548
1420
|
}
|
|
549
|
-
const
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
wasmBuffer.byteOffset + wasmBuffer.byteLength
|
|
553
|
-
);
|
|
554
|
-
const SQL = await initSqlJs({
|
|
555
|
-
wasmBinary
|
|
556
|
-
});
|
|
557
|
-
let db;
|
|
558
|
-
if (existsSync2(dbPath)) {
|
|
559
|
-
const fileBuffer = readFileSync2(dbPath);
|
|
560
|
-
db = new SQL.Database(fileBuffer);
|
|
561
|
-
} else {
|
|
562
|
-
db = new SQL.Database();
|
|
1421
|
+
const dir = dirname3(dbPath);
|
|
1422
|
+
if (!existsSync3(dir)) {
|
|
1423
|
+
mkdirSync2(dir, { recursive: true });
|
|
563
1424
|
}
|
|
564
|
-
|
|
1425
|
+
const db = openEngine(dbPath);
|
|
565
1426
|
connections.set(dbPath, db);
|
|
566
1427
|
return db;
|
|
567
1428
|
}
|
|
568
|
-
function saveDatabase(db, dbPath) {
|
|
569
|
-
const data = db.export();
|
|
570
|
-
const dir = dirname2(dbPath);
|
|
571
|
-
if (!existsSync2(dir)) {
|
|
572
|
-
mkdirSync2(dir, { recursive: true });
|
|
573
|
-
}
|
|
574
|
-
const tmpPath = `${dbPath}.${process.pid}.tmp`;
|
|
575
|
-
writeFileSync2(tmpPath, Buffer.from(data));
|
|
576
|
-
renameSync2(tmpPath, dbPath);
|
|
577
|
-
}
|
|
578
1429
|
async function getDb(ocrDir) {
|
|
579
|
-
const dbPath =
|
|
1430
|
+
const dbPath = join3(ocrDir, "data", "ocr.db");
|
|
580
1431
|
return openDatabase(dbPath);
|
|
581
1432
|
}
|
|
582
1433
|
async function ensureDatabase(ocrDir) {
|
|
583
|
-
const dataDir =
|
|
584
|
-
if (!
|
|
1434
|
+
const dataDir = join3(ocrDir, "data");
|
|
1435
|
+
if (!existsSync3(dataDir)) {
|
|
585
1436
|
mkdirSync2(dataDir, { recursive: true });
|
|
586
1437
|
}
|
|
587
|
-
const dbPath =
|
|
1438
|
+
const dbPath = join3(dataDir, "ocr.db");
|
|
588
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);
|
|
589
1448
|
runMigrations(db);
|
|
590
|
-
|
|
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
|
+
}
|
|
591
1463
|
return db;
|
|
592
1464
|
}
|
|
1465
|
+
function walCheckpointTruncate(dbPath) {
|
|
1466
|
+
if (!existsSync3(dbPath)) {
|
|
1467
|
+
return "skipped";
|
|
1468
|
+
}
|
|
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";
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
let transient;
|
|
1479
|
+
try {
|
|
1480
|
+
transient = openEngine(dbPath);
|
|
1481
|
+
transient.pragma("wal_checkpoint(TRUNCATE)");
|
|
1482
|
+
return "checkpointed";
|
|
1483
|
+
} catch {
|
|
1484
|
+
return "failed";
|
|
1485
|
+
} finally {
|
|
1486
|
+
try {
|
|
1487
|
+
transient?.raw.close();
|
|
1488
|
+
} catch {
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
593
1492
|
function closeDatabase(dbPath) {
|
|
594
1493
|
const db = connections.get(dbPath);
|
|
595
1494
|
if (db) {
|
|
@@ -604,30 +1503,57 @@ function closeAllDatabases() {
|
|
|
604
1503
|
}
|
|
605
1504
|
}
|
|
606
1505
|
export {
|
|
1506
|
+
CANCELLED_EXIT_CODE,
|
|
1507
|
+
CASCADE_CLOSE_EXIT_CODE,
|
|
607
1508
|
MIGRATIONS,
|
|
1509
|
+
ORPHAN_EXIT_CODE,
|
|
1510
|
+
PID_REUSE_GUARD_MS,
|
|
1511
|
+
STATE_EXIT,
|
|
1512
|
+
StateError,
|
|
608
1513
|
appendCommandLog,
|
|
609
|
-
|
|
1514
|
+
bindVendorSessionIdOpportunistically,
|
|
1515
|
+
bumpAgentSessionHeartbeat,
|
|
610
1516
|
cacheDir,
|
|
1517
|
+
cascadeTerminateExecutions,
|
|
611
1518
|
closeAllDatabases,
|
|
612
1519
|
closeDatabase,
|
|
613
1520
|
commandLogPath,
|
|
1521
|
+
commitReasonClose,
|
|
1522
|
+
defaultIsAlive,
|
|
614
1523
|
ensureDatabase,
|
|
1524
|
+
formatUpgradeNotice,
|
|
615
1525
|
generateCommandUid,
|
|
1526
|
+
getAgentSession,
|
|
616
1527
|
getAllSessions,
|
|
617
1528
|
getDb,
|
|
618
1529
|
getEventsForSession,
|
|
619
1530
|
getLatestActiveSession,
|
|
1531
|
+
getLatestAgentSessionWithVendorId,
|
|
620
1532
|
getLatestEventId,
|
|
1533
|
+
getSchemaVersion,
|
|
621
1534
|
getSession,
|
|
1535
|
+
insertAgentSession,
|
|
622
1536
|
insertEvent,
|
|
623
1537
|
insertSession,
|
|
624
|
-
|
|
1538
|
+
isBusyError,
|
|
1539
|
+
linkDashboardInvocationToWorkflow,
|
|
1540
|
+
listAgentSessionsForWorkflow,
|
|
625
1541
|
openDatabase,
|
|
1542
|
+
probeEngine,
|
|
626
1543
|
readCommandLog,
|
|
1544
|
+
reconcileLegacyState,
|
|
1545
|
+
recordVendorSessionIdForExecution,
|
|
627
1546
|
replayCommandLog,
|
|
628
1547
|
resultToRow,
|
|
629
1548
|
resultToRows,
|
|
1549
|
+
rowKind,
|
|
630
1550
|
runMigrations,
|
|
631
|
-
|
|
632
|
-
|
|
1551
|
+
setAgentSessionStatus,
|
|
1552
|
+
setAgentSessionVendorId,
|
|
1553
|
+
sqliteUtcMs,
|
|
1554
|
+
sweepStaleAgentSessions,
|
|
1555
|
+
sweepStaleSessions,
|
|
1556
|
+
updateAgentSession,
|
|
1557
|
+
updateSession,
|
|
1558
|
+
walCheckpointTruncate
|
|
633
1559
|
};
|