ai-whisper 0.2.0 → 0.2.1
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 +153 -16
- package/dist/bin/companion-agent.js +153 -16
- package/dist/bin/relay-monitor.js +153 -16
- package/dist/bin/whisper.js +629 -302
- package/package.json +3 -3
package/dist/bin/whisper.js
CHANGED
|
@@ -1105,6 +1105,7 @@ function rowToRecord(row) {
|
|
|
1105
1105
|
clipSample: row.clip_sample,
|
|
1106
1106
|
turnSample: row.turn_sample,
|
|
1107
1107
|
abortedByRaceGuard: row.aborted_by_race_guard === 1,
|
|
1108
|
+
interferenceDetected: row.interference_detected === 1,
|
|
1108
1109
|
createdAt: row.created_at
|
|
1109
1110
|
};
|
|
1110
1111
|
}
|
|
@@ -1112,8 +1113,9 @@ function insertCaptureDiagnostic(db, input) {
|
|
|
1112
1113
|
db.prepare(`INSERT INTO relay_capture_diagnostics
|
|
1113
1114
|
(capture_id, handoff_id, collab_id, chain_id, workflow_id, target_provider,
|
|
1114
1115
|
capture_status, clip_len, turn_len, turn_confidence, jaccard_score,
|
|
1115
|
-
containment_score, clip_sample, turn_sample, aborted_by_race_guard,
|
|
1116
|
-
|
|
1116
|
+
containment_score, clip_sample, turn_sample, aborted_by_race_guard,
|
|
1117
|
+
interference_detected, created_at)
|
|
1118
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(input.captureId, input.handoffId, input.collabId, input.chainId, input.workflowId, input.targetProvider, input.captureStatus, input.clipLen, input.turnLen, input.turnConfidence, input.jaccardScore, input.containmentScore, input.clipSample, input.turnSample, input.abortedByRaceGuard ? 1 : 0, input.interferenceDetected ? 1 : 0, input.createdAt);
|
|
1117
1119
|
}
|
|
1118
1120
|
function listCaptureDiagnosticsByCollab(db, collabId, limit, opts) {
|
|
1119
1121
|
const filter = opts?.workflowFilter;
|
|
@@ -3939,6 +3941,7 @@ function createControlService(db, events) {
|
|
|
3939
3941
|
clipSample: input.clipSample,
|
|
3940
3942
|
turnSample: input.turnSample,
|
|
3941
3943
|
abortedByRaceGuard: input.abortedByRaceGuard,
|
|
3944
|
+
interferenceDetected: input.interferenceDetected ?? false,
|
|
3942
3945
|
createdAt: input.now
|
|
3943
3946
|
});
|
|
3944
3947
|
return { captureId };
|
|
@@ -4015,8 +4018,153 @@ function createBrokerApp(input) {
|
|
|
4015
4018
|
return app;
|
|
4016
4019
|
}
|
|
4017
4020
|
|
|
4021
|
+
// ../broker/dist/storage/enforce-one-active-collab.js
|
|
4022
|
+
function defaultIsPidAlive(pid) {
|
|
4023
|
+
try {
|
|
4024
|
+
process.kill(pid, 0);
|
|
4025
|
+
return true;
|
|
4026
|
+
} catch (err) {
|
|
4027
|
+
return err.code === "EPERM";
|
|
4028
|
+
}
|
|
4029
|
+
}
|
|
4030
|
+
function dedupeActiveCollabs(db, opts) {
|
|
4031
|
+
const rows = db.prepare(`SELECT c.collab_id, c.workspace_id, c.created_at, d.pid AS pid,
|
|
4032
|
+
(SELECT COUNT(*) FROM workflows w
|
|
4033
|
+
WHERE w.collab_id = c.collab_id AND w.status = 'running') AS running_workflows
|
|
4034
|
+
FROM collab c
|
|
4035
|
+
LEFT JOIN broker_daemon d ON d.collab_id = c.collab_id
|
|
4036
|
+
WHERE c.status = 'active' AND c.workspace_id IS NOT NULL`).all();
|
|
4037
|
+
const byWorkspace = /* @__PURE__ */ new Map();
|
|
4038
|
+
for (const row of rows) {
|
|
4039
|
+
const key = row.workspace_id;
|
|
4040
|
+
const list = byWorkspace.get(key);
|
|
4041
|
+
if (list)
|
|
4042
|
+
list.push(row);
|
|
4043
|
+
else
|
|
4044
|
+
byWorkspace.set(key, [row]);
|
|
4045
|
+
}
|
|
4046
|
+
const conflicted = [];
|
|
4047
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4048
|
+
const stop = db.prepare("UPDATE collab SET status = 'stopped', stopped_at = ?, updated_at = ? WHERE collab_id = ?");
|
|
4049
|
+
for (const [workspaceId, group] of byWorkspace) {
|
|
4050
|
+
if (group.length < 2)
|
|
4051
|
+
continue;
|
|
4052
|
+
const workflowOwners = group.filter((r) => r.running_workflows > 0);
|
|
4053
|
+
if (workflowOwners.length >= 2) {
|
|
4054
|
+
conflicted.push(workspaceId);
|
|
4055
|
+
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.`);
|
|
4056
|
+
continue;
|
|
4057
|
+
}
|
|
4058
|
+
let survivor;
|
|
4059
|
+
if (workflowOwners.length === 1) {
|
|
4060
|
+
survivor = workflowOwners[0];
|
|
4061
|
+
} else {
|
|
4062
|
+
const live = group.filter((r) => r.pid !== null && opts.isPidAlive(r.pid));
|
|
4063
|
+
if (live.length >= 1) {
|
|
4064
|
+
survivor = live.reduce((a, b) => a.created_at >= b.created_at ? a : b);
|
|
4065
|
+
} else {
|
|
4066
|
+
survivor = group.reduce((a, b) => a.created_at >= b.created_at ? a : b);
|
|
4067
|
+
}
|
|
4068
|
+
}
|
|
4069
|
+
for (const row of group) {
|
|
4070
|
+
if (row.collab_id === survivor.collab_id)
|
|
4071
|
+
continue;
|
|
4072
|
+
stop.run(now, now, row.collab_id);
|
|
4073
|
+
}
|
|
4074
|
+
}
|
|
4075
|
+
return conflicted;
|
|
4076
|
+
}
|
|
4077
|
+
var CREATE_INDEX_SQL = "CREATE UNIQUE INDEX IF NOT EXISTS idx_collab_one_active_per_workspace ON collab(workspace_id) WHERE status = 'active'";
|
|
4078
|
+
function residualDuplicateCount(db) {
|
|
4079
|
+
const row = db.prepare(`SELECT COUNT(*) AS n FROM (
|
|
4080
|
+
SELECT workspace_id FROM collab
|
|
4081
|
+
WHERE status = 'active' AND workspace_id IS NOT NULL
|
|
4082
|
+
GROUP BY workspace_id HAVING COUNT(*) > 1
|
|
4083
|
+
)`).get();
|
|
4084
|
+
return row.n;
|
|
4085
|
+
}
|
|
4086
|
+
function enforceOneActiveCollabPerWorkspace(db, options = {}) {
|
|
4087
|
+
const opts = {
|
|
4088
|
+
isPidAlive: options.isPidAlive ?? defaultIsPidAlive,
|
|
4089
|
+
warn: options.warn ?? ((m) => console.error(m))
|
|
4090
|
+
};
|
|
4091
|
+
const tx = db.transaction(() => {
|
|
4092
|
+
dedupeActiveCollabs(db, opts);
|
|
4093
|
+
if (residualDuplicateCount(db) === 0) {
|
|
4094
|
+
db.exec(CREATE_INDEX_SQL);
|
|
4095
|
+
} else {
|
|
4096
|
+
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.");
|
|
4097
|
+
}
|
|
4098
|
+
});
|
|
4099
|
+
tx();
|
|
4100
|
+
}
|
|
4101
|
+
|
|
4102
|
+
// ../broker/dist/storage/clipboard-capture-lease.js
|
|
4103
|
+
var LEASE_ID = 1;
|
|
4104
|
+
var DEFAULT_LEASE_TTL_MS = 5e3;
|
|
4105
|
+
function defaultIsPidAlive2(pid) {
|
|
4106
|
+
try {
|
|
4107
|
+
process.kill(pid, 0);
|
|
4108
|
+
return true;
|
|
4109
|
+
} catch (err) {
|
|
4110
|
+
return err.code === "EPERM";
|
|
4111
|
+
}
|
|
4112
|
+
}
|
|
4113
|
+
function resolveOptions(options) {
|
|
4114
|
+
return {
|
|
4115
|
+
isPidAlive: options.isPidAlive ?? defaultIsPidAlive2,
|
|
4116
|
+
ttlMs: options.ttlMs ?? DEFAULT_LEASE_TTL_MS,
|
|
4117
|
+
now: options.now ?? Date.now
|
|
4118
|
+
};
|
|
4119
|
+
}
|
|
4120
|
+
function isStale(row, opts) {
|
|
4121
|
+
if (row.holder_collab_id === null)
|
|
4122
|
+
return true;
|
|
4123
|
+
if (row.holder_pid === null || !opts.isPidAlive(row.holder_pid))
|
|
4124
|
+
return true;
|
|
4125
|
+
if (row.acquired_at === null)
|
|
4126
|
+
return true;
|
|
4127
|
+
const age = opts.now() - Date.parse(row.acquired_at);
|
|
4128
|
+
return age > opts.ttlMs;
|
|
4129
|
+
}
|
|
4130
|
+
function acquireCaptureLease(db, collabId, pid, options = {}) {
|
|
4131
|
+
const opts = resolveOptions(options);
|
|
4132
|
+
const tx = db.transaction(() => {
|
|
4133
|
+
const row = db.prepare("SELECT holder_collab_id, holder_pid, acquired_at FROM clipboard_capture_lease WHERE id = ?").get(LEASE_ID);
|
|
4134
|
+
if (row && !isStale(row, opts))
|
|
4135
|
+
return false;
|
|
4136
|
+
const acquiredAt = new Date(opts.now()).toISOString();
|
|
4137
|
+
db.prepare(`INSERT INTO clipboard_capture_lease (id, holder_collab_id, holder_pid, acquired_at)
|
|
4138
|
+
VALUES (?, ?, ?, ?)
|
|
4139
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
4140
|
+
holder_collab_id = excluded.holder_collab_id,
|
|
4141
|
+
holder_pid = excluded.holder_pid,
|
|
4142
|
+
acquired_at = excluded.acquired_at`).run(LEASE_ID, collabId, pid, acquiredAt);
|
|
4143
|
+
return true;
|
|
4144
|
+
});
|
|
4145
|
+
return tx();
|
|
4146
|
+
}
|
|
4147
|
+
function releaseCaptureLease(db, collabId) {
|
|
4148
|
+
const tx = db.transaction(() => {
|
|
4149
|
+
db.prepare("UPDATE clipboard_capture_lease SET holder_collab_id = NULL, holder_pid = NULL, acquired_at = NULL WHERE id = ? AND holder_collab_id = ?").run(LEASE_ID, collabId);
|
|
4150
|
+
});
|
|
4151
|
+
tx();
|
|
4152
|
+
}
|
|
4153
|
+
function sweepStaleCaptureLease(db, options = {}) {
|
|
4154
|
+
const opts = resolveOptions(options);
|
|
4155
|
+
const tx = db.transaction(() => {
|
|
4156
|
+
const row = db.prepare("SELECT holder_collab_id, holder_pid, acquired_at FROM clipboard_capture_lease WHERE id = ?").get(LEASE_ID);
|
|
4157
|
+
if (!row || row.holder_collab_id === null)
|
|
4158
|
+
return;
|
|
4159
|
+
if (isStale(row, opts)) {
|
|
4160
|
+
db.prepare("UPDATE clipboard_capture_lease SET holder_collab_id = NULL, holder_pid = NULL, acquired_at = NULL WHERE id = ?").run(LEASE_ID);
|
|
4161
|
+
}
|
|
4162
|
+
});
|
|
4163
|
+
tx();
|
|
4164
|
+
}
|
|
4165
|
+
|
|
4018
4166
|
// ../broker/dist/storage/apply-migrations.js
|
|
4019
|
-
var CURRENT_SCHEMA_VERSION =
|
|
4167
|
+
var CURRENT_SCHEMA_VERSION = 5;
|
|
4020
4168
|
var initMigrationSql = `
|
|
4021
4169
|
CREATE TABLE IF NOT EXISTS broker_state (
|
|
4022
4170
|
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
@@ -4303,6 +4451,13 @@ CREATE TABLE IF NOT EXISTS recovery_state (
|
|
|
4303
4451
|
recovered_at TEXT
|
|
4304
4452
|
);
|
|
4305
4453
|
|
|
4454
|
+
CREATE TABLE IF NOT EXISTS clipboard_capture_lease (
|
|
4455
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
4456
|
+
holder_collab_id TEXT,
|
|
4457
|
+
holder_pid INTEGER,
|
|
4458
|
+
acquired_at TEXT
|
|
4459
|
+
);
|
|
4460
|
+
|
|
4306
4461
|
`;
|
|
4307
4462
|
function ensureBrokerStateRow(db) {
|
|
4308
4463
|
db.prepare(`INSERT INTO broker_state (id, schema_version, migrated)
|
|
@@ -4313,20 +4468,20 @@ function ensureBrokerStateRow(db) {
|
|
|
4313
4468
|
}
|
|
4314
4469
|
function applyMigrations(db) {
|
|
4315
4470
|
const current = db.pragma("user_version", { simple: true });
|
|
4316
|
-
if (current
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
} catch (err) {
|
|
4327
|
-
db.exec("ROLLBACK");
|
|
4328
|
-
throw err;
|
|
4471
|
+
if (current < CURRENT_SCHEMA_VERSION) {
|
|
4472
|
+
db.exec("BEGIN EXCLUSIVE");
|
|
4473
|
+
try {
|
|
4474
|
+
runMigrationBody(db);
|
|
4475
|
+
db.pragma(`user_version = ${CURRENT_SCHEMA_VERSION}`);
|
|
4476
|
+
db.exec("COMMIT");
|
|
4477
|
+
} catch (err) {
|
|
4478
|
+
db.exec("ROLLBACK");
|
|
4479
|
+
throw err;
|
|
4480
|
+
}
|
|
4329
4481
|
}
|
|
4482
|
+
ensureBrokerStateRow(db);
|
|
4483
|
+
enforceOneActiveCollabPerWorkspace(db);
|
|
4484
|
+
sweepStaleCaptureLease(db);
|
|
4330
4485
|
}
|
|
4331
4486
|
function runMigrationBody(db) {
|
|
4332
4487
|
db.exec(initMigrationSql);
|
|
@@ -4463,6 +4618,7 @@ function runMigrationBody(db) {
|
|
|
4463
4618
|
clip_sample TEXT,
|
|
4464
4619
|
turn_sample TEXT,
|
|
4465
4620
|
aborted_by_race_guard INTEGER NOT NULL DEFAULT 0,
|
|
4621
|
+
interference_detected INTEGER NOT NULL DEFAULT 0,
|
|
4466
4622
|
created_at TEXT NOT NULL
|
|
4467
4623
|
);
|
|
4468
4624
|
CREATE INDEX IF NOT EXISTS idx_relay_capture_diagnostics_collab_created
|
|
@@ -4474,6 +4630,10 @@ function runMigrationBody(db) {
|
|
|
4474
4630
|
CREATE INDEX IF NOT EXISTS idx_relay_capture_diagnostics_status
|
|
4475
4631
|
ON relay_capture_diagnostics (capture_status);
|
|
4476
4632
|
`);
|
|
4633
|
+
const captureDiagColumns = db.prepare("PRAGMA table_info(relay_capture_diagnostics)").all();
|
|
4634
|
+
if (!captureDiagColumns.some((column) => column.name === "interference_detected")) {
|
|
4635
|
+
db.exec("ALTER TABLE relay_capture_diagnostics ADD COLUMN interference_detected INTEGER NOT NULL DEFAULT 0");
|
|
4636
|
+
}
|
|
4477
4637
|
db.exec(`
|
|
4478
4638
|
CREATE TABLE IF NOT EXISTS relay_evaluator_diagnostics (
|
|
4479
4639
|
evaluator_id TEXT PRIMARY KEY,
|
|
@@ -7279,7 +7439,8 @@ ${hint}`,
|
|
|
7279
7439
|
let composed = null;
|
|
7280
7440
|
let initialValue = "";
|
|
7281
7441
|
if (input.captureHandbackText) {
|
|
7282
|
-
const
|
|
7442
|
+
const outcome = await input.captureHandbackText("") ?? "";
|
|
7443
|
+
const captured = typeof outcome === "string" ? outcome : outcome.text ?? "";
|
|
7283
7444
|
if (captured.trim().length > 0 && input.confirmHandbackCapture) {
|
|
7284
7445
|
const accepted = await input.confirmHandbackCapture({
|
|
7285
7446
|
target,
|
|
@@ -7341,14 +7502,29 @@ ${hint}`,
|
|
|
7341
7502
|
input.turnCapture?.finishAssistantTurn();
|
|
7342
7503
|
const turnResult = input.turnCapture?.extractLatestAssistantTurn() ?? { confidence: "low", text: null };
|
|
7343
7504
|
let clipboardText = null;
|
|
7505
|
+
let leaseDegraded = false;
|
|
7506
|
+
let interferenceDetected = false;
|
|
7344
7507
|
try {
|
|
7345
|
-
|
|
7508
|
+
const captureResult = await input.captureHandbackText?.(turnResult.text ?? "") ?? null;
|
|
7509
|
+
if (typeof captureResult === "string") {
|
|
7510
|
+
clipboardText = captureResult;
|
|
7511
|
+
} else if (captureResult !== null) {
|
|
7512
|
+
interferenceDetected = captureResult.interferenceDetected;
|
|
7513
|
+
if (captureResult.status === "captured") {
|
|
7514
|
+
clipboardText = captureResult.text;
|
|
7515
|
+
} else {
|
|
7516
|
+
leaseDegraded = true;
|
|
7517
|
+
}
|
|
7518
|
+
}
|
|
7346
7519
|
} catch {
|
|
7347
7520
|
clipboardText = null;
|
|
7348
7521
|
}
|
|
7349
7522
|
const classification = classifyCapture(turnResult, clipboardText);
|
|
7350
7523
|
const captureStatus = classification.status;
|
|
7351
|
-
|
|
7524
|
+
let requestText = captureStatus === "ok" ? clipboardText ?? "" : "";
|
|
7525
|
+
if (leaseDegraded && (turnResult.text ?? "").trim().length > 0) {
|
|
7526
|
+
requestText = turnResult.text;
|
|
7527
|
+
}
|
|
7352
7528
|
const currentAcceptedId = getAcceptedHandoff()?.handoffId;
|
|
7353
7529
|
const abortedByRaceGuard = currentAcceptedId !== accepted.handoffId;
|
|
7354
7530
|
const handoffMeta = input.broker.control.getHandoffWithWorkflowMeta?.(accepted.handoffId) ?? null;
|
|
@@ -7375,6 +7551,7 @@ ${hint}`,
|
|
|
7375
7551
|
clipSample: sampleOf(clipboardText),
|
|
7376
7552
|
turnSample: sampleOf(turnResult.text),
|
|
7377
7553
|
abortedByRaceGuard,
|
|
7554
|
+
interferenceDetected,
|
|
7378
7555
|
now
|
|
7379
7556
|
});
|
|
7380
7557
|
} catch (err) {
|
|
@@ -7679,6 +7856,107 @@ async function captureClipboardHandback(input) {
|
|
|
7679
7856
|
return null;
|
|
7680
7857
|
}
|
|
7681
7858
|
|
|
7859
|
+
// src/runtime/capture-handback-text.ts
|
|
7860
|
+
var defaultSleep = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
7861
|
+
function contentMatches(turnText, clip) {
|
|
7862
|
+
const t = turnText.trim();
|
|
7863
|
+
const c = clip.trim();
|
|
7864
|
+
if (c.length === 0) return false;
|
|
7865
|
+
if (t === c) return true;
|
|
7866
|
+
const jaccard = computeOrderedJaccard(t, c);
|
|
7867
|
+
const containment = computeContainment(c, t);
|
|
7868
|
+
return jaccard >= 0.6 || containment >= 0.8;
|
|
7869
|
+
}
|
|
7870
|
+
async function captureHandbackText(input) {
|
|
7871
|
+
const sleep3 = input.sleep ?? defaultSleep;
|
|
7872
|
+
const acquireMaxWaitMs = input.acquireMaxWaitMs ?? 4e3;
|
|
7873
|
+
const acquireBackoffMs = input.acquireBackoffMs ?? 50;
|
|
7874
|
+
const recaptureAttempts = input.recaptureAttempts ?? 2;
|
|
7875
|
+
const recaptureBackoffMs = input.recaptureBackoffMs ?? 50;
|
|
7876
|
+
let acquired = false;
|
|
7877
|
+
const deadline = Date.now() + acquireMaxWaitMs;
|
|
7878
|
+
for (; ; ) {
|
|
7879
|
+
acquired = acquireCaptureLease(
|
|
7880
|
+
input.db,
|
|
7881
|
+
input.collabId,
|
|
7882
|
+
input.pid,
|
|
7883
|
+
input.leaseOptions
|
|
7884
|
+
);
|
|
7885
|
+
if (acquired) break;
|
|
7886
|
+
if (Date.now() >= deadline) break;
|
|
7887
|
+
await sleep3(acquireBackoffMs);
|
|
7888
|
+
}
|
|
7889
|
+
if (!acquired) {
|
|
7890
|
+
return { status: "degraded_pty_only", text: null, interferenceDetected: false };
|
|
7891
|
+
}
|
|
7892
|
+
try {
|
|
7893
|
+
let interferenceDetected = false;
|
|
7894
|
+
for (let attempt = 0; attempt <= recaptureAttempts; attempt += 1) {
|
|
7895
|
+
if (attempt > 0) await sleep3(recaptureBackoffMs);
|
|
7896
|
+
const c0 = await input.readChangeCount();
|
|
7897
|
+
const clip = await input.runCapture();
|
|
7898
|
+
const cn = await input.readChangeCount();
|
|
7899
|
+
const checkAvailable = c0 !== null && cn !== null;
|
|
7900
|
+
const interfered = checkAvailable && cn - c0 > 1;
|
|
7901
|
+
if (clip === null || clip.trim().length === 0) {
|
|
7902
|
+
if (interfered) {
|
|
7903
|
+
interferenceDetected = true;
|
|
7904
|
+
continue;
|
|
7905
|
+
}
|
|
7906
|
+
return { status: "captured", text: null, interferenceDetected };
|
|
7907
|
+
}
|
|
7908
|
+
const clean = checkAvailable ? cn - c0 === 1 : true;
|
|
7909
|
+
if (clean) {
|
|
7910
|
+
return { status: "captured", text: clip, interferenceDetected };
|
|
7911
|
+
}
|
|
7912
|
+
interferenceDetected = true;
|
|
7913
|
+
if (contentMatches(input.turnText, clip)) {
|
|
7914
|
+
return { status: "captured", text: clip, interferenceDetected: true };
|
|
7915
|
+
}
|
|
7916
|
+
}
|
|
7917
|
+
return { status: "degraded_pty_only", text: null, interferenceDetected: true };
|
|
7918
|
+
} finally {
|
|
7919
|
+
releaseCaptureLease(input.db, input.collabId);
|
|
7920
|
+
}
|
|
7921
|
+
}
|
|
7922
|
+
|
|
7923
|
+
// src/runtime/clipboard-change-count.ts
|
|
7924
|
+
import { execFile as execFile3 } from "node:child_process";
|
|
7925
|
+
import { existsSync as existsSync7 } from "node:fs";
|
|
7926
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
7927
|
+
import { dirname as dirname3, join as join7 } from "node:path";
|
|
7928
|
+
var HELPER_BIN_NAME = "clipboard-change-count";
|
|
7929
|
+
function execFileText2(command, args = []) {
|
|
7930
|
+
return new Promise((resolve5, reject) => {
|
|
7931
|
+
execFile3(command, args, { encoding: "utf8" }, (error, stdout) => {
|
|
7932
|
+
if (error) {
|
|
7933
|
+
reject(error instanceof Error ? error : new Error("helper failed"));
|
|
7934
|
+
return;
|
|
7935
|
+
}
|
|
7936
|
+
resolve5(stdout);
|
|
7937
|
+
});
|
|
7938
|
+
});
|
|
7939
|
+
}
|
|
7940
|
+
function makeChangeCountReader(deps) {
|
|
7941
|
+
const platform = deps?.platform ?? process.platform;
|
|
7942
|
+
const runHelper = deps?.runHelper ?? (async () => {
|
|
7943
|
+
const here = dirname3(fileURLToPath2(import.meta.url));
|
|
7944
|
+
const bin = join7(here, "..", "native", HELPER_BIN_NAME);
|
|
7945
|
+
if (!existsSync7(bin)) throw new Error("changeCount helper not built");
|
|
7946
|
+
return execFileText2(bin);
|
|
7947
|
+
});
|
|
7948
|
+
return async () => {
|
|
7949
|
+
if (platform !== "darwin") return null;
|
|
7950
|
+
try {
|
|
7951
|
+
const out = (await runHelper()).trim();
|
|
7952
|
+
const n = Number.parseInt(out, 10);
|
|
7953
|
+
return Number.isFinite(n) && String(n) === out ? n : null;
|
|
7954
|
+
} catch {
|
|
7955
|
+
return null;
|
|
7956
|
+
}
|
|
7957
|
+
};
|
|
7958
|
+
}
|
|
7959
|
+
|
|
7682
7960
|
// src/runtime/provider-submit-strategy.ts
|
|
7683
7961
|
async function submitInjectedProviderInput(input) {
|
|
7684
7962
|
const sleep3 = input.sleep ?? ((ms) => new Promise((resolve5) => setTimeout(resolve5, ms)));
|
|
@@ -7723,6 +8001,7 @@ function createRuntimeDebugLogger(input) {
|
|
|
7723
8001
|
}
|
|
7724
8002
|
|
|
7725
8003
|
// src/runtime/mount-session-main.ts
|
|
8004
|
+
var readChangeCount = makeChangeCountReader();
|
|
7726
8005
|
function safeReapSessions(input) {
|
|
7727
8006
|
const reap = input.reap ?? ((collabId, agentType, keepSessionId) => {
|
|
7728
8007
|
const db = openDatabase(getSharedSqlitePath());
|
|
@@ -7807,6 +8086,7 @@ function createMountSessionRuntime(input) {
|
|
|
7807
8086
|
agentType: input.target,
|
|
7808
8087
|
attachmentKind: "mounted"
|
|
7809
8088
|
});
|
|
8089
|
+
releaseCaptureLease(db, resolvedClaim.collabId);
|
|
7810
8090
|
} finally {
|
|
7811
8091
|
db.close();
|
|
7812
8092
|
}
|
|
@@ -7966,17 +8246,30 @@ function createMountSessionRuntime(input) {
|
|
|
7966
8246
|
}
|
|
7967
8247
|
return runComposer();
|
|
7968
8248
|
},
|
|
7969
|
-
captureHandbackText: async () => {
|
|
7970
|
-
return
|
|
7971
|
-
|
|
7972
|
-
|
|
7973
|
-
|
|
7974
|
-
|
|
7975
|
-
|
|
7976
|
-
|
|
7977
|
-
|
|
7978
|
-
|
|
7979
|
-
|
|
8249
|
+
captureHandbackText: async (turnText) => {
|
|
8250
|
+
if (!resolvedClaim) return null;
|
|
8251
|
+
const db = openDatabase(getSharedSqlitePath());
|
|
8252
|
+
try {
|
|
8253
|
+
return await captureHandbackText({
|
|
8254
|
+
db,
|
|
8255
|
+
collabId: resolvedClaim.collabId,
|
|
8256
|
+
pid: process.pid,
|
|
8257
|
+
turnText,
|
|
8258
|
+
readChangeCount,
|
|
8259
|
+
runCapture: () => captureClipboardHandback({
|
|
8260
|
+
triggerCopy: () => submitInjectedInput2("/copy"),
|
|
8261
|
+
// Some provider versions show a picker after /copy (e.g. Claude
|
|
8262
|
+
// Code's content-type picker). Send Enter to confirm the default
|
|
8263
|
+
// selection if the clipboard has not changed yet after the delay.
|
|
8264
|
+
confirmPicker: () => {
|
|
8265
|
+
writeInjectedInput("mounted-submit-picker", "\r");
|
|
8266
|
+
},
|
|
8267
|
+
triggerDelayMs: 300
|
|
8268
|
+
})
|
|
8269
|
+
});
|
|
8270
|
+
} finally {
|
|
8271
|
+
db.close();
|
|
8272
|
+
}
|
|
7980
8273
|
},
|
|
7981
8274
|
confirmHandbackCapture: async () => {
|
|
7982
8275
|
const runConfirm = async () => {
|
|
@@ -8088,30 +8381,53 @@ async function isPortFree(port, host = "127.0.0.1") {
|
|
|
8088
8381
|
});
|
|
8089
8382
|
}
|
|
8090
8383
|
|
|
8091
|
-
// src/commands/collab/start.ts
|
|
8092
|
-
import { mkdirSync as mkdirSync3 } from "node:fs";
|
|
8093
|
-
|
|
8094
8384
|
// src/runtime/port-allocator.ts
|
|
8095
8385
|
var DEFAULT_PORT_RANGE = [4500, 4999];
|
|
8096
8386
|
|
|
8097
|
-
// src/commands/collab/
|
|
8387
|
+
// src/commands/collab/recover.ts
|
|
8098
8388
|
var READY_TIMEOUT_DEFAULT = 3e4;
|
|
8099
|
-
|
|
8100
|
-
|
|
8101
|
-
|
|
8389
|
+
var STALE_THRESHOLD_DEFAULT = 9e4;
|
|
8390
|
+
function defaultIsAliveImpl(pid) {
|
|
8391
|
+
try {
|
|
8392
|
+
process.kill(pid, 0);
|
|
8393
|
+
return Promise.resolve({ alive: true, startTime: null });
|
|
8394
|
+
} catch (err) {
|
|
8395
|
+
const code = err.code;
|
|
8396
|
+
if (code === "EPERM") return Promise.resolve({ alive: true, startTime: null });
|
|
8397
|
+
return Promise.resolve({ alive: false, startTime: null });
|
|
8398
|
+
}
|
|
8399
|
+
}
|
|
8400
|
+
async function runCollabRecover(opts) {
|
|
8102
8401
|
const sqlitePath = getSharedSqlitePath();
|
|
8103
8402
|
const db = openDatabase(sqlitePath);
|
|
8104
8403
|
applyMigrations(db);
|
|
8105
|
-
const
|
|
8106
|
-
const
|
|
8107
|
-
const
|
|
8108
|
-
|
|
8404
|
+
const isAlive = opts.isAlive ?? defaultIsAliveImpl;
|
|
8405
|
+
const staleThresholdMs = opts.staleThresholdMs ?? STALE_THRESHOLD_DEFAULT;
|
|
8406
|
+
const resolved = resolveCollab({
|
|
8407
|
+
db,
|
|
8408
|
+
cwd: opts.cwd,
|
|
8409
|
+
...opts.collabIdOverride ? { collabIdOverride: opts.collabIdOverride } : {},
|
|
8410
|
+
requireActive: true
|
|
8411
|
+
});
|
|
8412
|
+
const collabId = resolved.collabId;
|
|
8109
8413
|
const host = "127.0.0.1";
|
|
8110
|
-
const
|
|
8111
|
-
|
|
8112
|
-
|
|
8113
|
-
|
|
8114
|
-
)
|
|
8414
|
+
const preCheckRow = getBrokerDaemonByCollab(db, collabId);
|
|
8415
|
+
let preCheckPid = null;
|
|
8416
|
+
let preCheckPidStartTime = null;
|
|
8417
|
+
let preCheckHeartbeat = null;
|
|
8418
|
+
if (preCheckRow && preCheckRow.pid !== null) {
|
|
8419
|
+
preCheckPid = preCheckRow.pid;
|
|
8420
|
+
preCheckPidStartTime = preCheckRow.pidStartTime;
|
|
8421
|
+
preCheckHeartbeat = preCheckRow.lastHeartbeatAt;
|
|
8422
|
+
const liveness = await isAlive(preCheckRow.pid);
|
|
8423
|
+
const startTimeMatches = preCheckRow.pidStartTime === null || liveness.startTime === null || liveness.startTime === preCheckRow.pidStartTime;
|
|
8424
|
+
if (liveness.alive && startTimeMatches) {
|
|
8425
|
+
db.close();
|
|
8426
|
+
throw new Error(
|
|
8427
|
+
`daemon already running for collab ${collabId} (pid ${preCheckRow.pid})`
|
|
8428
|
+
);
|
|
8429
|
+
}
|
|
8430
|
+
}
|
|
8115
8431
|
const range = opts.portRange ?? DEFAULT_PORT_RANGE;
|
|
8116
8432
|
const osFreeCandidates = [];
|
|
8117
8433
|
if (opts.explicitPort !== void 0) {
|
|
@@ -8133,16 +8449,25 @@ async function runCollabStart(opts) {
|
|
|
8133
8449
|
);
|
|
8134
8450
|
}
|
|
8135
8451
|
}
|
|
8452
|
+
const now = opts.now();
|
|
8136
8453
|
let allocatedPort = 0;
|
|
8137
8454
|
const tx = db.transaction(() => {
|
|
8138
|
-
|
|
8139
|
-
|
|
8140
|
-
|
|
8141
|
-
|
|
8142
|
-
|
|
8143
|
-
|
|
8144
|
-
|
|
8145
|
-
|
|
8455
|
+
const existing = getBrokerDaemonByCollab(db, collabId);
|
|
8456
|
+
if (existing) {
|
|
8457
|
+
if (existing.pid === null) {
|
|
8458
|
+
const heartbeatMs = Date.parse(existing.lastHeartbeatAt);
|
|
8459
|
+
const nowMs = Date.parse(now);
|
|
8460
|
+
const ageMs = Number.isFinite(heartbeatMs) && Number.isFinite(nowMs) ? nowMs - heartbeatMs : Number.POSITIVE_INFINITY;
|
|
8461
|
+
if (ageMs < staleThresholdMs) {
|
|
8462
|
+
throw new Error("RECOVERY_ALREADY_IN_PROGRESS");
|
|
8463
|
+
}
|
|
8464
|
+
deleteBrokerDaemonByCollab(db, collabId);
|
|
8465
|
+
} else {
|
|
8466
|
+
if (preCheckPid === null || existing.pid !== preCheckPid || existing.pidStartTime !== preCheckPidStartTime || existing.lastHeartbeatAt !== preCheckHeartbeat) {
|
|
8467
|
+
throw new Error("DAEMON_NOW_RUNNING");
|
|
8468
|
+
}
|
|
8469
|
+
deleteBrokerDaemonByCollab(db, collabId);
|
|
8470
|
+
}
|
|
8146
8471
|
}
|
|
8147
8472
|
const takenPorts = new Set(
|
|
8148
8473
|
db.prepare("SELECT port FROM broker_daemon").all().map((r) => r.port)
|
|
@@ -8155,20 +8480,6 @@ async function runCollabStart(opts) {
|
|
|
8155
8480
|
);
|
|
8156
8481
|
}
|
|
8157
8482
|
allocatedPort = picked;
|
|
8158
|
-
db.prepare(
|
|
8159
|
-
"INSERT INTO collab (collab_id, workspace_root, display_name, status, workspace_id, launch_mode, tmux_session, created_at, updated_at, orchestrator_enabled, orchestrator_max_rounds) VALUES (?, ?, ?, 'active', ?, ?, ?, ?, ?, ?, ?)"
|
|
8160
|
-
).run(
|
|
8161
|
-
collabId,
|
|
8162
|
-
workspaceRoot,
|
|
8163
|
-
opts.displayName,
|
|
8164
|
-
workspaceId,
|
|
8165
|
-
opts.launchMode,
|
|
8166
|
-
opts.tmuxSession ?? null,
|
|
8167
|
-
now,
|
|
8168
|
-
now,
|
|
8169
|
-
orchestratorEnabled ? 1 : 0,
|
|
8170
|
-
orchestratorMaxRounds
|
|
8171
|
-
);
|
|
8172
8483
|
insertBrokerDaemon(db, {
|
|
8173
8484
|
collabId,
|
|
8174
8485
|
host,
|
|
@@ -8176,23 +8487,27 @@ async function runCollabStart(opts) {
|
|
|
8176
8487
|
startedAt: now,
|
|
8177
8488
|
lastHeartbeatAt: now
|
|
8178
8489
|
});
|
|
8179
|
-
upsertRecoveryState(db, {
|
|
8180
|
-
collabId,
|
|
8181
|
-
state: "normal",
|
|
8182
|
-
idleAfterRecovery: false,
|
|
8183
|
-
recoveredAt: null
|
|
8184
|
-
});
|
|
8185
8490
|
});
|
|
8186
8491
|
try {
|
|
8187
8492
|
tx.immediate();
|
|
8188
8493
|
} catch (err) {
|
|
8189
|
-
|
|
8190
|
-
|
|
8494
|
+
const msg = err.message;
|
|
8495
|
+
db.close();
|
|
8496
|
+
if (msg === "RECOVERY_ALREADY_IN_PROGRESS") {
|
|
8497
|
+
throw new Error(
|
|
8498
|
+
`recovery already in progress for collab ${collabId}`
|
|
8499
|
+
);
|
|
8500
|
+
}
|
|
8501
|
+
if (msg === "DAEMON_NOW_RUNNING") {
|
|
8502
|
+
throw new Error(
|
|
8503
|
+
`daemon already running for collab ${collabId}`
|
|
8504
|
+
);
|
|
8505
|
+
}
|
|
8506
|
+
if (msg === "ALL_CANDIDATES_TAKEN") {
|
|
8191
8507
|
throw new Error(
|
|
8192
8508
|
`every OS-free port in [${range[0]}, ${range[1]}] is reserved in the registry`
|
|
8193
8509
|
);
|
|
8194
8510
|
}
|
|
8195
|
-
db.close();
|
|
8196
8511
|
throw err;
|
|
8197
8512
|
}
|
|
8198
8513
|
const childPid = opts.spawnBroker({
|
|
@@ -8204,9 +8519,6 @@ async function runCollabStart(opts) {
|
|
|
8204
8519
|
const cleanupOnFailure = (msg) => {
|
|
8205
8520
|
const cleanup = db.transaction(() => {
|
|
8206
8521
|
deleteBrokerDaemonByCollab(db, collabId);
|
|
8207
|
-
db.prepare(
|
|
8208
|
-
"UPDATE collab SET status='stopped', stopped_at=?, updated_at=? WHERE collab_id = ?"
|
|
8209
|
-
).run(opts.now(), opts.now(), collabId);
|
|
8210
8522
|
});
|
|
8211
8523
|
cleanup.immediate();
|
|
8212
8524
|
try {
|
|
@@ -8234,23 +8546,197 @@ async function runCollabStart(opts) {
|
|
|
8234
8546
|
);
|
|
8235
8547
|
}
|
|
8236
8548
|
db.close();
|
|
8237
|
-
|
|
8238
|
-
|
|
8239
|
-
|
|
8240
|
-
|
|
8241
|
-
|
|
8242
|
-
|
|
8243
|
-
};
|
|
8244
|
-
}
|
|
8245
|
-
function recordLaunchedSessions(input) {
|
|
8246
|
-
const db = openDatabase(getSharedSqlitePath());
|
|
8549
|
+
const recoveryBroker = createBrokerRuntime({
|
|
8550
|
+
sqlitePath,
|
|
8551
|
+
runWorkflowDriver: false,
|
|
8552
|
+
runDiagnosticsSweep: false,
|
|
8553
|
+
runDaemonHeartbeat: false,
|
|
8554
|
+
runBrokerDaemonSweep: false
|
|
8555
|
+
});
|
|
8247
8556
|
try {
|
|
8248
|
-
|
|
8249
|
-
|
|
8250
|
-
|
|
8251
|
-
|
|
8252
|
-
|
|
8253
|
-
|
|
8557
|
+
const prepared = recoveryBroker.control.prepareCollabRecovery({
|
|
8558
|
+
collabId,
|
|
8559
|
+
now
|
|
8560
|
+
});
|
|
8561
|
+
const hasRememberedBindings = prepared.bindings.some(
|
|
8562
|
+
(b) => b.activeSessionId !== null
|
|
8563
|
+
);
|
|
8564
|
+
upsertRecoveryState(recoveryBroker.db, {
|
|
8565
|
+
collabId,
|
|
8566
|
+
state: hasRememberedBindings ? "recovered" : "normal",
|
|
8567
|
+
idleAfterRecovery: hasRememberedBindings,
|
|
8568
|
+
recoveredAt: hasRememberedBindings ? now : null
|
|
8569
|
+
});
|
|
8570
|
+
} finally {
|
|
8571
|
+
await recoveryBroker.stop();
|
|
8572
|
+
}
|
|
8573
|
+
return {
|
|
8574
|
+
collabId,
|
|
8575
|
+
host,
|
|
8576
|
+
port: allocatedPort,
|
|
8577
|
+
// biome-ignore lint/style/noNonNullAssertion: guarded by cleanupOnFailure
|
|
8578
|
+
pid: finalRow.pid
|
|
8579
|
+
};
|
|
8580
|
+
}
|
|
8581
|
+
|
|
8582
|
+
// src/commands/collab/start.ts
|
|
8583
|
+
import { mkdirSync as mkdirSync3 } from "node:fs";
|
|
8584
|
+
var READY_TIMEOUT_DEFAULT2 = 3e4;
|
|
8585
|
+
async function runCollabStart(opts) {
|
|
8586
|
+
const root = getStateRoot();
|
|
8587
|
+
mkdirSync3(root, { recursive: true });
|
|
8588
|
+
const sqlitePath = getSharedSqlitePath();
|
|
8589
|
+
const db = openDatabase(sqlitePath);
|
|
8590
|
+
applyMigrations(db);
|
|
8591
|
+
const workspaceRoot = canonicalWorkspaceRoot(opts.cwd);
|
|
8592
|
+
const workspaceId = workspaceIdFromPath(opts.cwd);
|
|
8593
|
+
const now = opts.now();
|
|
8594
|
+
const collabId = createCliCollabId(now);
|
|
8595
|
+
const host = "127.0.0.1";
|
|
8596
|
+
const orchestratorEnabled = process.env.AI_WHISPER_RELAY_ORCHESTRATOR_ENABLED !== "0";
|
|
8597
|
+
const orchestratorMaxRounds = Math.max(
|
|
8598
|
+
1,
|
|
8599
|
+
Number(process.env.AI_WHISPER_RELAY_ORCHESTRATOR_MAX_ROUNDS ?? "3") || 3
|
|
8600
|
+
);
|
|
8601
|
+
const range = opts.portRange ?? DEFAULT_PORT_RANGE;
|
|
8602
|
+
const osFreeCandidates = [];
|
|
8603
|
+
if (opts.explicitPort !== void 0) {
|
|
8604
|
+
if (!await opts.isPortFreeOs(opts.explicitPort)) {
|
|
8605
|
+
db.close();
|
|
8606
|
+
throw new Error(
|
|
8607
|
+
`port ${opts.explicitPort} is in use by another process`
|
|
8608
|
+
);
|
|
8609
|
+
}
|
|
8610
|
+
osFreeCandidates.push(opts.explicitPort);
|
|
8611
|
+
} else {
|
|
8612
|
+
for (let p = range[0]; p <= range[1]; p++) {
|
|
8613
|
+
if (await opts.isPortFreeOs(p)) osFreeCandidates.push(p);
|
|
8614
|
+
}
|
|
8615
|
+
if (osFreeCandidates.length === 0) {
|
|
8616
|
+
db.close();
|
|
8617
|
+
throw new Error(
|
|
8618
|
+
`No OS-free port in range [${range[0]}, ${range[1]}]`
|
|
8619
|
+
);
|
|
8620
|
+
}
|
|
8621
|
+
}
|
|
8622
|
+
let allocatedPort = 0;
|
|
8623
|
+
const tx = db.transaction(() => {
|
|
8624
|
+
upsertWorkspace(db, { id: workspaceId, workspaceRoot, now });
|
|
8625
|
+
const active = db.prepare(
|
|
8626
|
+
"SELECT collab_id FROM collab WHERE workspace_id = ? AND status = 'active' LIMIT 1"
|
|
8627
|
+
).get(workspaceId);
|
|
8628
|
+
if (active) {
|
|
8629
|
+
throw new Error(
|
|
8630
|
+
`active collab ${active.collab_id} already exists for workspace ${workspaceRoot}`
|
|
8631
|
+
);
|
|
8632
|
+
}
|
|
8633
|
+
const takenPorts = new Set(
|
|
8634
|
+
db.prepare("SELECT port FROM broker_daemon").all().map((r) => r.port)
|
|
8635
|
+
);
|
|
8636
|
+
const picked = osFreeCandidates.find((p) => !takenPorts.has(p));
|
|
8637
|
+
if (picked === void 0) throw new Error("ALL_CANDIDATES_TAKEN");
|
|
8638
|
+
if (opts.explicitPort !== void 0 && picked !== opts.explicitPort) {
|
|
8639
|
+
throw new Error(
|
|
8640
|
+
`port ${opts.explicitPort} already in use by another daemon`
|
|
8641
|
+
);
|
|
8642
|
+
}
|
|
8643
|
+
allocatedPort = picked;
|
|
8644
|
+
db.prepare(
|
|
8645
|
+
"INSERT INTO collab (collab_id, workspace_root, display_name, status, workspace_id, launch_mode, tmux_session, created_at, updated_at, orchestrator_enabled, orchestrator_max_rounds) VALUES (?, ?, ?, 'active', ?, ?, ?, ?, ?, ?, ?)"
|
|
8646
|
+
).run(
|
|
8647
|
+
collabId,
|
|
8648
|
+
workspaceRoot,
|
|
8649
|
+
opts.displayName,
|
|
8650
|
+
workspaceId,
|
|
8651
|
+
opts.launchMode,
|
|
8652
|
+
opts.tmuxSession ?? null,
|
|
8653
|
+
now,
|
|
8654
|
+
now,
|
|
8655
|
+
orchestratorEnabled ? 1 : 0,
|
|
8656
|
+
orchestratorMaxRounds
|
|
8657
|
+
);
|
|
8658
|
+
insertBrokerDaemon(db, {
|
|
8659
|
+
collabId,
|
|
8660
|
+
host,
|
|
8661
|
+
port: allocatedPort,
|
|
8662
|
+
startedAt: now,
|
|
8663
|
+
lastHeartbeatAt: now
|
|
8664
|
+
});
|
|
8665
|
+
upsertRecoveryState(db, {
|
|
8666
|
+
collabId,
|
|
8667
|
+
state: "normal",
|
|
8668
|
+
idleAfterRecovery: false,
|
|
8669
|
+
recoveredAt: null
|
|
8670
|
+
});
|
|
8671
|
+
});
|
|
8672
|
+
try {
|
|
8673
|
+
tx.immediate();
|
|
8674
|
+
} catch (err) {
|
|
8675
|
+
if (err.message === "ALL_CANDIDATES_TAKEN") {
|
|
8676
|
+
db.close();
|
|
8677
|
+
throw new Error(
|
|
8678
|
+
`every OS-free port in [${range[0]}, ${range[1]}] is reserved in the registry`
|
|
8679
|
+
);
|
|
8680
|
+
}
|
|
8681
|
+
db.close();
|
|
8682
|
+
throw err;
|
|
8683
|
+
}
|
|
8684
|
+
const childPid = opts.spawnBroker({
|
|
8685
|
+
collabId,
|
|
8686
|
+
host,
|
|
8687
|
+
port: allocatedPort,
|
|
8688
|
+
sqlitePath
|
|
8689
|
+
});
|
|
8690
|
+
const cleanupOnFailure = (msg) => {
|
|
8691
|
+
const cleanup = db.transaction(() => {
|
|
8692
|
+
deleteBrokerDaemonByCollab(db, collabId);
|
|
8693
|
+
db.prepare(
|
|
8694
|
+
"UPDATE collab SET status='stopped', stopped_at=?, updated_at=? WHERE collab_id = ?"
|
|
8695
|
+
).run(opts.now(), opts.now(), collabId);
|
|
8696
|
+
});
|
|
8697
|
+
cleanup.immediate();
|
|
8698
|
+
try {
|
|
8699
|
+
opts.signalProcess(childPid, "SIGTERM");
|
|
8700
|
+
} catch {
|
|
8701
|
+
}
|
|
8702
|
+
db.close();
|
|
8703
|
+
throw new Error(msg);
|
|
8704
|
+
};
|
|
8705
|
+
const ready = await opts.waitForReady({
|
|
8706
|
+
host,
|
|
8707
|
+
port: allocatedPort,
|
|
8708
|
+
collabId,
|
|
8709
|
+
timeoutMs: opts.readyTimeoutMs ?? READY_TIMEOUT_DEFAULT2
|
|
8710
|
+
});
|
|
8711
|
+
if (!ready) {
|
|
8712
|
+
cleanupOnFailure(
|
|
8713
|
+
`broker readiness check timed out for collab ${collabId}`
|
|
8714
|
+
);
|
|
8715
|
+
}
|
|
8716
|
+
const finalRow = getBrokerDaemonByCollab(db, collabId);
|
|
8717
|
+
if (!finalRow || finalRow.pid === null) {
|
|
8718
|
+
cleanupOnFailure(
|
|
8719
|
+
`daemon did not write its PID for collab ${collabId}`
|
|
8720
|
+
);
|
|
8721
|
+
}
|
|
8722
|
+
db.close();
|
|
8723
|
+
return {
|
|
8724
|
+
collabId,
|
|
8725
|
+
port: allocatedPort,
|
|
8726
|
+
host,
|
|
8727
|
+
// biome-ignore lint/style/noNonNullAssertion: guarded by cleanupOnFailure
|
|
8728
|
+
pid: finalRow.pid
|
|
8729
|
+
};
|
|
8730
|
+
}
|
|
8731
|
+
function recordLaunchedSessions(input) {
|
|
8732
|
+
const db = openDatabase(getSharedSqlitePath());
|
|
8733
|
+
try {
|
|
8734
|
+
if (input.launch.tmuxSession) {
|
|
8735
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8736
|
+
db.prepare(
|
|
8737
|
+
"UPDATE collab SET tmux_session = ?, updated_at = ? WHERE collab_id = ?"
|
|
8738
|
+
).run(input.launch.tmuxSession, now, input.collabId);
|
|
8739
|
+
}
|
|
8254
8740
|
if (input.launchMode === "terminals") {
|
|
8255
8741
|
const runtime = input.launch.runtime;
|
|
8256
8742
|
const agents = [
|
|
@@ -8302,7 +8788,7 @@ function recordLaunchedSessions(input) {
|
|
|
8302
8788
|
}
|
|
8303
8789
|
|
|
8304
8790
|
// src/commands/collab/mount.ts
|
|
8305
|
-
function
|
|
8791
|
+
function defaultIsPidAlive3(pid) {
|
|
8306
8792
|
try {
|
|
8307
8793
|
process.kill(pid, 0);
|
|
8308
8794
|
return true;
|
|
@@ -8358,10 +8844,46 @@ async function runCollabMount(input) {
|
|
|
8358
8844
|
db.close();
|
|
8359
8845
|
}
|
|
8360
8846
|
};
|
|
8847
|
+
const isDaemonAlive = input.isDaemonPidAlive ?? defaultIsPidAlive3;
|
|
8848
|
+
const reviveViaRecover = async () => {
|
|
8849
|
+
await (input.runRecoverFn ?? runCollabRecover)({
|
|
8850
|
+
cwd: input.workspaceRoot,
|
|
8851
|
+
now: () => (/* @__PURE__ */ new Date()).toISOString(),
|
|
8852
|
+
isPortFreeOs: (port) => isPortFree(port),
|
|
8853
|
+
spawnBroker: ({ collabId, host, port, sqlitePath }) => spawnBrokerDaemon(sqlitePath, host, port, collabId),
|
|
8854
|
+
waitForReady: ({ host, port, collabId, timeoutMs }) => waitForBrokerReady({ host, port, collabId, timeoutMs }),
|
|
8855
|
+
signalProcess: (pid, signal) => {
|
|
8856
|
+
try {
|
|
8857
|
+
process.kill(pid, signal);
|
|
8858
|
+
} catch {
|
|
8859
|
+
}
|
|
8860
|
+
}
|
|
8861
|
+
});
|
|
8862
|
+
const db = openDatabase(getSharedSqlitePath());
|
|
8863
|
+
try {
|
|
8864
|
+
const r = resolveCollab({ db, cwd: input.workspaceRoot });
|
|
8865
|
+
upsertRecoveryState(db, {
|
|
8866
|
+
collabId: r.collabId,
|
|
8867
|
+
state: "normal",
|
|
8868
|
+
idleAfterRecovery: false,
|
|
8869
|
+
recoveredAt: null
|
|
8870
|
+
});
|
|
8871
|
+
} finally {
|
|
8872
|
+
db.close();
|
|
8873
|
+
}
|
|
8874
|
+
return tryResolve();
|
|
8875
|
+
};
|
|
8361
8876
|
const resolveOrCreate = async () => {
|
|
8362
8877
|
try {
|
|
8363
|
-
|
|
8878
|
+
const resolved2 = tryResolve();
|
|
8879
|
+
if (input.collabIdOverride === void 0 && resolved2.daemon !== null && !isDaemonAlive(resolved2.daemon.pid)) {
|
|
8880
|
+
return await reviveViaRecover();
|
|
8881
|
+
}
|
|
8882
|
+
return resolved2;
|
|
8364
8883
|
} catch (err) {
|
|
8884
|
+
if (err instanceof CollabResolverError && err.kind === "NoLiveDaemonForCollab" && input.collabIdOverride === void 0) {
|
|
8885
|
+
return await reviveViaRecover();
|
|
8886
|
+
}
|
|
8365
8887
|
if (!(err instanceof CollabResolverError && (err.kind === "NoCollabFoundForCwd" || err.kind === "CollabAlreadyStopped"))) {
|
|
8366
8888
|
throw err;
|
|
8367
8889
|
}
|
|
@@ -8429,7 +8951,7 @@ async function runCollabMount(input) {
|
|
|
8429
8951
|
try {
|
|
8430
8952
|
const current = broker.control.listSessionBindings(resolved.collabId).find((binding) => binding.agentType === input.target);
|
|
8431
8953
|
if (current?.bindingState === "bound") {
|
|
8432
|
-
const isAlive = input.isPidAlive ??
|
|
8954
|
+
const isAlive = input.isPidAlive ?? defaultIsPidAlive3;
|
|
8433
8955
|
const liveOwner = broker.control.listSessionAttachments(resolved.collabId).some(
|
|
8434
8956
|
(a) => a.agentType === input.target && a.pid !== null && isAlive(a.pid)
|
|
8435
8957
|
);
|
|
@@ -8873,201 +9395,6 @@ async function runCollabInspect(input) {
|
|
|
8873
9395
|
}
|
|
8874
9396
|
}
|
|
8875
9397
|
|
|
8876
|
-
// src/commands/collab/recover.ts
|
|
8877
|
-
var READY_TIMEOUT_DEFAULT2 = 3e4;
|
|
8878
|
-
var STALE_THRESHOLD_DEFAULT = 9e4;
|
|
8879
|
-
function defaultIsAliveImpl(pid) {
|
|
8880
|
-
try {
|
|
8881
|
-
process.kill(pid, 0);
|
|
8882
|
-
return Promise.resolve({ alive: true, startTime: null });
|
|
8883
|
-
} catch (err) {
|
|
8884
|
-
const code = err.code;
|
|
8885
|
-
if (code === "EPERM") return Promise.resolve({ alive: true, startTime: null });
|
|
8886
|
-
return Promise.resolve({ alive: false, startTime: null });
|
|
8887
|
-
}
|
|
8888
|
-
}
|
|
8889
|
-
async function runCollabRecover(opts) {
|
|
8890
|
-
const sqlitePath = getSharedSqlitePath();
|
|
8891
|
-
const db = openDatabase(sqlitePath);
|
|
8892
|
-
applyMigrations(db);
|
|
8893
|
-
const isAlive = opts.isAlive ?? defaultIsAliveImpl;
|
|
8894
|
-
const staleThresholdMs = opts.staleThresholdMs ?? STALE_THRESHOLD_DEFAULT;
|
|
8895
|
-
const resolved = resolveCollab({
|
|
8896
|
-
db,
|
|
8897
|
-
cwd: opts.cwd,
|
|
8898
|
-
...opts.collabIdOverride ? { collabIdOverride: opts.collabIdOverride } : {},
|
|
8899
|
-
requireActive: true
|
|
8900
|
-
});
|
|
8901
|
-
const collabId = resolved.collabId;
|
|
8902
|
-
const host = "127.0.0.1";
|
|
8903
|
-
const preCheckRow = getBrokerDaemonByCollab(db, collabId);
|
|
8904
|
-
let preCheckPid = null;
|
|
8905
|
-
let preCheckPidStartTime = null;
|
|
8906
|
-
let preCheckHeartbeat = null;
|
|
8907
|
-
if (preCheckRow && preCheckRow.pid !== null) {
|
|
8908
|
-
preCheckPid = preCheckRow.pid;
|
|
8909
|
-
preCheckPidStartTime = preCheckRow.pidStartTime;
|
|
8910
|
-
preCheckHeartbeat = preCheckRow.lastHeartbeatAt;
|
|
8911
|
-
const liveness = await isAlive(preCheckRow.pid);
|
|
8912
|
-
const startTimeMatches = preCheckRow.pidStartTime === null || liveness.startTime === null || liveness.startTime === preCheckRow.pidStartTime;
|
|
8913
|
-
if (liveness.alive && startTimeMatches) {
|
|
8914
|
-
db.close();
|
|
8915
|
-
throw new Error(
|
|
8916
|
-
`daemon already running for collab ${collabId} (pid ${preCheckRow.pid})`
|
|
8917
|
-
);
|
|
8918
|
-
}
|
|
8919
|
-
}
|
|
8920
|
-
const range = opts.portRange ?? DEFAULT_PORT_RANGE;
|
|
8921
|
-
const osFreeCandidates = [];
|
|
8922
|
-
if (opts.explicitPort !== void 0) {
|
|
8923
|
-
if (!await opts.isPortFreeOs(opts.explicitPort)) {
|
|
8924
|
-
db.close();
|
|
8925
|
-
throw new Error(
|
|
8926
|
-
`port ${opts.explicitPort} is in use by another process`
|
|
8927
|
-
);
|
|
8928
|
-
}
|
|
8929
|
-
osFreeCandidates.push(opts.explicitPort);
|
|
8930
|
-
} else {
|
|
8931
|
-
for (let p = range[0]; p <= range[1]; p++) {
|
|
8932
|
-
if (await opts.isPortFreeOs(p)) osFreeCandidates.push(p);
|
|
8933
|
-
}
|
|
8934
|
-
if (osFreeCandidates.length === 0) {
|
|
8935
|
-
db.close();
|
|
8936
|
-
throw new Error(
|
|
8937
|
-
`No OS-free port in range [${range[0]}, ${range[1]}]`
|
|
8938
|
-
);
|
|
8939
|
-
}
|
|
8940
|
-
}
|
|
8941
|
-
const now = opts.now();
|
|
8942
|
-
let allocatedPort = 0;
|
|
8943
|
-
const tx = db.transaction(() => {
|
|
8944
|
-
const existing = getBrokerDaemonByCollab(db, collabId);
|
|
8945
|
-
if (existing) {
|
|
8946
|
-
if (existing.pid === null) {
|
|
8947
|
-
const heartbeatMs = Date.parse(existing.lastHeartbeatAt);
|
|
8948
|
-
const nowMs = Date.parse(now);
|
|
8949
|
-
const ageMs = Number.isFinite(heartbeatMs) && Number.isFinite(nowMs) ? nowMs - heartbeatMs : Number.POSITIVE_INFINITY;
|
|
8950
|
-
if (ageMs < staleThresholdMs) {
|
|
8951
|
-
throw new Error("RECOVERY_ALREADY_IN_PROGRESS");
|
|
8952
|
-
}
|
|
8953
|
-
deleteBrokerDaemonByCollab(db, collabId);
|
|
8954
|
-
} else {
|
|
8955
|
-
if (preCheckPid === null || existing.pid !== preCheckPid || existing.pidStartTime !== preCheckPidStartTime || existing.lastHeartbeatAt !== preCheckHeartbeat) {
|
|
8956
|
-
throw new Error("DAEMON_NOW_RUNNING");
|
|
8957
|
-
}
|
|
8958
|
-
deleteBrokerDaemonByCollab(db, collabId);
|
|
8959
|
-
}
|
|
8960
|
-
}
|
|
8961
|
-
const takenPorts = new Set(
|
|
8962
|
-
db.prepare("SELECT port FROM broker_daemon").all().map((r) => r.port)
|
|
8963
|
-
);
|
|
8964
|
-
const picked = osFreeCandidates.find((p) => !takenPorts.has(p));
|
|
8965
|
-
if (picked === void 0) throw new Error("ALL_CANDIDATES_TAKEN");
|
|
8966
|
-
if (opts.explicitPort !== void 0 && picked !== opts.explicitPort) {
|
|
8967
|
-
throw new Error(
|
|
8968
|
-
`port ${opts.explicitPort} already in use by another daemon`
|
|
8969
|
-
);
|
|
8970
|
-
}
|
|
8971
|
-
allocatedPort = picked;
|
|
8972
|
-
insertBrokerDaemon(db, {
|
|
8973
|
-
collabId,
|
|
8974
|
-
host,
|
|
8975
|
-
port: allocatedPort,
|
|
8976
|
-
startedAt: now,
|
|
8977
|
-
lastHeartbeatAt: now
|
|
8978
|
-
});
|
|
8979
|
-
});
|
|
8980
|
-
try {
|
|
8981
|
-
tx.immediate();
|
|
8982
|
-
} catch (err) {
|
|
8983
|
-
const msg = err.message;
|
|
8984
|
-
db.close();
|
|
8985
|
-
if (msg === "RECOVERY_ALREADY_IN_PROGRESS") {
|
|
8986
|
-
throw new Error(
|
|
8987
|
-
`recovery already in progress for collab ${collabId}`
|
|
8988
|
-
);
|
|
8989
|
-
}
|
|
8990
|
-
if (msg === "DAEMON_NOW_RUNNING") {
|
|
8991
|
-
throw new Error(
|
|
8992
|
-
`daemon already running for collab ${collabId}`
|
|
8993
|
-
);
|
|
8994
|
-
}
|
|
8995
|
-
if (msg === "ALL_CANDIDATES_TAKEN") {
|
|
8996
|
-
throw new Error(
|
|
8997
|
-
`every OS-free port in [${range[0]}, ${range[1]}] is reserved in the registry`
|
|
8998
|
-
);
|
|
8999
|
-
}
|
|
9000
|
-
throw err;
|
|
9001
|
-
}
|
|
9002
|
-
const childPid = opts.spawnBroker({
|
|
9003
|
-
collabId,
|
|
9004
|
-
host,
|
|
9005
|
-
port: allocatedPort,
|
|
9006
|
-
sqlitePath
|
|
9007
|
-
});
|
|
9008
|
-
const cleanupOnFailure = (msg) => {
|
|
9009
|
-
const cleanup = db.transaction(() => {
|
|
9010
|
-
deleteBrokerDaemonByCollab(db, collabId);
|
|
9011
|
-
});
|
|
9012
|
-
cleanup.immediate();
|
|
9013
|
-
try {
|
|
9014
|
-
opts.signalProcess(childPid, "SIGTERM");
|
|
9015
|
-
} catch {
|
|
9016
|
-
}
|
|
9017
|
-
db.close();
|
|
9018
|
-
throw new Error(msg);
|
|
9019
|
-
};
|
|
9020
|
-
const ready = await opts.waitForReady({
|
|
9021
|
-
host,
|
|
9022
|
-
port: allocatedPort,
|
|
9023
|
-
collabId,
|
|
9024
|
-
timeoutMs: opts.readyTimeoutMs ?? READY_TIMEOUT_DEFAULT2
|
|
9025
|
-
});
|
|
9026
|
-
if (!ready) {
|
|
9027
|
-
cleanupOnFailure(
|
|
9028
|
-
`broker readiness check timed out for collab ${collabId}`
|
|
9029
|
-
);
|
|
9030
|
-
}
|
|
9031
|
-
const finalRow = getBrokerDaemonByCollab(db, collabId);
|
|
9032
|
-
if (!finalRow || finalRow.pid === null) {
|
|
9033
|
-
cleanupOnFailure(
|
|
9034
|
-
`daemon did not write its PID for collab ${collabId}`
|
|
9035
|
-
);
|
|
9036
|
-
}
|
|
9037
|
-
db.close();
|
|
9038
|
-
const recoveryBroker = createBrokerRuntime({
|
|
9039
|
-
sqlitePath,
|
|
9040
|
-
runWorkflowDriver: false,
|
|
9041
|
-
runDiagnosticsSweep: false,
|
|
9042
|
-
runDaemonHeartbeat: false,
|
|
9043
|
-
runBrokerDaemonSweep: false
|
|
9044
|
-
});
|
|
9045
|
-
try {
|
|
9046
|
-
const prepared = recoveryBroker.control.prepareCollabRecovery({
|
|
9047
|
-
collabId,
|
|
9048
|
-
now
|
|
9049
|
-
});
|
|
9050
|
-
const hasRememberedBindings = prepared.bindings.some(
|
|
9051
|
-
(b) => b.activeSessionId !== null
|
|
9052
|
-
);
|
|
9053
|
-
upsertRecoveryState(recoveryBroker.db, {
|
|
9054
|
-
collabId,
|
|
9055
|
-
state: hasRememberedBindings ? "recovered" : "normal",
|
|
9056
|
-
idleAfterRecovery: hasRememberedBindings,
|
|
9057
|
-
recoveredAt: hasRememberedBindings ? now : null
|
|
9058
|
-
});
|
|
9059
|
-
} finally {
|
|
9060
|
-
await recoveryBroker.stop();
|
|
9061
|
-
}
|
|
9062
|
-
return {
|
|
9063
|
-
collabId,
|
|
9064
|
-
host,
|
|
9065
|
-
port: allocatedPort,
|
|
9066
|
-
// biome-ignore lint/style/noNonNullAssertion: guarded by cleanupOnFailure
|
|
9067
|
-
pid: finalRow.pid
|
|
9068
|
-
};
|
|
9069
|
-
}
|
|
9070
|
-
|
|
9071
9398
|
// src/commands/collab/reconnect.ts
|
|
9072
9399
|
async function runCollabReconnect(input) {
|
|
9073
9400
|
const db = openDatabase(getSharedSqlitePath());
|
|
@@ -10517,11 +10844,11 @@ async function runCollabDashboard(input) {
|
|
|
10517
10844
|
|
|
10518
10845
|
// src/commands/collab/status.ts
|
|
10519
10846
|
import Database2 from "better-sqlite3";
|
|
10520
|
-
import { existsSync as
|
|
10847
|
+
import { existsSync as existsSync8 } from "node:fs";
|
|
10521
10848
|
|
|
10522
10849
|
// src/runtime/evaluator-config.ts
|
|
10523
10850
|
import { readFileSync as readFileSync2, statSync as statSync2 } from "node:fs";
|
|
10524
|
-
import { join as
|
|
10851
|
+
import { join as join8 } from "node:path";
|
|
10525
10852
|
function isEvaluatorReady(status) {
|
|
10526
10853
|
return status === "ready" || status === "unknown";
|
|
10527
10854
|
}
|
|
@@ -10532,7 +10859,7 @@ function isEvaluatorPreflightBlocked(status) {
|
|
|
10532
10859
|
// src/commands/collab/status.ts
|
|
10533
10860
|
function runCollabStatus(input) {
|
|
10534
10861
|
const sqlitePath = getSharedSqlitePath();
|
|
10535
|
-
if (!
|
|
10862
|
+
if (!existsSync8(sqlitePath)) {
|
|
10536
10863
|
return input.json ? JSON.stringify({ error: "no_collab_for_cwd", cwd: input.cwd }) : `no active collab for ${input.cwd}`;
|
|
10537
10864
|
}
|
|
10538
10865
|
const db = new Database2(sqlitePath, { readonly: true });
|
|
@@ -10867,9 +11194,9 @@ async function runCollabTell(input) {
|
|
|
10867
11194
|
|
|
10868
11195
|
// src/runtime/launcher.ts
|
|
10869
11196
|
import { execSync as execSync3, spawn as nodeSpawn } from "node:child_process";
|
|
10870
|
-
import { dirname as
|
|
10871
|
-
import { fileURLToPath as
|
|
10872
|
-
var __dirname =
|
|
11197
|
+
import { dirname as dirname4, resolve as resolve4 } from "node:path";
|
|
11198
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
11199
|
+
var __dirname = dirname4(fileURLToPath3(import.meta.url));
|
|
10873
11200
|
var whisperBinPath = resolve4(__dirname, "../bin/whisper.js");
|
|
10874
11201
|
var relayMonitorBinPath = resolve4(__dirname, "../bin/relay-monitor.js");
|
|
10875
11202
|
function shellQuote(value) {
|
|
@@ -11111,9 +11438,9 @@ async function runWorkflowTypes() {
|
|
|
11111
11438
|
// src/commands/skill/install.ts
|
|
11112
11439
|
import { cp, mkdir, readdir, stat } from "node:fs/promises";
|
|
11113
11440
|
import path3 from "node:path";
|
|
11114
|
-
import { fileURLToPath as
|
|
11441
|
+
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
11115
11442
|
function defaultBundledSkillsDir() {
|
|
11116
|
-
const here = path3.dirname(
|
|
11443
|
+
const here = path3.dirname(fileURLToPath4(import.meta.url));
|
|
11117
11444
|
return path3.resolve(here, "..", "..", "skills");
|
|
11118
11445
|
}
|
|
11119
11446
|
function homeForTarget(target, fakeHome) {
|