@open-code-review/cli 1.10.4 → 1.11.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 (65) hide show
  1. package/README.md +23 -1
  2. package/dist/dashboard/client/assets/{_basePickBy-DbLJVCA4.js → _basePickBy-D8RU9s_y.js} +1 -1
  3. package/dist/dashboard/client/assets/{_baseUniq-IXEG0cJJ.js → _baseUniq-CjVeYx1J.js} +1 -1
  4. package/dist/dashboard/client/assets/{arc-lsKxmOJY.js → arc-DsFstmf9.js} +1 -1
  5. package/dist/dashboard/client/assets/{architectureDiagram-VXUJARFQ-DfMlzFJX.js → architectureDiagram-VXUJARFQ-iNJB-g1N.js} +1 -1
  6. package/dist/dashboard/client/assets/{blockDiagram-VD42YOAC-bSpnd26J.js → blockDiagram-VD42YOAC-Zp2Aw0zR.js} +1 -1
  7. package/dist/dashboard/client/assets/{c4Diagram-YG6GDRKO-DPYmVhCZ.js → c4Diagram-YG6GDRKO-BGppUmwT.js} +1 -1
  8. package/dist/dashboard/client/assets/channel-C8plpfdz.js +1 -0
  9. package/dist/dashboard/client/assets/{chunk-4BX2VUAB-CI9zC4lV.js → chunk-4BX2VUAB-CZcRxeE4.js} +1 -1
  10. package/dist/dashboard/client/assets/{chunk-55IACEB6-BqUdJdx5.js → chunk-55IACEB6-CVdL59yY.js} +1 -1
  11. package/dist/dashboard/client/assets/{chunk-B4BG7PRW-DymQrTp-.js → chunk-B4BG7PRW-CFPp6g6e.js} +1 -1
  12. package/dist/dashboard/client/assets/{chunk-DI55MBZ5-lZ_9LKGJ.js → chunk-DI55MBZ5-DH9BzE6I.js} +1 -1
  13. package/dist/dashboard/client/assets/{chunk-FMBD7UC4-DC5rgLNm.js → chunk-FMBD7UC4-DZ2DTwqS.js} +1 -1
  14. package/dist/dashboard/client/assets/{chunk-QN33PNHL-BrygpHrX.js → chunk-QN33PNHL-DODPm0CR.js} +1 -1
  15. package/dist/dashboard/client/assets/{chunk-QZHKN3VN-CWJqBdNg.js → chunk-QZHKN3VN-CNI_LxUf.js} +1 -1
  16. package/dist/dashboard/client/assets/{chunk-TZMSLE5B-BACgM5pG.js → chunk-TZMSLE5B-sxZQF02c.js} +1 -1
  17. package/dist/dashboard/client/assets/classDiagram-2ON5EDUG-Dqn6u1oQ.js +1 -0
  18. package/dist/dashboard/client/assets/classDiagram-v2-WZHVMYZB-Dqn6u1oQ.js +1 -0
  19. package/dist/dashboard/client/assets/clone-BQ8hOLqM.js +1 -0
  20. package/dist/dashboard/client/assets/{cose-bilkent-S5V4N54A-BYvGIfo0.js → cose-bilkent-S5V4N54A-BHa2lABH.js} +1 -1
  21. package/dist/dashboard/client/assets/{dagre-6UL2VRFP-B1rZyiLJ.js → dagre-6UL2VRFP-CvCLBtkz.js} +1 -1
  22. package/dist/dashboard/client/assets/{diagram-PSM6KHXK-Dvl5dQMd.js → diagram-PSM6KHXK-Cklwd4YA.js} +1 -1
  23. package/dist/dashboard/client/assets/{diagram-QEK2KX5R-Cmntmhht.js → diagram-QEK2KX5R-3bDERTbp.js} +1 -1
  24. package/dist/dashboard/client/assets/{diagram-S2PKOQOG-BqZcpG85.js → diagram-S2PKOQOG-DbiWlPc6.js} +1 -1
  25. package/dist/dashboard/client/assets/{erDiagram-Q2GNP2WA-Cw7BALso.js → erDiagram-Q2GNP2WA-BQa_VNbt.js} +1 -1
  26. package/dist/dashboard/client/assets/{flowDiagram-NV44I4VS-B_amTHzQ.js → flowDiagram-NV44I4VS-BDaJyl9N.js} +1 -1
  27. package/dist/dashboard/client/assets/{ganttDiagram-JELNMOA3-B1j2-sTo.js → ganttDiagram-JELNMOA3-DsTnleSr.js} +1 -1
  28. package/dist/dashboard/client/assets/{gitGraphDiagram-V2S2FVAM-D5BkfAMt.js → gitGraphDiagram-V2S2FVAM-BRuBadgn.js} +1 -1
  29. package/dist/dashboard/client/assets/{graph-B_v15DHv.js → graph-CYYqXm9c.js} +1 -1
  30. package/dist/dashboard/client/assets/index-Z1pPudAt.css +1 -0
  31. package/dist/dashboard/client/assets/index-eZMoytob.js +576 -0
  32. package/dist/dashboard/client/assets/{infoDiagram-HS3SLOUP-C4dtIkj3.js → infoDiagram-HS3SLOUP-CHnA8k7H.js} +1 -1
  33. package/dist/dashboard/client/assets/{journeyDiagram-XKPGCS4Q-hha4Am8v.js → journeyDiagram-XKPGCS4Q-CAXR1-Ju.js} +1 -1
  34. package/dist/dashboard/client/assets/{kanban-definition-3W4ZIXB7-1EY8l7Ng.js → kanban-definition-3W4ZIXB7-Clf3HfHz.js} +1 -1
  35. package/dist/dashboard/client/assets/{layout-7SmAbjFT.js → layout-DQPaNqnO.js} +1 -1
  36. package/dist/dashboard/client/assets/{linear-BfjSBezh.js → linear-qUnNXvWB.js} +1 -1
  37. package/dist/dashboard/client/assets/{mermaid-renderer-PPIt-kY4.js → mermaid-renderer-C7Se8vjl.js} +4 -4
  38. package/dist/dashboard/client/assets/{mindmap-definition-VGOIOE7T-BFpjN9LY.js → mindmap-definition-VGOIOE7T-DBIdG0OR.js} +1 -1
  39. package/dist/dashboard/client/assets/{pieDiagram-ADFJNKIX-GBbQtDBQ.js → pieDiagram-ADFJNKIX-DXAIiG6W.js} +1 -1
  40. package/dist/dashboard/client/assets/{quadrantDiagram-AYHSOK5B-Dm0vOhOw.js → quadrantDiagram-AYHSOK5B-D4yAxif0.js} +1 -1
  41. package/dist/dashboard/client/assets/{requirementDiagram-UZGBJVZJ-BrKONIV8.js → requirementDiagram-UZGBJVZJ-D27ME1VO.js} +1 -1
  42. package/dist/dashboard/client/assets/{sankeyDiagram-TZEHDZUN-IOobtmDc.js → sankeyDiagram-TZEHDZUN-BeEaA_QM.js} +1 -1
  43. package/dist/dashboard/client/assets/{sequenceDiagram-WL72ISMW-Dnb0bOW5.js → sequenceDiagram-WL72ISMW-GTI12qU0.js} +1 -1
  44. package/dist/dashboard/client/assets/{stateDiagram-FKZM4ZOC-C9-bf7bn.js → stateDiagram-FKZM4ZOC-ClSoeZM0.js} +1 -1
  45. package/dist/dashboard/client/assets/stateDiagram-v2-4FDKWEC3-Bim3s-dq.js +1 -0
  46. package/dist/dashboard/client/assets/{timeline-definition-IT6M3QCI-tJogDEHB.js → timeline-definition-IT6M3QCI-cj5d_Kyh.js} +1 -1
  47. package/dist/dashboard/client/assets/{treemap-GDKQZRPO-DQY6HADq.js → treemap-GDKQZRPO-BrRT1igb.js} +1 -1
  48. package/dist/dashboard/client/assets/{xychartDiagram-PRI3JC2R-DfxeQmTO.js → xychartDiagram-PRI3JC2R-DlzGitHh.js} +1 -1
  49. package/dist/dashboard/client/index.html +2 -2
  50. package/dist/dashboard/server.js +9525 -562
  51. package/dist/index.js +8777 -250
  52. package/dist/lib/db/index.js +392 -3
  53. package/dist/lib/models.js +85 -0
  54. package/dist/lib/runtime-config.js +39 -0
  55. package/dist/lib/team-config.js +175 -0
  56. package/dist/lib/vendor-resume.js +31 -0
  57. package/package.json +27 -2
  58. package/dist/dashboard/client/assets/channel-C--wY_Wd.js +0 -1
  59. package/dist/dashboard/client/assets/classDiagram-2ON5EDUG-DoxmMlnf.js +0 -1
  60. package/dist/dashboard/client/assets/classDiagram-v2-WZHVMYZB-DoxmMlnf.js +0 -1
  61. package/dist/dashboard/client/assets/clone-BgvweD4v.js +0 -1
  62. package/dist/dashboard/client/assets/index-UkJZZdYD.js +0 -548
  63. package/dist/dashboard/client/assets/index-Zl---B_3.css +0 -1
  64. package/dist/dashboard/client/assets/stateDiagram-v2-4FDKWEC3-C8Gr4khP.js +0 -1
  65. package/dist/package.json +0 -61
@@ -1,9 +1,8 @@
1
- import { createRequire as _cjsReq } from "module"; const require = _cjsReq(import.meta.url);
2
-
3
1
  // src/lib/db/index.ts
4
2
  import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "node:fs";
5
3
  import { dirname as dirname2, join as join2 } from "node:path";
6
4
  import { createRequire } from "node:module";
5
+ import { spawnSync } from "node:child_process";
7
6
  import initSqlJs from "sql.js";
8
7
 
9
8
  // src/lib/db/migrations.ts
@@ -261,6 +260,73 @@ var MIGRATIONS = [
261
260
  ALTER TABLE command_executions ADD COLUMN uid TEXT;
262
261
  CREATE UNIQUE INDEX idx_command_executions_uid ON command_executions(uid);
263
262
  `
263
+ },
264
+ {
265
+ version: 10,
266
+ description: "Add agent_sessions journal for per-instance lifecycle tracking",
267
+ sql: `
268
+ CREATE TABLE agent_sessions (
269
+ id TEXT PRIMARY KEY,
270
+ workflow_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE RESTRICT,
271
+ vendor TEXT NOT NULL,
272
+ vendor_session_id TEXT,
273
+ persona TEXT,
274
+ instance_index INTEGER,
275
+ name TEXT,
276
+ resolved_model TEXT,
277
+ phase TEXT,
278
+ status TEXT NOT NULL CHECK(status IN ('spawning', 'running', 'done', 'crashed', 'cancelled', 'orphaned')),
279
+ pid INTEGER,
280
+ started_at TEXT NOT NULL DEFAULT (datetime('now')),
281
+ last_heartbeat_at TEXT NOT NULL DEFAULT (datetime('now')),
282
+ ended_at TEXT,
283
+ exit_code INTEGER,
284
+ notes TEXT
285
+ );
286
+ CREATE INDEX idx_agent_sessions_workflow ON agent_sessions(workflow_id);
287
+ CREATE INDEX idx_agent_sessions_status_heartbeat ON agent_sessions(status, last_heartbeat_at);
288
+ `
289
+ },
290
+ {
291
+ version: 11,
292
+ description: "Unify agent_sessions into command_executions \u2014 every spawned process is one execution row",
293
+ sql: `
294
+ -- Extend command_executions with the journaling fields previously on agent_sessions.
295
+ -- A NULL workflow_id is allowed because some commands (e.g. sync-reviewers,
296
+ -- create-reviewer) don't tie to a review workflow. Existing rows get NULL by default.
297
+ ALTER TABLE command_executions ADD COLUMN workflow_id TEXT REFERENCES sessions(id) ON DELETE RESTRICT;
298
+ -- parent_id = the dashboard-spawn that's the "Tech Lead" parent of an AI-spawned
299
+ -- session-instance row. NULL for top-level dashboard spawns.
300
+ ALTER TABLE command_executions ADD COLUMN parent_id INTEGER REFERENCES command_executions(id);
301
+ -- Vendor metadata (claude | opencode | gemini | \u2026). NULL for non-AI commands.
302
+ ALTER TABLE command_executions ADD COLUMN vendor TEXT;
303
+ -- The underlying CLI's own session id, captured from stream events.
304
+ -- Used for resume / handoff. Hidden from users (ocr exposes its own id only).
305
+ ALTER TABLE command_executions ADD COLUMN vendor_session_id TEXT;
306
+ -- Persona/instance metadata for AI sub-agents (set when the AI calls
307
+ -- ocr session start-instance). NULL for the parent dashboard spawn.
308
+ ALTER TABLE command_executions ADD COLUMN persona TEXT;
309
+ ALTER TABLE command_executions ADD COLUMN instance_index INTEGER;
310
+ ALTER TABLE command_executions ADD COLUMN name TEXT;
311
+ -- Resolved model string passed to --model post-alias-expansion.
312
+ ALTER TABLE command_executions ADD COLUMN resolved_model TEXT;
313
+ -- Liveness heartbeat. Bumped on every state event the AI emits.
314
+ -- Stale rows past the threshold are reclassified to orphaned (exit_code=-3).
315
+ ALTER TABLE command_executions ADD COLUMN last_heartbeat_at TEXT;
316
+ -- Free-form annotations (sweep notes, host-CLI capability warnings, etc).
317
+ ALTER TABLE command_executions ADD COLUMN notes TEXT;
318
+ CREATE INDEX idx_command_executions_workflow ON command_executions(workflow_id);
319
+ CREATE INDEX idx_command_executions_parent ON command_executions(parent_id);
320
+ CREATE INDEX idx_command_executions_heartbeat ON command_executions(last_heartbeat_at);
321
+
322
+ -- The agent_sessions table is retired. Phase 1 was a parallel journal that
323
+ -- this migration consolidates. We drop the table outright \u2014 the only existing
324
+ -- consumers are the cli helpers and tests, which are updated alongside this
325
+ -- migration. No production deployments have agent_sessions data worth migrating.
326
+ DROP INDEX IF EXISTS idx_agent_sessions_workflow;
327
+ DROP INDEX IF EXISTS idx_agent_sessions_status_heartbeat;
328
+ DROP TABLE IF EXISTS agent_sessions;
329
+ `
264
330
  }
265
331
  ];
266
332
  function ensureSchemaVersionTable(db) {
@@ -432,6 +498,287 @@ function getLatestEventId(db) {
432
498
  return typeof val === "number" ? val : 0;
433
499
  }
434
500
 
501
+ // src/lib/db/agent-sessions.ts
502
+ var ORPHAN_EXIT_CODE = -3;
503
+ var CANCELLED_EXIT_CODE = -2;
504
+ var NOTE_ORPHAN_PREFIX = "orphaned by liveness sweep";
505
+ function rowToAgentSession(row) {
506
+ return {
507
+ // The OCR-owned id is the `uid` column. Fall back to the integer
508
+ // primary key for legacy command_executions rows without a uid.
509
+ id: row.uid ?? String(row.id),
510
+ workflow_id: row.workflow_id ?? "",
511
+ vendor: row.vendor ?? "",
512
+ vendor_session_id: row.vendor_session_id,
513
+ persona: row.persona,
514
+ instance_index: row.instance_index,
515
+ name: row.name,
516
+ resolved_model: row.resolved_model,
517
+ phase: null,
518
+ status: deriveStatus(row),
519
+ pid: row.pid,
520
+ started_at: row.started_at,
521
+ last_heartbeat_at: row.last_heartbeat_at ?? row.started_at,
522
+ ended_at: row.finished_at,
523
+ exit_code: row.exit_code,
524
+ notes: row.notes
525
+ };
526
+ }
527
+ function deriveStatus(row) {
528
+ if (row.finished_at === null) {
529
+ return "running";
530
+ }
531
+ if (row.exit_code === ORPHAN_EXIT_CODE) return "orphaned";
532
+ if (row.exit_code === CANCELLED_EXIT_CODE) return "cancelled";
533
+ if (row.exit_code === 0) return "done";
534
+ return "crashed";
535
+ }
536
+ function insertAgentSession(db, params) {
537
+ const {
538
+ id,
539
+ workflow_id,
540
+ vendor,
541
+ persona = null,
542
+ instance_index = null,
543
+ name = null,
544
+ resolved_model = null,
545
+ pid = null,
546
+ notes = null
547
+ } = params;
548
+ const command = persona && instance_index !== null ? `session-instance:${persona}-${instance_index}` : "session-instance";
549
+ db.run(
550
+ `INSERT INTO command_executions
551
+ (uid, command, args, workflow_id, vendor, persona, instance_index, name,
552
+ resolved_model, pid, notes, last_heartbeat_at)
553
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))`,
554
+ [
555
+ id,
556
+ command,
557
+ null,
558
+ workflow_id,
559
+ vendor,
560
+ persona,
561
+ instance_index,
562
+ name,
563
+ resolved_model,
564
+ pid,
565
+ notes
566
+ ]
567
+ );
568
+ }
569
+ function getAgentSession(db, id) {
570
+ const row = resultToRow(
571
+ db.exec(
572
+ `SELECT * FROM command_executions WHERE uid = ? AND last_heartbeat_at IS NOT NULL`,
573
+ [id]
574
+ )
575
+ );
576
+ return row ? rowToAgentSession(row) : void 0;
577
+ }
578
+ function listAgentSessionsForWorkflow(db, workflowId) {
579
+ const rows = resultToRows(
580
+ db.exec(
581
+ `SELECT * FROM command_executions
582
+ WHERE workflow_id = ? AND last_heartbeat_at IS NOT NULL
583
+ ORDER BY started_at ASC, id ASC`,
584
+ [workflowId]
585
+ )
586
+ );
587
+ return rows.map(rowToAgentSession);
588
+ }
589
+ function getLatestAgentSessionWithVendorId(db, workflowId) {
590
+ const row = resultToRow(
591
+ db.exec(
592
+ `SELECT * FROM command_executions
593
+ WHERE workflow_id = ? AND vendor_session_id IS NOT NULL
594
+ ORDER BY started_at DESC, id DESC
595
+ LIMIT 1`,
596
+ [workflowId]
597
+ )
598
+ );
599
+ return row ? rowToAgentSession(row) : void 0;
600
+ }
601
+ function bumpAgentSessionHeartbeat(db, id) {
602
+ db.run(
603
+ `UPDATE command_executions
604
+ SET last_heartbeat_at = datetime('now')
605
+ WHERE uid = ?`,
606
+ [id]
607
+ );
608
+ }
609
+ function setAgentSessionVendorId(db, id, vendorSessionId) {
610
+ const existing = getAgentSession(db, id);
611
+ if (!existing) {
612
+ throw new Error(`Agent session not found: ${id}`);
613
+ }
614
+ if (existing.vendor_session_id && existing.vendor_session_id !== vendorSessionId) {
615
+ throw new Error(
616
+ `Agent session ${id} already bound to vendor session ${existing.vendor_session_id}; refusing to rebind to ${vendorSessionId}`
617
+ );
618
+ }
619
+ db.run(
620
+ `UPDATE command_executions
621
+ SET vendor_session_id = ?,
622
+ last_heartbeat_at = datetime('now')
623
+ WHERE uid = ?`,
624
+ [vendorSessionId, id]
625
+ );
626
+ }
627
+ function bindVendorSessionIdOpportunistically(db, vendorSessionId) {
628
+ const alreadyBound = resultToRow(
629
+ db.exec(
630
+ `SELECT c.uid FROM command_executions c
631
+ INNER JOIN sessions s ON s.id = c.workflow_id
632
+ WHERE c.vendor_session_id = ?
633
+ LIMIT 1`,
634
+ [vendorSessionId]
635
+ )
636
+ );
637
+ if (alreadyBound?.uid) return alreadyBound.uid;
638
+ const candidate = resultToRow(
639
+ db.exec(
640
+ `SELECT c.uid, c.id FROM command_executions c
641
+ INNER JOIN sessions s ON s.id = c.workflow_id
642
+ WHERE c.finished_at IS NULL
643
+ AND c.vendor_session_id IS NULL
644
+ AND c.last_heartbeat_at IS NOT NULL
645
+ AND s.status = 'active'
646
+ ORDER BY c.started_at DESC, c.id DESC
647
+ LIMIT 1`
648
+ )
649
+ );
650
+ if (!candidate) return null;
651
+ db.run(
652
+ `UPDATE command_executions
653
+ SET vendor_session_id = ?,
654
+ last_heartbeat_at = datetime('now')
655
+ WHERE id = ?`,
656
+ [vendorSessionId, candidate.id]
657
+ );
658
+ return candidate.uid ?? String(candidate.id);
659
+ }
660
+ function recordVendorSessionIdForExecution(db, executionId, vendorSessionId) {
661
+ db.run(
662
+ `UPDATE command_executions
663
+ SET vendor_session_id = COALESCE(vendor_session_id, ?),
664
+ last_heartbeat_at = datetime('now')
665
+ WHERE id = ?`,
666
+ [vendorSessionId, executionId]
667
+ );
668
+ }
669
+ function linkDashboardInvocationToWorkflow(db, dashboardUid, workflowId) {
670
+ db.run(
671
+ `UPDATE command_executions
672
+ SET workflow_id = COALESCE(workflow_id, ?),
673
+ last_heartbeat_at = COALESCE(last_heartbeat_at, datetime('now'))
674
+ WHERE uid = ?`,
675
+ [workflowId, dashboardUid]
676
+ );
677
+ }
678
+ function setAgentSessionStatus(db, id, status, options = {}) {
679
+ const { exitCode, note, setEndedAt } = options;
680
+ const isTerminal = status === "done" || status === "crashed" || status === "cancelled" || status === "orphaned";
681
+ const stampEnded = setEndedAt ?? isTerminal;
682
+ let resolvedExit;
683
+ if (exitCode !== void 0) {
684
+ resolvedExit = exitCode;
685
+ } else if (status === "done") {
686
+ resolvedExit = 0;
687
+ } else if (status === "cancelled") {
688
+ resolvedExit = CANCELLED_EXIT_CODE;
689
+ } else if (status === "orphaned") {
690
+ resolvedExit = ORPHAN_EXIT_CODE;
691
+ } else if (status === "crashed") {
692
+ resolvedExit = 1;
693
+ } else {
694
+ resolvedExit = null;
695
+ }
696
+ const finishedClause = stampEnded ? ", finished_at = datetime('now')" : "";
697
+ if (note !== void 0) {
698
+ db.run(
699
+ `UPDATE command_executions
700
+ SET exit_code = ?,
701
+ notes = COALESCE(notes || char(10), '') || ?
702
+ ${finishedClause}
703
+ WHERE uid = ?`,
704
+ [resolvedExit, note, id]
705
+ );
706
+ } else {
707
+ db.run(
708
+ `UPDATE command_executions
709
+ SET exit_code = ?
710
+ ${finishedClause}
711
+ WHERE uid = ?`,
712
+ [resolvedExit, id]
713
+ );
714
+ }
715
+ }
716
+ function updateAgentSession(db, id, params) {
717
+ const setClauses = [];
718
+ const values = [];
719
+ if (params.vendor_session_id !== void 0) {
720
+ setClauses.push("vendor_session_id = ?");
721
+ values.push(params.vendor_session_id);
722
+ }
723
+ if (params.status !== void 0) {
724
+ setAgentSessionStatus(db, id, params.status, {
725
+ exitCode: params.exit_code ?? void 0,
726
+ note: params.notes ?? void 0
727
+ });
728
+ return;
729
+ }
730
+ if (params.pid !== void 0) {
731
+ setClauses.push("pid = ?");
732
+ values.push(params.pid);
733
+ }
734
+ if (params.ended_at !== void 0) {
735
+ setClauses.push("finished_at = ?");
736
+ values.push(params.ended_at);
737
+ }
738
+ if (params.exit_code !== void 0) {
739
+ setClauses.push("exit_code = ?");
740
+ values.push(params.exit_code);
741
+ }
742
+ if (params.notes !== void 0) {
743
+ setClauses.push("notes = ?");
744
+ values.push(params.notes);
745
+ }
746
+ if (setClauses.length === 0) return;
747
+ values.push(id);
748
+ db.run(
749
+ `UPDATE command_executions SET ${setClauses.join(", ")} WHERE uid = ?`,
750
+ values
751
+ );
752
+ }
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])
762
+ );
763
+ if (stale.length === 0) {
764
+ return { orphanedIds: [] };
765
+ }
766
+ 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
+ );
777
+ return {
778
+ orphanedIds: stale.map((row) => row.uid ?? String(row.id))
779
+ };
780
+ }
781
+
435
782
  // src/lib/db/command-log.ts
436
783
  import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
437
784
  import { dirname, join } from "node:path";
@@ -590,6 +937,35 @@ async function ensureDatabase(ocrDir) {
590
937
  saveDatabase(db, dbPath);
591
938
  return db;
592
939
  }
940
+ function walCheckpointTruncate(dbPath) {
941
+ if (!existsSync2(dbPath)) {
942
+ return "skipped";
943
+ }
944
+ try {
945
+ const probe = spawnSync("sqlite3", ["-version"], {
946
+ stdio: "ignore",
947
+ timeout: 2e3
948
+ });
949
+ if (probe.status !== 0) {
950
+ return "skipped";
951
+ }
952
+ } catch {
953
+ return "skipped";
954
+ }
955
+ 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";
965
+ } catch {
966
+ return "failed";
967
+ }
968
+ }
593
969
  function closeDatabase(dbPath) {
594
970
  const db = connections.get(dbPath);
595
971
  if (db) {
@@ -607,27 +983,40 @@ export {
607
983
  MIGRATIONS,
608
984
  appendCommandLog,
609
985
  applyPragmas,
986
+ bindVendorSessionIdOpportunistically,
987
+ bumpAgentSessionHeartbeat,
610
988
  cacheDir,
611
989
  closeAllDatabases,
612
990
  closeDatabase,
613
991
  commandLogPath,
614
992
  ensureDatabase,
615
993
  generateCommandUid,
994
+ getAgentSession,
616
995
  getAllSessions,
617
996
  getDb,
618
997
  getEventsForSession,
619
998
  getLatestActiveSession,
999
+ getLatestAgentSessionWithVendorId,
620
1000
  getLatestEventId,
621
1001
  getSession,
1002
+ insertAgentSession,
622
1003
  insertEvent,
623
1004
  insertSession,
1005
+ linkDashboardInvocationToWorkflow,
1006
+ listAgentSessionsForWorkflow,
624
1007
  locateWasm,
625
1008
  openDatabase,
626
1009
  readCommandLog,
1010
+ recordVendorSessionIdForExecution,
627
1011
  replayCommandLog,
628
1012
  resultToRow,
629
1013
  resultToRows,
630
1014
  runMigrations,
631
1015
  saveDatabase,
632
- updateSession
1016
+ setAgentSessionStatus,
1017
+ setAgentSessionVendorId,
1018
+ sweepStaleAgentSessions,
1019
+ updateAgentSession,
1020
+ updateSession,
1021
+ walCheckpointTruncate
633
1022
  };
@@ -0,0 +1,85 @@
1
+ // ../shared/platform/src/index.ts
2
+ import {
3
+ execFile,
4
+ execFileSync,
5
+ spawn
6
+ } from "node:child_process";
7
+ import { promisify } from "node:util";
8
+ var execFilePromise = promisify(execFile);
9
+ var isWindows = process.platform === "win32";
10
+ function execBinary(binary, args, opts) {
11
+ return execFileSync(binary, args, {
12
+ ...opts,
13
+ shell: isWindows
14
+ });
15
+ }
16
+
17
+ // src/lib/models.ts
18
+ var BUNDLED_CLAUDE_MODELS = [
19
+ { id: "claude-opus-4-7", displayName: "Claude Opus 4.7" },
20
+ { id: "claude-sonnet-4-6", displayName: "Claude Sonnet 4.6" },
21
+ { id: "claude-haiku-4-5-20251001", displayName: "Claude Haiku 4.5" }
22
+ ];
23
+ var BUNDLED_OPENCODE_MODELS = [
24
+ { id: "anthropic/claude-opus-4-7", provider: "anthropic" },
25
+ { id: "anthropic/claude-sonnet-4-6", provider: "anthropic" },
26
+ { id: "anthropic/claude-haiku-4-5-20251001", provider: "anthropic" }
27
+ ];
28
+ function detectActiveVendor() {
29
+ for (const vendor of ["claude", "opencode"]) {
30
+ try {
31
+ execBinary(vendor, ["--version"], {
32
+ encoding: "utf-8",
33
+ timeout: 3e3,
34
+ stdio: ["ignore", "pipe", "ignore"]
35
+ });
36
+ return vendor;
37
+ } catch {
38
+ }
39
+ }
40
+ return null;
41
+ }
42
+ function tryNativeEnumeration(vendor) {
43
+ try {
44
+ const output = execBinary(vendor, ["models", "--json"], {
45
+ encoding: "utf-8",
46
+ timeout: 5e3,
47
+ stdio: ["ignore", "pipe", "ignore"]
48
+ });
49
+ const parsed = JSON.parse(output);
50
+ if (!Array.isArray(parsed)) return null;
51
+ const models = [];
52
+ for (const item of parsed) {
53
+ if (typeof item === "string") {
54
+ models.push({ id: item });
55
+ } else if (typeof item === "object" && item !== null && "id" in item && typeof item.id === "string") {
56
+ const obj = item;
57
+ const desc = { id: obj.id };
58
+ if (typeof obj.displayName === "string") desc.displayName = obj.displayName;
59
+ if (typeof obj.provider === "string") desc.provider = obj.provider;
60
+ if (Array.isArray(obj.tags)) {
61
+ desc.tags = obj.tags.filter((t) => typeof t === "string");
62
+ }
63
+ models.push(desc);
64
+ }
65
+ }
66
+ return models.length > 0 ? models : null;
67
+ } catch {
68
+ return null;
69
+ }
70
+ }
71
+ function bundledForVendor(vendor) {
72
+ if (vendor === "claude") return BUNDLED_CLAUDE_MODELS;
73
+ return BUNDLED_OPENCODE_MODELS;
74
+ }
75
+ function listModelsForVendor(vendor) {
76
+ const native = tryNativeEnumeration(vendor);
77
+ if (native) {
78
+ return { vendor, source: "native", models: native };
79
+ }
80
+ return { vendor, source: "bundled", models: bundledForVendor(vendor) };
81
+ }
82
+ export {
83
+ detectActiveVendor,
84
+ listModelsForVendor
85
+ };
@@ -0,0 +1,39 @@
1
+ // src/lib/runtime-config.ts
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ var DEFAULT_AGENT_HEARTBEAT_SECONDS = 60;
5
+ function getAgentHeartbeatSeconds(ocrDir) {
6
+ const configPath = join(ocrDir, "config.yaml");
7
+ if (!existsSync(configPath)) {
8
+ return DEFAULT_AGENT_HEARTBEAT_SECONDS;
9
+ }
10
+ let content;
11
+ try {
12
+ content = readFileSync(configPath, "utf-8");
13
+ } catch {
14
+ return DEFAULT_AGENT_HEARTBEAT_SECONDS;
15
+ }
16
+ const blockMatch = content.match(
17
+ /^runtime:\s*\n(?:\s+[^\n]*\n)*?\s+agent_heartbeat_seconds:\s*([^\s#\n]+)/m
18
+ );
19
+ const inlineMatch = content.match(
20
+ /^runtime:\s*\{[^}]*\bagent_heartbeat_seconds:\s*([^\s,}]+)/m
21
+ );
22
+ const raw = blockMatch?.[1] ?? inlineMatch?.[1];
23
+ if (!raw) {
24
+ return DEFAULT_AGENT_HEARTBEAT_SECONDS;
25
+ }
26
+ const parsed = Number(raw);
27
+ if (!Number.isFinite(parsed) || parsed <= 0 || !Number.isInteger(parsed)) {
28
+ process.stderr.write(
29
+ `[ocr] runtime.agent_heartbeat_seconds is not a positive integer (got "${raw}"); falling back to ${DEFAULT_AGENT_HEARTBEAT_SECONDS}s.
30
+ `
31
+ );
32
+ return DEFAULT_AGENT_HEARTBEAT_SECONDS;
33
+ }
34
+ return parsed;
35
+ }
36
+ export {
37
+ DEFAULT_AGENT_HEARTBEAT_SECONDS,
38
+ getAgentHeartbeatSeconds
39
+ };