ai-whisper 0.2.0 → 0.3.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/dist/bin/broker-daemon.js +494 -117
- package/dist/bin/companion-agent.js +492 -115
- package/dist/bin/relay-monitor.js +492 -115
- package/dist/bin/whisper.js +999 -404
- package/dist/skills/ai-whisper-bugfix/SKILL.md +18 -0
- package/dist/skills/ai-whisper-ralph/SKILL.md +18 -0
- package/dist/skills/ai-whisper-sdd/SKILL.md +18 -0
- package/package.json +4 -4
- package/skills/ai-whisper-bugfix/SKILL.md +18 -0
- package/skills/ai-whisper-ralph/SKILL.md +18 -0
- package/skills/ai-whisper-sdd/SKILL.md +18 -0
package/dist/bin/whisper.js
CHANGED
|
@@ -599,6 +599,100 @@ function getCollab(db, collabId) {
|
|
|
599
599
|
});
|
|
600
600
|
}
|
|
601
601
|
|
|
602
|
+
// ../broker/dist/storage/repositories/workflow-repository.js
|
|
603
|
+
function rowToRecord(row) {
|
|
604
|
+
return {
|
|
605
|
+
workflowId: row.workflow_id,
|
|
606
|
+
collabId: row.collab_id,
|
|
607
|
+
workflowType: row.workflow_type,
|
|
608
|
+
name: row.name,
|
|
609
|
+
specPath: row.spec_path,
|
|
610
|
+
roleBindings: JSON.parse(row.role_bindings),
|
|
611
|
+
status: row.status,
|
|
612
|
+
currentPhaseIndex: row.current_phase_index,
|
|
613
|
+
haltReason: row.halt_reason,
|
|
614
|
+
workflowContext: JSON.parse(row.workflow_context),
|
|
615
|
+
createdAt: row.created_at,
|
|
616
|
+
updatedAt: row.updated_at
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
function insertWorkflow(db, input) {
|
|
620
|
+
db.prepare(`INSERT INTO workflows
|
|
621
|
+
(workflow_id, collab_id, workflow_type, name, spec_path, role_bindings, status,
|
|
622
|
+
current_phase_index, halt_reason, workflow_context, created_at, updated_at)
|
|
623
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?)`).run(input.workflowId, input.collabId, input.workflowType, input.name, input.specPath, JSON.stringify(input.roleBindings), input.status, input.currentPhaseIndex, JSON.stringify(input.workflowContext), input.now, input.now);
|
|
624
|
+
}
|
|
625
|
+
function getWorkflowById(db, workflowId) {
|
|
626
|
+
const row = db.prepare("SELECT * FROM workflows WHERE workflow_id = ?").get(workflowId);
|
|
627
|
+
return row ? rowToRecord(row) : null;
|
|
628
|
+
}
|
|
629
|
+
function listWorkflows(db, filter = {}) {
|
|
630
|
+
const clauses = [];
|
|
631
|
+
const args = [];
|
|
632
|
+
if (filter.collabId) {
|
|
633
|
+
clauses.push("collab_id = ?");
|
|
634
|
+
args.push(filter.collabId);
|
|
635
|
+
}
|
|
636
|
+
if (filter.status) {
|
|
637
|
+
clauses.push("status = ?");
|
|
638
|
+
args.push(filter.status);
|
|
639
|
+
}
|
|
640
|
+
const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
641
|
+
const rows = db.prepare(`SELECT * FROM workflows ${where} ORDER BY created_at DESC`).all(...args);
|
|
642
|
+
return rows.map(rowToRecord);
|
|
643
|
+
}
|
|
644
|
+
function setWorkflowStatus(db, input) {
|
|
645
|
+
db.prepare(`UPDATE workflows
|
|
646
|
+
SET status = ?, halt_reason = ?, updated_at = ?
|
|
647
|
+
WHERE workflow_id = ?`).run(input.status, input.haltReason, input.now, input.workflowId);
|
|
648
|
+
}
|
|
649
|
+
function updateWorkflowContext(db, input) {
|
|
650
|
+
const existing = getWorkflowById(db, input.workflowId);
|
|
651
|
+
if (!existing) {
|
|
652
|
+
throw new Error(`updateWorkflowContext: unknown workflowId ${input.workflowId}`);
|
|
653
|
+
}
|
|
654
|
+
const merged = { ...existing.workflowContext, ...input.patch };
|
|
655
|
+
db.prepare("UPDATE workflows SET workflow_context = ?, updated_at = ? WHERE workflow_id = ?").run(JSON.stringify(merged), input.now, input.workflowId);
|
|
656
|
+
}
|
|
657
|
+
function incrementCurrentPhaseIndex(db, input) {
|
|
658
|
+
db.prepare(`UPDATE workflows
|
|
659
|
+
SET current_phase_index = current_phase_index + 1, updated_at = ?
|
|
660
|
+
WHERE workflow_id = ?`).run(input.now, input.workflowId);
|
|
661
|
+
}
|
|
662
|
+
function countActiveWorkflowsForCollab(db, collabId) {
|
|
663
|
+
const row = db.prepare("SELECT COUNT(*) AS n FROM workflows WHERE collab_id = ? AND status IN ('running', 'paused')").get(collabId);
|
|
664
|
+
return row.n;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// ../broker/dist/runtime/workspace-snapshot.js
|
|
668
|
+
import { execFileSync } from "node:child_process";
|
|
669
|
+
function captureWorkspaceSnapshotSync(workspaceRoot) {
|
|
670
|
+
try {
|
|
671
|
+
const ref = execFileSync("git", ["-C", workspaceRoot, "stash", "create"], {
|
|
672
|
+
encoding: "utf8",
|
|
673
|
+
timeout: 1e4
|
|
674
|
+
}).trim();
|
|
675
|
+
if (ref)
|
|
676
|
+
return ref;
|
|
677
|
+
const head = execFileSync("git", ["-C", workspaceRoot, "rev-parse", "HEAD"], {
|
|
678
|
+
encoding: "utf8",
|
|
679
|
+
timeout: 1e4
|
|
680
|
+
}).trim();
|
|
681
|
+
return head || null;
|
|
682
|
+
} catch {
|
|
683
|
+
return null;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
function diffChangedFilesSince(workspaceRoot, sinceRef) {
|
|
687
|
+
try {
|
|
688
|
+
const stdout = execFileSync("git", ["-C", workspaceRoot, "diff", "--name-only", sinceRef], { encoding: "utf8", timeout: 1e4 });
|
|
689
|
+
const files = stdout.split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith(".ai-whisper/"));
|
|
690
|
+
return [...new Set(files)].sort();
|
|
691
|
+
} catch {
|
|
692
|
+
return [];
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
602
696
|
// ../broker/dist/storage/repositories/reply-repository.js
|
|
603
697
|
function insertReply(db, reply) {
|
|
604
698
|
db.prepare(`INSERT INTO reply (
|
|
@@ -1088,7 +1182,7 @@ function upsertRelayTurnState(db, input) {
|
|
|
1088
1182
|
}
|
|
1089
1183
|
|
|
1090
1184
|
// ../broker/dist/storage/repositories/relay-capture-diagnostics-repository.js
|
|
1091
|
-
function
|
|
1185
|
+
function rowToRecord2(row) {
|
|
1092
1186
|
return {
|
|
1093
1187
|
captureId: row.capture_id,
|
|
1094
1188
|
handoffId: row.handoff_id,
|
|
@@ -1105,6 +1199,7 @@ function rowToRecord(row) {
|
|
|
1105
1199
|
clipSample: row.clip_sample,
|
|
1106
1200
|
turnSample: row.turn_sample,
|
|
1107
1201
|
abortedByRaceGuard: row.aborted_by_race_guard === 1,
|
|
1202
|
+
interferenceDetected: row.interference_detected === 1,
|
|
1108
1203
|
createdAt: row.created_at
|
|
1109
1204
|
};
|
|
1110
1205
|
}
|
|
@@ -1112,8 +1207,9 @@ function insertCaptureDiagnostic(db, input) {
|
|
|
1112
1207
|
db.prepare(`INSERT INTO relay_capture_diagnostics
|
|
1113
1208
|
(capture_id, handoff_id, collab_id, chain_id, workflow_id, target_provider,
|
|
1114
1209
|
capture_status, clip_len, turn_len, turn_confidence, jaccard_score,
|
|
1115
|
-
containment_score, clip_sample, turn_sample, aborted_by_race_guard,
|
|
1116
|
-
|
|
1210
|
+
containment_score, clip_sample, turn_sample, aborted_by_race_guard,
|
|
1211
|
+
interference_detected, created_at)
|
|
1212
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(input.captureId, input.handoffId, input.collabId, input.chainId, input.workflowId, input.targetProvider, input.captureStatus, input.clipLen, input.turnLen, input.turnConfidence, input.jaccardScore, input.containmentScore, input.clipSample, input.turnSample, input.abortedByRaceGuard ? 1 : 0, input.interferenceDetected ? 1 : 0, input.createdAt);
|
|
1117
1213
|
}
|
|
1118
1214
|
function listCaptureDiagnosticsByCollab(db, collabId, limit, opts) {
|
|
1119
1215
|
const filter = opts?.workflowFilter;
|
|
@@ -1123,32 +1219,32 @@ function listCaptureDiagnosticsByCollab(db, collabId, limit, opts) {
|
|
|
1123
1219
|
const rows2 = db.prepare(`SELECT * FROM relay_capture_diagnostics
|
|
1124
1220
|
WHERE collab_id = ?${filterClause}
|
|
1125
1221
|
ORDER BY created_at DESC`).all(collabId, ...filterArgs);
|
|
1126
|
-
return rows2.map(
|
|
1222
|
+
return rows2.map(rowToRecord2);
|
|
1127
1223
|
}
|
|
1128
1224
|
const rows = db.prepare(`SELECT * FROM relay_capture_diagnostics
|
|
1129
1225
|
WHERE collab_id = ?${filterClause}
|
|
1130
1226
|
ORDER BY created_at DESC
|
|
1131
1227
|
LIMIT ?`).all(collabId, ...filterArgs, limit);
|
|
1132
|
-
return rows.map(
|
|
1228
|
+
return rows.map(rowToRecord2);
|
|
1133
1229
|
}
|
|
1134
1230
|
function listCaptureDiagnosticsByCollabAndChain(db, collabId, chainId, limit) {
|
|
1135
1231
|
if (limit === null) {
|
|
1136
1232
|
const rows2 = db.prepare(`SELECT * FROM relay_capture_diagnostics
|
|
1137
1233
|
WHERE collab_id = ? AND chain_id = ?
|
|
1138
1234
|
ORDER BY created_at DESC`).all(collabId, chainId);
|
|
1139
|
-
return rows2.map(
|
|
1235
|
+
return rows2.map(rowToRecord2);
|
|
1140
1236
|
}
|
|
1141
1237
|
const rows = db.prepare(`SELECT * FROM relay_capture_diagnostics
|
|
1142
1238
|
WHERE collab_id = ? AND chain_id = ?
|
|
1143
1239
|
ORDER BY created_at DESC
|
|
1144
1240
|
LIMIT ?`).all(collabId, chainId, limit);
|
|
1145
|
-
return rows.map(
|
|
1241
|
+
return rows.map(rowToRecord2);
|
|
1146
1242
|
}
|
|
1147
1243
|
function listCaptureDiagnosticsByHandoff(db, handoffId) {
|
|
1148
1244
|
const rows = db.prepare(`SELECT * FROM relay_capture_diagnostics
|
|
1149
1245
|
WHERE handoff_id = ?
|
|
1150
1246
|
ORDER BY created_at ASC`).all(handoffId);
|
|
1151
|
-
return rows.map(
|
|
1247
|
+
return rows.map(rowToRecord2);
|
|
1152
1248
|
}
|
|
1153
1249
|
function deleteCaptureDiagnosticsOlderThan(db, cutoffIso) {
|
|
1154
1250
|
const result = db.prepare("DELETE FROM relay_capture_diagnostics WHERE created_at < ?").run(cutoffIso);
|
|
@@ -1156,7 +1252,7 @@ function deleteCaptureDiagnosticsOlderThan(db, cutoffIso) {
|
|
|
1156
1252
|
}
|
|
1157
1253
|
|
|
1158
1254
|
// ../broker/dist/storage/repositories/relay-evaluator-diagnostics-repository.js
|
|
1159
|
-
function
|
|
1255
|
+
function rowToRecord3(row) {
|
|
1160
1256
|
return {
|
|
1161
1257
|
evaluatorId: row.evaluator_id,
|
|
1162
1258
|
handoffId: row.handoff_id,
|
|
@@ -1201,32 +1297,32 @@ function listEvaluatorDiagnosticsByCollab(db, collabId, limit, opts) {
|
|
|
1201
1297
|
const rows2 = db.prepare(`SELECT * FROM relay_evaluator_diagnostics
|
|
1202
1298
|
WHERE collab_id = ?${filterClause}
|
|
1203
1299
|
ORDER BY created_at DESC`).all(collabId, ...filterArgs);
|
|
1204
|
-
return rows2.map(
|
|
1300
|
+
return rows2.map(rowToRecord3);
|
|
1205
1301
|
}
|
|
1206
1302
|
const rows = db.prepare(`SELECT * FROM relay_evaluator_diagnostics
|
|
1207
1303
|
WHERE collab_id = ?${filterClause}
|
|
1208
1304
|
ORDER BY created_at DESC
|
|
1209
1305
|
LIMIT ?`).all(collabId, ...filterArgs, limit);
|
|
1210
|
-
return rows.map(
|
|
1306
|
+
return rows.map(rowToRecord3);
|
|
1211
1307
|
}
|
|
1212
1308
|
function listEvaluatorDiagnosticsByCollabAndChain(db, collabId, chainId, limit) {
|
|
1213
1309
|
if (limit === null) {
|
|
1214
1310
|
const rows2 = db.prepare(`SELECT * FROM relay_evaluator_diagnostics
|
|
1215
1311
|
WHERE collab_id = ? AND chain_id = ?
|
|
1216
1312
|
ORDER BY created_at DESC`).all(collabId, chainId);
|
|
1217
|
-
return rows2.map(
|
|
1313
|
+
return rows2.map(rowToRecord3);
|
|
1218
1314
|
}
|
|
1219
1315
|
const rows = db.prepare(`SELECT * FROM relay_evaluator_diagnostics
|
|
1220
1316
|
WHERE collab_id = ? AND chain_id = ?
|
|
1221
1317
|
ORDER BY created_at DESC
|
|
1222
1318
|
LIMIT ?`).all(collabId, chainId, limit);
|
|
1223
|
-
return rows.map(
|
|
1319
|
+
return rows.map(rowToRecord3);
|
|
1224
1320
|
}
|
|
1225
1321
|
function listEvaluatorDiagnosticsByHandoff(db, handoffId) {
|
|
1226
1322
|
const rows = db.prepare(`SELECT * FROM relay_evaluator_diagnostics
|
|
1227
1323
|
WHERE handoff_id = ?
|
|
1228
1324
|
ORDER BY created_at ASC`).all(handoffId);
|
|
1229
|
-
return rows.map(
|
|
1325
|
+
return rows.map(rowToRecord3);
|
|
1230
1326
|
}
|
|
1231
1327
|
function deleteEvaluatorDiagnosticsOlderThan(db, cutoffIso) {
|
|
1232
1328
|
const result = db.prepare("DELETE FROM relay_evaluator_diagnostics WHERE created_at < ?").run(cutoffIso);
|
|
@@ -1234,7 +1330,7 @@ function deleteEvaluatorDiagnosticsOlderThan(db, cutoffIso) {
|
|
|
1234
1330
|
}
|
|
1235
1331
|
|
|
1236
1332
|
// ../broker/dist/storage/repositories/relay-handoff-repository.js
|
|
1237
|
-
function
|
|
1333
|
+
function rowToRecord4(row) {
|
|
1238
1334
|
return {
|
|
1239
1335
|
handoffId: row.handoff_id,
|
|
1240
1336
|
collabId: row.collab_id,
|
|
@@ -1271,7 +1367,7 @@ function queryRelayHandoff(db, handoffId) {
|
|
|
1271
1367
|
FROM relay_handoff h
|
|
1272
1368
|
JOIN collab c ON c.collab_id = h.collab_id
|
|
1273
1369
|
WHERE h.handoff_id = ?`).get(handoffId);
|
|
1274
|
-
return row ?
|
|
1370
|
+
return row ? rowToRecord4(row) : null;
|
|
1275
1371
|
}
|
|
1276
1372
|
function createRelayHandoffTxn(db, input) {
|
|
1277
1373
|
return db.transaction(() => {
|
|
@@ -1423,7 +1519,7 @@ function queryLatestHandedBackHandoff(db, collabId) {
|
|
|
1423
1519
|
WHERE h.collab_id = ? AND h.status = 'handed_back'
|
|
1424
1520
|
ORDER BY h.resolved_at DESC
|
|
1425
1521
|
LIMIT 1`).get(collabId);
|
|
1426
|
-
return row ?
|
|
1522
|
+
return row ? rowToRecord4(row) : null;
|
|
1427
1523
|
}
|
|
1428
1524
|
function failRelayHandoffOnDisconnectTxn(db, input) {
|
|
1429
1525
|
db.transaction(() => {
|
|
@@ -1447,6 +1543,30 @@ function failRelayHandoffOnDisconnectTxn(db, input) {
|
|
|
1447
1543
|
}
|
|
1448
1544
|
})();
|
|
1449
1545
|
}
|
|
1546
|
+
function hasInFlightAcceptedHandoffForWorkflow(db, workflowId) {
|
|
1547
|
+
const row = db.prepare("SELECT COUNT(*) AS n FROM relay_handoff WHERE workflow_id = ? AND status = 'accepted'").get(workflowId);
|
|
1548
|
+
return row.n > 0;
|
|
1549
|
+
}
|
|
1550
|
+
function resetStrandedOrchestrationForWorkflow(db, workflowId) {
|
|
1551
|
+
const result = db.prepare(`UPDATE relay_handoff
|
|
1552
|
+
SET orchestrator_status = 'idle', orchestrator_claimed_at = NULL
|
|
1553
|
+
WHERE workflow_id = ?
|
|
1554
|
+
AND status = 'handed_back'
|
|
1555
|
+
AND orchestrator_status = 'pending'
|
|
1556
|
+
AND evaluator_verdict IS NULL`).run(workflowId);
|
|
1557
|
+
return result.changes;
|
|
1558
|
+
}
|
|
1559
|
+
function prependToPendingHandoffRequestText(db, input) {
|
|
1560
|
+
const row = db.prepare(`SELECT handoff_id, request_text FROM relay_handoff
|
|
1561
|
+
WHERE workflow_id = ? AND status = 'pending'
|
|
1562
|
+
ORDER BY created_at DESC LIMIT 1`).get(input.workflowId);
|
|
1563
|
+
if (!row)
|
|
1564
|
+
return false;
|
|
1565
|
+
db.prepare("UPDATE relay_handoff SET request_text = ?, last_activity_at = ? WHERE handoff_id = ?").run(`${input.prefix}
|
|
1566
|
+
|
|
1567
|
+
${row.request_text}`, input.now, row.handoff_id);
|
|
1568
|
+
return true;
|
|
1569
|
+
}
|
|
1450
1570
|
function claimRelayHandoffForOrchestrationTxn(db, input) {
|
|
1451
1571
|
const result = db.prepare(`UPDATE relay_handoff
|
|
1452
1572
|
SET orchestrator_status = 'pending',
|
|
@@ -1465,10 +1585,12 @@ function listRelayHandoffsPendingOrchestration(db, collabId) {
|
|
|
1465
1585
|
c.orchestrator_max_rounds AS max_rounds
|
|
1466
1586
|
FROM relay_handoff h
|
|
1467
1587
|
JOIN collab c ON c.collab_id = h.collab_id
|
|
1588
|
+
LEFT JOIN workflows w ON w.workflow_id = h.workflow_id
|
|
1468
1589
|
WHERE h.collab_id = ?
|
|
1469
1590
|
AND h.status = 'handed_back'
|
|
1470
|
-
AND h.orchestrator_status = 'idle'
|
|
1471
|
-
|
|
1591
|
+
AND h.orchestrator_status = 'idle'
|
|
1592
|
+
AND (w.status IS NULL OR w.status <> 'paused')`).all(collabId);
|
|
1593
|
+
return rows.map(rowToRecord4);
|
|
1472
1594
|
}
|
|
1473
1595
|
function createLoopRelayHandoffTxn(db, input) {
|
|
1474
1596
|
return db.transaction(() => {
|
|
@@ -1664,7 +1786,7 @@ function getHandoffWithWorkflowMetaById(db, handoffId) {
|
|
|
1664
1786
|
WHERE h.handoff_id = ?`).get(handoffId);
|
|
1665
1787
|
if (!row)
|
|
1666
1788
|
return null;
|
|
1667
|
-
const base =
|
|
1789
|
+
const base = rowToRecord4(row);
|
|
1668
1790
|
return {
|
|
1669
1791
|
...base,
|
|
1670
1792
|
handoffStep: row.handoff_step,
|
|
@@ -1914,71 +2036,6 @@ function listRunCostRows(db, input) {
|
|
|
1914
2036
|
// ../broker/dist/control/workflow-control.js
|
|
1915
2037
|
import { randomUUID } from "node:crypto";
|
|
1916
2038
|
|
|
1917
|
-
// ../broker/dist/storage/repositories/workflow-repository.js
|
|
1918
|
-
function rowToRecord4(row) {
|
|
1919
|
-
return {
|
|
1920
|
-
workflowId: row.workflow_id,
|
|
1921
|
-
collabId: row.collab_id,
|
|
1922
|
-
workflowType: row.workflow_type,
|
|
1923
|
-
name: row.name,
|
|
1924
|
-
specPath: row.spec_path,
|
|
1925
|
-
roleBindings: JSON.parse(row.role_bindings),
|
|
1926
|
-
status: row.status,
|
|
1927
|
-
currentPhaseIndex: row.current_phase_index,
|
|
1928
|
-
haltReason: row.halt_reason,
|
|
1929
|
-
workflowContext: JSON.parse(row.workflow_context),
|
|
1930
|
-
createdAt: row.created_at,
|
|
1931
|
-
updatedAt: row.updated_at
|
|
1932
|
-
};
|
|
1933
|
-
}
|
|
1934
|
-
function insertWorkflow(db, input) {
|
|
1935
|
-
db.prepare(`INSERT INTO workflows
|
|
1936
|
-
(workflow_id, collab_id, workflow_type, name, spec_path, role_bindings, status,
|
|
1937
|
-
current_phase_index, halt_reason, workflow_context, created_at, updated_at)
|
|
1938
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?)`).run(input.workflowId, input.collabId, input.workflowType, input.name, input.specPath, JSON.stringify(input.roleBindings), input.status, input.currentPhaseIndex, JSON.stringify(input.workflowContext), input.now, input.now);
|
|
1939
|
-
}
|
|
1940
|
-
function getWorkflowById(db, workflowId) {
|
|
1941
|
-
const row = db.prepare("SELECT * FROM workflows WHERE workflow_id = ?").get(workflowId);
|
|
1942
|
-
return row ? rowToRecord4(row) : null;
|
|
1943
|
-
}
|
|
1944
|
-
function listWorkflows(db, filter = {}) {
|
|
1945
|
-
const clauses = [];
|
|
1946
|
-
const args = [];
|
|
1947
|
-
if (filter.collabId) {
|
|
1948
|
-
clauses.push("collab_id = ?");
|
|
1949
|
-
args.push(filter.collabId);
|
|
1950
|
-
}
|
|
1951
|
-
if (filter.status) {
|
|
1952
|
-
clauses.push("status = ?");
|
|
1953
|
-
args.push(filter.status);
|
|
1954
|
-
}
|
|
1955
|
-
const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
1956
|
-
const rows = db.prepare(`SELECT * FROM workflows ${where} ORDER BY created_at DESC`).all(...args);
|
|
1957
|
-
return rows.map(rowToRecord4);
|
|
1958
|
-
}
|
|
1959
|
-
function setWorkflowStatus(db, input) {
|
|
1960
|
-
db.prepare(`UPDATE workflows
|
|
1961
|
-
SET status = ?, halt_reason = ?, updated_at = ?
|
|
1962
|
-
WHERE workflow_id = ?`).run(input.status, input.haltReason, input.now, input.workflowId);
|
|
1963
|
-
}
|
|
1964
|
-
function updateWorkflowContext(db, input) {
|
|
1965
|
-
const existing = getWorkflowById(db, input.workflowId);
|
|
1966
|
-
if (!existing) {
|
|
1967
|
-
throw new Error(`updateWorkflowContext: unknown workflowId ${input.workflowId}`);
|
|
1968
|
-
}
|
|
1969
|
-
const merged = { ...existing.workflowContext, ...input.patch };
|
|
1970
|
-
db.prepare("UPDATE workflows SET workflow_context = ?, updated_at = ? WHERE workflow_id = ?").run(JSON.stringify(merged), input.now, input.workflowId);
|
|
1971
|
-
}
|
|
1972
|
-
function incrementCurrentPhaseIndex(db, input) {
|
|
1973
|
-
db.prepare(`UPDATE workflows
|
|
1974
|
-
SET current_phase_index = current_phase_index + 1, updated_at = ?
|
|
1975
|
-
WHERE workflow_id = ?`).run(input.now, input.workflowId);
|
|
1976
|
-
}
|
|
1977
|
-
function countRunningWorkflowsForCollab(db, collabId) {
|
|
1978
|
-
const row = db.prepare("SELECT COUNT(*) AS n FROM workflows WHERE collab_id = ? AND status = 'running'").get(collabId);
|
|
1979
|
-
return row.n;
|
|
1980
|
-
}
|
|
1981
|
-
|
|
1982
2039
|
// ../broker/dist/storage/repositories/workflow-phase-repository.js
|
|
1983
2040
|
function rowToRecord5(row) {
|
|
1984
2041
|
return {
|
|
@@ -2099,6 +2156,14 @@ Findings: (omit this block entirely if none)
|
|
|
2099
2156
|
Non-blocking risks:
|
|
2100
2157
|
- <quality risk that does NOT block this gate, or "None.">
|
|
2101
2158
|
--- end protocol ---`;
|
|
2159
|
+
var WORKFLOW_OPERATOR_CONTROL = `--- ai-whisper operator control ---
|
|
2160
|
+
If the operator interrupts you and asks to pause the workflow (e.g. "pause the workflow, I need to fix X"):
|
|
2161
|
+
1. Find the active workflow id: run \`whisper workflow list\`.
|
|
2162
|
+
2. Run \`whisper workflow pause <id>\`.
|
|
2163
|
+
3. Acknowledge and STOP working \u2014 do not start the next change.
|
|
2164
|
+
The operator will edit artifacts and later run \`whisper workflow resume <id> [--message "..."]\`; you will then receive a notice of what changed and must re-read those files before continuing.
|
|
2165
|
+
Provider note: the Codex CLI EXITS its session on Ctrl+C at an idle prompt (a mid-task Ctrl+C only interrupts the running task). The operator typically interrupts you while you are BUSY before issuing this instruction; do not assume Ctrl+C is a safe no-op.
|
|
2166
|
+
--- end operator control ---`;
|
|
2102
2167
|
var WORKFLOW_DIAGNOSIS_PROTOCOL = `--- ai-whisper diagnosis review protocol ---
|
|
2103
2168
|
You are the gatekeeper for an ai-whisper complex-bug-fixing DIAGNOSIS gate. No human is in the loop. Do NOT trust the implementer's diagnosis \u2014 verify it yourself. The gate stays shut until YOU independently agree on the root cause and that the fix is net-safe.
|
|
2104
2169
|
|
|
@@ -2337,10 +2402,21 @@ var COMPLEX_BUG_FIXING = {
|
|
|
2337
2402
|
}
|
|
2338
2403
|
]
|
|
2339
2404
|
};
|
|
2405
|
+
function withOperatorControl(def) {
|
|
2406
|
+
return {
|
|
2407
|
+
...def,
|
|
2408
|
+
phases: def.phases.map((p) => ({
|
|
2409
|
+
...p,
|
|
2410
|
+
kickoffTemplate: `${p.kickoffTemplate}
|
|
2411
|
+
|
|
2412
|
+
${WORKFLOW_OPERATOR_CONTROL}`
|
|
2413
|
+
}))
|
|
2414
|
+
};
|
|
2415
|
+
}
|
|
2340
2416
|
var REGISTRY = {
|
|
2341
|
-
[SPEC_DRIVEN_DEVELOPMENT.type]: SPEC_DRIVEN_DEVELOPMENT,
|
|
2342
|
-
[RALPH_LOOP.type]: RALPH_LOOP,
|
|
2343
|
-
[COMPLEX_BUG_FIXING.type]: COMPLEX_BUG_FIXING
|
|
2417
|
+
[SPEC_DRIVEN_DEVELOPMENT.type]: withOperatorControl(SPEC_DRIVEN_DEVELOPMENT),
|
|
2418
|
+
[RALPH_LOOP.type]: withOperatorControl(RALPH_LOOP),
|
|
2419
|
+
[COMPLEX_BUG_FIXING.type]: withOperatorControl(COMPLEX_BUG_FIXING)
|
|
2344
2420
|
};
|
|
2345
2421
|
function ralphRunDir(workspaceRoot, workflowId) {
|
|
2346
2422
|
return join(workspaceRoot, ".ai-whisper", "ralph", workflowId);
|
|
@@ -2396,6 +2472,23 @@ function safeDerivePlanPath(specPath, createdAt) {
|
|
|
2396
2472
|
return `${dir}${stem}.plan.md`;
|
|
2397
2473
|
}
|
|
2398
2474
|
}
|
|
2475
|
+
function composeResumeNotice(input) {
|
|
2476
|
+
const hasFiles = input.changedFiles.length > 0;
|
|
2477
|
+
const hasMessage = !!input.message && input.message.trim().length > 0;
|
|
2478
|
+
if (!hasFiles && !hasMessage)
|
|
2479
|
+
return null;
|
|
2480
|
+
const lines = [];
|
|
2481
|
+
if (hasFiles) {
|
|
2482
|
+
lines.push("While paused, the operator modified these files:");
|
|
2483
|
+
for (const f of input.changedFiles)
|
|
2484
|
+
lines.push(` - ${f}`);
|
|
2485
|
+
lines.push("Re-read them before continuing.");
|
|
2486
|
+
}
|
|
2487
|
+
if (hasMessage)
|
|
2488
|
+
lines.push(`Operator note: ${input.message.trim()}`);
|
|
2489
|
+
lines.push("Re-evaluate whether your current direction still holds; correct course before proceeding.");
|
|
2490
|
+
return lines.join("\n");
|
|
2491
|
+
}
|
|
2399
2492
|
function createWorkflowControl(deps) {
|
|
2400
2493
|
const { db, events } = deps;
|
|
2401
2494
|
function createWorkflow(input) {
|
|
@@ -2418,8 +2511,9 @@ function createWorkflowControl(deps) {
|
|
|
2418
2511
|
}
|
|
2419
2512
|
const workflowId = `wf_${randomUUID().replace(/-/g, "").slice(0, 16)}`;
|
|
2420
2513
|
const tx = db.transaction(() => {
|
|
2421
|
-
if (
|
|
2422
|
-
|
|
2514
|
+
if (countActiveWorkflowsForCollab(db, input.collabId) > 0) {
|
|
2515
|
+
const active = listWorkflows(db, { collabId: input.collabId }).find((w) => w.status === "running" || w.status === "paused");
|
|
2516
|
+
throw new Error(`another workflow is already active on this collab (${input.collabId})` + (active ? `: ${active.workflowId} (${active.status})` : ""));
|
|
2423
2517
|
}
|
|
2424
2518
|
insertWorkflow(db, {
|
|
2425
2519
|
workflowId,
|
|
@@ -2587,6 +2681,24 @@ function createWorkflowControl(deps) {
|
|
|
2587
2681
|
postmortemPath: bf.postmortemPath
|
|
2588
2682
|
});
|
|
2589
2683
|
}
|
|
2684
|
+
function consumeResumeNotice(input) {
|
|
2685
|
+
const wf = getWorkflowById(db, input.workflowId);
|
|
2686
|
+
const notice = wf?.workflowContext?.resumeNotice ?? null;
|
|
2687
|
+
if (notice) {
|
|
2688
|
+
updateWorkflowContext(db, {
|
|
2689
|
+
workflowId: input.workflowId,
|
|
2690
|
+
patch: { resumeNotice: null },
|
|
2691
|
+
now: input.now
|
|
2692
|
+
});
|
|
2693
|
+
}
|
|
2694
|
+
return notice;
|
|
2695
|
+
}
|
|
2696
|
+
function withResumeNotice(workflowId, requestText, now) {
|
|
2697
|
+
const notice = consumeResumeNotice({ workflowId, now });
|
|
2698
|
+
return notice ? `${notice}
|
|
2699
|
+
|
|
2700
|
+
${requestText}` : requestText;
|
|
2701
|
+
}
|
|
2590
2702
|
function createContinuationHandoff(input) {
|
|
2591
2703
|
const handoffId = `ho_${randomUUID().replace(/-/g, "").slice(0, 16)}`;
|
|
2592
2704
|
insertWorkflowOwnedRelayHandoff(db, {
|
|
@@ -2594,7 +2706,9 @@ function createWorkflowControl(deps) {
|
|
|
2594
2706
|
collabId: input.workflow.collabId,
|
|
2595
2707
|
senderAgent: input.sender,
|
|
2596
2708
|
targetAgent: input.target,
|
|
2597
|
-
|
|
2709
|
+
// Prepend the one-time resume notice (if any) to the FIRST outgoing request
|
|
2710
|
+
// after a resume, then it is cleared — so the agent re-reads changed files.
|
|
2711
|
+
requestText: withResumeNotice(input.workflow.workflowId, input.requestText, input.now),
|
|
2598
2712
|
chainId: input.chain.chainId,
|
|
2599
2713
|
roundNumber: input.incrementRound ? input.chain.currentRound + 1 : input.chain.currentRound,
|
|
2600
2714
|
maxRounds: input.chain.maxRounds,
|
|
@@ -2654,7 +2768,9 @@ function createWorkflowControl(deps) {
|
|
|
2654
2768
|
collabId: input.workflow.collabId,
|
|
2655
2769
|
senderAgent: sender,
|
|
2656
2770
|
targetAgent: target,
|
|
2657
|
-
|
|
2771
|
+
// Prepend a pending resume notice (consumed once) so a phase-advance kickoff
|
|
2772
|
+
// after a resume also carries the operator's change notice.
|
|
2773
|
+
requestText: withResumeNotice(input.workflow.workflowId, kickoffText, input.now),
|
|
2658
2774
|
chainId,
|
|
2659
2775
|
roundNumber: 1,
|
|
2660
2776
|
maxRounds: phase.maxRounds,
|
|
@@ -3115,20 +3231,100 @@ ${findingsText}`;
|
|
|
3115
3231
|
reason: input.reason
|
|
3116
3232
|
});
|
|
3117
3233
|
}
|
|
3234
|
+
function maybeCaptureQuiesceSnapshot(input) {
|
|
3235
|
+
const wf = getWorkflowById(db, input.workflowId);
|
|
3236
|
+
if (!wf || wf.status !== "paused")
|
|
3237
|
+
return;
|
|
3238
|
+
if (wf.workflowContext.pauseSnapshotRef !== void 0) {
|
|
3239
|
+
return;
|
|
3240
|
+
}
|
|
3241
|
+
if (hasInFlightAcceptedHandoffForWorkflow(db, input.workflowId))
|
|
3242
|
+
return;
|
|
3243
|
+
const ref = deps.captureSnapshotRef ? deps.captureSnapshotRef(input.workflowId) : null;
|
|
3244
|
+
updateWorkflowContext(db, {
|
|
3245
|
+
workflowId: input.workflowId,
|
|
3246
|
+
patch: { pauseSnapshotRef: ref },
|
|
3247
|
+
now: input.now
|
|
3248
|
+
});
|
|
3249
|
+
}
|
|
3250
|
+
function pauseWorkflow(input) {
|
|
3251
|
+
const workflow = getWorkflowById(db, input.workflowId);
|
|
3252
|
+
if (!workflow) {
|
|
3253
|
+
throw new Error(`pauseWorkflow: unknown workflowId ${input.workflowId}`);
|
|
3254
|
+
}
|
|
3255
|
+
if (workflow.status !== "running") {
|
|
3256
|
+
throw new Error(`pauseWorkflow: workflow ${input.workflowId} is ${workflow.status}, only running workflows can be paused`);
|
|
3257
|
+
}
|
|
3258
|
+
const tx = db.transaction(() => {
|
|
3259
|
+
const current = getWorkflowById(db, input.workflowId);
|
|
3260
|
+
if (!current || current.status !== "running")
|
|
3261
|
+
return;
|
|
3262
|
+
setWorkflowStatus(db, {
|
|
3263
|
+
workflowId: input.workflowId,
|
|
3264
|
+
status: "paused",
|
|
3265
|
+
haltReason: null,
|
|
3266
|
+
now: input.now
|
|
3267
|
+
});
|
|
3268
|
+
updateWorkflowContext(db, {
|
|
3269
|
+
workflowId: input.workflowId,
|
|
3270
|
+
patch: { pausedAt: input.now },
|
|
3271
|
+
now: input.now
|
|
3272
|
+
});
|
|
3273
|
+
});
|
|
3274
|
+
tx.immediate();
|
|
3275
|
+
events.emit("workflow.paused", { workflowId: input.workflowId });
|
|
3276
|
+
maybeCaptureQuiesceSnapshot({ workflowId: input.workflowId, now: input.now });
|
|
3277
|
+
}
|
|
3278
|
+
function resumeHaltedWorkflow(workflow, now) {
|
|
3279
|
+
const tx = db.transaction(() => {
|
|
3280
|
+
const others = listWorkflows(db, { collabId: workflow.collabId }).filter((w) => w.workflowId !== workflow.workflowId && (w.status === "running" || w.status === "paused"));
|
|
3281
|
+
if (others.length > 0) {
|
|
3282
|
+
throw new Error(`resumeWorkflow: another workflow is already active on collab ${workflow.collabId}`);
|
|
3283
|
+
}
|
|
3284
|
+
setWorkflowStatus(db, {
|
|
3285
|
+
workflowId: workflow.workflowId,
|
|
3286
|
+
status: "running",
|
|
3287
|
+
haltReason: null,
|
|
3288
|
+
now
|
|
3289
|
+
});
|
|
3290
|
+
});
|
|
3291
|
+
tx.immediate();
|
|
3292
|
+
events.emit("workflow.resumed", {
|
|
3293
|
+
workflowId: workflow.workflowId,
|
|
3294
|
+
phaseIndex: workflow.currentPhaseIndex
|
|
3295
|
+
});
|
|
3296
|
+
}
|
|
3118
3297
|
function resumeWorkflow(input) {
|
|
3119
3298
|
const workflow = getWorkflowById(db, input.workflowId);
|
|
3120
3299
|
if (!workflow) {
|
|
3121
3300
|
throw new Error(`resumeWorkflow: unknown workflowId ${input.workflowId}`);
|
|
3122
3301
|
}
|
|
3123
|
-
if (workflow.status === "
|
|
3124
|
-
|
|
3302
|
+
if (workflow.status === "halted") {
|
|
3303
|
+
resumeHaltedWorkflow(workflow, input.now);
|
|
3304
|
+
return;
|
|
3125
3305
|
}
|
|
3126
|
-
if (workflow.status !== "
|
|
3127
|
-
throw new Error(`resumeWorkflow: workflow ${input.workflowId} is
|
|
3306
|
+
if (workflow.status !== "paused") {
|
|
3307
|
+
throw new Error(`resumeWorkflow: workflow ${input.workflowId} is ${workflow.status}, only paused or halted workflows can be resumed`);
|
|
3128
3308
|
}
|
|
3309
|
+
const ref = workflow.workflowContext.pauseSnapshotRef ?? null;
|
|
3310
|
+
const changedFiles = ref && deps.diffChangedFilesSinceSnapshot ? deps.diffChangedFilesSinceSnapshot(input.workflowId, ref) : [];
|
|
3311
|
+
const notice = composeResumeNotice({
|
|
3312
|
+
changedFiles,
|
|
3313
|
+
message: input.message ?? null
|
|
3314
|
+
});
|
|
3129
3315
|
const tx = db.transaction(() => {
|
|
3130
|
-
|
|
3131
|
-
|
|
3316
|
+
const others = listWorkflows(db, { collabId: workflow.collabId }).filter((w) => w.workflowId !== input.workflowId && (w.status === "running" || w.status === "paused"));
|
|
3317
|
+
if (others.length > 0) {
|
|
3318
|
+
throw new Error(`resumeWorkflow: another workflow is already active on collab ${workflow.collabId}`);
|
|
3319
|
+
}
|
|
3320
|
+
resetStrandedOrchestrationForWorkflow(db, input.workflowId);
|
|
3321
|
+
let bakedIntoPending = false;
|
|
3322
|
+
if (notice) {
|
|
3323
|
+
bakedIntoPending = prependToPendingHandoffRequestText(db, {
|
|
3324
|
+
workflowId: input.workflowId,
|
|
3325
|
+
prefix: notice,
|
|
3326
|
+
now: input.now
|
|
3327
|
+
});
|
|
3132
3328
|
}
|
|
3133
3329
|
setWorkflowStatus(db, {
|
|
3134
3330
|
workflowId: input.workflowId,
|
|
@@ -3136,6 +3332,15 @@ ${findingsText}`;
|
|
|
3136
3332
|
haltReason: null,
|
|
3137
3333
|
now: input.now
|
|
3138
3334
|
});
|
|
3335
|
+
updateWorkflowContext(db, {
|
|
3336
|
+
workflowId: input.workflowId,
|
|
3337
|
+
patch: {
|
|
3338
|
+
resumeNotice: bakedIntoPending ? null : notice,
|
|
3339
|
+
pausedAt: null,
|
|
3340
|
+
pauseSnapshotRef: null
|
|
3341
|
+
},
|
|
3342
|
+
now: input.now
|
|
3343
|
+
});
|
|
3139
3344
|
});
|
|
3140
3345
|
tx.immediate();
|
|
3141
3346
|
events.emit("workflow.resumed", {
|
|
@@ -3236,6 +3441,9 @@ ${findingsText}`;
|
|
|
3236
3441
|
beginPhaseRun,
|
|
3237
3442
|
applyOrchestratorVerdict,
|
|
3238
3443
|
haltWorkflow,
|
|
3444
|
+
pauseWorkflow,
|
|
3445
|
+
maybeCaptureQuiesceSnapshot,
|
|
3446
|
+
consumeResumeNotice,
|
|
3239
3447
|
resumeWorkflow,
|
|
3240
3448
|
cancelWorkflow,
|
|
3241
3449
|
getHandoffWithWorkflowMeta,
|
|
@@ -3251,8 +3459,32 @@ function buildEventId(kind, subjectId, timestamp) {
|
|
|
3251
3459
|
return `evt_${kind}_${subjectId}_${normalizeTimestampForEventId(timestamp)}`;
|
|
3252
3460
|
}
|
|
3253
3461
|
function createControlService(db, events) {
|
|
3254
|
-
const
|
|
3462
|
+
const resolveWorkspaceRoot = (workflowId) => {
|
|
3463
|
+
const wf = getWorkflowById(db, workflowId);
|
|
3464
|
+
if (!wf)
|
|
3465
|
+
return null;
|
|
3466
|
+
return getCollab(db, wf.collabId)?.workspaceRoot ?? null;
|
|
3467
|
+
};
|
|
3468
|
+
const workflowControl = createWorkflowControl({
|
|
3469
|
+
db,
|
|
3470
|
+
events,
|
|
3471
|
+
captureSnapshotRef: (workflowId) => {
|
|
3472
|
+
const root = resolveWorkspaceRoot(workflowId);
|
|
3473
|
+
return root ? captureWorkspaceSnapshotSync(root) : null;
|
|
3474
|
+
},
|
|
3475
|
+
diffChangedFilesSinceSnapshot: (workflowId, sinceRef) => {
|
|
3476
|
+
const root = resolveWorkspaceRoot(workflowId);
|
|
3477
|
+
return root ? diffChangedFilesSince(root, sinceRef) : [];
|
|
3478
|
+
}
|
|
3479
|
+
});
|
|
3480
|
+
const isWorkflowDeliverySuspended = (handoffId) => {
|
|
3481
|
+
const meta = workflowControl.getHandoffWithWorkflowMeta(handoffId);
|
|
3482
|
+
if (!meta?.workflowId)
|
|
3483
|
+
return false;
|
|
3484
|
+
return workflowControl.getWorkflow(meta.workflowId)?.status === "paused";
|
|
3485
|
+
};
|
|
3255
3486
|
return Object.assign({
|
|
3487
|
+
isWorkflowDeliverySuspended,
|
|
3256
3488
|
startCollab(input) {
|
|
3257
3489
|
const collab = collabSchema.parse({
|
|
3258
3490
|
version: 1,
|
|
@@ -3880,6 +4112,8 @@ function createControlService(db, events) {
|
|
|
3880
4112
|
return createRelayHandoffTxn(db, input);
|
|
3881
4113
|
},
|
|
3882
4114
|
acceptRelayHandoff(input) {
|
|
4115
|
+
if (isWorkflowDeliverySuspended(input.handoffId))
|
|
4116
|
+
return;
|
|
3883
4117
|
return acceptRelayHandoffTxn(db, input);
|
|
3884
4118
|
},
|
|
3885
4119
|
deferRelayHandoff(input) {
|
|
@@ -3892,7 +4126,12 @@ function createControlService(db, events) {
|
|
|
3892
4126
|
return markRelayHandoffStaleTxn(db, input);
|
|
3893
4127
|
},
|
|
3894
4128
|
handoffBackRelay(input) {
|
|
3895
|
-
|
|
4129
|
+
const result = handoffBackRelayTxn(db, input);
|
|
4130
|
+
const meta = workflowControl.getHandoffWithWorkflowMeta(input.handoffId);
|
|
4131
|
+
if (meta?.workflowId && workflowControl.getWorkflow(meta.workflowId)?.status === "paused") {
|
|
4132
|
+
workflowControl.maybeCaptureQuiesceSnapshot({ workflowId: meta.workflowId, now: input.now });
|
|
4133
|
+
}
|
|
4134
|
+
return result;
|
|
3896
4135
|
},
|
|
3897
4136
|
failRelayHandoffOnDisconnect(input) {
|
|
3898
4137
|
return failRelayHandoffOnDisconnectTxn(db, input);
|
|
@@ -3904,6 +4143,8 @@ function createControlService(db, events) {
|
|
|
3904
4143
|
return queryLatestHandedBackHandoff(db, collabId);
|
|
3905
4144
|
},
|
|
3906
4145
|
claimRelayHandoffForOrchestration(input) {
|
|
4146
|
+
if (isWorkflowDeliverySuspended(input.handoffId))
|
|
4147
|
+
return null;
|
|
3907
4148
|
return claimRelayHandoffForOrchestrationTxn(db, input);
|
|
3908
4149
|
},
|
|
3909
4150
|
listRelayHandoffsPendingOrchestration(collabId) {
|
|
@@ -3939,6 +4180,7 @@ function createControlService(db, events) {
|
|
|
3939
4180
|
clipSample: input.clipSample,
|
|
3940
4181
|
turnSample: input.turnSample,
|
|
3941
4182
|
abortedByRaceGuard: input.abortedByRaceGuard,
|
|
4183
|
+
interferenceDetected: input.interferenceDetected ?? false,
|
|
3942
4184
|
createdAt: input.now
|
|
3943
4185
|
});
|
|
3944
4186
|
return { captureId };
|
|
@@ -4015,10 +4257,155 @@ function createBrokerApp(input) {
|
|
|
4015
4257
|
return app;
|
|
4016
4258
|
}
|
|
4017
4259
|
|
|
4018
|
-
// ../broker/dist/storage/
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4260
|
+
// ../broker/dist/storage/enforce-one-active-collab.js
|
|
4261
|
+
function defaultIsPidAlive(pid) {
|
|
4262
|
+
try {
|
|
4263
|
+
process.kill(pid, 0);
|
|
4264
|
+
return true;
|
|
4265
|
+
} catch (err) {
|
|
4266
|
+
return err.code === "EPERM";
|
|
4267
|
+
}
|
|
4268
|
+
}
|
|
4269
|
+
function dedupeActiveCollabs(db, opts) {
|
|
4270
|
+
const rows = db.prepare(`SELECT c.collab_id, c.workspace_id, c.created_at, d.pid AS pid,
|
|
4271
|
+
(SELECT COUNT(*) FROM workflows w
|
|
4272
|
+
WHERE w.collab_id = c.collab_id AND w.status = 'running') AS running_workflows
|
|
4273
|
+
FROM collab c
|
|
4274
|
+
LEFT JOIN broker_daemon d ON d.collab_id = c.collab_id
|
|
4275
|
+
WHERE c.status = 'active' AND c.workspace_id IS NOT NULL`).all();
|
|
4276
|
+
const byWorkspace = /* @__PURE__ */ new Map();
|
|
4277
|
+
for (const row of rows) {
|
|
4278
|
+
const key = row.workspace_id;
|
|
4279
|
+
const list = byWorkspace.get(key);
|
|
4280
|
+
if (list)
|
|
4281
|
+
list.push(row);
|
|
4282
|
+
else
|
|
4283
|
+
byWorkspace.set(key, [row]);
|
|
4284
|
+
}
|
|
4285
|
+
const conflicted = [];
|
|
4286
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4287
|
+
const stop = db.prepare("UPDATE collab SET status = 'stopped', stopped_at = ?, updated_at = ? WHERE collab_id = ?");
|
|
4288
|
+
for (const [workspaceId, group] of byWorkspace) {
|
|
4289
|
+
if (group.length < 2)
|
|
4290
|
+
continue;
|
|
4291
|
+
const workflowOwners = group.filter((r) => r.running_workflows > 0);
|
|
4292
|
+
if (workflowOwners.length >= 2) {
|
|
4293
|
+
conflicted.push(workspaceId);
|
|
4294
|
+
opts.warn(`Workspace ${workspaceId} has ${workflowOwners.length} active collabs that each own a running workflow: ${workflowOwners.map((r) => r.collab_id).join(", ")}. Refusing to auto-stop any (that would orphan a live run). Stop the extra collab(s) manually with \`whisper collab stop --collab <id>\`. The one-active-collab-per-workspace index will be created automatically once only one remains.`);
|
|
4295
|
+
continue;
|
|
4296
|
+
}
|
|
4297
|
+
let survivor;
|
|
4298
|
+
if (workflowOwners.length === 1) {
|
|
4299
|
+
survivor = workflowOwners[0];
|
|
4300
|
+
} else {
|
|
4301
|
+
const live = group.filter((r) => r.pid !== null && opts.isPidAlive(r.pid));
|
|
4302
|
+
if (live.length >= 1) {
|
|
4303
|
+
survivor = live.reduce((a, b) => a.created_at >= b.created_at ? a : b);
|
|
4304
|
+
} else {
|
|
4305
|
+
survivor = group.reduce((a, b) => a.created_at >= b.created_at ? a : b);
|
|
4306
|
+
}
|
|
4307
|
+
}
|
|
4308
|
+
for (const row of group) {
|
|
4309
|
+
if (row.collab_id === survivor.collab_id)
|
|
4310
|
+
continue;
|
|
4311
|
+
stop.run(now, now, row.collab_id);
|
|
4312
|
+
}
|
|
4313
|
+
}
|
|
4314
|
+
return conflicted;
|
|
4315
|
+
}
|
|
4316
|
+
var CREATE_INDEX_SQL = "CREATE UNIQUE INDEX IF NOT EXISTS idx_collab_one_active_per_workspace ON collab(workspace_id) WHERE status = 'active'";
|
|
4317
|
+
function residualDuplicateCount(db) {
|
|
4318
|
+
const row = db.prepare(`SELECT COUNT(*) AS n FROM (
|
|
4319
|
+
SELECT workspace_id FROM collab
|
|
4320
|
+
WHERE status = 'active' AND workspace_id IS NOT NULL
|
|
4321
|
+
GROUP BY workspace_id HAVING COUNT(*) > 1
|
|
4322
|
+
)`).get();
|
|
4323
|
+
return row.n;
|
|
4324
|
+
}
|
|
4325
|
+
function enforceOneActiveCollabPerWorkspace(db, options = {}) {
|
|
4326
|
+
const opts = {
|
|
4327
|
+
isPidAlive: options.isPidAlive ?? defaultIsPidAlive,
|
|
4328
|
+
warn: options.warn ?? ((m) => console.error(m))
|
|
4329
|
+
};
|
|
4330
|
+
const tx = db.transaction(() => {
|
|
4331
|
+
dedupeActiveCollabs(db, opts);
|
|
4332
|
+
if (residualDuplicateCount(db) === 0) {
|
|
4333
|
+
db.exec(CREATE_INDEX_SQL);
|
|
4334
|
+
} else {
|
|
4335
|
+
opts.warn("Skipping idx_collab_one_active_per_workspace: an irreducible duplicate active collab remains. The index will be created on a later startup once only one active collab per workspace exists.");
|
|
4336
|
+
}
|
|
4337
|
+
});
|
|
4338
|
+
tx();
|
|
4339
|
+
}
|
|
4340
|
+
|
|
4341
|
+
// ../broker/dist/storage/clipboard-capture-lease.js
|
|
4342
|
+
var LEASE_ID = 1;
|
|
4343
|
+
var DEFAULT_LEASE_TTL_MS = 5e3;
|
|
4344
|
+
function defaultIsPidAlive2(pid) {
|
|
4345
|
+
try {
|
|
4346
|
+
process.kill(pid, 0);
|
|
4347
|
+
return true;
|
|
4348
|
+
} catch (err) {
|
|
4349
|
+
return err.code === "EPERM";
|
|
4350
|
+
}
|
|
4351
|
+
}
|
|
4352
|
+
function resolveOptions(options) {
|
|
4353
|
+
return {
|
|
4354
|
+
isPidAlive: options.isPidAlive ?? defaultIsPidAlive2,
|
|
4355
|
+
ttlMs: options.ttlMs ?? DEFAULT_LEASE_TTL_MS,
|
|
4356
|
+
now: options.now ?? Date.now
|
|
4357
|
+
};
|
|
4358
|
+
}
|
|
4359
|
+
function isStale(row, opts) {
|
|
4360
|
+
if (row.holder_collab_id === null)
|
|
4361
|
+
return true;
|
|
4362
|
+
if (row.holder_pid === null || !opts.isPidAlive(row.holder_pid))
|
|
4363
|
+
return true;
|
|
4364
|
+
if (row.acquired_at === null)
|
|
4365
|
+
return true;
|
|
4366
|
+
const age = opts.now() - Date.parse(row.acquired_at);
|
|
4367
|
+
return age > opts.ttlMs;
|
|
4368
|
+
}
|
|
4369
|
+
function acquireCaptureLease(db, collabId, pid, options = {}) {
|
|
4370
|
+
const opts = resolveOptions(options);
|
|
4371
|
+
const tx = db.transaction(() => {
|
|
4372
|
+
const row = db.prepare("SELECT holder_collab_id, holder_pid, acquired_at FROM clipboard_capture_lease WHERE id = ?").get(LEASE_ID);
|
|
4373
|
+
if (row && !isStale(row, opts))
|
|
4374
|
+
return false;
|
|
4375
|
+
const acquiredAt = new Date(opts.now()).toISOString();
|
|
4376
|
+
db.prepare(`INSERT INTO clipboard_capture_lease (id, holder_collab_id, holder_pid, acquired_at)
|
|
4377
|
+
VALUES (?, ?, ?, ?)
|
|
4378
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
4379
|
+
holder_collab_id = excluded.holder_collab_id,
|
|
4380
|
+
holder_pid = excluded.holder_pid,
|
|
4381
|
+
acquired_at = excluded.acquired_at`).run(LEASE_ID, collabId, pid, acquiredAt);
|
|
4382
|
+
return true;
|
|
4383
|
+
});
|
|
4384
|
+
return tx();
|
|
4385
|
+
}
|
|
4386
|
+
function releaseCaptureLease(db, collabId) {
|
|
4387
|
+
const tx = db.transaction(() => {
|
|
4388
|
+
db.prepare("UPDATE clipboard_capture_lease SET holder_collab_id = NULL, holder_pid = NULL, acquired_at = NULL WHERE id = ? AND holder_collab_id = ?").run(LEASE_ID, collabId);
|
|
4389
|
+
});
|
|
4390
|
+
tx();
|
|
4391
|
+
}
|
|
4392
|
+
function sweepStaleCaptureLease(db, options = {}) {
|
|
4393
|
+
const opts = resolveOptions(options);
|
|
4394
|
+
const tx = db.transaction(() => {
|
|
4395
|
+
const row = db.prepare("SELECT holder_collab_id, holder_pid, acquired_at FROM clipboard_capture_lease WHERE id = ?").get(LEASE_ID);
|
|
4396
|
+
if (!row || row.holder_collab_id === null)
|
|
4397
|
+
return;
|
|
4398
|
+
if (isStale(row, opts)) {
|
|
4399
|
+
db.prepare("UPDATE clipboard_capture_lease SET holder_collab_id = NULL, holder_pid = NULL, acquired_at = NULL WHERE id = ?").run(LEASE_ID);
|
|
4400
|
+
}
|
|
4401
|
+
});
|
|
4402
|
+
tx();
|
|
4403
|
+
}
|
|
4404
|
+
|
|
4405
|
+
// ../broker/dist/storage/apply-migrations.js
|
|
4406
|
+
var CURRENT_SCHEMA_VERSION = 5;
|
|
4407
|
+
var initMigrationSql = `
|
|
4408
|
+
CREATE TABLE IF NOT EXISTS broker_state (
|
|
4022
4409
|
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
4023
4410
|
schema_version INTEGER NOT NULL,
|
|
4024
4411
|
migrated INTEGER NOT NULL
|
|
@@ -4234,8 +4621,9 @@ CREATE TABLE IF NOT EXISTS workflows (
|
|
|
4234
4621
|
updated_at TEXT NOT NULL
|
|
4235
4622
|
);
|
|
4236
4623
|
|
|
4624
|
+
DROP INDEX IF EXISTS workflows_one_running_per_collab;
|
|
4237
4625
|
CREATE UNIQUE INDEX IF NOT EXISTS workflows_one_running_per_collab
|
|
4238
|
-
ON workflows(collab_id) WHERE status
|
|
4626
|
+
ON workflows(collab_id) WHERE status IN ('running', 'paused');
|
|
4239
4627
|
|
|
4240
4628
|
CREATE TABLE IF NOT EXISTS workflow_phases (
|
|
4241
4629
|
phase_run_id TEXT PRIMARY KEY,
|
|
@@ -4303,6 +4691,13 @@ CREATE TABLE IF NOT EXISTS recovery_state (
|
|
|
4303
4691
|
recovered_at TEXT
|
|
4304
4692
|
);
|
|
4305
4693
|
|
|
4694
|
+
CREATE TABLE IF NOT EXISTS clipboard_capture_lease (
|
|
4695
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
4696
|
+
holder_collab_id TEXT,
|
|
4697
|
+
holder_pid INTEGER,
|
|
4698
|
+
acquired_at TEXT
|
|
4699
|
+
);
|
|
4700
|
+
|
|
4306
4701
|
`;
|
|
4307
4702
|
function ensureBrokerStateRow(db) {
|
|
4308
4703
|
db.prepare(`INSERT INTO broker_state (id, schema_version, migrated)
|
|
@@ -4313,20 +4708,20 @@ function ensureBrokerStateRow(db) {
|
|
|
4313
4708
|
}
|
|
4314
4709
|
function applyMigrations(db) {
|
|
4315
4710
|
const current = db.pragma("user_version", { simple: true });
|
|
4316
|
-
if (current
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
} catch (err) {
|
|
4327
|
-
db.exec("ROLLBACK");
|
|
4328
|
-
throw err;
|
|
4711
|
+
if (current < CURRENT_SCHEMA_VERSION) {
|
|
4712
|
+
db.exec("BEGIN EXCLUSIVE");
|
|
4713
|
+
try {
|
|
4714
|
+
runMigrationBody(db);
|
|
4715
|
+
db.pragma(`user_version = ${CURRENT_SCHEMA_VERSION}`);
|
|
4716
|
+
db.exec("COMMIT");
|
|
4717
|
+
} catch (err) {
|
|
4718
|
+
db.exec("ROLLBACK");
|
|
4719
|
+
throw err;
|
|
4720
|
+
}
|
|
4329
4721
|
}
|
|
4722
|
+
ensureBrokerStateRow(db);
|
|
4723
|
+
enforceOneActiveCollabPerWorkspace(db);
|
|
4724
|
+
sweepStaleCaptureLease(db);
|
|
4330
4725
|
}
|
|
4331
4726
|
function runMigrationBody(db) {
|
|
4332
4727
|
db.exec(initMigrationSql);
|
|
@@ -4463,6 +4858,7 @@ function runMigrationBody(db) {
|
|
|
4463
4858
|
clip_sample TEXT,
|
|
4464
4859
|
turn_sample TEXT,
|
|
4465
4860
|
aborted_by_race_guard INTEGER NOT NULL DEFAULT 0,
|
|
4861
|
+
interference_detected INTEGER NOT NULL DEFAULT 0,
|
|
4466
4862
|
created_at TEXT NOT NULL
|
|
4467
4863
|
);
|
|
4468
4864
|
CREATE INDEX IF NOT EXISTS idx_relay_capture_diagnostics_collab_created
|
|
@@ -4474,6 +4870,10 @@ function runMigrationBody(db) {
|
|
|
4474
4870
|
CREATE INDEX IF NOT EXISTS idx_relay_capture_diagnostics_status
|
|
4475
4871
|
ON relay_capture_diagnostics (capture_status);
|
|
4476
4872
|
`);
|
|
4873
|
+
const captureDiagColumns = db.prepare("PRAGMA table_info(relay_capture_diagnostics)").all();
|
|
4874
|
+
if (!captureDiagColumns.some((column) => column.name === "interference_detected")) {
|
|
4875
|
+
db.exec("ALTER TABLE relay_capture_diagnostics ADD COLUMN interference_detected INTEGER NOT NULL DEFAULT 0");
|
|
4876
|
+
}
|
|
4477
4877
|
db.exec(`
|
|
4478
4878
|
CREATE TABLE IF NOT EXISTS relay_evaluator_diagnostics (
|
|
4479
4879
|
evaluator_id TEXT PRIMARY KEY,
|
|
@@ -5254,11 +5654,11 @@ function lookupByCwd(db, cwd) {
|
|
|
5254
5654
|
}
|
|
5255
5655
|
|
|
5256
5656
|
// src/runtime/current-tty.ts
|
|
5257
|
-
import { execFileSync } from "node:child_process";
|
|
5657
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
5258
5658
|
import { existsSync as existsSync5 } from "node:fs";
|
|
5259
5659
|
function resolveCurrentTty() {
|
|
5260
5660
|
const stdin = process.stdin;
|
|
5261
|
-
const ttyPath = stdin.isTTY && typeof stdin.path === "string" ? stdin.path :
|
|
5661
|
+
const ttyPath = stdin.isTTY && typeof stdin.path === "string" ? stdin.path : execFileSync2("tty", [], {
|
|
5262
5662
|
encoding: "utf8",
|
|
5263
5663
|
stdio: ["inherit", "pipe", "pipe"]
|
|
5264
5664
|
}).trim();
|
|
@@ -7098,6 +7498,9 @@ function createMountedTurnOwnedRelay(input) {
|
|
|
7098
7498
|
if (!handoff) return null;
|
|
7099
7499
|
if (handoff.status !== "pending" && handoff.status !== "deferred") return null;
|
|
7100
7500
|
if (handoff.targetAgent !== input.currentAgent) return null;
|
|
7501
|
+
if (input.broker.control.isWorkflowDeliverySuspended?.(handoff.handoffId)) {
|
|
7502
|
+
return null;
|
|
7503
|
+
}
|
|
7101
7504
|
return handoff;
|
|
7102
7505
|
}
|
|
7103
7506
|
function getAcceptedHandoff() {
|
|
@@ -7279,7 +7682,8 @@ ${hint}`,
|
|
|
7279
7682
|
let composed = null;
|
|
7280
7683
|
let initialValue = "";
|
|
7281
7684
|
if (input.captureHandbackText) {
|
|
7282
|
-
const
|
|
7685
|
+
const outcome = await input.captureHandbackText("") ?? "";
|
|
7686
|
+
const captured = typeof outcome === "string" ? outcome : outcome.text ?? "";
|
|
7283
7687
|
if (captured.trim().length > 0 && input.confirmHandbackCapture) {
|
|
7284
7688
|
const accepted = await input.confirmHandbackCapture({
|
|
7285
7689
|
target,
|
|
@@ -7341,14 +7745,29 @@ ${hint}`,
|
|
|
7341
7745
|
input.turnCapture?.finishAssistantTurn();
|
|
7342
7746
|
const turnResult = input.turnCapture?.extractLatestAssistantTurn() ?? { confidence: "low", text: null };
|
|
7343
7747
|
let clipboardText = null;
|
|
7748
|
+
let leaseDegraded = false;
|
|
7749
|
+
let interferenceDetected = false;
|
|
7344
7750
|
try {
|
|
7345
|
-
|
|
7751
|
+
const captureResult = await input.captureHandbackText?.(turnResult.text ?? "") ?? null;
|
|
7752
|
+
if (typeof captureResult === "string") {
|
|
7753
|
+
clipboardText = captureResult;
|
|
7754
|
+
} else if (captureResult !== null) {
|
|
7755
|
+
interferenceDetected = captureResult.interferenceDetected;
|
|
7756
|
+
if (captureResult.status === "captured") {
|
|
7757
|
+
clipboardText = captureResult.text;
|
|
7758
|
+
} else {
|
|
7759
|
+
leaseDegraded = true;
|
|
7760
|
+
}
|
|
7761
|
+
}
|
|
7346
7762
|
} catch {
|
|
7347
7763
|
clipboardText = null;
|
|
7348
7764
|
}
|
|
7349
7765
|
const classification = classifyCapture(turnResult, clipboardText);
|
|
7350
7766
|
const captureStatus = classification.status;
|
|
7351
|
-
|
|
7767
|
+
let requestText = captureStatus === "ok" ? clipboardText ?? "" : "";
|
|
7768
|
+
if (leaseDegraded && (turnResult.text ?? "").trim().length > 0) {
|
|
7769
|
+
requestText = turnResult.text;
|
|
7770
|
+
}
|
|
7352
7771
|
const currentAcceptedId = getAcceptedHandoff()?.handoffId;
|
|
7353
7772
|
const abortedByRaceGuard = currentAcceptedId !== accepted.handoffId;
|
|
7354
7773
|
const handoffMeta = input.broker.control.getHandoffWithWorkflowMeta?.(accepted.handoffId) ?? null;
|
|
@@ -7375,6 +7794,7 @@ ${hint}`,
|
|
|
7375
7794
|
clipSample: sampleOf(clipboardText),
|
|
7376
7795
|
turnSample: sampleOf(turnResult.text),
|
|
7377
7796
|
abortedByRaceGuard,
|
|
7797
|
+
interferenceDetected,
|
|
7378
7798
|
now
|
|
7379
7799
|
});
|
|
7380
7800
|
} catch (err) {
|
|
@@ -7679,6 +8099,107 @@ async function captureClipboardHandback(input) {
|
|
|
7679
8099
|
return null;
|
|
7680
8100
|
}
|
|
7681
8101
|
|
|
8102
|
+
// src/runtime/capture-handback-text.ts
|
|
8103
|
+
var defaultSleep = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
8104
|
+
function contentMatches(turnText, clip) {
|
|
8105
|
+
const t = turnText.trim();
|
|
8106
|
+
const c = clip.trim();
|
|
8107
|
+
if (c.length === 0) return false;
|
|
8108
|
+
if (t === c) return true;
|
|
8109
|
+
const jaccard = computeOrderedJaccard(t, c);
|
|
8110
|
+
const containment = computeContainment(c, t);
|
|
8111
|
+
return jaccard >= 0.6 || containment >= 0.8;
|
|
8112
|
+
}
|
|
8113
|
+
async function captureHandbackText(input) {
|
|
8114
|
+
const sleep3 = input.sleep ?? defaultSleep;
|
|
8115
|
+
const acquireMaxWaitMs = input.acquireMaxWaitMs ?? 4e3;
|
|
8116
|
+
const acquireBackoffMs = input.acquireBackoffMs ?? 50;
|
|
8117
|
+
const recaptureAttempts = input.recaptureAttempts ?? 2;
|
|
8118
|
+
const recaptureBackoffMs = input.recaptureBackoffMs ?? 50;
|
|
8119
|
+
let acquired = false;
|
|
8120
|
+
const deadline = Date.now() + acquireMaxWaitMs;
|
|
8121
|
+
for (; ; ) {
|
|
8122
|
+
acquired = acquireCaptureLease(
|
|
8123
|
+
input.db,
|
|
8124
|
+
input.collabId,
|
|
8125
|
+
input.pid,
|
|
8126
|
+
input.leaseOptions
|
|
8127
|
+
);
|
|
8128
|
+
if (acquired) break;
|
|
8129
|
+
if (Date.now() >= deadline) break;
|
|
8130
|
+
await sleep3(acquireBackoffMs);
|
|
8131
|
+
}
|
|
8132
|
+
if (!acquired) {
|
|
8133
|
+
return { status: "degraded_pty_only", text: null, interferenceDetected: false };
|
|
8134
|
+
}
|
|
8135
|
+
try {
|
|
8136
|
+
let interferenceDetected = false;
|
|
8137
|
+
for (let attempt = 0; attempt <= recaptureAttempts; attempt += 1) {
|
|
8138
|
+
if (attempt > 0) await sleep3(recaptureBackoffMs);
|
|
8139
|
+
const c0 = await input.readChangeCount();
|
|
8140
|
+
const clip = await input.runCapture();
|
|
8141
|
+
const cn = await input.readChangeCount();
|
|
8142
|
+
const checkAvailable = c0 !== null && cn !== null;
|
|
8143
|
+
const interfered = checkAvailable && cn - c0 > 1;
|
|
8144
|
+
if (clip === null || clip.trim().length === 0) {
|
|
8145
|
+
if (interfered) {
|
|
8146
|
+
interferenceDetected = true;
|
|
8147
|
+
continue;
|
|
8148
|
+
}
|
|
8149
|
+
return { status: "captured", text: null, interferenceDetected };
|
|
8150
|
+
}
|
|
8151
|
+
const clean = checkAvailable ? cn - c0 === 1 : true;
|
|
8152
|
+
if (clean) {
|
|
8153
|
+
return { status: "captured", text: clip, interferenceDetected };
|
|
8154
|
+
}
|
|
8155
|
+
interferenceDetected = true;
|
|
8156
|
+
if (contentMatches(input.turnText, clip)) {
|
|
8157
|
+
return { status: "captured", text: clip, interferenceDetected: true };
|
|
8158
|
+
}
|
|
8159
|
+
}
|
|
8160
|
+
return { status: "degraded_pty_only", text: null, interferenceDetected: true };
|
|
8161
|
+
} finally {
|
|
8162
|
+
releaseCaptureLease(input.db, input.collabId);
|
|
8163
|
+
}
|
|
8164
|
+
}
|
|
8165
|
+
|
|
8166
|
+
// src/runtime/clipboard-change-count.ts
|
|
8167
|
+
import { execFile as execFile3 } from "node:child_process";
|
|
8168
|
+
import { existsSync as existsSync7 } from "node:fs";
|
|
8169
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
8170
|
+
import { dirname as dirname3, join as join7 } from "node:path";
|
|
8171
|
+
var HELPER_BIN_NAME = "clipboard-change-count";
|
|
8172
|
+
function execFileText2(command, args = []) {
|
|
8173
|
+
return new Promise((resolve5, reject) => {
|
|
8174
|
+
execFile3(command, args, { encoding: "utf8" }, (error, stdout) => {
|
|
8175
|
+
if (error) {
|
|
8176
|
+
reject(error instanceof Error ? error : new Error("helper failed"));
|
|
8177
|
+
return;
|
|
8178
|
+
}
|
|
8179
|
+
resolve5(stdout);
|
|
8180
|
+
});
|
|
8181
|
+
});
|
|
8182
|
+
}
|
|
8183
|
+
function makeChangeCountReader(deps) {
|
|
8184
|
+
const platform = deps?.platform ?? process.platform;
|
|
8185
|
+
const runHelper = deps?.runHelper ?? (async () => {
|
|
8186
|
+
const here = dirname3(fileURLToPath2(import.meta.url));
|
|
8187
|
+
const bin = join7(here, "..", "native", HELPER_BIN_NAME);
|
|
8188
|
+
if (!existsSync7(bin)) throw new Error("changeCount helper not built");
|
|
8189
|
+
return execFileText2(bin);
|
|
8190
|
+
});
|
|
8191
|
+
return async () => {
|
|
8192
|
+
if (platform !== "darwin") return null;
|
|
8193
|
+
try {
|
|
8194
|
+
const out = (await runHelper()).trim();
|
|
8195
|
+
const n = Number.parseInt(out, 10);
|
|
8196
|
+
return Number.isFinite(n) && String(n) === out ? n : null;
|
|
8197
|
+
} catch {
|
|
8198
|
+
return null;
|
|
8199
|
+
}
|
|
8200
|
+
};
|
|
8201
|
+
}
|
|
8202
|
+
|
|
7682
8203
|
// src/runtime/provider-submit-strategy.ts
|
|
7683
8204
|
async function submitInjectedProviderInput(input) {
|
|
7684
8205
|
const sleep3 = input.sleep ?? ((ms) => new Promise((resolve5) => setTimeout(resolve5, ms)));
|
|
@@ -7723,6 +8244,7 @@ function createRuntimeDebugLogger(input) {
|
|
|
7723
8244
|
}
|
|
7724
8245
|
|
|
7725
8246
|
// src/runtime/mount-session-main.ts
|
|
8247
|
+
var readChangeCount = makeChangeCountReader();
|
|
7726
8248
|
function safeReapSessions(input) {
|
|
7727
8249
|
const reap = input.reap ?? ((collabId, agentType, keepSessionId) => {
|
|
7728
8250
|
const db = openDatabase(getSharedSqlitePath());
|
|
@@ -7807,6 +8329,7 @@ function createMountSessionRuntime(input) {
|
|
|
7807
8329
|
agentType: input.target,
|
|
7808
8330
|
attachmentKind: "mounted"
|
|
7809
8331
|
});
|
|
8332
|
+
releaseCaptureLease(db, resolvedClaim.collabId);
|
|
7810
8333
|
} finally {
|
|
7811
8334
|
db.close();
|
|
7812
8335
|
}
|
|
@@ -7966,17 +8489,30 @@ function createMountSessionRuntime(input) {
|
|
|
7966
8489
|
}
|
|
7967
8490
|
return runComposer();
|
|
7968
8491
|
},
|
|
7969
|
-
captureHandbackText: async () => {
|
|
7970
|
-
return
|
|
7971
|
-
|
|
7972
|
-
|
|
7973
|
-
|
|
7974
|
-
|
|
7975
|
-
|
|
7976
|
-
|
|
7977
|
-
|
|
7978
|
-
|
|
7979
|
-
|
|
8492
|
+
captureHandbackText: async (turnText) => {
|
|
8493
|
+
if (!resolvedClaim) return null;
|
|
8494
|
+
const db = openDatabase(getSharedSqlitePath());
|
|
8495
|
+
try {
|
|
8496
|
+
return await captureHandbackText({
|
|
8497
|
+
db,
|
|
8498
|
+
collabId: resolvedClaim.collabId,
|
|
8499
|
+
pid: process.pid,
|
|
8500
|
+
turnText,
|
|
8501
|
+
readChangeCount,
|
|
8502
|
+
runCapture: () => captureClipboardHandback({
|
|
8503
|
+
triggerCopy: () => submitInjectedInput2("/copy"),
|
|
8504
|
+
// Some provider versions show a picker after /copy (e.g. Claude
|
|
8505
|
+
// Code's content-type picker). Send Enter to confirm the default
|
|
8506
|
+
// selection if the clipboard has not changed yet after the delay.
|
|
8507
|
+
confirmPicker: () => {
|
|
8508
|
+
writeInjectedInput("mounted-submit-picker", "\r");
|
|
8509
|
+
},
|
|
8510
|
+
triggerDelayMs: 300
|
|
8511
|
+
})
|
|
8512
|
+
});
|
|
8513
|
+
} finally {
|
|
8514
|
+
db.close();
|
|
8515
|
+
}
|
|
7980
8516
|
},
|
|
7981
8517
|
confirmHandbackCapture: async () => {
|
|
7982
8518
|
const runConfirm = async () => {
|
|
@@ -8088,30 +8624,53 @@ async function isPortFree(port, host = "127.0.0.1") {
|
|
|
8088
8624
|
});
|
|
8089
8625
|
}
|
|
8090
8626
|
|
|
8091
|
-
// src/commands/collab/start.ts
|
|
8092
|
-
import { mkdirSync as mkdirSync3 } from "node:fs";
|
|
8093
|
-
|
|
8094
8627
|
// src/runtime/port-allocator.ts
|
|
8095
8628
|
var DEFAULT_PORT_RANGE = [4500, 4999];
|
|
8096
8629
|
|
|
8097
|
-
// src/commands/collab/
|
|
8630
|
+
// src/commands/collab/recover.ts
|
|
8098
8631
|
var READY_TIMEOUT_DEFAULT = 3e4;
|
|
8099
|
-
|
|
8100
|
-
|
|
8101
|
-
|
|
8632
|
+
var STALE_THRESHOLD_DEFAULT = 9e4;
|
|
8633
|
+
function defaultIsAliveImpl(pid) {
|
|
8634
|
+
try {
|
|
8635
|
+
process.kill(pid, 0);
|
|
8636
|
+
return Promise.resolve({ alive: true, startTime: null });
|
|
8637
|
+
} catch (err) {
|
|
8638
|
+
const code = err.code;
|
|
8639
|
+
if (code === "EPERM") return Promise.resolve({ alive: true, startTime: null });
|
|
8640
|
+
return Promise.resolve({ alive: false, startTime: null });
|
|
8641
|
+
}
|
|
8642
|
+
}
|
|
8643
|
+
async function runCollabRecover(opts) {
|
|
8102
8644
|
const sqlitePath = getSharedSqlitePath();
|
|
8103
8645
|
const db = openDatabase(sqlitePath);
|
|
8104
8646
|
applyMigrations(db);
|
|
8105
|
-
const
|
|
8106
|
-
const
|
|
8107
|
-
const
|
|
8108
|
-
|
|
8647
|
+
const isAlive = opts.isAlive ?? defaultIsAliveImpl;
|
|
8648
|
+
const staleThresholdMs = opts.staleThresholdMs ?? STALE_THRESHOLD_DEFAULT;
|
|
8649
|
+
const resolved = resolveCollab({
|
|
8650
|
+
db,
|
|
8651
|
+
cwd: opts.cwd,
|
|
8652
|
+
...opts.collabIdOverride ? { collabIdOverride: opts.collabIdOverride } : {},
|
|
8653
|
+
requireActive: true
|
|
8654
|
+
});
|
|
8655
|
+
const collabId = resolved.collabId;
|
|
8109
8656
|
const host = "127.0.0.1";
|
|
8110
|
-
const
|
|
8111
|
-
|
|
8112
|
-
|
|
8113
|
-
|
|
8114
|
-
)
|
|
8657
|
+
const preCheckRow = getBrokerDaemonByCollab(db, collabId);
|
|
8658
|
+
let preCheckPid = null;
|
|
8659
|
+
let preCheckPidStartTime = null;
|
|
8660
|
+
let preCheckHeartbeat = null;
|
|
8661
|
+
if (preCheckRow && preCheckRow.pid !== null) {
|
|
8662
|
+
preCheckPid = preCheckRow.pid;
|
|
8663
|
+
preCheckPidStartTime = preCheckRow.pidStartTime;
|
|
8664
|
+
preCheckHeartbeat = preCheckRow.lastHeartbeatAt;
|
|
8665
|
+
const liveness = await isAlive(preCheckRow.pid);
|
|
8666
|
+
const startTimeMatches = preCheckRow.pidStartTime === null || liveness.startTime === null || liveness.startTime === preCheckRow.pidStartTime;
|
|
8667
|
+
if (liveness.alive && startTimeMatches) {
|
|
8668
|
+
db.close();
|
|
8669
|
+
throw new Error(
|
|
8670
|
+
`daemon already running for collab ${collabId} (pid ${preCheckRow.pid})`
|
|
8671
|
+
);
|
|
8672
|
+
}
|
|
8673
|
+
}
|
|
8115
8674
|
const range = opts.portRange ?? DEFAULT_PORT_RANGE;
|
|
8116
8675
|
const osFreeCandidates = [];
|
|
8117
8676
|
if (opts.explicitPort !== void 0) {
|
|
@@ -8133,16 +8692,25 @@ async function runCollabStart(opts) {
|
|
|
8133
8692
|
);
|
|
8134
8693
|
}
|
|
8135
8694
|
}
|
|
8695
|
+
const now = opts.now();
|
|
8136
8696
|
let allocatedPort = 0;
|
|
8137
8697
|
const tx = db.transaction(() => {
|
|
8138
|
-
|
|
8139
|
-
|
|
8140
|
-
|
|
8141
|
-
|
|
8142
|
-
|
|
8143
|
-
|
|
8144
|
-
|
|
8145
|
-
|
|
8698
|
+
const existing = getBrokerDaemonByCollab(db, collabId);
|
|
8699
|
+
if (existing) {
|
|
8700
|
+
if (existing.pid === null) {
|
|
8701
|
+
const heartbeatMs = Date.parse(existing.lastHeartbeatAt);
|
|
8702
|
+
const nowMs = Date.parse(now);
|
|
8703
|
+
const ageMs = Number.isFinite(heartbeatMs) && Number.isFinite(nowMs) ? nowMs - heartbeatMs : Number.POSITIVE_INFINITY;
|
|
8704
|
+
if (ageMs < staleThresholdMs) {
|
|
8705
|
+
throw new Error("RECOVERY_ALREADY_IN_PROGRESS");
|
|
8706
|
+
}
|
|
8707
|
+
deleteBrokerDaemonByCollab(db, collabId);
|
|
8708
|
+
} else {
|
|
8709
|
+
if (preCheckPid === null || existing.pid !== preCheckPid || existing.pidStartTime !== preCheckPidStartTime || existing.lastHeartbeatAt !== preCheckHeartbeat) {
|
|
8710
|
+
throw new Error("DAEMON_NOW_RUNNING");
|
|
8711
|
+
}
|
|
8712
|
+
deleteBrokerDaemonByCollab(db, collabId);
|
|
8713
|
+
}
|
|
8146
8714
|
}
|
|
8147
8715
|
const takenPorts = new Set(
|
|
8148
8716
|
db.prepare("SELECT port FROM broker_daemon").all().map((r) => r.port)
|
|
@@ -8155,20 +8723,6 @@ async function runCollabStart(opts) {
|
|
|
8155
8723
|
);
|
|
8156
8724
|
}
|
|
8157
8725
|
allocatedPort = picked;
|
|
8158
|
-
db.prepare(
|
|
8159
|
-
"INSERT INTO collab (collab_id, workspace_root, display_name, status, workspace_id, launch_mode, tmux_session, created_at, updated_at, orchestrator_enabled, orchestrator_max_rounds) VALUES (?, ?, ?, 'active', ?, ?, ?, ?, ?, ?, ?)"
|
|
8160
|
-
).run(
|
|
8161
|
-
collabId,
|
|
8162
|
-
workspaceRoot,
|
|
8163
|
-
opts.displayName,
|
|
8164
|
-
workspaceId,
|
|
8165
|
-
opts.launchMode,
|
|
8166
|
-
opts.tmuxSession ?? null,
|
|
8167
|
-
now,
|
|
8168
|
-
now,
|
|
8169
|
-
orchestratorEnabled ? 1 : 0,
|
|
8170
|
-
orchestratorMaxRounds
|
|
8171
|
-
);
|
|
8172
8726
|
insertBrokerDaemon(db, {
|
|
8173
8727
|
collabId,
|
|
8174
8728
|
host,
|
|
@@ -8176,23 +8730,27 @@ async function runCollabStart(opts) {
|
|
|
8176
8730
|
startedAt: now,
|
|
8177
8731
|
lastHeartbeatAt: now
|
|
8178
8732
|
});
|
|
8179
|
-
upsertRecoveryState(db, {
|
|
8180
|
-
collabId,
|
|
8181
|
-
state: "normal",
|
|
8182
|
-
idleAfterRecovery: false,
|
|
8183
|
-
recoveredAt: null
|
|
8184
|
-
});
|
|
8185
8733
|
});
|
|
8186
8734
|
try {
|
|
8187
8735
|
tx.immediate();
|
|
8188
8736
|
} catch (err) {
|
|
8189
|
-
|
|
8190
|
-
|
|
8737
|
+
const msg = err.message;
|
|
8738
|
+
db.close();
|
|
8739
|
+
if (msg === "RECOVERY_ALREADY_IN_PROGRESS") {
|
|
8740
|
+
throw new Error(
|
|
8741
|
+
`recovery already in progress for collab ${collabId}`
|
|
8742
|
+
);
|
|
8743
|
+
}
|
|
8744
|
+
if (msg === "DAEMON_NOW_RUNNING") {
|
|
8745
|
+
throw new Error(
|
|
8746
|
+
`daemon already running for collab ${collabId}`
|
|
8747
|
+
);
|
|
8748
|
+
}
|
|
8749
|
+
if (msg === "ALL_CANDIDATES_TAKEN") {
|
|
8191
8750
|
throw new Error(
|
|
8192
8751
|
`every OS-free port in [${range[0]}, ${range[1]}] is reserved in the registry`
|
|
8193
8752
|
);
|
|
8194
8753
|
}
|
|
8195
|
-
db.close();
|
|
8196
8754
|
throw err;
|
|
8197
8755
|
}
|
|
8198
8756
|
const childPid = opts.spawnBroker({
|
|
@@ -8204,9 +8762,6 @@ async function runCollabStart(opts) {
|
|
|
8204
8762
|
const cleanupOnFailure = (msg) => {
|
|
8205
8763
|
const cleanup = db.transaction(() => {
|
|
8206
8764
|
deleteBrokerDaemonByCollab(db, collabId);
|
|
8207
|
-
db.prepare(
|
|
8208
|
-
"UPDATE collab SET status='stopped', stopped_at=?, updated_at=? WHERE collab_id = ?"
|
|
8209
|
-
).run(opts.now(), opts.now(), collabId);
|
|
8210
8765
|
});
|
|
8211
8766
|
cleanup.immediate();
|
|
8212
8767
|
try {
|
|
@@ -8234,16 +8789,190 @@ async function runCollabStart(opts) {
|
|
|
8234
8789
|
);
|
|
8235
8790
|
}
|
|
8236
8791
|
db.close();
|
|
8237
|
-
|
|
8238
|
-
|
|
8239
|
-
|
|
8240
|
-
|
|
8241
|
-
|
|
8242
|
-
|
|
8243
|
-
};
|
|
8244
|
-
|
|
8245
|
-
|
|
8246
|
-
|
|
8792
|
+
const recoveryBroker = createBrokerRuntime({
|
|
8793
|
+
sqlitePath,
|
|
8794
|
+
runWorkflowDriver: false,
|
|
8795
|
+
runDiagnosticsSweep: false,
|
|
8796
|
+
runDaemonHeartbeat: false,
|
|
8797
|
+
runBrokerDaemonSweep: false
|
|
8798
|
+
});
|
|
8799
|
+
try {
|
|
8800
|
+
const prepared = recoveryBroker.control.prepareCollabRecovery({
|
|
8801
|
+
collabId,
|
|
8802
|
+
now
|
|
8803
|
+
});
|
|
8804
|
+
const hasRememberedBindings = prepared.bindings.some(
|
|
8805
|
+
(b) => b.activeSessionId !== null
|
|
8806
|
+
);
|
|
8807
|
+
upsertRecoveryState(recoveryBroker.db, {
|
|
8808
|
+
collabId,
|
|
8809
|
+
state: hasRememberedBindings ? "recovered" : "normal",
|
|
8810
|
+
idleAfterRecovery: hasRememberedBindings,
|
|
8811
|
+
recoveredAt: hasRememberedBindings ? now : null
|
|
8812
|
+
});
|
|
8813
|
+
} finally {
|
|
8814
|
+
await recoveryBroker.stop();
|
|
8815
|
+
}
|
|
8816
|
+
return {
|
|
8817
|
+
collabId,
|
|
8818
|
+
host,
|
|
8819
|
+
port: allocatedPort,
|
|
8820
|
+
// biome-ignore lint/style/noNonNullAssertion: guarded by cleanupOnFailure
|
|
8821
|
+
pid: finalRow.pid
|
|
8822
|
+
};
|
|
8823
|
+
}
|
|
8824
|
+
|
|
8825
|
+
// src/commands/collab/start.ts
|
|
8826
|
+
import { mkdirSync as mkdirSync3 } from "node:fs";
|
|
8827
|
+
var READY_TIMEOUT_DEFAULT2 = 3e4;
|
|
8828
|
+
async function runCollabStart(opts) {
|
|
8829
|
+
const root = getStateRoot();
|
|
8830
|
+
mkdirSync3(root, { recursive: true });
|
|
8831
|
+
const sqlitePath = getSharedSqlitePath();
|
|
8832
|
+
const db = openDatabase(sqlitePath);
|
|
8833
|
+
applyMigrations(db);
|
|
8834
|
+
const workspaceRoot = canonicalWorkspaceRoot(opts.cwd);
|
|
8835
|
+
const workspaceId = workspaceIdFromPath(opts.cwd);
|
|
8836
|
+
const now = opts.now();
|
|
8837
|
+
const collabId = createCliCollabId(now);
|
|
8838
|
+
const host = "127.0.0.1";
|
|
8839
|
+
const orchestratorEnabled = process.env.AI_WHISPER_RELAY_ORCHESTRATOR_ENABLED !== "0";
|
|
8840
|
+
const orchestratorMaxRounds = Math.max(
|
|
8841
|
+
1,
|
|
8842
|
+
Number(process.env.AI_WHISPER_RELAY_ORCHESTRATOR_MAX_ROUNDS ?? "3") || 3
|
|
8843
|
+
);
|
|
8844
|
+
const range = opts.portRange ?? DEFAULT_PORT_RANGE;
|
|
8845
|
+
const osFreeCandidates = [];
|
|
8846
|
+
if (opts.explicitPort !== void 0) {
|
|
8847
|
+
if (!await opts.isPortFreeOs(opts.explicitPort)) {
|
|
8848
|
+
db.close();
|
|
8849
|
+
throw new Error(
|
|
8850
|
+
`port ${opts.explicitPort} is in use by another process`
|
|
8851
|
+
);
|
|
8852
|
+
}
|
|
8853
|
+
osFreeCandidates.push(opts.explicitPort);
|
|
8854
|
+
} else {
|
|
8855
|
+
for (let p = range[0]; p <= range[1]; p++) {
|
|
8856
|
+
if (await opts.isPortFreeOs(p)) osFreeCandidates.push(p);
|
|
8857
|
+
}
|
|
8858
|
+
if (osFreeCandidates.length === 0) {
|
|
8859
|
+
db.close();
|
|
8860
|
+
throw new Error(
|
|
8861
|
+
`No OS-free port in range [${range[0]}, ${range[1]}]`
|
|
8862
|
+
);
|
|
8863
|
+
}
|
|
8864
|
+
}
|
|
8865
|
+
let allocatedPort = 0;
|
|
8866
|
+
const tx = db.transaction(() => {
|
|
8867
|
+
upsertWorkspace(db, { id: workspaceId, workspaceRoot, now });
|
|
8868
|
+
const active = db.prepare(
|
|
8869
|
+
"SELECT collab_id FROM collab WHERE workspace_id = ? AND status = 'active' LIMIT 1"
|
|
8870
|
+
).get(workspaceId);
|
|
8871
|
+
if (active) {
|
|
8872
|
+
throw new Error(
|
|
8873
|
+
`active collab ${active.collab_id} already exists for workspace ${workspaceRoot}`
|
|
8874
|
+
);
|
|
8875
|
+
}
|
|
8876
|
+
const takenPorts = new Set(
|
|
8877
|
+
db.prepare("SELECT port FROM broker_daemon").all().map((r) => r.port)
|
|
8878
|
+
);
|
|
8879
|
+
const picked = osFreeCandidates.find((p) => !takenPorts.has(p));
|
|
8880
|
+
if (picked === void 0) throw new Error("ALL_CANDIDATES_TAKEN");
|
|
8881
|
+
if (opts.explicitPort !== void 0 && picked !== opts.explicitPort) {
|
|
8882
|
+
throw new Error(
|
|
8883
|
+
`port ${opts.explicitPort} already in use by another daemon`
|
|
8884
|
+
);
|
|
8885
|
+
}
|
|
8886
|
+
allocatedPort = picked;
|
|
8887
|
+
db.prepare(
|
|
8888
|
+
"INSERT INTO collab (collab_id, workspace_root, display_name, status, workspace_id, launch_mode, tmux_session, created_at, updated_at, orchestrator_enabled, orchestrator_max_rounds) VALUES (?, ?, ?, 'active', ?, ?, ?, ?, ?, ?, ?)"
|
|
8889
|
+
).run(
|
|
8890
|
+
collabId,
|
|
8891
|
+
workspaceRoot,
|
|
8892
|
+
opts.displayName,
|
|
8893
|
+
workspaceId,
|
|
8894
|
+
opts.launchMode,
|
|
8895
|
+
opts.tmuxSession ?? null,
|
|
8896
|
+
now,
|
|
8897
|
+
now,
|
|
8898
|
+
orchestratorEnabled ? 1 : 0,
|
|
8899
|
+
orchestratorMaxRounds
|
|
8900
|
+
);
|
|
8901
|
+
insertBrokerDaemon(db, {
|
|
8902
|
+
collabId,
|
|
8903
|
+
host,
|
|
8904
|
+
port: allocatedPort,
|
|
8905
|
+
startedAt: now,
|
|
8906
|
+
lastHeartbeatAt: now
|
|
8907
|
+
});
|
|
8908
|
+
upsertRecoveryState(db, {
|
|
8909
|
+
collabId,
|
|
8910
|
+
state: "normal",
|
|
8911
|
+
idleAfterRecovery: false,
|
|
8912
|
+
recoveredAt: null
|
|
8913
|
+
});
|
|
8914
|
+
});
|
|
8915
|
+
try {
|
|
8916
|
+
tx.immediate();
|
|
8917
|
+
} catch (err) {
|
|
8918
|
+
if (err.message === "ALL_CANDIDATES_TAKEN") {
|
|
8919
|
+
db.close();
|
|
8920
|
+
throw new Error(
|
|
8921
|
+
`every OS-free port in [${range[0]}, ${range[1]}] is reserved in the registry`
|
|
8922
|
+
);
|
|
8923
|
+
}
|
|
8924
|
+
db.close();
|
|
8925
|
+
throw err;
|
|
8926
|
+
}
|
|
8927
|
+
const childPid = opts.spawnBroker({
|
|
8928
|
+
collabId,
|
|
8929
|
+
host,
|
|
8930
|
+
port: allocatedPort,
|
|
8931
|
+
sqlitePath
|
|
8932
|
+
});
|
|
8933
|
+
const cleanupOnFailure = (msg) => {
|
|
8934
|
+
const cleanup = db.transaction(() => {
|
|
8935
|
+
deleteBrokerDaemonByCollab(db, collabId);
|
|
8936
|
+
db.prepare(
|
|
8937
|
+
"UPDATE collab SET status='stopped', stopped_at=?, updated_at=? WHERE collab_id = ?"
|
|
8938
|
+
).run(opts.now(), opts.now(), collabId);
|
|
8939
|
+
});
|
|
8940
|
+
cleanup.immediate();
|
|
8941
|
+
try {
|
|
8942
|
+
opts.signalProcess(childPid, "SIGTERM");
|
|
8943
|
+
} catch {
|
|
8944
|
+
}
|
|
8945
|
+
db.close();
|
|
8946
|
+
throw new Error(msg);
|
|
8947
|
+
};
|
|
8948
|
+
const ready = await opts.waitForReady({
|
|
8949
|
+
host,
|
|
8950
|
+
port: allocatedPort,
|
|
8951
|
+
collabId,
|
|
8952
|
+
timeoutMs: opts.readyTimeoutMs ?? READY_TIMEOUT_DEFAULT2
|
|
8953
|
+
});
|
|
8954
|
+
if (!ready) {
|
|
8955
|
+
cleanupOnFailure(
|
|
8956
|
+
`broker readiness check timed out for collab ${collabId}`
|
|
8957
|
+
);
|
|
8958
|
+
}
|
|
8959
|
+
const finalRow = getBrokerDaemonByCollab(db, collabId);
|
|
8960
|
+
if (!finalRow || finalRow.pid === null) {
|
|
8961
|
+
cleanupOnFailure(
|
|
8962
|
+
`daemon did not write its PID for collab ${collabId}`
|
|
8963
|
+
);
|
|
8964
|
+
}
|
|
8965
|
+
db.close();
|
|
8966
|
+
return {
|
|
8967
|
+
collabId,
|
|
8968
|
+
port: allocatedPort,
|
|
8969
|
+
host,
|
|
8970
|
+
// biome-ignore lint/style/noNonNullAssertion: guarded by cleanupOnFailure
|
|
8971
|
+
pid: finalRow.pid
|
|
8972
|
+
};
|
|
8973
|
+
}
|
|
8974
|
+
function recordLaunchedSessions(input) {
|
|
8975
|
+
const db = openDatabase(getSharedSqlitePath());
|
|
8247
8976
|
try {
|
|
8248
8977
|
if (input.launch.tmuxSession) {
|
|
8249
8978
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -8302,7 +9031,7 @@ function recordLaunchedSessions(input) {
|
|
|
8302
9031
|
}
|
|
8303
9032
|
|
|
8304
9033
|
// src/commands/collab/mount.ts
|
|
8305
|
-
function
|
|
9034
|
+
function defaultIsPidAlive3(pid) {
|
|
8306
9035
|
try {
|
|
8307
9036
|
process.kill(pid, 0);
|
|
8308
9037
|
return true;
|
|
@@ -8358,10 +9087,46 @@ async function runCollabMount(input) {
|
|
|
8358
9087
|
db.close();
|
|
8359
9088
|
}
|
|
8360
9089
|
};
|
|
9090
|
+
const isDaemonAlive = input.isDaemonPidAlive ?? defaultIsPidAlive3;
|
|
9091
|
+
const reviveViaRecover = async () => {
|
|
9092
|
+
await (input.runRecoverFn ?? runCollabRecover)({
|
|
9093
|
+
cwd: input.workspaceRoot,
|
|
9094
|
+
now: () => (/* @__PURE__ */ new Date()).toISOString(),
|
|
9095
|
+
isPortFreeOs: (port) => isPortFree(port),
|
|
9096
|
+
spawnBroker: ({ collabId, host, port, sqlitePath }) => spawnBrokerDaemon(sqlitePath, host, port, collabId),
|
|
9097
|
+
waitForReady: ({ host, port, collabId, timeoutMs }) => waitForBrokerReady({ host, port, collabId, timeoutMs }),
|
|
9098
|
+
signalProcess: (pid, signal) => {
|
|
9099
|
+
try {
|
|
9100
|
+
process.kill(pid, signal);
|
|
9101
|
+
} catch {
|
|
9102
|
+
}
|
|
9103
|
+
}
|
|
9104
|
+
});
|
|
9105
|
+
const db = openDatabase(getSharedSqlitePath());
|
|
9106
|
+
try {
|
|
9107
|
+
const r = resolveCollab({ db, cwd: input.workspaceRoot });
|
|
9108
|
+
upsertRecoveryState(db, {
|
|
9109
|
+
collabId: r.collabId,
|
|
9110
|
+
state: "normal",
|
|
9111
|
+
idleAfterRecovery: false,
|
|
9112
|
+
recoveredAt: null
|
|
9113
|
+
});
|
|
9114
|
+
} finally {
|
|
9115
|
+
db.close();
|
|
9116
|
+
}
|
|
9117
|
+
return tryResolve();
|
|
9118
|
+
};
|
|
8361
9119
|
const resolveOrCreate = async () => {
|
|
8362
9120
|
try {
|
|
8363
|
-
|
|
9121
|
+
const resolved2 = tryResolve();
|
|
9122
|
+
if (input.collabIdOverride === void 0 && resolved2.daemon !== null && !isDaemonAlive(resolved2.daemon.pid)) {
|
|
9123
|
+
return await reviveViaRecover();
|
|
9124
|
+
}
|
|
9125
|
+
return resolved2;
|
|
8364
9126
|
} catch (err) {
|
|
9127
|
+
if (err instanceof CollabResolverError && err.kind === "NoLiveDaemonForCollab" && input.collabIdOverride === void 0) {
|
|
9128
|
+
return await reviveViaRecover();
|
|
9129
|
+
}
|
|
8365
9130
|
if (!(err instanceof CollabResolverError && (err.kind === "NoCollabFoundForCwd" || err.kind === "CollabAlreadyStopped"))) {
|
|
8366
9131
|
throw err;
|
|
8367
9132
|
}
|
|
@@ -8429,7 +9194,7 @@ async function runCollabMount(input) {
|
|
|
8429
9194
|
try {
|
|
8430
9195
|
const current = broker.control.listSessionBindings(resolved.collabId).find((binding) => binding.agentType === input.target);
|
|
8431
9196
|
if (current?.bindingState === "bound") {
|
|
8432
|
-
const isAlive = input.isPidAlive ??
|
|
9197
|
+
const isAlive = input.isPidAlive ?? defaultIsPidAlive3;
|
|
8433
9198
|
const liveOwner = broker.control.listSessionAttachments(resolved.collabId).some(
|
|
8434
9199
|
(a) => a.agentType === input.target && a.pid !== null && isAlive(a.pid)
|
|
8435
9200
|
);
|
|
@@ -8873,201 +9638,6 @@ async function runCollabInspect(input) {
|
|
|
8873
9638
|
}
|
|
8874
9639
|
}
|
|
8875
9640
|
|
|
8876
|
-
// src/commands/collab/recover.ts
|
|
8877
|
-
var READY_TIMEOUT_DEFAULT2 = 3e4;
|
|
8878
|
-
var STALE_THRESHOLD_DEFAULT = 9e4;
|
|
8879
|
-
function defaultIsAliveImpl(pid) {
|
|
8880
|
-
try {
|
|
8881
|
-
process.kill(pid, 0);
|
|
8882
|
-
return Promise.resolve({ alive: true, startTime: null });
|
|
8883
|
-
} catch (err) {
|
|
8884
|
-
const code = err.code;
|
|
8885
|
-
if (code === "EPERM") return Promise.resolve({ alive: true, startTime: null });
|
|
8886
|
-
return Promise.resolve({ alive: false, startTime: null });
|
|
8887
|
-
}
|
|
8888
|
-
}
|
|
8889
|
-
async function runCollabRecover(opts) {
|
|
8890
|
-
const sqlitePath = getSharedSqlitePath();
|
|
8891
|
-
const db = openDatabase(sqlitePath);
|
|
8892
|
-
applyMigrations(db);
|
|
8893
|
-
const isAlive = opts.isAlive ?? defaultIsAliveImpl;
|
|
8894
|
-
const staleThresholdMs = opts.staleThresholdMs ?? STALE_THRESHOLD_DEFAULT;
|
|
8895
|
-
const resolved = resolveCollab({
|
|
8896
|
-
db,
|
|
8897
|
-
cwd: opts.cwd,
|
|
8898
|
-
...opts.collabIdOverride ? { collabIdOverride: opts.collabIdOverride } : {},
|
|
8899
|
-
requireActive: true
|
|
8900
|
-
});
|
|
8901
|
-
const collabId = resolved.collabId;
|
|
8902
|
-
const host = "127.0.0.1";
|
|
8903
|
-
const preCheckRow = getBrokerDaemonByCollab(db, collabId);
|
|
8904
|
-
let preCheckPid = null;
|
|
8905
|
-
let preCheckPidStartTime = null;
|
|
8906
|
-
let preCheckHeartbeat = null;
|
|
8907
|
-
if (preCheckRow && preCheckRow.pid !== null) {
|
|
8908
|
-
preCheckPid = preCheckRow.pid;
|
|
8909
|
-
preCheckPidStartTime = preCheckRow.pidStartTime;
|
|
8910
|
-
preCheckHeartbeat = preCheckRow.lastHeartbeatAt;
|
|
8911
|
-
const liveness = await isAlive(preCheckRow.pid);
|
|
8912
|
-
const startTimeMatches = preCheckRow.pidStartTime === null || liveness.startTime === null || liveness.startTime === preCheckRow.pidStartTime;
|
|
8913
|
-
if (liveness.alive && startTimeMatches) {
|
|
8914
|
-
db.close();
|
|
8915
|
-
throw new Error(
|
|
8916
|
-
`daemon already running for collab ${collabId} (pid ${preCheckRow.pid})`
|
|
8917
|
-
);
|
|
8918
|
-
}
|
|
8919
|
-
}
|
|
8920
|
-
const range = opts.portRange ?? DEFAULT_PORT_RANGE;
|
|
8921
|
-
const osFreeCandidates = [];
|
|
8922
|
-
if (opts.explicitPort !== void 0) {
|
|
8923
|
-
if (!await opts.isPortFreeOs(opts.explicitPort)) {
|
|
8924
|
-
db.close();
|
|
8925
|
-
throw new Error(
|
|
8926
|
-
`port ${opts.explicitPort} is in use by another process`
|
|
8927
|
-
);
|
|
8928
|
-
}
|
|
8929
|
-
osFreeCandidates.push(opts.explicitPort);
|
|
8930
|
-
} else {
|
|
8931
|
-
for (let p = range[0]; p <= range[1]; p++) {
|
|
8932
|
-
if (await opts.isPortFreeOs(p)) osFreeCandidates.push(p);
|
|
8933
|
-
}
|
|
8934
|
-
if (osFreeCandidates.length === 0) {
|
|
8935
|
-
db.close();
|
|
8936
|
-
throw new Error(
|
|
8937
|
-
`No OS-free port in range [${range[0]}, ${range[1]}]`
|
|
8938
|
-
);
|
|
8939
|
-
}
|
|
8940
|
-
}
|
|
8941
|
-
const now = opts.now();
|
|
8942
|
-
let allocatedPort = 0;
|
|
8943
|
-
const tx = db.transaction(() => {
|
|
8944
|
-
const existing = getBrokerDaemonByCollab(db, collabId);
|
|
8945
|
-
if (existing) {
|
|
8946
|
-
if (existing.pid === null) {
|
|
8947
|
-
const heartbeatMs = Date.parse(existing.lastHeartbeatAt);
|
|
8948
|
-
const nowMs = Date.parse(now);
|
|
8949
|
-
const ageMs = Number.isFinite(heartbeatMs) && Number.isFinite(nowMs) ? nowMs - heartbeatMs : Number.POSITIVE_INFINITY;
|
|
8950
|
-
if (ageMs < staleThresholdMs) {
|
|
8951
|
-
throw new Error("RECOVERY_ALREADY_IN_PROGRESS");
|
|
8952
|
-
}
|
|
8953
|
-
deleteBrokerDaemonByCollab(db, collabId);
|
|
8954
|
-
} else {
|
|
8955
|
-
if (preCheckPid === null || existing.pid !== preCheckPid || existing.pidStartTime !== preCheckPidStartTime || existing.lastHeartbeatAt !== preCheckHeartbeat) {
|
|
8956
|
-
throw new Error("DAEMON_NOW_RUNNING");
|
|
8957
|
-
}
|
|
8958
|
-
deleteBrokerDaemonByCollab(db, collabId);
|
|
8959
|
-
}
|
|
8960
|
-
}
|
|
8961
|
-
const takenPorts = new Set(
|
|
8962
|
-
db.prepare("SELECT port FROM broker_daemon").all().map((r) => r.port)
|
|
8963
|
-
);
|
|
8964
|
-
const picked = osFreeCandidates.find((p) => !takenPorts.has(p));
|
|
8965
|
-
if (picked === void 0) throw new Error("ALL_CANDIDATES_TAKEN");
|
|
8966
|
-
if (opts.explicitPort !== void 0 && picked !== opts.explicitPort) {
|
|
8967
|
-
throw new Error(
|
|
8968
|
-
`port ${opts.explicitPort} already in use by another daemon`
|
|
8969
|
-
);
|
|
8970
|
-
}
|
|
8971
|
-
allocatedPort = picked;
|
|
8972
|
-
insertBrokerDaemon(db, {
|
|
8973
|
-
collabId,
|
|
8974
|
-
host,
|
|
8975
|
-
port: allocatedPort,
|
|
8976
|
-
startedAt: now,
|
|
8977
|
-
lastHeartbeatAt: now
|
|
8978
|
-
});
|
|
8979
|
-
});
|
|
8980
|
-
try {
|
|
8981
|
-
tx.immediate();
|
|
8982
|
-
} catch (err) {
|
|
8983
|
-
const msg = err.message;
|
|
8984
|
-
db.close();
|
|
8985
|
-
if (msg === "RECOVERY_ALREADY_IN_PROGRESS") {
|
|
8986
|
-
throw new Error(
|
|
8987
|
-
`recovery already in progress for collab ${collabId}`
|
|
8988
|
-
);
|
|
8989
|
-
}
|
|
8990
|
-
if (msg === "DAEMON_NOW_RUNNING") {
|
|
8991
|
-
throw new Error(
|
|
8992
|
-
`daemon already running for collab ${collabId}`
|
|
8993
|
-
);
|
|
8994
|
-
}
|
|
8995
|
-
if (msg === "ALL_CANDIDATES_TAKEN") {
|
|
8996
|
-
throw new Error(
|
|
8997
|
-
`every OS-free port in [${range[0]}, ${range[1]}] is reserved in the registry`
|
|
8998
|
-
);
|
|
8999
|
-
}
|
|
9000
|
-
throw err;
|
|
9001
|
-
}
|
|
9002
|
-
const childPid = opts.spawnBroker({
|
|
9003
|
-
collabId,
|
|
9004
|
-
host,
|
|
9005
|
-
port: allocatedPort,
|
|
9006
|
-
sqlitePath
|
|
9007
|
-
});
|
|
9008
|
-
const cleanupOnFailure = (msg) => {
|
|
9009
|
-
const cleanup = db.transaction(() => {
|
|
9010
|
-
deleteBrokerDaemonByCollab(db, collabId);
|
|
9011
|
-
});
|
|
9012
|
-
cleanup.immediate();
|
|
9013
|
-
try {
|
|
9014
|
-
opts.signalProcess(childPid, "SIGTERM");
|
|
9015
|
-
} catch {
|
|
9016
|
-
}
|
|
9017
|
-
db.close();
|
|
9018
|
-
throw new Error(msg);
|
|
9019
|
-
};
|
|
9020
|
-
const ready = await opts.waitForReady({
|
|
9021
|
-
host,
|
|
9022
|
-
port: allocatedPort,
|
|
9023
|
-
collabId,
|
|
9024
|
-
timeoutMs: opts.readyTimeoutMs ?? READY_TIMEOUT_DEFAULT2
|
|
9025
|
-
});
|
|
9026
|
-
if (!ready) {
|
|
9027
|
-
cleanupOnFailure(
|
|
9028
|
-
`broker readiness check timed out for collab ${collabId}`
|
|
9029
|
-
);
|
|
9030
|
-
}
|
|
9031
|
-
const finalRow = getBrokerDaemonByCollab(db, collabId);
|
|
9032
|
-
if (!finalRow || finalRow.pid === null) {
|
|
9033
|
-
cleanupOnFailure(
|
|
9034
|
-
`daemon did not write its PID for collab ${collabId}`
|
|
9035
|
-
);
|
|
9036
|
-
}
|
|
9037
|
-
db.close();
|
|
9038
|
-
const recoveryBroker = createBrokerRuntime({
|
|
9039
|
-
sqlitePath,
|
|
9040
|
-
runWorkflowDriver: false,
|
|
9041
|
-
runDiagnosticsSweep: false,
|
|
9042
|
-
runDaemonHeartbeat: false,
|
|
9043
|
-
runBrokerDaemonSweep: false
|
|
9044
|
-
});
|
|
9045
|
-
try {
|
|
9046
|
-
const prepared = recoveryBroker.control.prepareCollabRecovery({
|
|
9047
|
-
collabId,
|
|
9048
|
-
now
|
|
9049
|
-
});
|
|
9050
|
-
const hasRememberedBindings = prepared.bindings.some(
|
|
9051
|
-
(b) => b.activeSessionId !== null
|
|
9052
|
-
);
|
|
9053
|
-
upsertRecoveryState(recoveryBroker.db, {
|
|
9054
|
-
collabId,
|
|
9055
|
-
state: hasRememberedBindings ? "recovered" : "normal",
|
|
9056
|
-
idleAfterRecovery: hasRememberedBindings,
|
|
9057
|
-
recoveredAt: hasRememberedBindings ? now : null
|
|
9058
|
-
});
|
|
9059
|
-
} finally {
|
|
9060
|
-
await recoveryBroker.stop();
|
|
9061
|
-
}
|
|
9062
|
-
return {
|
|
9063
|
-
collabId,
|
|
9064
|
-
host,
|
|
9065
|
-
port: allocatedPort,
|
|
9066
|
-
// biome-ignore lint/style/noNonNullAssertion: guarded by cleanupOnFailure
|
|
9067
|
-
pid: finalRow.pid
|
|
9068
|
-
};
|
|
9069
|
-
}
|
|
9070
|
-
|
|
9071
9641
|
// src/commands/collab/reconnect.ts
|
|
9072
9642
|
async function runCollabReconnect(input) {
|
|
9073
9643
|
const db = openDatabase(getSharedSqlitePath());
|
|
@@ -10517,11 +11087,11 @@ async function runCollabDashboard(input) {
|
|
|
10517
11087
|
|
|
10518
11088
|
// src/commands/collab/status.ts
|
|
10519
11089
|
import Database2 from "better-sqlite3";
|
|
10520
|
-
import { existsSync as
|
|
11090
|
+
import { existsSync as existsSync8 } from "node:fs";
|
|
10521
11091
|
|
|
10522
11092
|
// src/runtime/evaluator-config.ts
|
|
10523
11093
|
import { readFileSync as readFileSync2, statSync as statSync2 } from "node:fs";
|
|
10524
|
-
import { join as
|
|
11094
|
+
import { join as join8 } from "node:path";
|
|
10525
11095
|
function isEvaluatorReady(status) {
|
|
10526
11096
|
return status === "ready" || status === "unknown";
|
|
10527
11097
|
}
|
|
@@ -10532,7 +11102,7 @@ function isEvaluatorPreflightBlocked(status) {
|
|
|
10532
11102
|
// src/commands/collab/status.ts
|
|
10533
11103
|
function runCollabStatus(input) {
|
|
10534
11104
|
const sqlitePath = getSharedSqlitePath();
|
|
10535
|
-
if (!
|
|
11105
|
+
if (!existsSync8(sqlitePath)) {
|
|
10536
11106
|
return input.json ? JSON.stringify({ error: "no_collab_for_cwd", cwd: input.cwd }) : `no active collab for ${input.cwd}`;
|
|
10537
11107
|
}
|
|
10538
11108
|
const db = new Database2(sqlitePath, { readonly: true });
|
|
@@ -10867,9 +11437,9 @@ async function runCollabTell(input) {
|
|
|
10867
11437
|
|
|
10868
11438
|
// src/runtime/launcher.ts
|
|
10869
11439
|
import { execSync as execSync3, spawn as nodeSpawn } from "node:child_process";
|
|
10870
|
-
import { dirname as
|
|
10871
|
-
import { fileURLToPath as
|
|
10872
|
-
var __dirname =
|
|
11440
|
+
import { dirname as dirname4, resolve as resolve4 } from "node:path";
|
|
11441
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
11442
|
+
var __dirname = dirname4(fileURLToPath3(import.meta.url));
|
|
10873
11443
|
var whisperBinPath = resolve4(__dirname, "../bin/whisper.js");
|
|
10874
11444
|
var relayMonitorBinPath = resolve4(__dirname, "../bin/relay-monitor.js");
|
|
10875
11445
|
function shellQuote(value) {
|
|
@@ -11095,7 +11665,16 @@ async function runWorkflowInspect(deps) {
|
|
|
11095
11665
|
|
|
11096
11666
|
// src/commands/workflow/resume.ts
|
|
11097
11667
|
async function runWorkflowResume(deps) {
|
|
11098
|
-
deps.broker.control.resumeWorkflow({
|
|
11668
|
+
deps.broker.control.resumeWorkflow({
|
|
11669
|
+
workflowId: deps.workflowId,
|
|
11670
|
+
now: deps.now,
|
|
11671
|
+
...deps.message !== void 0 ? { message: deps.message } : {}
|
|
11672
|
+
});
|
|
11673
|
+
}
|
|
11674
|
+
|
|
11675
|
+
// src/commands/workflow/pause.ts
|
|
11676
|
+
async function runWorkflowPause(deps) {
|
|
11677
|
+
deps.broker.control.pauseWorkflow({ workflowId: deps.workflowId, now: deps.now });
|
|
11099
11678
|
}
|
|
11100
11679
|
|
|
11101
11680
|
// src/commands/workflow/cancel.ts
|
|
@@ -11111,9 +11690,9 @@ async function runWorkflowTypes() {
|
|
|
11111
11690
|
// src/commands/skill/install.ts
|
|
11112
11691
|
import { cp, mkdir, readdir, stat } from "node:fs/promises";
|
|
11113
11692
|
import path3 from "node:path";
|
|
11114
|
-
import { fileURLToPath as
|
|
11693
|
+
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
11115
11694
|
function defaultBundledSkillsDir() {
|
|
11116
|
-
const here = path3.dirname(
|
|
11695
|
+
const here = path3.dirname(fileURLToPath4(import.meta.url));
|
|
11117
11696
|
return path3.resolve(here, "..", "..", "skills");
|
|
11118
11697
|
}
|
|
11119
11698
|
function homeForTarget(target, fakeHome) {
|
|
@@ -11483,15 +12062,31 @@ Not attached (stdin/stdout is not a TTY). To view the tmux session, run:
|
|
|
11483
12062
|
await broker.stop();
|
|
11484
12063
|
}
|
|
11485
12064
|
});
|
|
11486
|
-
workflow.command("
|
|
12065
|
+
workflow.command("pause").description("Pause a running workflow").argument("<workflowId>", "Workflow ID").option("--workspace <path>", "Workspace root", process.cwd()).action(async (workflowId, opts) => {
|
|
11487
12066
|
const { broker } = await connectToWorkspaceBroker({ cwd: opts.workspace });
|
|
11488
12067
|
try {
|
|
11489
|
-
await
|
|
11490
|
-
console.log(`Workflow
|
|
12068
|
+
await runWorkflowPause({ broker, workflowId, now: (/* @__PURE__ */ new Date()).toISOString() });
|
|
12069
|
+
console.log(`Workflow paused: ${workflowId}`);
|
|
11491
12070
|
} finally {
|
|
11492
12071
|
await broker.stop();
|
|
11493
12072
|
}
|
|
11494
12073
|
});
|
|
12074
|
+
workflow.command("resume").description("Resume a paused or halted workflow").argument("<workflowId>", "Workflow ID").option("--workspace <path>", "Workspace root", process.cwd()).option("--message <note>", "Operator note delivered to the agents on resume").action(
|
|
12075
|
+
async (workflowId, opts) => {
|
|
12076
|
+
const { broker } = await connectToWorkspaceBroker({ cwd: opts.workspace });
|
|
12077
|
+
try {
|
|
12078
|
+
await runWorkflowResume({
|
|
12079
|
+
broker,
|
|
12080
|
+
workflowId,
|
|
12081
|
+
now: (/* @__PURE__ */ new Date()).toISOString(),
|
|
12082
|
+
...opts.message !== void 0 ? { message: opts.message } : {}
|
|
12083
|
+
});
|
|
12084
|
+
console.log(`Workflow resumed: ${workflowId}`);
|
|
12085
|
+
} finally {
|
|
12086
|
+
await broker.stop();
|
|
12087
|
+
}
|
|
12088
|
+
}
|
|
12089
|
+
);
|
|
11495
12090
|
workflow.command("cancel").description("Cancel a running or halted workflow").argument("<workflowId>", "Workflow ID").option("--workspace <path>", "Workspace root", process.cwd()).action(async (workflowId, opts) => {
|
|
11496
12091
|
const { broker } = await connectToWorkspaceBroker({ cwd: opts.workspace });
|
|
11497
12092
|
try {
|