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
|
@@ -582,6 +582,100 @@ function getCollab(db, collabId) {
|
|
|
582
582
|
});
|
|
583
583
|
}
|
|
584
584
|
|
|
585
|
+
// ../broker/dist/storage/repositories/workflow-repository.js
|
|
586
|
+
function rowToRecord(row) {
|
|
587
|
+
return {
|
|
588
|
+
workflowId: row.workflow_id,
|
|
589
|
+
collabId: row.collab_id,
|
|
590
|
+
workflowType: row.workflow_type,
|
|
591
|
+
name: row.name,
|
|
592
|
+
specPath: row.spec_path,
|
|
593
|
+
roleBindings: JSON.parse(row.role_bindings),
|
|
594
|
+
status: row.status,
|
|
595
|
+
currentPhaseIndex: row.current_phase_index,
|
|
596
|
+
haltReason: row.halt_reason,
|
|
597
|
+
workflowContext: JSON.parse(row.workflow_context),
|
|
598
|
+
createdAt: row.created_at,
|
|
599
|
+
updatedAt: row.updated_at
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
function insertWorkflow(db, input) {
|
|
603
|
+
db.prepare(`INSERT INTO workflows
|
|
604
|
+
(workflow_id, collab_id, workflow_type, name, spec_path, role_bindings, status,
|
|
605
|
+
current_phase_index, halt_reason, workflow_context, created_at, updated_at)
|
|
606
|
+
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);
|
|
607
|
+
}
|
|
608
|
+
function getWorkflowById(db, workflowId) {
|
|
609
|
+
const row = db.prepare("SELECT * FROM workflows WHERE workflow_id = ?").get(workflowId);
|
|
610
|
+
return row ? rowToRecord(row) : null;
|
|
611
|
+
}
|
|
612
|
+
function listWorkflows(db, filter = {}) {
|
|
613
|
+
const clauses = [];
|
|
614
|
+
const args = [];
|
|
615
|
+
if (filter.collabId) {
|
|
616
|
+
clauses.push("collab_id = ?");
|
|
617
|
+
args.push(filter.collabId);
|
|
618
|
+
}
|
|
619
|
+
if (filter.status) {
|
|
620
|
+
clauses.push("status = ?");
|
|
621
|
+
args.push(filter.status);
|
|
622
|
+
}
|
|
623
|
+
const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
624
|
+
const rows = db.prepare(`SELECT * FROM workflows ${where} ORDER BY created_at DESC`).all(...args);
|
|
625
|
+
return rows.map(rowToRecord);
|
|
626
|
+
}
|
|
627
|
+
function setWorkflowStatus(db, input) {
|
|
628
|
+
db.prepare(`UPDATE workflows
|
|
629
|
+
SET status = ?, halt_reason = ?, updated_at = ?
|
|
630
|
+
WHERE workflow_id = ?`).run(input.status, input.haltReason, input.now, input.workflowId);
|
|
631
|
+
}
|
|
632
|
+
function updateWorkflowContext(db, input) {
|
|
633
|
+
const existing = getWorkflowById(db, input.workflowId);
|
|
634
|
+
if (!existing) {
|
|
635
|
+
throw new Error(`updateWorkflowContext: unknown workflowId ${input.workflowId}`);
|
|
636
|
+
}
|
|
637
|
+
const merged = { ...existing.workflowContext, ...input.patch };
|
|
638
|
+
db.prepare("UPDATE workflows SET workflow_context = ?, updated_at = ? WHERE workflow_id = ?").run(JSON.stringify(merged), input.now, input.workflowId);
|
|
639
|
+
}
|
|
640
|
+
function incrementCurrentPhaseIndex(db, input) {
|
|
641
|
+
db.prepare(`UPDATE workflows
|
|
642
|
+
SET current_phase_index = current_phase_index + 1, updated_at = ?
|
|
643
|
+
WHERE workflow_id = ?`).run(input.now, input.workflowId);
|
|
644
|
+
}
|
|
645
|
+
function countActiveWorkflowsForCollab(db, collabId) {
|
|
646
|
+
const row = db.prepare("SELECT COUNT(*) AS n FROM workflows WHERE collab_id = ? AND status IN ('running', 'paused')").get(collabId);
|
|
647
|
+
return row.n;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// ../broker/dist/runtime/workspace-snapshot.js
|
|
651
|
+
import { execFileSync } from "node:child_process";
|
|
652
|
+
function captureWorkspaceSnapshotSync(workspaceRoot) {
|
|
653
|
+
try {
|
|
654
|
+
const ref = execFileSync("git", ["-C", workspaceRoot, "stash", "create"], {
|
|
655
|
+
encoding: "utf8",
|
|
656
|
+
timeout: 1e4
|
|
657
|
+
}).trim();
|
|
658
|
+
if (ref)
|
|
659
|
+
return ref;
|
|
660
|
+
const head = execFileSync("git", ["-C", workspaceRoot, "rev-parse", "HEAD"], {
|
|
661
|
+
encoding: "utf8",
|
|
662
|
+
timeout: 1e4
|
|
663
|
+
}).trim();
|
|
664
|
+
return head || null;
|
|
665
|
+
} catch {
|
|
666
|
+
return null;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
function diffChangedFilesSince(workspaceRoot, sinceRef) {
|
|
670
|
+
try {
|
|
671
|
+
const stdout = execFileSync("git", ["-C", workspaceRoot, "diff", "--name-only", sinceRef], { encoding: "utf8", timeout: 1e4 });
|
|
672
|
+
const files = stdout.split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith(".ai-whisper/"));
|
|
673
|
+
return [...new Set(files)].sort();
|
|
674
|
+
} catch {
|
|
675
|
+
return [];
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
585
679
|
// ../broker/dist/storage/repositories/reply-repository.js
|
|
586
680
|
function insertReply(db, reply) {
|
|
587
681
|
db.prepare(`INSERT INTO reply (
|
|
@@ -1047,7 +1141,7 @@ function upsertRelayTurnState(db, input) {
|
|
|
1047
1141
|
}
|
|
1048
1142
|
|
|
1049
1143
|
// ../broker/dist/storage/repositories/relay-capture-diagnostics-repository.js
|
|
1050
|
-
function
|
|
1144
|
+
function rowToRecord2(row) {
|
|
1051
1145
|
return {
|
|
1052
1146
|
captureId: row.capture_id,
|
|
1053
1147
|
handoffId: row.handoff_id,
|
|
@@ -1064,6 +1158,7 @@ function rowToRecord(row) {
|
|
|
1064
1158
|
clipSample: row.clip_sample,
|
|
1065
1159
|
turnSample: row.turn_sample,
|
|
1066
1160
|
abortedByRaceGuard: row.aborted_by_race_guard === 1,
|
|
1161
|
+
interferenceDetected: row.interference_detected === 1,
|
|
1067
1162
|
createdAt: row.created_at
|
|
1068
1163
|
};
|
|
1069
1164
|
}
|
|
@@ -1071,8 +1166,9 @@ function insertCaptureDiagnostic(db, input) {
|
|
|
1071
1166
|
db.prepare(`INSERT INTO relay_capture_diagnostics
|
|
1072
1167
|
(capture_id, handoff_id, collab_id, chain_id, workflow_id, target_provider,
|
|
1073
1168
|
capture_status, clip_len, turn_len, turn_confidence, jaccard_score,
|
|
1074
|
-
containment_score, clip_sample, turn_sample, aborted_by_race_guard,
|
|
1075
|
-
|
|
1169
|
+
containment_score, clip_sample, turn_sample, aborted_by_race_guard,
|
|
1170
|
+
interference_detected, created_at)
|
|
1171
|
+
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);
|
|
1076
1172
|
}
|
|
1077
1173
|
function listCaptureDiagnosticsByCollab(db, collabId, limit, opts) {
|
|
1078
1174
|
const filter = opts?.workflowFilter;
|
|
@@ -1082,32 +1178,32 @@ function listCaptureDiagnosticsByCollab(db, collabId, limit, opts) {
|
|
|
1082
1178
|
const rows2 = db.prepare(`SELECT * FROM relay_capture_diagnostics
|
|
1083
1179
|
WHERE collab_id = ?${filterClause}
|
|
1084
1180
|
ORDER BY created_at DESC`).all(collabId, ...filterArgs);
|
|
1085
|
-
return rows2.map(
|
|
1181
|
+
return rows2.map(rowToRecord2);
|
|
1086
1182
|
}
|
|
1087
1183
|
const rows = db.prepare(`SELECT * FROM relay_capture_diagnostics
|
|
1088
1184
|
WHERE collab_id = ?${filterClause}
|
|
1089
1185
|
ORDER BY created_at DESC
|
|
1090
1186
|
LIMIT ?`).all(collabId, ...filterArgs, limit);
|
|
1091
|
-
return rows.map(
|
|
1187
|
+
return rows.map(rowToRecord2);
|
|
1092
1188
|
}
|
|
1093
1189
|
function listCaptureDiagnosticsByCollabAndChain(db, collabId, chainId, limit) {
|
|
1094
1190
|
if (limit === null) {
|
|
1095
1191
|
const rows2 = db.prepare(`SELECT * FROM relay_capture_diagnostics
|
|
1096
1192
|
WHERE collab_id = ? AND chain_id = ?
|
|
1097
1193
|
ORDER BY created_at DESC`).all(collabId, chainId);
|
|
1098
|
-
return rows2.map(
|
|
1194
|
+
return rows2.map(rowToRecord2);
|
|
1099
1195
|
}
|
|
1100
1196
|
const rows = db.prepare(`SELECT * FROM relay_capture_diagnostics
|
|
1101
1197
|
WHERE collab_id = ? AND chain_id = ?
|
|
1102
1198
|
ORDER BY created_at DESC
|
|
1103
1199
|
LIMIT ?`).all(collabId, chainId, limit);
|
|
1104
|
-
return rows.map(
|
|
1200
|
+
return rows.map(rowToRecord2);
|
|
1105
1201
|
}
|
|
1106
1202
|
function listCaptureDiagnosticsByHandoff(db, handoffId) {
|
|
1107
1203
|
const rows = db.prepare(`SELECT * FROM relay_capture_diagnostics
|
|
1108
1204
|
WHERE handoff_id = ?
|
|
1109
1205
|
ORDER BY created_at ASC`).all(handoffId);
|
|
1110
|
-
return rows.map(
|
|
1206
|
+
return rows.map(rowToRecord2);
|
|
1111
1207
|
}
|
|
1112
1208
|
function deleteCaptureDiagnosticsOlderThan(db, cutoffIso) {
|
|
1113
1209
|
const result = db.prepare("DELETE FROM relay_capture_diagnostics WHERE created_at < ?").run(cutoffIso);
|
|
@@ -1115,7 +1211,7 @@ function deleteCaptureDiagnosticsOlderThan(db, cutoffIso) {
|
|
|
1115
1211
|
}
|
|
1116
1212
|
|
|
1117
1213
|
// ../broker/dist/storage/repositories/relay-evaluator-diagnostics-repository.js
|
|
1118
|
-
function
|
|
1214
|
+
function rowToRecord3(row) {
|
|
1119
1215
|
return {
|
|
1120
1216
|
evaluatorId: row.evaluator_id,
|
|
1121
1217
|
handoffId: row.handoff_id,
|
|
@@ -1160,32 +1256,32 @@ function listEvaluatorDiagnosticsByCollab(db, collabId, limit, opts) {
|
|
|
1160
1256
|
const rows2 = db.prepare(`SELECT * FROM relay_evaluator_diagnostics
|
|
1161
1257
|
WHERE collab_id = ?${filterClause}
|
|
1162
1258
|
ORDER BY created_at DESC`).all(collabId, ...filterArgs);
|
|
1163
|
-
return rows2.map(
|
|
1259
|
+
return rows2.map(rowToRecord3);
|
|
1164
1260
|
}
|
|
1165
1261
|
const rows = db.prepare(`SELECT * FROM relay_evaluator_diagnostics
|
|
1166
1262
|
WHERE collab_id = ?${filterClause}
|
|
1167
1263
|
ORDER BY created_at DESC
|
|
1168
1264
|
LIMIT ?`).all(collabId, ...filterArgs, limit);
|
|
1169
|
-
return rows.map(
|
|
1265
|
+
return rows.map(rowToRecord3);
|
|
1170
1266
|
}
|
|
1171
1267
|
function listEvaluatorDiagnosticsByCollabAndChain(db, collabId, chainId, limit) {
|
|
1172
1268
|
if (limit === null) {
|
|
1173
1269
|
const rows2 = db.prepare(`SELECT * FROM relay_evaluator_diagnostics
|
|
1174
1270
|
WHERE collab_id = ? AND chain_id = ?
|
|
1175
1271
|
ORDER BY created_at DESC`).all(collabId, chainId);
|
|
1176
|
-
return rows2.map(
|
|
1272
|
+
return rows2.map(rowToRecord3);
|
|
1177
1273
|
}
|
|
1178
1274
|
const rows = db.prepare(`SELECT * FROM relay_evaluator_diagnostics
|
|
1179
1275
|
WHERE collab_id = ? AND chain_id = ?
|
|
1180
1276
|
ORDER BY created_at DESC
|
|
1181
1277
|
LIMIT ?`).all(collabId, chainId, limit);
|
|
1182
|
-
return rows.map(
|
|
1278
|
+
return rows.map(rowToRecord3);
|
|
1183
1279
|
}
|
|
1184
1280
|
function listEvaluatorDiagnosticsByHandoff(db, handoffId) {
|
|
1185
1281
|
const rows = db.prepare(`SELECT * FROM relay_evaluator_diagnostics
|
|
1186
1282
|
WHERE handoff_id = ?
|
|
1187
1283
|
ORDER BY created_at ASC`).all(handoffId);
|
|
1188
|
-
return rows.map(
|
|
1284
|
+
return rows.map(rowToRecord3);
|
|
1189
1285
|
}
|
|
1190
1286
|
function deleteEvaluatorDiagnosticsOlderThan(db, cutoffIso) {
|
|
1191
1287
|
const result = db.prepare("DELETE FROM relay_evaluator_diagnostics WHERE created_at < ?").run(cutoffIso);
|
|
@@ -1193,7 +1289,7 @@ function deleteEvaluatorDiagnosticsOlderThan(db, cutoffIso) {
|
|
|
1193
1289
|
}
|
|
1194
1290
|
|
|
1195
1291
|
// ../broker/dist/storage/repositories/relay-handoff-repository.js
|
|
1196
|
-
function
|
|
1292
|
+
function rowToRecord4(row) {
|
|
1197
1293
|
return {
|
|
1198
1294
|
handoffId: row.handoff_id,
|
|
1199
1295
|
collabId: row.collab_id,
|
|
@@ -1230,7 +1326,7 @@ function queryRelayHandoff(db, handoffId) {
|
|
|
1230
1326
|
FROM relay_handoff h
|
|
1231
1327
|
JOIN collab c ON c.collab_id = h.collab_id
|
|
1232
1328
|
WHERE h.handoff_id = ?`).get(handoffId);
|
|
1233
|
-
return row ?
|
|
1329
|
+
return row ? rowToRecord4(row) : null;
|
|
1234
1330
|
}
|
|
1235
1331
|
function createRelayHandoffTxn(db, input) {
|
|
1236
1332
|
return db.transaction(() => {
|
|
@@ -1382,7 +1478,7 @@ function queryLatestHandedBackHandoff(db, collabId) {
|
|
|
1382
1478
|
WHERE h.collab_id = ? AND h.status = 'handed_back'
|
|
1383
1479
|
ORDER BY h.resolved_at DESC
|
|
1384
1480
|
LIMIT 1`).get(collabId);
|
|
1385
|
-
return row ?
|
|
1481
|
+
return row ? rowToRecord4(row) : null;
|
|
1386
1482
|
}
|
|
1387
1483
|
function failRelayHandoffOnDisconnectTxn(db, input) {
|
|
1388
1484
|
db.transaction(() => {
|
|
@@ -1406,6 +1502,30 @@ function failRelayHandoffOnDisconnectTxn(db, input) {
|
|
|
1406
1502
|
}
|
|
1407
1503
|
})();
|
|
1408
1504
|
}
|
|
1505
|
+
function hasInFlightAcceptedHandoffForWorkflow(db, workflowId) {
|
|
1506
|
+
const row = db.prepare("SELECT COUNT(*) AS n FROM relay_handoff WHERE workflow_id = ? AND status = 'accepted'").get(workflowId);
|
|
1507
|
+
return row.n > 0;
|
|
1508
|
+
}
|
|
1509
|
+
function resetStrandedOrchestrationForWorkflow(db, workflowId) {
|
|
1510
|
+
const result = db.prepare(`UPDATE relay_handoff
|
|
1511
|
+
SET orchestrator_status = 'idle', orchestrator_claimed_at = NULL
|
|
1512
|
+
WHERE workflow_id = ?
|
|
1513
|
+
AND status = 'handed_back'
|
|
1514
|
+
AND orchestrator_status = 'pending'
|
|
1515
|
+
AND evaluator_verdict IS NULL`).run(workflowId);
|
|
1516
|
+
return result.changes;
|
|
1517
|
+
}
|
|
1518
|
+
function prependToPendingHandoffRequestText(db, input) {
|
|
1519
|
+
const row = db.prepare(`SELECT handoff_id, request_text FROM relay_handoff
|
|
1520
|
+
WHERE workflow_id = ? AND status = 'pending'
|
|
1521
|
+
ORDER BY created_at DESC LIMIT 1`).get(input.workflowId);
|
|
1522
|
+
if (!row)
|
|
1523
|
+
return false;
|
|
1524
|
+
db.prepare("UPDATE relay_handoff SET request_text = ?, last_activity_at = ? WHERE handoff_id = ?").run(`${input.prefix}
|
|
1525
|
+
|
|
1526
|
+
${row.request_text}`, input.now, row.handoff_id);
|
|
1527
|
+
return true;
|
|
1528
|
+
}
|
|
1409
1529
|
function claimRelayHandoffForOrchestrationTxn(db, input) {
|
|
1410
1530
|
const result = db.prepare(`UPDATE relay_handoff
|
|
1411
1531
|
SET orchestrator_status = 'pending',
|
|
@@ -1424,10 +1544,12 @@ function listRelayHandoffsPendingOrchestration(db, collabId) {
|
|
|
1424
1544
|
c.orchestrator_max_rounds AS max_rounds
|
|
1425
1545
|
FROM relay_handoff h
|
|
1426
1546
|
JOIN collab c ON c.collab_id = h.collab_id
|
|
1547
|
+
LEFT JOIN workflows w ON w.workflow_id = h.workflow_id
|
|
1427
1548
|
WHERE h.collab_id = ?
|
|
1428
1549
|
AND h.status = 'handed_back'
|
|
1429
|
-
AND h.orchestrator_status = 'idle'
|
|
1430
|
-
|
|
1550
|
+
AND h.orchestrator_status = 'idle'
|
|
1551
|
+
AND (w.status IS NULL OR w.status <> 'paused')`).all(collabId);
|
|
1552
|
+
return rows.map(rowToRecord4);
|
|
1431
1553
|
}
|
|
1432
1554
|
function createLoopRelayHandoffTxn(db, input) {
|
|
1433
1555
|
return db.transaction(() => {
|
|
@@ -1623,7 +1745,7 @@ function getHandoffWithWorkflowMetaById(db, handoffId) {
|
|
|
1623
1745
|
WHERE h.handoff_id = ?`).get(handoffId);
|
|
1624
1746
|
if (!row)
|
|
1625
1747
|
return null;
|
|
1626
|
-
const base =
|
|
1748
|
+
const base = rowToRecord4(row);
|
|
1627
1749
|
return {
|
|
1628
1750
|
...base,
|
|
1629
1751
|
handoffStep: row.handoff_step,
|
|
@@ -1873,71 +1995,6 @@ function listRunCostRows(db, input) {
|
|
|
1873
1995
|
// ../broker/dist/control/workflow-control.js
|
|
1874
1996
|
import { randomUUID } from "node:crypto";
|
|
1875
1997
|
|
|
1876
|
-
// ../broker/dist/storage/repositories/workflow-repository.js
|
|
1877
|
-
function rowToRecord4(row) {
|
|
1878
|
-
return {
|
|
1879
|
-
workflowId: row.workflow_id,
|
|
1880
|
-
collabId: row.collab_id,
|
|
1881
|
-
workflowType: row.workflow_type,
|
|
1882
|
-
name: row.name,
|
|
1883
|
-
specPath: row.spec_path,
|
|
1884
|
-
roleBindings: JSON.parse(row.role_bindings),
|
|
1885
|
-
status: row.status,
|
|
1886
|
-
currentPhaseIndex: row.current_phase_index,
|
|
1887
|
-
haltReason: row.halt_reason,
|
|
1888
|
-
workflowContext: JSON.parse(row.workflow_context),
|
|
1889
|
-
createdAt: row.created_at,
|
|
1890
|
-
updatedAt: row.updated_at
|
|
1891
|
-
};
|
|
1892
|
-
}
|
|
1893
|
-
function insertWorkflow(db, input) {
|
|
1894
|
-
db.prepare(`INSERT INTO workflows
|
|
1895
|
-
(workflow_id, collab_id, workflow_type, name, spec_path, role_bindings, status,
|
|
1896
|
-
current_phase_index, halt_reason, workflow_context, created_at, updated_at)
|
|
1897
|
-
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);
|
|
1898
|
-
}
|
|
1899
|
-
function getWorkflowById(db, workflowId) {
|
|
1900
|
-
const row = db.prepare("SELECT * FROM workflows WHERE workflow_id = ?").get(workflowId);
|
|
1901
|
-
return row ? rowToRecord4(row) : null;
|
|
1902
|
-
}
|
|
1903
|
-
function listWorkflows(db, filter = {}) {
|
|
1904
|
-
const clauses = [];
|
|
1905
|
-
const args = [];
|
|
1906
|
-
if (filter.collabId) {
|
|
1907
|
-
clauses.push("collab_id = ?");
|
|
1908
|
-
args.push(filter.collabId);
|
|
1909
|
-
}
|
|
1910
|
-
if (filter.status) {
|
|
1911
|
-
clauses.push("status = ?");
|
|
1912
|
-
args.push(filter.status);
|
|
1913
|
-
}
|
|
1914
|
-
const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
1915
|
-
const rows = db.prepare(`SELECT * FROM workflows ${where} ORDER BY created_at DESC`).all(...args);
|
|
1916
|
-
return rows.map(rowToRecord4);
|
|
1917
|
-
}
|
|
1918
|
-
function setWorkflowStatus(db, input) {
|
|
1919
|
-
db.prepare(`UPDATE workflows
|
|
1920
|
-
SET status = ?, halt_reason = ?, updated_at = ?
|
|
1921
|
-
WHERE workflow_id = ?`).run(input.status, input.haltReason, input.now, input.workflowId);
|
|
1922
|
-
}
|
|
1923
|
-
function updateWorkflowContext(db, input) {
|
|
1924
|
-
const existing = getWorkflowById(db, input.workflowId);
|
|
1925
|
-
if (!existing) {
|
|
1926
|
-
throw new Error(`updateWorkflowContext: unknown workflowId ${input.workflowId}`);
|
|
1927
|
-
}
|
|
1928
|
-
const merged = { ...existing.workflowContext, ...input.patch };
|
|
1929
|
-
db.prepare("UPDATE workflows SET workflow_context = ?, updated_at = ? WHERE workflow_id = ?").run(JSON.stringify(merged), input.now, input.workflowId);
|
|
1930
|
-
}
|
|
1931
|
-
function incrementCurrentPhaseIndex(db, input) {
|
|
1932
|
-
db.prepare(`UPDATE workflows
|
|
1933
|
-
SET current_phase_index = current_phase_index + 1, updated_at = ?
|
|
1934
|
-
WHERE workflow_id = ?`).run(input.now, input.workflowId);
|
|
1935
|
-
}
|
|
1936
|
-
function countRunningWorkflowsForCollab(db, collabId) {
|
|
1937
|
-
const row = db.prepare("SELECT COUNT(*) AS n FROM workflows WHERE collab_id = ? AND status = 'running'").get(collabId);
|
|
1938
|
-
return row.n;
|
|
1939
|
-
}
|
|
1940
|
-
|
|
1941
1998
|
// ../broker/dist/storage/repositories/workflow-phase-repository.js
|
|
1942
1999
|
function rowToRecord5(row) {
|
|
1943
2000
|
return {
|
|
@@ -2058,6 +2115,14 @@ Findings: (omit this block entirely if none)
|
|
|
2058
2115
|
Non-blocking risks:
|
|
2059
2116
|
- <quality risk that does NOT block this gate, or "None.">
|
|
2060
2117
|
--- end protocol ---`;
|
|
2118
|
+
var WORKFLOW_OPERATOR_CONTROL = `--- ai-whisper operator control ---
|
|
2119
|
+
If the operator interrupts you and asks to pause the workflow (e.g. "pause the workflow, I need to fix X"):
|
|
2120
|
+
1. Find the active workflow id: run \`whisper workflow list\`.
|
|
2121
|
+
2. Run \`whisper workflow pause <id>\`.
|
|
2122
|
+
3. Acknowledge and STOP working \u2014 do not start the next change.
|
|
2123
|
+
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.
|
|
2124
|
+
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.
|
|
2125
|
+
--- end operator control ---`;
|
|
2061
2126
|
var WORKFLOW_DIAGNOSIS_PROTOCOL = `--- ai-whisper diagnosis review protocol ---
|
|
2062
2127
|
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.
|
|
2063
2128
|
|
|
@@ -2296,10 +2361,21 @@ var COMPLEX_BUG_FIXING = {
|
|
|
2296
2361
|
}
|
|
2297
2362
|
]
|
|
2298
2363
|
};
|
|
2364
|
+
function withOperatorControl(def) {
|
|
2365
|
+
return {
|
|
2366
|
+
...def,
|
|
2367
|
+
phases: def.phases.map((p) => ({
|
|
2368
|
+
...p,
|
|
2369
|
+
kickoffTemplate: `${p.kickoffTemplate}
|
|
2370
|
+
|
|
2371
|
+
${WORKFLOW_OPERATOR_CONTROL}`
|
|
2372
|
+
}))
|
|
2373
|
+
};
|
|
2374
|
+
}
|
|
2299
2375
|
var REGISTRY = {
|
|
2300
|
-
[SPEC_DRIVEN_DEVELOPMENT.type]: SPEC_DRIVEN_DEVELOPMENT,
|
|
2301
|
-
[RALPH_LOOP.type]: RALPH_LOOP,
|
|
2302
|
-
[COMPLEX_BUG_FIXING.type]: COMPLEX_BUG_FIXING
|
|
2376
|
+
[SPEC_DRIVEN_DEVELOPMENT.type]: withOperatorControl(SPEC_DRIVEN_DEVELOPMENT),
|
|
2377
|
+
[RALPH_LOOP.type]: withOperatorControl(RALPH_LOOP),
|
|
2378
|
+
[COMPLEX_BUG_FIXING.type]: withOperatorControl(COMPLEX_BUG_FIXING)
|
|
2303
2379
|
};
|
|
2304
2380
|
function ralphRunDir(workspaceRoot, workflowId) {
|
|
2305
2381
|
return join(workspaceRoot, ".ai-whisper", "ralph", workflowId);
|
|
@@ -2355,6 +2431,23 @@ function safeDerivePlanPath(specPath, createdAt) {
|
|
|
2355
2431
|
return `${dir}${stem}.plan.md`;
|
|
2356
2432
|
}
|
|
2357
2433
|
}
|
|
2434
|
+
function composeResumeNotice(input) {
|
|
2435
|
+
const hasFiles = input.changedFiles.length > 0;
|
|
2436
|
+
const hasMessage = !!input.message && input.message.trim().length > 0;
|
|
2437
|
+
if (!hasFiles && !hasMessage)
|
|
2438
|
+
return null;
|
|
2439
|
+
const lines = [];
|
|
2440
|
+
if (hasFiles) {
|
|
2441
|
+
lines.push("While paused, the operator modified these files:");
|
|
2442
|
+
for (const f of input.changedFiles)
|
|
2443
|
+
lines.push(` - ${f}`);
|
|
2444
|
+
lines.push("Re-read them before continuing.");
|
|
2445
|
+
}
|
|
2446
|
+
if (hasMessage)
|
|
2447
|
+
lines.push(`Operator note: ${input.message.trim()}`);
|
|
2448
|
+
lines.push("Re-evaluate whether your current direction still holds; correct course before proceeding.");
|
|
2449
|
+
return lines.join("\n");
|
|
2450
|
+
}
|
|
2358
2451
|
function createWorkflowControl(deps) {
|
|
2359
2452
|
const { db, events } = deps;
|
|
2360
2453
|
function createWorkflow(input) {
|
|
@@ -2377,8 +2470,9 @@ function createWorkflowControl(deps) {
|
|
|
2377
2470
|
}
|
|
2378
2471
|
const workflowId = `wf_${randomUUID().replace(/-/g, "").slice(0, 16)}`;
|
|
2379
2472
|
const tx = db.transaction(() => {
|
|
2380
|
-
if (
|
|
2381
|
-
|
|
2473
|
+
if (countActiveWorkflowsForCollab(db, input.collabId) > 0) {
|
|
2474
|
+
const active = listWorkflows(db, { collabId: input.collabId }).find((w) => w.status === "running" || w.status === "paused");
|
|
2475
|
+
throw new Error(`another workflow is already active on this collab (${input.collabId})` + (active ? `: ${active.workflowId} (${active.status})` : ""));
|
|
2382
2476
|
}
|
|
2383
2477
|
insertWorkflow(db, {
|
|
2384
2478
|
workflowId,
|
|
@@ -2546,6 +2640,24 @@ function createWorkflowControl(deps) {
|
|
|
2546
2640
|
postmortemPath: bf.postmortemPath
|
|
2547
2641
|
});
|
|
2548
2642
|
}
|
|
2643
|
+
function consumeResumeNotice(input) {
|
|
2644
|
+
const wf = getWorkflowById(db, input.workflowId);
|
|
2645
|
+
const notice = wf?.workflowContext?.resumeNotice ?? null;
|
|
2646
|
+
if (notice) {
|
|
2647
|
+
updateWorkflowContext(db, {
|
|
2648
|
+
workflowId: input.workflowId,
|
|
2649
|
+
patch: { resumeNotice: null },
|
|
2650
|
+
now: input.now
|
|
2651
|
+
});
|
|
2652
|
+
}
|
|
2653
|
+
return notice;
|
|
2654
|
+
}
|
|
2655
|
+
function withResumeNotice(workflowId, requestText, now) {
|
|
2656
|
+
const notice = consumeResumeNotice({ workflowId, now });
|
|
2657
|
+
return notice ? `${notice}
|
|
2658
|
+
|
|
2659
|
+
${requestText}` : requestText;
|
|
2660
|
+
}
|
|
2549
2661
|
function createContinuationHandoff(input) {
|
|
2550
2662
|
const handoffId = `ho_${randomUUID().replace(/-/g, "").slice(0, 16)}`;
|
|
2551
2663
|
insertWorkflowOwnedRelayHandoff(db, {
|
|
@@ -2553,7 +2665,9 @@ function createWorkflowControl(deps) {
|
|
|
2553
2665
|
collabId: input.workflow.collabId,
|
|
2554
2666
|
senderAgent: input.sender,
|
|
2555
2667
|
targetAgent: input.target,
|
|
2556
|
-
|
|
2668
|
+
// Prepend the one-time resume notice (if any) to the FIRST outgoing request
|
|
2669
|
+
// after a resume, then it is cleared — so the agent re-reads changed files.
|
|
2670
|
+
requestText: withResumeNotice(input.workflow.workflowId, input.requestText, input.now),
|
|
2557
2671
|
chainId: input.chain.chainId,
|
|
2558
2672
|
roundNumber: input.incrementRound ? input.chain.currentRound + 1 : input.chain.currentRound,
|
|
2559
2673
|
maxRounds: input.chain.maxRounds,
|
|
@@ -2613,7 +2727,9 @@ function createWorkflowControl(deps) {
|
|
|
2613
2727
|
collabId: input.workflow.collabId,
|
|
2614
2728
|
senderAgent: sender,
|
|
2615
2729
|
targetAgent: target,
|
|
2616
|
-
|
|
2730
|
+
// Prepend a pending resume notice (consumed once) so a phase-advance kickoff
|
|
2731
|
+
// after a resume also carries the operator's change notice.
|
|
2732
|
+
requestText: withResumeNotice(input.workflow.workflowId, kickoffText, input.now),
|
|
2617
2733
|
chainId,
|
|
2618
2734
|
roundNumber: 1,
|
|
2619
2735
|
maxRounds: phase.maxRounds,
|
|
@@ -3074,20 +3190,100 @@ ${findingsText}`;
|
|
|
3074
3190
|
reason: input.reason
|
|
3075
3191
|
});
|
|
3076
3192
|
}
|
|
3193
|
+
function maybeCaptureQuiesceSnapshot(input) {
|
|
3194
|
+
const wf = getWorkflowById(db, input.workflowId);
|
|
3195
|
+
if (!wf || wf.status !== "paused")
|
|
3196
|
+
return;
|
|
3197
|
+
if (wf.workflowContext.pauseSnapshotRef !== void 0) {
|
|
3198
|
+
return;
|
|
3199
|
+
}
|
|
3200
|
+
if (hasInFlightAcceptedHandoffForWorkflow(db, input.workflowId))
|
|
3201
|
+
return;
|
|
3202
|
+
const ref = deps.captureSnapshotRef ? deps.captureSnapshotRef(input.workflowId) : null;
|
|
3203
|
+
updateWorkflowContext(db, {
|
|
3204
|
+
workflowId: input.workflowId,
|
|
3205
|
+
patch: { pauseSnapshotRef: ref },
|
|
3206
|
+
now: input.now
|
|
3207
|
+
});
|
|
3208
|
+
}
|
|
3209
|
+
function pauseWorkflow(input) {
|
|
3210
|
+
const workflow = getWorkflowById(db, input.workflowId);
|
|
3211
|
+
if (!workflow) {
|
|
3212
|
+
throw new Error(`pauseWorkflow: unknown workflowId ${input.workflowId}`);
|
|
3213
|
+
}
|
|
3214
|
+
if (workflow.status !== "running") {
|
|
3215
|
+
throw new Error(`pauseWorkflow: workflow ${input.workflowId} is ${workflow.status}, only running workflows can be paused`);
|
|
3216
|
+
}
|
|
3217
|
+
const tx = db.transaction(() => {
|
|
3218
|
+
const current = getWorkflowById(db, input.workflowId);
|
|
3219
|
+
if (!current || current.status !== "running")
|
|
3220
|
+
return;
|
|
3221
|
+
setWorkflowStatus(db, {
|
|
3222
|
+
workflowId: input.workflowId,
|
|
3223
|
+
status: "paused",
|
|
3224
|
+
haltReason: null,
|
|
3225
|
+
now: input.now
|
|
3226
|
+
});
|
|
3227
|
+
updateWorkflowContext(db, {
|
|
3228
|
+
workflowId: input.workflowId,
|
|
3229
|
+
patch: { pausedAt: input.now },
|
|
3230
|
+
now: input.now
|
|
3231
|
+
});
|
|
3232
|
+
});
|
|
3233
|
+
tx.immediate();
|
|
3234
|
+
events.emit("workflow.paused", { workflowId: input.workflowId });
|
|
3235
|
+
maybeCaptureQuiesceSnapshot({ workflowId: input.workflowId, now: input.now });
|
|
3236
|
+
}
|
|
3237
|
+
function resumeHaltedWorkflow(workflow, now) {
|
|
3238
|
+
const tx = db.transaction(() => {
|
|
3239
|
+
const others = listWorkflows(db, { collabId: workflow.collabId }).filter((w) => w.workflowId !== workflow.workflowId && (w.status === "running" || w.status === "paused"));
|
|
3240
|
+
if (others.length > 0) {
|
|
3241
|
+
throw new Error(`resumeWorkflow: another workflow is already active on collab ${workflow.collabId}`);
|
|
3242
|
+
}
|
|
3243
|
+
setWorkflowStatus(db, {
|
|
3244
|
+
workflowId: workflow.workflowId,
|
|
3245
|
+
status: "running",
|
|
3246
|
+
haltReason: null,
|
|
3247
|
+
now
|
|
3248
|
+
});
|
|
3249
|
+
});
|
|
3250
|
+
tx.immediate();
|
|
3251
|
+
events.emit("workflow.resumed", {
|
|
3252
|
+
workflowId: workflow.workflowId,
|
|
3253
|
+
phaseIndex: workflow.currentPhaseIndex
|
|
3254
|
+
});
|
|
3255
|
+
}
|
|
3077
3256
|
function resumeWorkflow(input) {
|
|
3078
3257
|
const workflow = getWorkflowById(db, input.workflowId);
|
|
3079
3258
|
if (!workflow) {
|
|
3080
3259
|
throw new Error(`resumeWorkflow: unknown workflowId ${input.workflowId}`);
|
|
3081
3260
|
}
|
|
3082
|
-
if (workflow.status === "
|
|
3083
|
-
|
|
3261
|
+
if (workflow.status === "halted") {
|
|
3262
|
+
resumeHaltedWorkflow(workflow, input.now);
|
|
3263
|
+
return;
|
|
3084
3264
|
}
|
|
3085
|
-
if (workflow.status !== "
|
|
3086
|
-
throw new Error(`resumeWorkflow: workflow ${input.workflowId} is
|
|
3265
|
+
if (workflow.status !== "paused") {
|
|
3266
|
+
throw new Error(`resumeWorkflow: workflow ${input.workflowId} is ${workflow.status}, only paused or halted workflows can be resumed`);
|
|
3087
3267
|
}
|
|
3268
|
+
const ref = workflow.workflowContext.pauseSnapshotRef ?? null;
|
|
3269
|
+
const changedFiles = ref && deps.diffChangedFilesSinceSnapshot ? deps.diffChangedFilesSinceSnapshot(input.workflowId, ref) : [];
|
|
3270
|
+
const notice = composeResumeNotice({
|
|
3271
|
+
changedFiles,
|
|
3272
|
+
message: input.message ?? null
|
|
3273
|
+
});
|
|
3088
3274
|
const tx = db.transaction(() => {
|
|
3089
|
-
|
|
3090
|
-
|
|
3275
|
+
const others = listWorkflows(db, { collabId: workflow.collabId }).filter((w) => w.workflowId !== input.workflowId && (w.status === "running" || w.status === "paused"));
|
|
3276
|
+
if (others.length > 0) {
|
|
3277
|
+
throw new Error(`resumeWorkflow: another workflow is already active on collab ${workflow.collabId}`);
|
|
3278
|
+
}
|
|
3279
|
+
resetStrandedOrchestrationForWorkflow(db, input.workflowId);
|
|
3280
|
+
let bakedIntoPending = false;
|
|
3281
|
+
if (notice) {
|
|
3282
|
+
bakedIntoPending = prependToPendingHandoffRequestText(db, {
|
|
3283
|
+
workflowId: input.workflowId,
|
|
3284
|
+
prefix: notice,
|
|
3285
|
+
now: input.now
|
|
3286
|
+
});
|
|
3091
3287
|
}
|
|
3092
3288
|
setWorkflowStatus(db, {
|
|
3093
3289
|
workflowId: input.workflowId,
|
|
@@ -3095,6 +3291,15 @@ ${findingsText}`;
|
|
|
3095
3291
|
haltReason: null,
|
|
3096
3292
|
now: input.now
|
|
3097
3293
|
});
|
|
3294
|
+
updateWorkflowContext(db, {
|
|
3295
|
+
workflowId: input.workflowId,
|
|
3296
|
+
patch: {
|
|
3297
|
+
resumeNotice: bakedIntoPending ? null : notice,
|
|
3298
|
+
pausedAt: null,
|
|
3299
|
+
pauseSnapshotRef: null
|
|
3300
|
+
},
|
|
3301
|
+
now: input.now
|
|
3302
|
+
});
|
|
3098
3303
|
});
|
|
3099
3304
|
tx.immediate();
|
|
3100
3305
|
events.emit("workflow.resumed", {
|
|
@@ -3195,6 +3400,9 @@ ${findingsText}`;
|
|
|
3195
3400
|
beginPhaseRun,
|
|
3196
3401
|
applyOrchestratorVerdict,
|
|
3197
3402
|
haltWorkflow,
|
|
3403
|
+
pauseWorkflow,
|
|
3404
|
+
maybeCaptureQuiesceSnapshot,
|
|
3405
|
+
consumeResumeNotice,
|
|
3198
3406
|
resumeWorkflow,
|
|
3199
3407
|
cancelWorkflow,
|
|
3200
3408
|
getHandoffWithWorkflowMeta,
|
|
@@ -3210,8 +3418,32 @@ function buildEventId(kind, subjectId, timestamp) {
|
|
|
3210
3418
|
return `evt_${kind}_${subjectId}_${normalizeTimestampForEventId(timestamp)}`;
|
|
3211
3419
|
}
|
|
3212
3420
|
function createControlService(db, events) {
|
|
3213
|
-
const
|
|
3421
|
+
const resolveWorkspaceRoot = (workflowId) => {
|
|
3422
|
+
const wf = getWorkflowById(db, workflowId);
|
|
3423
|
+
if (!wf)
|
|
3424
|
+
return null;
|
|
3425
|
+
return getCollab(db, wf.collabId)?.workspaceRoot ?? null;
|
|
3426
|
+
};
|
|
3427
|
+
const workflowControl = createWorkflowControl({
|
|
3428
|
+
db,
|
|
3429
|
+
events,
|
|
3430
|
+
captureSnapshotRef: (workflowId) => {
|
|
3431
|
+
const root = resolveWorkspaceRoot(workflowId);
|
|
3432
|
+
return root ? captureWorkspaceSnapshotSync(root) : null;
|
|
3433
|
+
},
|
|
3434
|
+
diffChangedFilesSinceSnapshot: (workflowId, sinceRef) => {
|
|
3435
|
+
const root = resolveWorkspaceRoot(workflowId);
|
|
3436
|
+
return root ? diffChangedFilesSince(root, sinceRef) : [];
|
|
3437
|
+
}
|
|
3438
|
+
});
|
|
3439
|
+
const isWorkflowDeliverySuspended = (handoffId) => {
|
|
3440
|
+
const meta = workflowControl.getHandoffWithWorkflowMeta(handoffId);
|
|
3441
|
+
if (!meta?.workflowId)
|
|
3442
|
+
return false;
|
|
3443
|
+
return workflowControl.getWorkflow(meta.workflowId)?.status === "paused";
|
|
3444
|
+
};
|
|
3214
3445
|
return Object.assign({
|
|
3446
|
+
isWorkflowDeliverySuspended,
|
|
3215
3447
|
startCollab(input) {
|
|
3216
3448
|
const collab = collabSchema.parse({
|
|
3217
3449
|
version: 1,
|
|
@@ -3839,6 +4071,8 @@ function createControlService(db, events) {
|
|
|
3839
4071
|
return createRelayHandoffTxn(db, input);
|
|
3840
4072
|
},
|
|
3841
4073
|
acceptRelayHandoff(input) {
|
|
4074
|
+
if (isWorkflowDeliverySuspended(input.handoffId))
|
|
4075
|
+
return;
|
|
3842
4076
|
return acceptRelayHandoffTxn(db, input);
|
|
3843
4077
|
},
|
|
3844
4078
|
deferRelayHandoff(input) {
|
|
@@ -3851,7 +4085,12 @@ function createControlService(db, events) {
|
|
|
3851
4085
|
return markRelayHandoffStaleTxn(db, input);
|
|
3852
4086
|
},
|
|
3853
4087
|
handoffBackRelay(input) {
|
|
3854
|
-
|
|
4088
|
+
const result = handoffBackRelayTxn(db, input);
|
|
4089
|
+
const meta = workflowControl.getHandoffWithWorkflowMeta(input.handoffId);
|
|
4090
|
+
if (meta?.workflowId && workflowControl.getWorkflow(meta.workflowId)?.status === "paused") {
|
|
4091
|
+
workflowControl.maybeCaptureQuiesceSnapshot({ workflowId: meta.workflowId, now: input.now });
|
|
4092
|
+
}
|
|
4093
|
+
return result;
|
|
3855
4094
|
},
|
|
3856
4095
|
failRelayHandoffOnDisconnect(input) {
|
|
3857
4096
|
return failRelayHandoffOnDisconnectTxn(db, input);
|
|
@@ -3863,6 +4102,8 @@ function createControlService(db, events) {
|
|
|
3863
4102
|
return queryLatestHandedBackHandoff(db, collabId);
|
|
3864
4103
|
},
|
|
3865
4104
|
claimRelayHandoffForOrchestration(input) {
|
|
4105
|
+
if (isWorkflowDeliverySuspended(input.handoffId))
|
|
4106
|
+
return null;
|
|
3866
4107
|
return claimRelayHandoffForOrchestrationTxn(db, input);
|
|
3867
4108
|
},
|
|
3868
4109
|
listRelayHandoffsPendingOrchestration(collabId) {
|
|
@@ -3898,6 +4139,7 @@ function createControlService(db, events) {
|
|
|
3898
4139
|
clipSample: input.clipSample,
|
|
3899
4140
|
turnSample: input.turnSample,
|
|
3900
4141
|
abortedByRaceGuard: input.abortedByRaceGuard,
|
|
4142
|
+
interferenceDetected: input.interferenceDetected ?? false,
|
|
3901
4143
|
createdAt: input.now
|
|
3902
4144
|
});
|
|
3903
4145
|
return { captureId };
|
|
@@ -3974,8 +4216,130 @@ function createBrokerApp(input) {
|
|
|
3974
4216
|
return app;
|
|
3975
4217
|
}
|
|
3976
4218
|
|
|
4219
|
+
// ../broker/dist/storage/enforce-one-active-collab.js
|
|
4220
|
+
function defaultIsPidAlive(pid) {
|
|
4221
|
+
try {
|
|
4222
|
+
process.kill(pid, 0);
|
|
4223
|
+
return true;
|
|
4224
|
+
} catch (err) {
|
|
4225
|
+
return err.code === "EPERM";
|
|
4226
|
+
}
|
|
4227
|
+
}
|
|
4228
|
+
function dedupeActiveCollabs(db, opts) {
|
|
4229
|
+
const rows = db.prepare(`SELECT c.collab_id, c.workspace_id, c.created_at, d.pid AS pid,
|
|
4230
|
+
(SELECT COUNT(*) FROM workflows w
|
|
4231
|
+
WHERE w.collab_id = c.collab_id AND w.status = 'running') AS running_workflows
|
|
4232
|
+
FROM collab c
|
|
4233
|
+
LEFT JOIN broker_daemon d ON d.collab_id = c.collab_id
|
|
4234
|
+
WHERE c.status = 'active' AND c.workspace_id IS NOT NULL`).all();
|
|
4235
|
+
const byWorkspace = /* @__PURE__ */ new Map();
|
|
4236
|
+
for (const row of rows) {
|
|
4237
|
+
const key = row.workspace_id;
|
|
4238
|
+
const list = byWorkspace.get(key);
|
|
4239
|
+
if (list)
|
|
4240
|
+
list.push(row);
|
|
4241
|
+
else
|
|
4242
|
+
byWorkspace.set(key, [row]);
|
|
4243
|
+
}
|
|
4244
|
+
const conflicted = [];
|
|
4245
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4246
|
+
const stop = db.prepare("UPDATE collab SET status = 'stopped', stopped_at = ?, updated_at = ? WHERE collab_id = ?");
|
|
4247
|
+
for (const [workspaceId, group] of byWorkspace) {
|
|
4248
|
+
if (group.length < 2)
|
|
4249
|
+
continue;
|
|
4250
|
+
const workflowOwners = group.filter((r) => r.running_workflows > 0);
|
|
4251
|
+
if (workflowOwners.length >= 2) {
|
|
4252
|
+
conflicted.push(workspaceId);
|
|
4253
|
+
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.`);
|
|
4254
|
+
continue;
|
|
4255
|
+
}
|
|
4256
|
+
let survivor;
|
|
4257
|
+
if (workflowOwners.length === 1) {
|
|
4258
|
+
survivor = workflowOwners[0];
|
|
4259
|
+
} else {
|
|
4260
|
+
const live = group.filter((r) => r.pid !== null && opts.isPidAlive(r.pid));
|
|
4261
|
+
if (live.length >= 1) {
|
|
4262
|
+
survivor = live.reduce((a, b) => a.created_at >= b.created_at ? a : b);
|
|
4263
|
+
} else {
|
|
4264
|
+
survivor = group.reduce((a, b) => a.created_at >= b.created_at ? a : b);
|
|
4265
|
+
}
|
|
4266
|
+
}
|
|
4267
|
+
for (const row of group) {
|
|
4268
|
+
if (row.collab_id === survivor.collab_id)
|
|
4269
|
+
continue;
|
|
4270
|
+
stop.run(now, now, row.collab_id);
|
|
4271
|
+
}
|
|
4272
|
+
}
|
|
4273
|
+
return conflicted;
|
|
4274
|
+
}
|
|
4275
|
+
var CREATE_INDEX_SQL = "CREATE UNIQUE INDEX IF NOT EXISTS idx_collab_one_active_per_workspace ON collab(workspace_id) WHERE status = 'active'";
|
|
4276
|
+
function residualDuplicateCount(db) {
|
|
4277
|
+
const row = db.prepare(`SELECT COUNT(*) AS n FROM (
|
|
4278
|
+
SELECT workspace_id FROM collab
|
|
4279
|
+
WHERE status = 'active' AND workspace_id IS NOT NULL
|
|
4280
|
+
GROUP BY workspace_id HAVING COUNT(*) > 1
|
|
4281
|
+
)`).get();
|
|
4282
|
+
return row.n;
|
|
4283
|
+
}
|
|
4284
|
+
function enforceOneActiveCollabPerWorkspace(db, options = {}) {
|
|
4285
|
+
const opts = {
|
|
4286
|
+
isPidAlive: options.isPidAlive ?? defaultIsPidAlive,
|
|
4287
|
+
warn: options.warn ?? ((m) => console.error(m))
|
|
4288
|
+
};
|
|
4289
|
+
const tx = db.transaction(() => {
|
|
4290
|
+
dedupeActiveCollabs(db, opts);
|
|
4291
|
+
if (residualDuplicateCount(db) === 0) {
|
|
4292
|
+
db.exec(CREATE_INDEX_SQL);
|
|
4293
|
+
} else {
|
|
4294
|
+
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.");
|
|
4295
|
+
}
|
|
4296
|
+
});
|
|
4297
|
+
tx();
|
|
4298
|
+
}
|
|
4299
|
+
|
|
4300
|
+
// ../broker/dist/storage/clipboard-capture-lease.js
|
|
4301
|
+
var LEASE_ID = 1;
|
|
4302
|
+
var DEFAULT_LEASE_TTL_MS = 5e3;
|
|
4303
|
+
function defaultIsPidAlive2(pid) {
|
|
4304
|
+
try {
|
|
4305
|
+
process.kill(pid, 0);
|
|
4306
|
+
return true;
|
|
4307
|
+
} catch (err) {
|
|
4308
|
+
return err.code === "EPERM";
|
|
4309
|
+
}
|
|
4310
|
+
}
|
|
4311
|
+
function resolveOptions(options) {
|
|
4312
|
+
return {
|
|
4313
|
+
isPidAlive: options.isPidAlive ?? defaultIsPidAlive2,
|
|
4314
|
+
ttlMs: options.ttlMs ?? DEFAULT_LEASE_TTL_MS,
|
|
4315
|
+
now: options.now ?? Date.now
|
|
4316
|
+
};
|
|
4317
|
+
}
|
|
4318
|
+
function isStale(row, opts) {
|
|
4319
|
+
if (row.holder_collab_id === null)
|
|
4320
|
+
return true;
|
|
4321
|
+
if (row.holder_pid === null || !opts.isPidAlive(row.holder_pid))
|
|
4322
|
+
return true;
|
|
4323
|
+
if (row.acquired_at === null)
|
|
4324
|
+
return true;
|
|
4325
|
+
const age = opts.now() - Date.parse(row.acquired_at);
|
|
4326
|
+
return age > opts.ttlMs;
|
|
4327
|
+
}
|
|
4328
|
+
function sweepStaleCaptureLease(db, options = {}) {
|
|
4329
|
+
const opts = resolveOptions(options);
|
|
4330
|
+
const tx = db.transaction(() => {
|
|
4331
|
+
const row = db.prepare("SELECT holder_collab_id, holder_pid, acquired_at FROM clipboard_capture_lease WHERE id = ?").get(LEASE_ID);
|
|
4332
|
+
if (!row || row.holder_collab_id === null)
|
|
4333
|
+
return;
|
|
4334
|
+
if (isStale(row, opts)) {
|
|
4335
|
+
db.prepare("UPDATE clipboard_capture_lease SET holder_collab_id = NULL, holder_pid = NULL, acquired_at = NULL WHERE id = ?").run(LEASE_ID);
|
|
4336
|
+
}
|
|
4337
|
+
});
|
|
4338
|
+
tx();
|
|
4339
|
+
}
|
|
4340
|
+
|
|
3977
4341
|
// ../broker/dist/storage/apply-migrations.js
|
|
3978
|
-
var CURRENT_SCHEMA_VERSION =
|
|
4342
|
+
var CURRENT_SCHEMA_VERSION = 5;
|
|
3979
4343
|
var initMigrationSql = `
|
|
3980
4344
|
CREATE TABLE IF NOT EXISTS broker_state (
|
|
3981
4345
|
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
@@ -4193,8 +4557,9 @@ CREATE TABLE IF NOT EXISTS workflows (
|
|
|
4193
4557
|
updated_at TEXT NOT NULL
|
|
4194
4558
|
);
|
|
4195
4559
|
|
|
4560
|
+
DROP INDEX IF EXISTS workflows_one_running_per_collab;
|
|
4196
4561
|
CREATE UNIQUE INDEX IF NOT EXISTS workflows_one_running_per_collab
|
|
4197
|
-
ON workflows(collab_id) WHERE status
|
|
4562
|
+
ON workflows(collab_id) WHERE status IN ('running', 'paused');
|
|
4198
4563
|
|
|
4199
4564
|
CREATE TABLE IF NOT EXISTS workflow_phases (
|
|
4200
4565
|
phase_run_id TEXT PRIMARY KEY,
|
|
@@ -4262,6 +4627,13 @@ CREATE TABLE IF NOT EXISTS recovery_state (
|
|
|
4262
4627
|
recovered_at TEXT
|
|
4263
4628
|
);
|
|
4264
4629
|
|
|
4630
|
+
CREATE TABLE IF NOT EXISTS clipboard_capture_lease (
|
|
4631
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
4632
|
+
holder_collab_id TEXT,
|
|
4633
|
+
holder_pid INTEGER,
|
|
4634
|
+
acquired_at TEXT
|
|
4635
|
+
);
|
|
4636
|
+
|
|
4265
4637
|
`;
|
|
4266
4638
|
function ensureBrokerStateRow(db) {
|
|
4267
4639
|
db.prepare(`INSERT INTO broker_state (id, schema_version, migrated)
|
|
@@ -4272,20 +4644,20 @@ function ensureBrokerStateRow(db) {
|
|
|
4272
4644
|
}
|
|
4273
4645
|
function applyMigrations(db) {
|
|
4274
4646
|
const current = db.pragma("user_version", { simple: true });
|
|
4275
|
-
if (current
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
} catch (err) {
|
|
4286
|
-
db.exec("ROLLBACK");
|
|
4287
|
-
throw err;
|
|
4647
|
+
if (current < CURRENT_SCHEMA_VERSION) {
|
|
4648
|
+
db.exec("BEGIN EXCLUSIVE");
|
|
4649
|
+
try {
|
|
4650
|
+
runMigrationBody(db);
|
|
4651
|
+
db.pragma(`user_version = ${CURRENT_SCHEMA_VERSION}`);
|
|
4652
|
+
db.exec("COMMIT");
|
|
4653
|
+
} catch (err) {
|
|
4654
|
+
db.exec("ROLLBACK");
|
|
4655
|
+
throw err;
|
|
4656
|
+
}
|
|
4288
4657
|
}
|
|
4658
|
+
ensureBrokerStateRow(db);
|
|
4659
|
+
enforceOneActiveCollabPerWorkspace(db);
|
|
4660
|
+
sweepStaleCaptureLease(db);
|
|
4289
4661
|
}
|
|
4290
4662
|
function runMigrationBody(db) {
|
|
4291
4663
|
db.exec(initMigrationSql);
|
|
@@ -4422,6 +4794,7 @@ function runMigrationBody(db) {
|
|
|
4422
4794
|
clip_sample TEXT,
|
|
4423
4795
|
turn_sample TEXT,
|
|
4424
4796
|
aborted_by_race_guard INTEGER NOT NULL DEFAULT 0,
|
|
4797
|
+
interference_detected INTEGER NOT NULL DEFAULT 0,
|
|
4425
4798
|
created_at TEXT NOT NULL
|
|
4426
4799
|
);
|
|
4427
4800
|
CREATE INDEX IF NOT EXISTS idx_relay_capture_diagnostics_collab_created
|
|
@@ -4433,6 +4806,10 @@ function runMigrationBody(db) {
|
|
|
4433
4806
|
CREATE INDEX IF NOT EXISTS idx_relay_capture_diagnostics_status
|
|
4434
4807
|
ON relay_capture_diagnostics (capture_status);
|
|
4435
4808
|
`);
|
|
4809
|
+
const captureDiagColumns = db.prepare("PRAGMA table_info(relay_capture_diagnostics)").all();
|
|
4810
|
+
if (!captureDiagColumns.some((column) => column.name === "interference_detected")) {
|
|
4811
|
+
db.exec("ALTER TABLE relay_capture_diagnostics ADD COLUMN interference_detected INTEGER NOT NULL DEFAULT 0");
|
|
4812
|
+
}
|
|
4436
4813
|
db.exec(`
|
|
4437
4814
|
CREATE TABLE IF NOT EXISTS relay_evaluator_diagnostics (
|
|
4438
4815
|
evaluator_id TEXT PRIMARY KEY,
|