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.
@@ -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, created_at)
1116
- 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.createdAt);
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 = 4;
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 >= CURRENT_SCHEMA_VERSION) {
4317
- ensureBrokerStateRow(db);
4318
- return;
4319
- }
4320
- db.exec("BEGIN EXCLUSIVE");
4321
- try {
4322
- runMigrationBody(db);
4323
- db.pragma(`user_version = ${CURRENT_SCHEMA_VERSION}`);
4324
- ensureBrokerStateRow(db);
4325
- db.exec("COMMIT");
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 captured = await input.captureHandbackText() ?? "";
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
- clipboardText = await input.captureHandbackText?.() ?? null;
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
- const requestText = captureStatus === "ok" ? clipboardText ?? "" : "";
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 captureClipboardHandback({
7971
- triggerCopy: () => submitInjectedInput2("/copy"),
7972
- // Some provider versions show a picker after /copy (e.g. Claude Code's
7973
- // content-type picker). Send Enter to confirm the default selection
7974
- // if the clipboard has not changed yet after the trigger delay.
7975
- confirmPicker: () => {
7976
- writeInjectedInput("mounted-submit-picker", "\r");
7977
- },
7978
- triggerDelayMs: 300
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/start.ts
8387
+ // src/commands/collab/recover.ts
8098
8388
  var READY_TIMEOUT_DEFAULT = 3e4;
8099
- async function runCollabStart(opts) {
8100
- const root = getStateRoot();
8101
- mkdirSync3(root, { recursive: true });
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 workspaceRoot = canonicalWorkspaceRoot(opts.cwd);
8106
- const workspaceId = workspaceIdFromPath(opts.cwd);
8107
- const now = opts.now();
8108
- const collabId = createCliCollabId(now);
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 orchestratorEnabled = process.env.AI_WHISPER_RELAY_ORCHESTRATOR_ENABLED !== "0";
8111
- const orchestratorMaxRounds = Math.max(
8112
- 1,
8113
- Number(process.env.AI_WHISPER_RELAY_ORCHESTRATOR_MAX_ROUNDS ?? "3") || 3
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
- upsertWorkspace(db, { id: workspaceId, workspaceRoot, now });
8139
- const active = db.prepare(
8140
- "SELECT collab_id FROM collab WHERE workspace_id = ? AND status = 'active' LIMIT 1"
8141
- ).get(workspaceId);
8142
- if (active) {
8143
- throw new Error(
8144
- `active collab ${active.collab_id} already exists for workspace ${workspaceRoot}`
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
- if (err.message === "ALL_CANDIDATES_TAKEN") {
8190
- db.close();
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
- return {
8238
- collabId,
8239
- port: allocatedPort,
8240
- host,
8241
- // biome-ignore lint/style/noNonNullAssertion: guarded by cleanupOnFailure
8242
- pid: finalRow.pid
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
- if (input.launch.tmuxSession) {
8249
- const now = (/* @__PURE__ */ new Date()).toISOString();
8250
- db.prepare(
8251
- "UPDATE collab SET tmux_session = ?, updated_at = ? WHERE collab_id = ?"
8252
- ).run(input.launch.tmuxSession, now, input.collabId);
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 defaultIsPidAlive(pid) {
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
- return tryResolve();
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 ?? defaultIsPidAlive;
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 existsSync7 } from "node:fs";
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 join7 } from "node:path";
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 (!existsSync7(sqlitePath)) {
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 dirname3, resolve as resolve4 } from "node:path";
10871
- import { fileURLToPath as fileURLToPath2 } from "node:url";
10872
- var __dirname = dirname3(fileURLToPath2(import.meta.url));
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 fileURLToPath3 } from "node:url";
11441
+ import { fileURLToPath as fileURLToPath4 } from "node:url";
11115
11442
  function defaultBundledSkillsDir() {
11116
- const here = path3.dirname(fileURLToPath3(import.meta.url));
11443
+ const here = path3.dirname(fileURLToPath4(import.meta.url));
11117
11444
  return path3.resolve(here, "..", "..", "skills");
11118
11445
  }
11119
11446
  function homeForTarget(target, fakeHome) {