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