@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.
- package/README.md +23 -1
- package/dist/dashboard/client/assets/{_basePickBy-DbLJVCA4.js → _basePickBy-D8RU9s_y.js} +1 -1
- package/dist/dashboard/client/assets/{_baseUniq-IXEG0cJJ.js → _baseUniq-CjVeYx1J.js} +1 -1
- package/dist/dashboard/client/assets/{arc-lsKxmOJY.js → arc-DsFstmf9.js} +1 -1
- package/dist/dashboard/client/assets/{architectureDiagram-VXUJARFQ-DfMlzFJX.js → architectureDiagram-VXUJARFQ-iNJB-g1N.js} +1 -1
- package/dist/dashboard/client/assets/{blockDiagram-VD42YOAC-bSpnd26J.js → blockDiagram-VD42YOAC-Zp2Aw0zR.js} +1 -1
- package/dist/dashboard/client/assets/{c4Diagram-YG6GDRKO-DPYmVhCZ.js → c4Diagram-YG6GDRKO-BGppUmwT.js} +1 -1
- package/dist/dashboard/client/assets/channel-C8plpfdz.js +1 -0
- package/dist/dashboard/client/assets/{chunk-4BX2VUAB-CI9zC4lV.js → chunk-4BX2VUAB-CZcRxeE4.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-55IACEB6-BqUdJdx5.js → chunk-55IACEB6-CVdL59yY.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-B4BG7PRW-DymQrTp-.js → chunk-B4BG7PRW-CFPp6g6e.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-DI55MBZ5-lZ_9LKGJ.js → chunk-DI55MBZ5-DH9BzE6I.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-FMBD7UC4-DC5rgLNm.js → chunk-FMBD7UC4-DZ2DTwqS.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-QN33PNHL-BrygpHrX.js → chunk-QN33PNHL-DODPm0CR.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-QZHKN3VN-CWJqBdNg.js → chunk-QZHKN3VN-CNI_LxUf.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-TZMSLE5B-BACgM5pG.js → chunk-TZMSLE5B-sxZQF02c.js} +1 -1
- package/dist/dashboard/client/assets/classDiagram-2ON5EDUG-Dqn6u1oQ.js +1 -0
- package/dist/dashboard/client/assets/classDiagram-v2-WZHVMYZB-Dqn6u1oQ.js +1 -0
- package/dist/dashboard/client/assets/clone-BQ8hOLqM.js +1 -0
- package/dist/dashboard/client/assets/{cose-bilkent-S5V4N54A-BYvGIfo0.js → cose-bilkent-S5V4N54A-BHa2lABH.js} +1 -1
- package/dist/dashboard/client/assets/{dagre-6UL2VRFP-B1rZyiLJ.js → dagre-6UL2VRFP-CvCLBtkz.js} +1 -1
- package/dist/dashboard/client/assets/{diagram-PSM6KHXK-Dvl5dQMd.js → diagram-PSM6KHXK-Cklwd4YA.js} +1 -1
- package/dist/dashboard/client/assets/{diagram-QEK2KX5R-Cmntmhht.js → diagram-QEK2KX5R-3bDERTbp.js} +1 -1
- package/dist/dashboard/client/assets/{diagram-S2PKOQOG-BqZcpG85.js → diagram-S2PKOQOG-DbiWlPc6.js} +1 -1
- package/dist/dashboard/client/assets/{erDiagram-Q2GNP2WA-Cw7BALso.js → erDiagram-Q2GNP2WA-BQa_VNbt.js} +1 -1
- package/dist/dashboard/client/assets/{flowDiagram-NV44I4VS-B_amTHzQ.js → flowDiagram-NV44I4VS-BDaJyl9N.js} +1 -1
- package/dist/dashboard/client/assets/{ganttDiagram-JELNMOA3-B1j2-sTo.js → ganttDiagram-JELNMOA3-DsTnleSr.js} +1 -1
- package/dist/dashboard/client/assets/{gitGraphDiagram-V2S2FVAM-D5BkfAMt.js → gitGraphDiagram-V2S2FVAM-BRuBadgn.js} +1 -1
- package/dist/dashboard/client/assets/{graph-B_v15DHv.js → graph-CYYqXm9c.js} +1 -1
- package/dist/dashboard/client/assets/index-Z1pPudAt.css +1 -0
- package/dist/dashboard/client/assets/index-eZMoytob.js +576 -0
- package/dist/dashboard/client/assets/{infoDiagram-HS3SLOUP-C4dtIkj3.js → infoDiagram-HS3SLOUP-CHnA8k7H.js} +1 -1
- package/dist/dashboard/client/assets/{journeyDiagram-XKPGCS4Q-hha4Am8v.js → journeyDiagram-XKPGCS4Q-CAXR1-Ju.js} +1 -1
- package/dist/dashboard/client/assets/{kanban-definition-3W4ZIXB7-1EY8l7Ng.js → kanban-definition-3W4ZIXB7-Clf3HfHz.js} +1 -1
- package/dist/dashboard/client/assets/{layout-7SmAbjFT.js → layout-DQPaNqnO.js} +1 -1
- package/dist/dashboard/client/assets/{linear-BfjSBezh.js → linear-qUnNXvWB.js} +1 -1
- package/dist/dashboard/client/assets/{mermaid-renderer-PPIt-kY4.js → mermaid-renderer-C7Se8vjl.js} +4 -4
- package/dist/dashboard/client/assets/{mindmap-definition-VGOIOE7T-BFpjN9LY.js → mindmap-definition-VGOIOE7T-DBIdG0OR.js} +1 -1
- package/dist/dashboard/client/assets/{pieDiagram-ADFJNKIX-GBbQtDBQ.js → pieDiagram-ADFJNKIX-DXAIiG6W.js} +1 -1
- package/dist/dashboard/client/assets/{quadrantDiagram-AYHSOK5B-Dm0vOhOw.js → quadrantDiagram-AYHSOK5B-D4yAxif0.js} +1 -1
- package/dist/dashboard/client/assets/{requirementDiagram-UZGBJVZJ-BrKONIV8.js → requirementDiagram-UZGBJVZJ-D27ME1VO.js} +1 -1
- package/dist/dashboard/client/assets/{sankeyDiagram-TZEHDZUN-IOobtmDc.js → sankeyDiagram-TZEHDZUN-BeEaA_QM.js} +1 -1
- package/dist/dashboard/client/assets/{sequenceDiagram-WL72ISMW-Dnb0bOW5.js → sequenceDiagram-WL72ISMW-GTI12qU0.js} +1 -1
- package/dist/dashboard/client/assets/{stateDiagram-FKZM4ZOC-C9-bf7bn.js → stateDiagram-FKZM4ZOC-ClSoeZM0.js} +1 -1
- package/dist/dashboard/client/assets/stateDiagram-v2-4FDKWEC3-Bim3s-dq.js +1 -0
- package/dist/dashboard/client/assets/{timeline-definition-IT6M3QCI-tJogDEHB.js → timeline-definition-IT6M3QCI-cj5d_Kyh.js} +1 -1
- package/dist/dashboard/client/assets/{treemap-GDKQZRPO-DQY6HADq.js → treemap-GDKQZRPO-BrRT1igb.js} +1 -1
- package/dist/dashboard/client/assets/{xychartDiagram-PRI3JC2R-DfxeQmTO.js → xychartDiagram-PRI3JC2R-DlzGitHh.js} +1 -1
- package/dist/dashboard/client/index.html +2 -2
- package/dist/dashboard/server.js +9525 -562
- package/dist/index.js +8777 -250
- package/dist/lib/db/index.js +392 -3
- 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 +27 -2
- 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,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
|
-
|
|
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
|
+
};
|