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
|
@@ -549,6 +549,100 @@ function getCollab(db, collabId2) {
|
|
|
549
549
|
});
|
|
550
550
|
}
|
|
551
551
|
|
|
552
|
+
// ../broker/dist/storage/repositories/workflow-repository.js
|
|
553
|
+
function rowToRecord(row) {
|
|
554
|
+
return {
|
|
555
|
+
workflowId: row.workflow_id,
|
|
556
|
+
collabId: row.collab_id,
|
|
557
|
+
workflowType: row.workflow_type,
|
|
558
|
+
name: row.name,
|
|
559
|
+
specPath: row.spec_path,
|
|
560
|
+
roleBindings: JSON.parse(row.role_bindings),
|
|
561
|
+
status: row.status,
|
|
562
|
+
currentPhaseIndex: row.current_phase_index,
|
|
563
|
+
haltReason: row.halt_reason,
|
|
564
|
+
workflowContext: JSON.parse(row.workflow_context),
|
|
565
|
+
createdAt: row.created_at,
|
|
566
|
+
updatedAt: row.updated_at
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
function insertWorkflow(db, input) {
|
|
570
|
+
db.prepare(`INSERT INTO workflows
|
|
571
|
+
(workflow_id, collab_id, workflow_type, name, spec_path, role_bindings, status,
|
|
572
|
+
current_phase_index, halt_reason, workflow_context, created_at, updated_at)
|
|
573
|
+
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);
|
|
574
|
+
}
|
|
575
|
+
function getWorkflowById(db, workflowId) {
|
|
576
|
+
const row = db.prepare("SELECT * FROM workflows WHERE workflow_id = ?").get(workflowId);
|
|
577
|
+
return row ? rowToRecord(row) : null;
|
|
578
|
+
}
|
|
579
|
+
function listWorkflows(db, filter = {}) {
|
|
580
|
+
const clauses = [];
|
|
581
|
+
const args = [];
|
|
582
|
+
if (filter.collabId) {
|
|
583
|
+
clauses.push("collab_id = ?");
|
|
584
|
+
args.push(filter.collabId);
|
|
585
|
+
}
|
|
586
|
+
if (filter.status) {
|
|
587
|
+
clauses.push("status = ?");
|
|
588
|
+
args.push(filter.status);
|
|
589
|
+
}
|
|
590
|
+
const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
591
|
+
const rows = db.prepare(`SELECT * FROM workflows ${where} ORDER BY created_at DESC`).all(...args);
|
|
592
|
+
return rows.map(rowToRecord);
|
|
593
|
+
}
|
|
594
|
+
function setWorkflowStatus(db, input) {
|
|
595
|
+
db.prepare(`UPDATE workflows
|
|
596
|
+
SET status = ?, halt_reason = ?, updated_at = ?
|
|
597
|
+
WHERE workflow_id = ?`).run(input.status, input.haltReason, input.now, input.workflowId);
|
|
598
|
+
}
|
|
599
|
+
function updateWorkflowContext(db, input) {
|
|
600
|
+
const existing = getWorkflowById(db, input.workflowId);
|
|
601
|
+
if (!existing) {
|
|
602
|
+
throw new Error(`updateWorkflowContext: unknown workflowId ${input.workflowId}`);
|
|
603
|
+
}
|
|
604
|
+
const merged = { ...existing.workflowContext, ...input.patch };
|
|
605
|
+
db.prepare("UPDATE workflows SET workflow_context = ?, updated_at = ? WHERE workflow_id = ?").run(JSON.stringify(merged), input.now, input.workflowId);
|
|
606
|
+
}
|
|
607
|
+
function incrementCurrentPhaseIndex(db, input) {
|
|
608
|
+
db.prepare(`UPDATE workflows
|
|
609
|
+
SET current_phase_index = current_phase_index + 1, updated_at = ?
|
|
610
|
+
WHERE workflow_id = ?`).run(input.now, input.workflowId);
|
|
611
|
+
}
|
|
612
|
+
function countActiveWorkflowsForCollab(db, collabId2) {
|
|
613
|
+
const row = db.prepare("SELECT COUNT(*) AS n FROM workflows WHERE collab_id = ? AND status IN ('running', 'paused')").get(collabId2);
|
|
614
|
+
return row.n;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// ../broker/dist/runtime/workspace-snapshot.js
|
|
618
|
+
import { execFileSync } from "node:child_process";
|
|
619
|
+
function captureWorkspaceSnapshotSync(workspaceRoot) {
|
|
620
|
+
try {
|
|
621
|
+
const ref = execFileSync("git", ["-C", workspaceRoot, "stash", "create"], {
|
|
622
|
+
encoding: "utf8",
|
|
623
|
+
timeout: 1e4
|
|
624
|
+
}).trim();
|
|
625
|
+
if (ref)
|
|
626
|
+
return ref;
|
|
627
|
+
const head = execFileSync("git", ["-C", workspaceRoot, "rev-parse", "HEAD"], {
|
|
628
|
+
encoding: "utf8",
|
|
629
|
+
timeout: 1e4
|
|
630
|
+
}).trim();
|
|
631
|
+
return head || null;
|
|
632
|
+
} catch {
|
|
633
|
+
return null;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
function diffChangedFilesSince(workspaceRoot, sinceRef) {
|
|
637
|
+
try {
|
|
638
|
+
const stdout = execFileSync("git", ["-C", workspaceRoot, "diff", "--name-only", sinceRef], { encoding: "utf8", timeout: 1e4 });
|
|
639
|
+
const files = stdout.split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith(".ai-whisper/"));
|
|
640
|
+
return [...new Set(files)].sort();
|
|
641
|
+
} catch {
|
|
642
|
+
return [];
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
552
646
|
// ../broker/dist/storage/repositories/reply-repository.js
|
|
553
647
|
function insertReply(db, reply) {
|
|
554
648
|
db.prepare(`INSERT INTO reply (
|
|
@@ -1014,7 +1108,7 @@ function upsertRelayTurnState(db, input) {
|
|
|
1014
1108
|
}
|
|
1015
1109
|
|
|
1016
1110
|
// ../broker/dist/storage/repositories/relay-capture-diagnostics-repository.js
|
|
1017
|
-
function
|
|
1111
|
+
function rowToRecord2(row) {
|
|
1018
1112
|
return {
|
|
1019
1113
|
captureId: row.capture_id,
|
|
1020
1114
|
handoffId: row.handoff_id,
|
|
@@ -1031,6 +1125,7 @@ function rowToRecord(row) {
|
|
|
1031
1125
|
clipSample: row.clip_sample,
|
|
1032
1126
|
turnSample: row.turn_sample,
|
|
1033
1127
|
abortedByRaceGuard: row.aborted_by_race_guard === 1,
|
|
1128
|
+
interferenceDetected: row.interference_detected === 1,
|
|
1034
1129
|
createdAt: row.created_at
|
|
1035
1130
|
};
|
|
1036
1131
|
}
|
|
@@ -1038,8 +1133,9 @@ function insertCaptureDiagnostic(db, input) {
|
|
|
1038
1133
|
db.prepare(`INSERT INTO relay_capture_diagnostics
|
|
1039
1134
|
(capture_id, handoff_id, collab_id, chain_id, workflow_id, target_provider,
|
|
1040
1135
|
capture_status, clip_len, turn_len, turn_confidence, jaccard_score,
|
|
1041
|
-
containment_score, clip_sample, turn_sample, aborted_by_race_guard,
|
|
1042
|
-
|
|
1136
|
+
containment_score, clip_sample, turn_sample, aborted_by_race_guard,
|
|
1137
|
+
interference_detected, created_at)
|
|
1138
|
+
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);
|
|
1043
1139
|
}
|
|
1044
1140
|
function listCaptureDiagnosticsByCollab(db, collabId2, limit, opts) {
|
|
1045
1141
|
const filter = opts?.workflowFilter;
|
|
@@ -1049,32 +1145,32 @@ function listCaptureDiagnosticsByCollab(db, collabId2, limit, opts) {
|
|
|
1049
1145
|
const rows2 = db.prepare(`SELECT * FROM relay_capture_diagnostics
|
|
1050
1146
|
WHERE collab_id = ?${filterClause}
|
|
1051
1147
|
ORDER BY created_at DESC`).all(collabId2, ...filterArgs);
|
|
1052
|
-
return rows2.map(
|
|
1148
|
+
return rows2.map(rowToRecord2);
|
|
1053
1149
|
}
|
|
1054
1150
|
const rows = db.prepare(`SELECT * FROM relay_capture_diagnostics
|
|
1055
1151
|
WHERE collab_id = ?${filterClause}
|
|
1056
1152
|
ORDER BY created_at DESC
|
|
1057
1153
|
LIMIT ?`).all(collabId2, ...filterArgs, limit);
|
|
1058
|
-
return rows.map(
|
|
1154
|
+
return rows.map(rowToRecord2);
|
|
1059
1155
|
}
|
|
1060
1156
|
function listCaptureDiagnosticsByCollabAndChain(db, collabId2, chainId, limit) {
|
|
1061
1157
|
if (limit === null) {
|
|
1062
1158
|
const rows2 = db.prepare(`SELECT * FROM relay_capture_diagnostics
|
|
1063
1159
|
WHERE collab_id = ? AND chain_id = ?
|
|
1064
1160
|
ORDER BY created_at DESC`).all(collabId2, chainId);
|
|
1065
|
-
return rows2.map(
|
|
1161
|
+
return rows2.map(rowToRecord2);
|
|
1066
1162
|
}
|
|
1067
1163
|
const rows = db.prepare(`SELECT * FROM relay_capture_diagnostics
|
|
1068
1164
|
WHERE collab_id = ? AND chain_id = ?
|
|
1069
1165
|
ORDER BY created_at DESC
|
|
1070
1166
|
LIMIT ?`).all(collabId2, chainId, limit);
|
|
1071
|
-
return rows.map(
|
|
1167
|
+
return rows.map(rowToRecord2);
|
|
1072
1168
|
}
|
|
1073
1169
|
function listCaptureDiagnosticsByHandoff(db, handoffId) {
|
|
1074
1170
|
const rows = db.prepare(`SELECT * FROM relay_capture_diagnostics
|
|
1075
1171
|
WHERE handoff_id = ?
|
|
1076
1172
|
ORDER BY created_at ASC`).all(handoffId);
|
|
1077
|
-
return rows.map(
|
|
1173
|
+
return rows.map(rowToRecord2);
|
|
1078
1174
|
}
|
|
1079
1175
|
function deleteCaptureDiagnosticsOlderThan(db, cutoffIso) {
|
|
1080
1176
|
const result = db.prepare("DELETE FROM relay_capture_diagnostics WHERE created_at < ?").run(cutoffIso);
|
|
@@ -1082,7 +1178,7 @@ function deleteCaptureDiagnosticsOlderThan(db, cutoffIso) {
|
|
|
1082
1178
|
}
|
|
1083
1179
|
|
|
1084
1180
|
// ../broker/dist/storage/repositories/relay-evaluator-diagnostics-repository.js
|
|
1085
|
-
function
|
|
1181
|
+
function rowToRecord3(row) {
|
|
1086
1182
|
return {
|
|
1087
1183
|
evaluatorId: row.evaluator_id,
|
|
1088
1184
|
handoffId: row.handoff_id,
|
|
@@ -1127,32 +1223,32 @@ function listEvaluatorDiagnosticsByCollab(db, collabId2, limit, opts) {
|
|
|
1127
1223
|
const rows2 = db.prepare(`SELECT * FROM relay_evaluator_diagnostics
|
|
1128
1224
|
WHERE collab_id = ?${filterClause}
|
|
1129
1225
|
ORDER BY created_at DESC`).all(collabId2, ...filterArgs);
|
|
1130
|
-
return rows2.map(
|
|
1226
|
+
return rows2.map(rowToRecord3);
|
|
1131
1227
|
}
|
|
1132
1228
|
const rows = db.prepare(`SELECT * FROM relay_evaluator_diagnostics
|
|
1133
1229
|
WHERE collab_id = ?${filterClause}
|
|
1134
1230
|
ORDER BY created_at DESC
|
|
1135
1231
|
LIMIT ?`).all(collabId2, ...filterArgs, limit);
|
|
1136
|
-
return rows.map(
|
|
1232
|
+
return rows.map(rowToRecord3);
|
|
1137
1233
|
}
|
|
1138
1234
|
function listEvaluatorDiagnosticsByCollabAndChain(db, collabId2, chainId, limit) {
|
|
1139
1235
|
if (limit === null) {
|
|
1140
1236
|
const rows2 = db.prepare(`SELECT * FROM relay_evaluator_diagnostics
|
|
1141
1237
|
WHERE collab_id = ? AND chain_id = ?
|
|
1142
1238
|
ORDER BY created_at DESC`).all(collabId2, chainId);
|
|
1143
|
-
return rows2.map(
|
|
1239
|
+
return rows2.map(rowToRecord3);
|
|
1144
1240
|
}
|
|
1145
1241
|
const rows = db.prepare(`SELECT * FROM relay_evaluator_diagnostics
|
|
1146
1242
|
WHERE collab_id = ? AND chain_id = ?
|
|
1147
1243
|
ORDER BY created_at DESC
|
|
1148
1244
|
LIMIT ?`).all(collabId2, chainId, limit);
|
|
1149
|
-
return rows.map(
|
|
1245
|
+
return rows.map(rowToRecord3);
|
|
1150
1246
|
}
|
|
1151
1247
|
function listEvaluatorDiagnosticsByHandoff(db, handoffId) {
|
|
1152
1248
|
const rows = db.prepare(`SELECT * FROM relay_evaluator_diagnostics
|
|
1153
1249
|
WHERE handoff_id = ?
|
|
1154
1250
|
ORDER BY created_at ASC`).all(handoffId);
|
|
1155
|
-
return rows.map(
|
|
1251
|
+
return rows.map(rowToRecord3);
|
|
1156
1252
|
}
|
|
1157
1253
|
function deleteEvaluatorDiagnosticsOlderThan(db, cutoffIso) {
|
|
1158
1254
|
const result = db.prepare("DELETE FROM relay_evaluator_diagnostics WHERE created_at < ?").run(cutoffIso);
|
|
@@ -1160,7 +1256,7 @@ function deleteEvaluatorDiagnosticsOlderThan(db, cutoffIso) {
|
|
|
1160
1256
|
}
|
|
1161
1257
|
|
|
1162
1258
|
// ../broker/dist/storage/repositories/relay-handoff-repository.js
|
|
1163
|
-
function
|
|
1259
|
+
function rowToRecord4(row) {
|
|
1164
1260
|
return {
|
|
1165
1261
|
handoffId: row.handoff_id,
|
|
1166
1262
|
collabId: row.collab_id,
|
|
@@ -1197,7 +1293,7 @@ function queryRelayHandoff(db, handoffId) {
|
|
|
1197
1293
|
FROM relay_handoff h
|
|
1198
1294
|
JOIN collab c ON c.collab_id = h.collab_id
|
|
1199
1295
|
WHERE h.handoff_id = ?`).get(handoffId);
|
|
1200
|
-
return row ?
|
|
1296
|
+
return row ? rowToRecord4(row) : null;
|
|
1201
1297
|
}
|
|
1202
1298
|
function createRelayHandoffTxn(db, input) {
|
|
1203
1299
|
return db.transaction(() => {
|
|
@@ -1349,7 +1445,7 @@ function queryLatestHandedBackHandoff(db, collabId2) {
|
|
|
1349
1445
|
WHERE h.collab_id = ? AND h.status = 'handed_back'
|
|
1350
1446
|
ORDER BY h.resolved_at DESC
|
|
1351
1447
|
LIMIT 1`).get(collabId2);
|
|
1352
|
-
return row ?
|
|
1448
|
+
return row ? rowToRecord4(row) : null;
|
|
1353
1449
|
}
|
|
1354
1450
|
function failRelayHandoffOnDisconnectTxn(db, input) {
|
|
1355
1451
|
db.transaction(() => {
|
|
@@ -1373,6 +1469,30 @@ function failRelayHandoffOnDisconnectTxn(db, input) {
|
|
|
1373
1469
|
}
|
|
1374
1470
|
})();
|
|
1375
1471
|
}
|
|
1472
|
+
function hasInFlightAcceptedHandoffForWorkflow(db, workflowId) {
|
|
1473
|
+
const row = db.prepare("SELECT COUNT(*) AS n FROM relay_handoff WHERE workflow_id = ? AND status = 'accepted'").get(workflowId);
|
|
1474
|
+
return row.n > 0;
|
|
1475
|
+
}
|
|
1476
|
+
function resetStrandedOrchestrationForWorkflow(db, workflowId) {
|
|
1477
|
+
const result = db.prepare(`UPDATE relay_handoff
|
|
1478
|
+
SET orchestrator_status = 'idle', orchestrator_claimed_at = NULL
|
|
1479
|
+
WHERE workflow_id = ?
|
|
1480
|
+
AND status = 'handed_back'
|
|
1481
|
+
AND orchestrator_status = 'pending'
|
|
1482
|
+
AND evaluator_verdict IS NULL`).run(workflowId);
|
|
1483
|
+
return result.changes;
|
|
1484
|
+
}
|
|
1485
|
+
function prependToPendingHandoffRequestText(db, input) {
|
|
1486
|
+
const row = db.prepare(`SELECT handoff_id, request_text FROM relay_handoff
|
|
1487
|
+
WHERE workflow_id = ? AND status = 'pending'
|
|
1488
|
+
ORDER BY created_at DESC LIMIT 1`).get(input.workflowId);
|
|
1489
|
+
if (!row)
|
|
1490
|
+
return false;
|
|
1491
|
+
db.prepare("UPDATE relay_handoff SET request_text = ?, last_activity_at = ? WHERE handoff_id = ?").run(`${input.prefix}
|
|
1492
|
+
|
|
1493
|
+
${row.request_text}`, input.now, row.handoff_id);
|
|
1494
|
+
return true;
|
|
1495
|
+
}
|
|
1376
1496
|
function claimRelayHandoffForOrchestrationTxn(db, input) {
|
|
1377
1497
|
const result = db.prepare(`UPDATE relay_handoff
|
|
1378
1498
|
SET orchestrator_status = 'pending',
|
|
@@ -1391,10 +1511,12 @@ function listRelayHandoffsPendingOrchestration(db, collabId2) {
|
|
|
1391
1511
|
c.orchestrator_max_rounds AS max_rounds
|
|
1392
1512
|
FROM relay_handoff h
|
|
1393
1513
|
JOIN collab c ON c.collab_id = h.collab_id
|
|
1514
|
+
LEFT JOIN workflows w ON w.workflow_id = h.workflow_id
|
|
1394
1515
|
WHERE h.collab_id = ?
|
|
1395
1516
|
AND h.status = 'handed_back'
|
|
1396
|
-
AND h.orchestrator_status = 'idle'
|
|
1397
|
-
|
|
1517
|
+
AND h.orchestrator_status = 'idle'
|
|
1518
|
+
AND (w.status IS NULL OR w.status <> 'paused')`).all(collabId2);
|
|
1519
|
+
return rows.map(rowToRecord4);
|
|
1398
1520
|
}
|
|
1399
1521
|
function createLoopRelayHandoffTxn(db, input) {
|
|
1400
1522
|
return db.transaction(() => {
|
|
@@ -1590,7 +1712,7 @@ function getHandoffWithWorkflowMetaById(db, handoffId) {
|
|
|
1590
1712
|
WHERE h.handoff_id = ?`).get(handoffId);
|
|
1591
1713
|
if (!row)
|
|
1592
1714
|
return null;
|
|
1593
|
-
const base =
|
|
1715
|
+
const base = rowToRecord4(row);
|
|
1594
1716
|
return {
|
|
1595
1717
|
...base,
|
|
1596
1718
|
handoffStep: row.handoff_step,
|
|
@@ -1840,71 +1962,6 @@ function listRunCostRows(db, input) {
|
|
|
1840
1962
|
// ../broker/dist/control/workflow-control.js
|
|
1841
1963
|
import { randomUUID } from "node:crypto";
|
|
1842
1964
|
|
|
1843
|
-
// ../broker/dist/storage/repositories/workflow-repository.js
|
|
1844
|
-
function rowToRecord4(row) {
|
|
1845
|
-
return {
|
|
1846
|
-
workflowId: row.workflow_id,
|
|
1847
|
-
collabId: row.collab_id,
|
|
1848
|
-
workflowType: row.workflow_type,
|
|
1849
|
-
name: row.name,
|
|
1850
|
-
specPath: row.spec_path,
|
|
1851
|
-
roleBindings: JSON.parse(row.role_bindings),
|
|
1852
|
-
status: row.status,
|
|
1853
|
-
currentPhaseIndex: row.current_phase_index,
|
|
1854
|
-
haltReason: row.halt_reason,
|
|
1855
|
-
workflowContext: JSON.parse(row.workflow_context),
|
|
1856
|
-
createdAt: row.created_at,
|
|
1857
|
-
updatedAt: row.updated_at
|
|
1858
|
-
};
|
|
1859
|
-
}
|
|
1860
|
-
function insertWorkflow(db, input) {
|
|
1861
|
-
db.prepare(`INSERT INTO workflows
|
|
1862
|
-
(workflow_id, collab_id, workflow_type, name, spec_path, role_bindings, status,
|
|
1863
|
-
current_phase_index, halt_reason, workflow_context, created_at, updated_at)
|
|
1864
|
-
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);
|
|
1865
|
-
}
|
|
1866
|
-
function getWorkflowById(db, workflowId) {
|
|
1867
|
-
const row = db.prepare("SELECT * FROM workflows WHERE workflow_id = ?").get(workflowId);
|
|
1868
|
-
return row ? rowToRecord4(row) : null;
|
|
1869
|
-
}
|
|
1870
|
-
function listWorkflows(db, filter = {}) {
|
|
1871
|
-
const clauses = [];
|
|
1872
|
-
const args = [];
|
|
1873
|
-
if (filter.collabId) {
|
|
1874
|
-
clauses.push("collab_id = ?");
|
|
1875
|
-
args.push(filter.collabId);
|
|
1876
|
-
}
|
|
1877
|
-
if (filter.status) {
|
|
1878
|
-
clauses.push("status = ?");
|
|
1879
|
-
args.push(filter.status);
|
|
1880
|
-
}
|
|
1881
|
-
const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
1882
|
-
const rows = db.prepare(`SELECT * FROM workflows ${where} ORDER BY created_at DESC`).all(...args);
|
|
1883
|
-
return rows.map(rowToRecord4);
|
|
1884
|
-
}
|
|
1885
|
-
function setWorkflowStatus(db, input) {
|
|
1886
|
-
db.prepare(`UPDATE workflows
|
|
1887
|
-
SET status = ?, halt_reason = ?, updated_at = ?
|
|
1888
|
-
WHERE workflow_id = ?`).run(input.status, input.haltReason, input.now, input.workflowId);
|
|
1889
|
-
}
|
|
1890
|
-
function updateWorkflowContext(db, input) {
|
|
1891
|
-
const existing = getWorkflowById(db, input.workflowId);
|
|
1892
|
-
if (!existing) {
|
|
1893
|
-
throw new Error(`updateWorkflowContext: unknown workflowId ${input.workflowId}`);
|
|
1894
|
-
}
|
|
1895
|
-
const merged = { ...existing.workflowContext, ...input.patch };
|
|
1896
|
-
db.prepare("UPDATE workflows SET workflow_context = ?, updated_at = ? WHERE workflow_id = ?").run(JSON.stringify(merged), input.now, input.workflowId);
|
|
1897
|
-
}
|
|
1898
|
-
function incrementCurrentPhaseIndex(db, input) {
|
|
1899
|
-
db.prepare(`UPDATE workflows
|
|
1900
|
-
SET current_phase_index = current_phase_index + 1, updated_at = ?
|
|
1901
|
-
WHERE workflow_id = ?`).run(input.now, input.workflowId);
|
|
1902
|
-
}
|
|
1903
|
-
function countRunningWorkflowsForCollab(db, collabId2) {
|
|
1904
|
-
const row = db.prepare("SELECT COUNT(*) AS n FROM workflows WHERE collab_id = ? AND status = 'running'").get(collabId2);
|
|
1905
|
-
return row.n;
|
|
1906
|
-
}
|
|
1907
|
-
|
|
1908
1965
|
// ../broker/dist/storage/repositories/workflow-phase-repository.js
|
|
1909
1966
|
function rowToRecord5(row) {
|
|
1910
1967
|
return {
|
|
@@ -2025,6 +2082,14 @@ Findings: (omit this block entirely if none)
|
|
|
2025
2082
|
Non-blocking risks:
|
|
2026
2083
|
- <quality risk that does NOT block this gate, or "None.">
|
|
2027
2084
|
--- end protocol ---`;
|
|
2085
|
+
var WORKFLOW_OPERATOR_CONTROL = `--- ai-whisper operator control ---
|
|
2086
|
+
If the operator interrupts you and asks to pause the workflow (e.g. "pause the workflow, I need to fix X"):
|
|
2087
|
+
1. Find the active workflow id: run \`whisper workflow list\`.
|
|
2088
|
+
2. Run \`whisper workflow pause <id>\`.
|
|
2089
|
+
3. Acknowledge and STOP working \u2014 do not start the next change.
|
|
2090
|
+
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.
|
|
2091
|
+
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.
|
|
2092
|
+
--- end operator control ---`;
|
|
2028
2093
|
var WORKFLOW_DIAGNOSIS_PROTOCOL = `--- ai-whisper diagnosis review protocol ---
|
|
2029
2094
|
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.
|
|
2030
2095
|
|
|
@@ -2263,10 +2328,21 @@ var COMPLEX_BUG_FIXING = {
|
|
|
2263
2328
|
}
|
|
2264
2329
|
]
|
|
2265
2330
|
};
|
|
2331
|
+
function withOperatorControl(def) {
|
|
2332
|
+
return {
|
|
2333
|
+
...def,
|
|
2334
|
+
phases: def.phases.map((p) => ({
|
|
2335
|
+
...p,
|
|
2336
|
+
kickoffTemplate: `${p.kickoffTemplate}
|
|
2337
|
+
|
|
2338
|
+
${WORKFLOW_OPERATOR_CONTROL}`
|
|
2339
|
+
}))
|
|
2340
|
+
};
|
|
2341
|
+
}
|
|
2266
2342
|
var REGISTRY = {
|
|
2267
|
-
[SPEC_DRIVEN_DEVELOPMENT.type]: SPEC_DRIVEN_DEVELOPMENT,
|
|
2268
|
-
[RALPH_LOOP.type]: RALPH_LOOP,
|
|
2269
|
-
[COMPLEX_BUG_FIXING.type]: COMPLEX_BUG_FIXING
|
|
2343
|
+
[SPEC_DRIVEN_DEVELOPMENT.type]: withOperatorControl(SPEC_DRIVEN_DEVELOPMENT),
|
|
2344
|
+
[RALPH_LOOP.type]: withOperatorControl(RALPH_LOOP),
|
|
2345
|
+
[COMPLEX_BUG_FIXING.type]: withOperatorControl(COMPLEX_BUG_FIXING)
|
|
2270
2346
|
};
|
|
2271
2347
|
function ralphRunDir(workspaceRoot, workflowId) {
|
|
2272
2348
|
return join(workspaceRoot, ".ai-whisper", "ralph", workflowId);
|
|
@@ -2322,6 +2398,23 @@ function safeDerivePlanPath(specPath, createdAt) {
|
|
|
2322
2398
|
return `${dir}${stem}.plan.md`;
|
|
2323
2399
|
}
|
|
2324
2400
|
}
|
|
2401
|
+
function composeResumeNotice(input) {
|
|
2402
|
+
const hasFiles = input.changedFiles.length > 0;
|
|
2403
|
+
const hasMessage = !!input.message && input.message.trim().length > 0;
|
|
2404
|
+
if (!hasFiles && !hasMessage)
|
|
2405
|
+
return null;
|
|
2406
|
+
const lines = [];
|
|
2407
|
+
if (hasFiles) {
|
|
2408
|
+
lines.push("While paused, the operator modified these files:");
|
|
2409
|
+
for (const f of input.changedFiles)
|
|
2410
|
+
lines.push(` - ${f}`);
|
|
2411
|
+
lines.push("Re-read them before continuing.");
|
|
2412
|
+
}
|
|
2413
|
+
if (hasMessage)
|
|
2414
|
+
lines.push(`Operator note: ${input.message.trim()}`);
|
|
2415
|
+
lines.push("Re-evaluate whether your current direction still holds; correct course before proceeding.");
|
|
2416
|
+
return lines.join("\n");
|
|
2417
|
+
}
|
|
2325
2418
|
function createWorkflowControl(deps) {
|
|
2326
2419
|
const { db, events } = deps;
|
|
2327
2420
|
function createWorkflow(input) {
|
|
@@ -2344,8 +2437,9 @@ function createWorkflowControl(deps) {
|
|
|
2344
2437
|
}
|
|
2345
2438
|
const workflowId = `wf_${randomUUID().replace(/-/g, "").slice(0, 16)}`;
|
|
2346
2439
|
const tx = db.transaction(() => {
|
|
2347
|
-
if (
|
|
2348
|
-
|
|
2440
|
+
if (countActiveWorkflowsForCollab(db, input.collabId) > 0) {
|
|
2441
|
+
const active = listWorkflows(db, { collabId: input.collabId }).find((w) => w.status === "running" || w.status === "paused");
|
|
2442
|
+
throw new Error(`another workflow is already active on this collab (${input.collabId})` + (active ? `: ${active.workflowId} (${active.status})` : ""));
|
|
2349
2443
|
}
|
|
2350
2444
|
insertWorkflow(db, {
|
|
2351
2445
|
workflowId,
|
|
@@ -2513,6 +2607,24 @@ function createWorkflowControl(deps) {
|
|
|
2513
2607
|
postmortemPath: bf.postmortemPath
|
|
2514
2608
|
});
|
|
2515
2609
|
}
|
|
2610
|
+
function consumeResumeNotice(input) {
|
|
2611
|
+
const wf = getWorkflowById(db, input.workflowId);
|
|
2612
|
+
const notice = wf?.workflowContext?.resumeNotice ?? null;
|
|
2613
|
+
if (notice) {
|
|
2614
|
+
updateWorkflowContext(db, {
|
|
2615
|
+
workflowId: input.workflowId,
|
|
2616
|
+
patch: { resumeNotice: null },
|
|
2617
|
+
now: input.now
|
|
2618
|
+
});
|
|
2619
|
+
}
|
|
2620
|
+
return notice;
|
|
2621
|
+
}
|
|
2622
|
+
function withResumeNotice(workflowId, requestText, now) {
|
|
2623
|
+
const notice = consumeResumeNotice({ workflowId, now });
|
|
2624
|
+
return notice ? `${notice}
|
|
2625
|
+
|
|
2626
|
+
${requestText}` : requestText;
|
|
2627
|
+
}
|
|
2516
2628
|
function createContinuationHandoff(input) {
|
|
2517
2629
|
const handoffId = `ho_${randomUUID().replace(/-/g, "").slice(0, 16)}`;
|
|
2518
2630
|
insertWorkflowOwnedRelayHandoff(db, {
|
|
@@ -2520,7 +2632,9 @@ function createWorkflowControl(deps) {
|
|
|
2520
2632
|
collabId: input.workflow.collabId,
|
|
2521
2633
|
senderAgent: input.sender,
|
|
2522
2634
|
targetAgent: input.target,
|
|
2523
|
-
|
|
2635
|
+
// Prepend the one-time resume notice (if any) to the FIRST outgoing request
|
|
2636
|
+
// after a resume, then it is cleared — so the agent re-reads changed files.
|
|
2637
|
+
requestText: withResumeNotice(input.workflow.workflowId, input.requestText, input.now),
|
|
2524
2638
|
chainId: input.chain.chainId,
|
|
2525
2639
|
roundNumber: input.incrementRound ? input.chain.currentRound + 1 : input.chain.currentRound,
|
|
2526
2640
|
maxRounds: input.chain.maxRounds,
|
|
@@ -2580,7 +2694,9 @@ function createWorkflowControl(deps) {
|
|
|
2580
2694
|
collabId: input.workflow.collabId,
|
|
2581
2695
|
senderAgent: sender,
|
|
2582
2696
|
targetAgent: target,
|
|
2583
|
-
|
|
2697
|
+
// Prepend a pending resume notice (consumed once) so a phase-advance kickoff
|
|
2698
|
+
// after a resume also carries the operator's change notice.
|
|
2699
|
+
requestText: withResumeNotice(input.workflow.workflowId, kickoffText, input.now),
|
|
2584
2700
|
chainId,
|
|
2585
2701
|
roundNumber: 1,
|
|
2586
2702
|
maxRounds: phase.maxRounds,
|
|
@@ -3041,20 +3157,100 @@ ${findingsText}`;
|
|
|
3041
3157
|
reason: input.reason
|
|
3042
3158
|
});
|
|
3043
3159
|
}
|
|
3160
|
+
function maybeCaptureQuiesceSnapshot(input) {
|
|
3161
|
+
const wf = getWorkflowById(db, input.workflowId);
|
|
3162
|
+
if (!wf || wf.status !== "paused")
|
|
3163
|
+
return;
|
|
3164
|
+
if (wf.workflowContext.pauseSnapshotRef !== void 0) {
|
|
3165
|
+
return;
|
|
3166
|
+
}
|
|
3167
|
+
if (hasInFlightAcceptedHandoffForWorkflow(db, input.workflowId))
|
|
3168
|
+
return;
|
|
3169
|
+
const ref = deps.captureSnapshotRef ? deps.captureSnapshotRef(input.workflowId) : null;
|
|
3170
|
+
updateWorkflowContext(db, {
|
|
3171
|
+
workflowId: input.workflowId,
|
|
3172
|
+
patch: { pauseSnapshotRef: ref },
|
|
3173
|
+
now: input.now
|
|
3174
|
+
});
|
|
3175
|
+
}
|
|
3176
|
+
function pauseWorkflow(input) {
|
|
3177
|
+
const workflow = getWorkflowById(db, input.workflowId);
|
|
3178
|
+
if (!workflow) {
|
|
3179
|
+
throw new Error(`pauseWorkflow: unknown workflowId ${input.workflowId}`);
|
|
3180
|
+
}
|
|
3181
|
+
if (workflow.status !== "running") {
|
|
3182
|
+
throw new Error(`pauseWorkflow: workflow ${input.workflowId} is ${workflow.status}, only running workflows can be paused`);
|
|
3183
|
+
}
|
|
3184
|
+
const tx = db.transaction(() => {
|
|
3185
|
+
const current = getWorkflowById(db, input.workflowId);
|
|
3186
|
+
if (!current || current.status !== "running")
|
|
3187
|
+
return;
|
|
3188
|
+
setWorkflowStatus(db, {
|
|
3189
|
+
workflowId: input.workflowId,
|
|
3190
|
+
status: "paused",
|
|
3191
|
+
haltReason: null,
|
|
3192
|
+
now: input.now
|
|
3193
|
+
});
|
|
3194
|
+
updateWorkflowContext(db, {
|
|
3195
|
+
workflowId: input.workflowId,
|
|
3196
|
+
patch: { pausedAt: input.now },
|
|
3197
|
+
now: input.now
|
|
3198
|
+
});
|
|
3199
|
+
});
|
|
3200
|
+
tx.immediate();
|
|
3201
|
+
events.emit("workflow.paused", { workflowId: input.workflowId });
|
|
3202
|
+
maybeCaptureQuiesceSnapshot({ workflowId: input.workflowId, now: input.now });
|
|
3203
|
+
}
|
|
3204
|
+
function resumeHaltedWorkflow(workflow, now) {
|
|
3205
|
+
const tx = db.transaction(() => {
|
|
3206
|
+
const others = listWorkflows(db, { collabId: workflow.collabId }).filter((w) => w.workflowId !== workflow.workflowId && (w.status === "running" || w.status === "paused"));
|
|
3207
|
+
if (others.length > 0) {
|
|
3208
|
+
throw new Error(`resumeWorkflow: another workflow is already active on collab ${workflow.collabId}`);
|
|
3209
|
+
}
|
|
3210
|
+
setWorkflowStatus(db, {
|
|
3211
|
+
workflowId: workflow.workflowId,
|
|
3212
|
+
status: "running",
|
|
3213
|
+
haltReason: null,
|
|
3214
|
+
now
|
|
3215
|
+
});
|
|
3216
|
+
});
|
|
3217
|
+
tx.immediate();
|
|
3218
|
+
events.emit("workflow.resumed", {
|
|
3219
|
+
workflowId: workflow.workflowId,
|
|
3220
|
+
phaseIndex: workflow.currentPhaseIndex
|
|
3221
|
+
});
|
|
3222
|
+
}
|
|
3044
3223
|
function resumeWorkflow(input) {
|
|
3045
3224
|
const workflow = getWorkflowById(db, input.workflowId);
|
|
3046
3225
|
if (!workflow) {
|
|
3047
3226
|
throw new Error(`resumeWorkflow: unknown workflowId ${input.workflowId}`);
|
|
3048
3227
|
}
|
|
3049
|
-
if (workflow.status === "
|
|
3050
|
-
|
|
3228
|
+
if (workflow.status === "halted") {
|
|
3229
|
+
resumeHaltedWorkflow(workflow, input.now);
|
|
3230
|
+
return;
|
|
3051
3231
|
}
|
|
3052
|
-
if (workflow.status !== "
|
|
3053
|
-
throw new Error(`resumeWorkflow: workflow ${input.workflowId} is
|
|
3232
|
+
if (workflow.status !== "paused") {
|
|
3233
|
+
throw new Error(`resumeWorkflow: workflow ${input.workflowId} is ${workflow.status}, only paused or halted workflows can be resumed`);
|
|
3054
3234
|
}
|
|
3235
|
+
const ref = workflow.workflowContext.pauseSnapshotRef ?? null;
|
|
3236
|
+
const changedFiles = ref && deps.diffChangedFilesSinceSnapshot ? deps.diffChangedFilesSinceSnapshot(input.workflowId, ref) : [];
|
|
3237
|
+
const notice = composeResumeNotice({
|
|
3238
|
+
changedFiles,
|
|
3239
|
+
message: input.message ?? null
|
|
3240
|
+
});
|
|
3055
3241
|
const tx = db.transaction(() => {
|
|
3056
|
-
|
|
3057
|
-
|
|
3242
|
+
const others = listWorkflows(db, { collabId: workflow.collabId }).filter((w) => w.workflowId !== input.workflowId && (w.status === "running" || w.status === "paused"));
|
|
3243
|
+
if (others.length > 0) {
|
|
3244
|
+
throw new Error(`resumeWorkflow: another workflow is already active on collab ${workflow.collabId}`);
|
|
3245
|
+
}
|
|
3246
|
+
resetStrandedOrchestrationForWorkflow(db, input.workflowId);
|
|
3247
|
+
let bakedIntoPending = false;
|
|
3248
|
+
if (notice) {
|
|
3249
|
+
bakedIntoPending = prependToPendingHandoffRequestText(db, {
|
|
3250
|
+
workflowId: input.workflowId,
|
|
3251
|
+
prefix: notice,
|
|
3252
|
+
now: input.now
|
|
3253
|
+
});
|
|
3058
3254
|
}
|
|
3059
3255
|
setWorkflowStatus(db, {
|
|
3060
3256
|
workflowId: input.workflowId,
|
|
@@ -3062,6 +3258,15 @@ ${findingsText}`;
|
|
|
3062
3258
|
haltReason: null,
|
|
3063
3259
|
now: input.now
|
|
3064
3260
|
});
|
|
3261
|
+
updateWorkflowContext(db, {
|
|
3262
|
+
workflowId: input.workflowId,
|
|
3263
|
+
patch: {
|
|
3264
|
+
resumeNotice: bakedIntoPending ? null : notice,
|
|
3265
|
+
pausedAt: null,
|
|
3266
|
+
pauseSnapshotRef: null
|
|
3267
|
+
},
|
|
3268
|
+
now: input.now
|
|
3269
|
+
});
|
|
3065
3270
|
});
|
|
3066
3271
|
tx.immediate();
|
|
3067
3272
|
events.emit("workflow.resumed", {
|
|
@@ -3162,6 +3367,9 @@ ${findingsText}`;
|
|
|
3162
3367
|
beginPhaseRun,
|
|
3163
3368
|
applyOrchestratorVerdict,
|
|
3164
3369
|
haltWorkflow,
|
|
3370
|
+
pauseWorkflow,
|
|
3371
|
+
maybeCaptureQuiesceSnapshot,
|
|
3372
|
+
consumeResumeNotice,
|
|
3165
3373
|
resumeWorkflow,
|
|
3166
3374
|
cancelWorkflow,
|
|
3167
3375
|
getHandoffWithWorkflowMeta,
|
|
@@ -3177,8 +3385,32 @@ function buildEventId(kind, subjectId, timestamp) {
|
|
|
3177
3385
|
return `evt_${kind}_${subjectId}_${normalizeTimestampForEventId(timestamp)}`;
|
|
3178
3386
|
}
|
|
3179
3387
|
function createControlService(db, events) {
|
|
3180
|
-
const
|
|
3388
|
+
const resolveWorkspaceRoot = (workflowId) => {
|
|
3389
|
+
const wf = getWorkflowById(db, workflowId);
|
|
3390
|
+
if (!wf)
|
|
3391
|
+
return null;
|
|
3392
|
+
return getCollab(db, wf.collabId)?.workspaceRoot ?? null;
|
|
3393
|
+
};
|
|
3394
|
+
const workflowControl = createWorkflowControl({
|
|
3395
|
+
db,
|
|
3396
|
+
events,
|
|
3397
|
+
captureSnapshotRef: (workflowId) => {
|
|
3398
|
+
const root = resolveWorkspaceRoot(workflowId);
|
|
3399
|
+
return root ? captureWorkspaceSnapshotSync(root) : null;
|
|
3400
|
+
},
|
|
3401
|
+
diffChangedFilesSinceSnapshot: (workflowId, sinceRef) => {
|
|
3402
|
+
const root = resolveWorkspaceRoot(workflowId);
|
|
3403
|
+
return root ? diffChangedFilesSince(root, sinceRef) : [];
|
|
3404
|
+
}
|
|
3405
|
+
});
|
|
3406
|
+
const isWorkflowDeliverySuspended = (handoffId) => {
|
|
3407
|
+
const meta = workflowControl.getHandoffWithWorkflowMeta(handoffId);
|
|
3408
|
+
if (!meta?.workflowId)
|
|
3409
|
+
return false;
|
|
3410
|
+
return workflowControl.getWorkflow(meta.workflowId)?.status === "paused";
|
|
3411
|
+
};
|
|
3181
3412
|
return Object.assign({
|
|
3413
|
+
isWorkflowDeliverySuspended,
|
|
3182
3414
|
startCollab(input) {
|
|
3183
3415
|
const collab2 = collabSchema.parse({
|
|
3184
3416
|
version: 1,
|
|
@@ -3806,6 +4038,8 @@ function createControlService(db, events) {
|
|
|
3806
4038
|
return createRelayHandoffTxn(db, input);
|
|
3807
4039
|
},
|
|
3808
4040
|
acceptRelayHandoff(input) {
|
|
4041
|
+
if (isWorkflowDeliverySuspended(input.handoffId))
|
|
4042
|
+
return;
|
|
3809
4043
|
return acceptRelayHandoffTxn(db, input);
|
|
3810
4044
|
},
|
|
3811
4045
|
deferRelayHandoff(input) {
|
|
@@ -3818,7 +4052,12 @@ function createControlService(db, events) {
|
|
|
3818
4052
|
return markRelayHandoffStaleTxn(db, input);
|
|
3819
4053
|
},
|
|
3820
4054
|
handoffBackRelay(input) {
|
|
3821
|
-
|
|
4055
|
+
const result = handoffBackRelayTxn(db, input);
|
|
4056
|
+
const meta = workflowControl.getHandoffWithWorkflowMeta(input.handoffId);
|
|
4057
|
+
if (meta?.workflowId && workflowControl.getWorkflow(meta.workflowId)?.status === "paused") {
|
|
4058
|
+
workflowControl.maybeCaptureQuiesceSnapshot({ workflowId: meta.workflowId, now: input.now });
|
|
4059
|
+
}
|
|
4060
|
+
return result;
|
|
3822
4061
|
},
|
|
3823
4062
|
failRelayHandoffOnDisconnect(input) {
|
|
3824
4063
|
return failRelayHandoffOnDisconnectTxn(db, input);
|
|
@@ -3830,6 +4069,8 @@ function createControlService(db, events) {
|
|
|
3830
4069
|
return queryLatestHandedBackHandoff(db, collabId2);
|
|
3831
4070
|
},
|
|
3832
4071
|
claimRelayHandoffForOrchestration(input) {
|
|
4072
|
+
if (isWorkflowDeliverySuspended(input.handoffId))
|
|
4073
|
+
return null;
|
|
3833
4074
|
return claimRelayHandoffForOrchestrationTxn(db, input);
|
|
3834
4075
|
},
|
|
3835
4076
|
listRelayHandoffsPendingOrchestration(collabId2) {
|
|
@@ -3865,6 +4106,7 @@ function createControlService(db, events) {
|
|
|
3865
4106
|
clipSample: input.clipSample,
|
|
3866
4107
|
turnSample: input.turnSample,
|
|
3867
4108
|
abortedByRaceGuard: input.abortedByRaceGuard,
|
|
4109
|
+
interferenceDetected: input.interferenceDetected ?? false,
|
|
3868
4110
|
createdAt: input.now
|
|
3869
4111
|
});
|
|
3870
4112
|
return { captureId };
|
|
@@ -3941,8 +4183,130 @@ function createBrokerApp(input) {
|
|
|
3941
4183
|
return app;
|
|
3942
4184
|
}
|
|
3943
4185
|
|
|
4186
|
+
// ../broker/dist/storage/enforce-one-active-collab.js
|
|
4187
|
+
function defaultIsPidAlive(pid) {
|
|
4188
|
+
try {
|
|
4189
|
+
process.kill(pid, 0);
|
|
4190
|
+
return true;
|
|
4191
|
+
} catch (err) {
|
|
4192
|
+
return err.code === "EPERM";
|
|
4193
|
+
}
|
|
4194
|
+
}
|
|
4195
|
+
function dedupeActiveCollabs(db, opts) {
|
|
4196
|
+
const rows = db.prepare(`SELECT c.collab_id, c.workspace_id, c.created_at, d.pid AS pid,
|
|
4197
|
+
(SELECT COUNT(*) FROM workflows w
|
|
4198
|
+
WHERE w.collab_id = c.collab_id AND w.status = 'running') AS running_workflows
|
|
4199
|
+
FROM collab c
|
|
4200
|
+
LEFT JOIN broker_daemon d ON d.collab_id = c.collab_id
|
|
4201
|
+
WHERE c.status = 'active' AND c.workspace_id IS NOT NULL`).all();
|
|
4202
|
+
const byWorkspace = /* @__PURE__ */ new Map();
|
|
4203
|
+
for (const row of rows) {
|
|
4204
|
+
const key = row.workspace_id;
|
|
4205
|
+
const list = byWorkspace.get(key);
|
|
4206
|
+
if (list)
|
|
4207
|
+
list.push(row);
|
|
4208
|
+
else
|
|
4209
|
+
byWorkspace.set(key, [row]);
|
|
4210
|
+
}
|
|
4211
|
+
const conflicted = [];
|
|
4212
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4213
|
+
const stop = db.prepare("UPDATE collab SET status = 'stopped', stopped_at = ?, updated_at = ? WHERE collab_id = ?");
|
|
4214
|
+
for (const [workspaceId, group] of byWorkspace) {
|
|
4215
|
+
if (group.length < 2)
|
|
4216
|
+
continue;
|
|
4217
|
+
const workflowOwners = group.filter((r) => r.running_workflows > 0);
|
|
4218
|
+
if (workflowOwners.length >= 2) {
|
|
4219
|
+
conflicted.push(workspaceId);
|
|
4220
|
+
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.`);
|
|
4221
|
+
continue;
|
|
4222
|
+
}
|
|
4223
|
+
let survivor;
|
|
4224
|
+
if (workflowOwners.length === 1) {
|
|
4225
|
+
survivor = workflowOwners[0];
|
|
4226
|
+
} else {
|
|
4227
|
+
const live = group.filter((r) => r.pid !== null && opts.isPidAlive(r.pid));
|
|
4228
|
+
if (live.length >= 1) {
|
|
4229
|
+
survivor = live.reduce((a, b) => a.created_at >= b.created_at ? a : b);
|
|
4230
|
+
} else {
|
|
4231
|
+
survivor = group.reduce((a, b) => a.created_at >= b.created_at ? a : b);
|
|
4232
|
+
}
|
|
4233
|
+
}
|
|
4234
|
+
for (const row of group) {
|
|
4235
|
+
if (row.collab_id === survivor.collab_id)
|
|
4236
|
+
continue;
|
|
4237
|
+
stop.run(now, now, row.collab_id);
|
|
4238
|
+
}
|
|
4239
|
+
}
|
|
4240
|
+
return conflicted;
|
|
4241
|
+
}
|
|
4242
|
+
var CREATE_INDEX_SQL = "CREATE UNIQUE INDEX IF NOT EXISTS idx_collab_one_active_per_workspace ON collab(workspace_id) WHERE status = 'active'";
|
|
4243
|
+
function residualDuplicateCount(db) {
|
|
4244
|
+
const row = db.prepare(`SELECT COUNT(*) AS n FROM (
|
|
4245
|
+
SELECT workspace_id FROM collab
|
|
4246
|
+
WHERE status = 'active' AND workspace_id IS NOT NULL
|
|
4247
|
+
GROUP BY workspace_id HAVING COUNT(*) > 1
|
|
4248
|
+
)`).get();
|
|
4249
|
+
return row.n;
|
|
4250
|
+
}
|
|
4251
|
+
function enforceOneActiveCollabPerWorkspace(db, options = {}) {
|
|
4252
|
+
const opts = {
|
|
4253
|
+
isPidAlive: options.isPidAlive ?? defaultIsPidAlive,
|
|
4254
|
+
warn: options.warn ?? ((m) => console.error(m))
|
|
4255
|
+
};
|
|
4256
|
+
const tx = db.transaction(() => {
|
|
4257
|
+
dedupeActiveCollabs(db, opts);
|
|
4258
|
+
if (residualDuplicateCount(db) === 0) {
|
|
4259
|
+
db.exec(CREATE_INDEX_SQL);
|
|
4260
|
+
} else {
|
|
4261
|
+
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.");
|
|
4262
|
+
}
|
|
4263
|
+
});
|
|
4264
|
+
tx();
|
|
4265
|
+
}
|
|
4266
|
+
|
|
4267
|
+
// ../broker/dist/storage/clipboard-capture-lease.js
|
|
4268
|
+
var LEASE_ID = 1;
|
|
4269
|
+
var DEFAULT_LEASE_TTL_MS = 5e3;
|
|
4270
|
+
function defaultIsPidAlive2(pid) {
|
|
4271
|
+
try {
|
|
4272
|
+
process.kill(pid, 0);
|
|
4273
|
+
return true;
|
|
4274
|
+
} catch (err) {
|
|
4275
|
+
return err.code === "EPERM";
|
|
4276
|
+
}
|
|
4277
|
+
}
|
|
4278
|
+
function resolveOptions(options) {
|
|
4279
|
+
return {
|
|
4280
|
+
isPidAlive: options.isPidAlive ?? defaultIsPidAlive2,
|
|
4281
|
+
ttlMs: options.ttlMs ?? DEFAULT_LEASE_TTL_MS,
|
|
4282
|
+
now: options.now ?? Date.now
|
|
4283
|
+
};
|
|
4284
|
+
}
|
|
4285
|
+
function isStale(row, opts) {
|
|
4286
|
+
if (row.holder_collab_id === null)
|
|
4287
|
+
return true;
|
|
4288
|
+
if (row.holder_pid === null || !opts.isPidAlive(row.holder_pid))
|
|
4289
|
+
return true;
|
|
4290
|
+
if (row.acquired_at === null)
|
|
4291
|
+
return true;
|
|
4292
|
+
const age = opts.now() - Date.parse(row.acquired_at);
|
|
4293
|
+
return age > opts.ttlMs;
|
|
4294
|
+
}
|
|
4295
|
+
function sweepStaleCaptureLease(db, options = {}) {
|
|
4296
|
+
const opts = resolveOptions(options);
|
|
4297
|
+
const tx = db.transaction(() => {
|
|
4298
|
+
const row = db.prepare("SELECT holder_collab_id, holder_pid, acquired_at FROM clipboard_capture_lease WHERE id = ?").get(LEASE_ID);
|
|
4299
|
+
if (!row || row.holder_collab_id === null)
|
|
4300
|
+
return;
|
|
4301
|
+
if (isStale(row, opts)) {
|
|
4302
|
+
db.prepare("UPDATE clipboard_capture_lease SET holder_collab_id = NULL, holder_pid = NULL, acquired_at = NULL WHERE id = ?").run(LEASE_ID);
|
|
4303
|
+
}
|
|
4304
|
+
});
|
|
4305
|
+
tx();
|
|
4306
|
+
}
|
|
4307
|
+
|
|
3944
4308
|
// ../broker/dist/storage/apply-migrations.js
|
|
3945
|
-
var CURRENT_SCHEMA_VERSION =
|
|
4309
|
+
var CURRENT_SCHEMA_VERSION = 5;
|
|
3946
4310
|
var initMigrationSql = `
|
|
3947
4311
|
CREATE TABLE IF NOT EXISTS broker_state (
|
|
3948
4312
|
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
@@ -4160,8 +4524,9 @@ CREATE TABLE IF NOT EXISTS workflows (
|
|
|
4160
4524
|
updated_at TEXT NOT NULL
|
|
4161
4525
|
);
|
|
4162
4526
|
|
|
4527
|
+
DROP INDEX IF EXISTS workflows_one_running_per_collab;
|
|
4163
4528
|
CREATE UNIQUE INDEX IF NOT EXISTS workflows_one_running_per_collab
|
|
4164
|
-
ON workflows(collab_id) WHERE status
|
|
4529
|
+
ON workflows(collab_id) WHERE status IN ('running', 'paused');
|
|
4165
4530
|
|
|
4166
4531
|
CREATE TABLE IF NOT EXISTS workflow_phases (
|
|
4167
4532
|
phase_run_id TEXT PRIMARY KEY,
|
|
@@ -4229,6 +4594,13 @@ CREATE TABLE IF NOT EXISTS recovery_state (
|
|
|
4229
4594
|
recovered_at TEXT
|
|
4230
4595
|
);
|
|
4231
4596
|
|
|
4597
|
+
CREATE TABLE IF NOT EXISTS clipboard_capture_lease (
|
|
4598
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
4599
|
+
holder_collab_id TEXT,
|
|
4600
|
+
holder_pid INTEGER,
|
|
4601
|
+
acquired_at TEXT
|
|
4602
|
+
);
|
|
4603
|
+
|
|
4232
4604
|
`;
|
|
4233
4605
|
function ensureBrokerStateRow(db) {
|
|
4234
4606
|
db.prepare(`INSERT INTO broker_state (id, schema_version, migrated)
|
|
@@ -4239,20 +4611,20 @@ function ensureBrokerStateRow(db) {
|
|
|
4239
4611
|
}
|
|
4240
4612
|
function applyMigrations(db) {
|
|
4241
4613
|
const current = db.pragma("user_version", { simple: true });
|
|
4242
|
-
if (current
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
} catch (err) {
|
|
4253
|
-
db.exec("ROLLBACK");
|
|
4254
|
-
throw err;
|
|
4614
|
+
if (current < CURRENT_SCHEMA_VERSION) {
|
|
4615
|
+
db.exec("BEGIN EXCLUSIVE");
|
|
4616
|
+
try {
|
|
4617
|
+
runMigrationBody(db);
|
|
4618
|
+
db.pragma(`user_version = ${CURRENT_SCHEMA_VERSION}`);
|
|
4619
|
+
db.exec("COMMIT");
|
|
4620
|
+
} catch (err) {
|
|
4621
|
+
db.exec("ROLLBACK");
|
|
4622
|
+
throw err;
|
|
4623
|
+
}
|
|
4255
4624
|
}
|
|
4625
|
+
ensureBrokerStateRow(db);
|
|
4626
|
+
enforceOneActiveCollabPerWorkspace(db);
|
|
4627
|
+
sweepStaleCaptureLease(db);
|
|
4256
4628
|
}
|
|
4257
4629
|
function runMigrationBody(db) {
|
|
4258
4630
|
db.exec(initMigrationSql);
|
|
@@ -4389,6 +4761,7 @@ function runMigrationBody(db) {
|
|
|
4389
4761
|
clip_sample TEXT,
|
|
4390
4762
|
turn_sample TEXT,
|
|
4391
4763
|
aborted_by_race_guard INTEGER NOT NULL DEFAULT 0,
|
|
4764
|
+
interference_detected INTEGER NOT NULL DEFAULT 0,
|
|
4392
4765
|
created_at TEXT NOT NULL
|
|
4393
4766
|
);
|
|
4394
4767
|
CREATE INDEX IF NOT EXISTS idx_relay_capture_diagnostics_collab_created
|
|
@@ -4400,6 +4773,10 @@ function runMigrationBody(db) {
|
|
|
4400
4773
|
CREATE INDEX IF NOT EXISTS idx_relay_capture_diagnostics_status
|
|
4401
4774
|
ON relay_capture_diagnostics (capture_status);
|
|
4402
4775
|
`);
|
|
4776
|
+
const captureDiagColumns = db.prepare("PRAGMA table_info(relay_capture_diagnostics)").all();
|
|
4777
|
+
if (!captureDiagColumns.some((column) => column.name === "interference_detected")) {
|
|
4778
|
+
db.exec("ALTER TABLE relay_capture_diagnostics ADD COLUMN interference_detected INTEGER NOT NULL DEFAULT 0");
|
|
4779
|
+
}
|
|
4403
4780
|
db.exec(`
|
|
4404
4781
|
CREATE TABLE IF NOT EXISTS relay_evaluator_diagnostics (
|
|
4405
4782
|
evaluator_id TEXT PRIMARY KEY,
|
|
@@ -5683,7 +6060,7 @@ ${JSON.stringify(event.payload)}`;
|
|
|
5683
6060
|
}
|
|
5684
6061
|
|
|
5685
6062
|
// src/runtime/process-start-time.ts
|
|
5686
|
-
import { execFileSync } from "node:child_process";
|
|
6063
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
5687
6064
|
import { readFileSync } from "node:fs";
|
|
5688
6065
|
function readProcessStartTime(pid) {
|
|
5689
6066
|
if (process.platform === "linux") {
|
|
@@ -5697,7 +6074,7 @@ function readProcessStartTime(pid) {
|
|
|
5697
6074
|
}
|
|
5698
6075
|
if (process.platform === "darwin") {
|
|
5699
6076
|
try {
|
|
5700
|
-
const out =
|
|
6077
|
+
const out = execFileSync2("ps", ["-o", "lstart=", "-p", String(pid)], {
|
|
5701
6078
|
encoding: "utf8"
|
|
5702
6079
|
});
|
|
5703
6080
|
const trimmed = out.trim();
|