@open-code-review/cli 1.11.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +8 -4
  2. package/dist/dashboard/client/assets/{_basePickBy-D8RU9s_y.js → _basePickBy-B3ALyupE.js} +1 -1
  3. package/dist/dashboard/client/assets/{_baseUniq-CjVeYx1J.js → _baseUniq-b2RALAWc.js} +1 -1
  4. package/dist/dashboard/client/assets/{arc-DsFstmf9.js → arc-DcSVvhUd.js} +1 -1
  5. package/dist/dashboard/client/assets/{architectureDiagram-VXUJARFQ-iNJB-g1N.js → architectureDiagram-VXUJARFQ-BNUlmSCS.js} +1 -1
  6. package/dist/dashboard/client/assets/{blockDiagram-VD42YOAC-Zp2Aw0zR.js → blockDiagram-VD42YOAC-BmhiQVwa.js} +1 -1
  7. package/dist/dashboard/client/assets/{c4Diagram-YG6GDRKO-BGppUmwT.js → c4Diagram-YG6GDRKO-jyJ3WOv5.js} +1 -1
  8. package/dist/dashboard/client/assets/channel-D3J8-GF_.js +1 -0
  9. package/dist/dashboard/client/assets/{chunk-4BX2VUAB-CZcRxeE4.js → chunk-4BX2VUAB-x1dQU_s3.js} +1 -1
  10. package/dist/dashboard/client/assets/{chunk-55IACEB6-CVdL59yY.js → chunk-55IACEB6-CwbsE2XQ.js} +1 -1
  11. package/dist/dashboard/client/assets/{chunk-B4BG7PRW-CFPp6g6e.js → chunk-B4BG7PRW-BaE7c-ti.js} +1 -1
  12. package/dist/dashboard/client/assets/{chunk-DI55MBZ5-DH9BzE6I.js → chunk-DI55MBZ5-Bw5PUaMK.js} +1 -1
  13. package/dist/dashboard/client/assets/{chunk-FMBD7UC4-DZ2DTwqS.js → chunk-FMBD7UC4-B7cF6P3s.js} +1 -1
  14. package/dist/dashboard/client/assets/{chunk-QN33PNHL-DODPm0CR.js → chunk-QN33PNHL-OY4evNHd.js} +1 -1
  15. package/dist/dashboard/client/assets/{chunk-QZHKN3VN-CNI_LxUf.js → chunk-QZHKN3VN-BpjQwIWz.js} +1 -1
  16. package/dist/dashboard/client/assets/{chunk-TZMSLE5B-sxZQF02c.js → chunk-TZMSLE5B-D8b_Oq9B.js} +1 -1
  17. package/dist/dashboard/client/assets/classDiagram-2ON5EDUG-tkFUL-1Y.js +1 -0
  18. package/dist/dashboard/client/assets/classDiagram-v2-WZHVMYZB-tkFUL-1Y.js +1 -0
  19. package/dist/dashboard/client/assets/clone-CkY5ajLr.js +1 -0
  20. package/dist/dashboard/client/assets/{cose-bilkent-S5V4N54A-BHa2lABH.js → cose-bilkent-S5V4N54A-C-sfP8PN.js} +1 -1
  21. package/dist/dashboard/client/assets/{dagre-6UL2VRFP-CvCLBtkz.js → dagre-6UL2VRFP-Cqfo0NRg.js} +1 -1
  22. package/dist/dashboard/client/assets/{diagram-PSM6KHXK-Cklwd4YA.js → diagram-PSM6KHXK-BR3ppxqI.js} +1 -1
  23. package/dist/dashboard/client/assets/{diagram-QEK2KX5R-3bDERTbp.js → diagram-QEK2KX5R-Dvcx6x3R.js} +1 -1
  24. package/dist/dashboard/client/assets/{diagram-S2PKOQOG-DbiWlPc6.js → diagram-S2PKOQOG-DoyBLnVN.js} +1 -1
  25. package/dist/dashboard/client/assets/{erDiagram-Q2GNP2WA-BQa_VNbt.js → erDiagram-Q2GNP2WA-hy77l1cL.js} +1 -1
  26. package/dist/dashboard/client/assets/{flowDiagram-NV44I4VS-BDaJyl9N.js → flowDiagram-NV44I4VS-Bz0B1rKM.js} +1 -1
  27. package/dist/dashboard/client/assets/{ganttDiagram-JELNMOA3-DsTnleSr.js → ganttDiagram-JELNMOA3-CLgrZPoC.js} +1 -1
  28. package/dist/dashboard/client/assets/{gitGraphDiagram-V2S2FVAM-BRuBadgn.js → gitGraphDiagram-V2S2FVAM-DwJ-1f-v.js} +1 -1
  29. package/dist/dashboard/client/assets/{graph-CYYqXm9c.js → graph-DDBMM_t2.js} +1 -1
  30. package/dist/dashboard/client/assets/{index-eZMoytob.js → index-Cr9yEo_B.js} +123 -123
  31. package/dist/dashboard/client/assets/{infoDiagram-HS3SLOUP-CHnA8k7H.js → infoDiagram-HS3SLOUP-Bhn1FmAk.js} +1 -1
  32. package/dist/dashboard/client/assets/{journeyDiagram-XKPGCS4Q-CAXR1-Ju.js → journeyDiagram-XKPGCS4Q-CzGbjX1y.js} +1 -1
  33. package/dist/dashboard/client/assets/{kanban-definition-3W4ZIXB7-Clf3HfHz.js → kanban-definition-3W4ZIXB7-Da77-WYk.js} +1 -1
  34. package/dist/dashboard/client/assets/{layout-DQPaNqnO.js → layout-CVwSB-GS.js} +1 -1
  35. package/dist/dashboard/client/assets/{linear-qUnNXvWB.js → linear-CTRAc5Jn.js} +1 -1
  36. package/dist/dashboard/client/assets/{mermaid-renderer-C7Se8vjl.js → mermaid-renderer-Bjo170ax.js} +4 -4
  37. package/dist/dashboard/client/assets/{mindmap-definition-VGOIOE7T-DBIdG0OR.js → mindmap-definition-VGOIOE7T-B55C2odl.js} +1 -1
  38. package/dist/dashboard/client/assets/{pieDiagram-ADFJNKIX-DXAIiG6W.js → pieDiagram-ADFJNKIX-5lrQLrSz.js} +1 -1
  39. package/dist/dashboard/client/assets/{quadrantDiagram-AYHSOK5B-D4yAxif0.js → quadrantDiagram-AYHSOK5B-Bg55gC30.js} +1 -1
  40. package/dist/dashboard/client/assets/{requirementDiagram-UZGBJVZJ-D27ME1VO.js → requirementDiagram-UZGBJVZJ-CyR4YFJY.js} +1 -1
  41. package/dist/dashboard/client/assets/{sankeyDiagram-TZEHDZUN-BeEaA_QM.js → sankeyDiagram-TZEHDZUN-BVWKr9_-.js} +1 -1
  42. package/dist/dashboard/client/assets/{sequenceDiagram-WL72ISMW-GTI12qU0.js → sequenceDiagram-WL72ISMW-D0AJg_tE.js} +1 -1
  43. package/dist/dashboard/client/assets/{stateDiagram-FKZM4ZOC-ClSoeZM0.js → stateDiagram-FKZM4ZOC-BuHpTgim.js} +1 -1
  44. package/dist/dashboard/client/assets/stateDiagram-v2-4FDKWEC3-DwAPhteN.js +1 -0
  45. package/dist/dashboard/client/assets/{timeline-definition-IT6M3QCI-cj5d_Kyh.js → timeline-definition-IT6M3QCI-LDhpAmDd.js} +1 -1
  46. package/dist/dashboard/client/assets/{treemap-GDKQZRPO-BrRT1igb.js → treemap-GDKQZRPO-Dd4gjvUl.js} +1 -1
  47. package/dist/dashboard/client/assets/{xychartDiagram-PRI3JC2R-DlzGitHh.js → xychartDiagram-PRI3JC2R-B9RDod39.js} +1 -1
  48. package/dist/dashboard/client/index.html +1 -1
  49. package/dist/dashboard/server.js +1113 -657
  50. package/dist/index.js +1719 -718
  51. package/dist/lib/db/index.js +638 -101
  52. package/package.json +4 -4
  53. package/dist/dashboard/client/assets/channel-C8plpfdz.js +0 -1
  54. package/dist/dashboard/client/assets/classDiagram-2ON5EDUG-Dqn6u1oQ.js +0 -1
  55. package/dist/dashboard/client/assets/classDiagram-v2-WZHVMYZB-Dqn6u1oQ.js +0 -1
  56. package/dist/dashboard/client/assets/clone-BQ8hOLqM.js +0 -1
  57. package/dist/dashboard/client/assets/stateDiagram-v2-4FDKWEC3-Bim3s-dq.js +0 -1
@@ -1,9 +1,88 @@
1
1
  // src/lib/db/index.ts
2
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "node:fs";
3
- import { dirname as dirname2, join as join2 } from "node:path";
4
- import { createRequire } from "node:module";
5
- import { spawnSync } from "node:child_process";
6
- import initSqlJs from "sql.js";
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 TRANSACTION;");
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/agent-sessions.ts
502
- var ORPHAN_EXIT_CODE = -3;
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) return "cancelled";
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 ? `session-instance:${persona}-${instance_index}` : "session-instance";
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 staleSql = `
755
- SELECT uid, id FROM command_executions
756
- WHERE finished_at IS NULL
757
- AND last_heartbeat_at IS NOT NULL
758
- AND (julianday('now') - julianday(last_heartbeat_at)) * 86400 > ?
759
- `;
760
- const stale = resultToRows(
761
- db.exec(staleSql, [thresholdSeconds])
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 (stale.length === 0) {
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
- db.run(
768
- `UPDATE command_executions
769
- SET finished_at = datetime('now'),
770
- exit_code = ?,
771
- notes = COALESCE(notes || char(10), '') || ?
772
- WHERE finished_at IS NULL
773
- AND last_heartbeat_at IS NOT NULL
774
- AND (julianday('now') - julianday(last_heartbeat_at)) * 86400 > ?`,
775
- [ORPHAN_EXIT_CODE, note, thresholdSeconds]
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: stale.map((row) => row.uid ?? String(row.id))
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 join(ocrDir, "data", CACHE_DIR);
1294
+ return join2(ocrDir, "data", CACHE_DIR);
796
1295
  }
797
1296
  function commandLogPath(ocrDir) {
798
- return join(cacheDir(ocrDir), FILENAME);
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 = dirname(filePath);
804
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
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 (!existsSync(filePath)) return [];
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 connections = /* @__PURE__ */ new Map();
881
- function locateWasm() {
882
- const require2 = createRequire(import.meta.url);
883
- const sqlJsPath = require2.resolve("sql.js");
884
- return join2(dirname2(sqlJsPath), "sql-wasm.wasm");
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 applyPragmas(db) {
887
- db.run("PRAGMA foreign_keys = ON;");
888
- db.run("PRAGMA journal_mode = WAL;");
889
- db.run("PRAGMA busy_timeout = 5000;");
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 wasmBuffer = readFileSync2(locateWasm());
897
- const wasmBinary = wasmBuffer.buffer.slice(
898
- wasmBuffer.byteOffset,
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
- applyPragmas(db);
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 = join2(ocrDir, "data", "ocr.db");
1430
+ const dbPath = join3(ocrDir, "data", "ocr.db");
927
1431
  return openDatabase(dbPath);
928
1432
  }
929
1433
  async function ensureDatabase(ocrDir) {
930
- const dataDir = join2(ocrDir, "data");
931
- if (!existsSync2(dataDir)) {
1434
+ const dataDir = join3(ocrDir, "data");
1435
+ if (!existsSync3(dataDir)) {
932
1436
  mkdirSync2(dataDir, { recursive: true });
933
1437
  }
934
- const dbPath = join2(dataDir, "ocr.db");
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
- saveDatabase(db, dbPath);
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 (!existsSync2(dbPath)) {
1466
+ if (!existsSync3(dbPath)) {
942
1467
  return "skipped";
943
1468
  }
944
- try {
945
- const probe = spawnSync("sqlite3", ["-version"], {
946
- stdio: "ignore",
947
- timeout: 2e3
948
- });
949
- if (probe.status !== 0) {
950
- return "skipped";
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
- const result = spawnSync(
957
- "sqlite3",
958
- [dbPath, "PRAGMA wal_checkpoint(TRUNCATE);"],
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