ai-whisper 0.6.0 → 0.6.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/README.md CHANGED
@@ -61,6 +61,8 @@ You pair any two of three agents — `claude`, `codex`, and `ezio`. ai-whisper d
61
61
  - **An LLM evaluator with credentials** — workflows are gated by it and refuse to start without it. See [Evaluator configuration](docs/evaluator-configuration.md).
62
62
  - **tmux** *(optional)* — only for `whisper collab start`, which auto-arranges both agents into panes. The mount flow below does not need it.
63
63
 
64
+ > **Platform support:** ai-whisper is terminal-native and Unix-oriented — it drives interactive PTY sessions, so it runs on **macOS and Linux**. It is **not supported natively on Windows**: `whisper collab mount` / `reconnect` require a Unix tty-backed shell and will exit with an error pointing here. On Windows, run ai-whisper inside **[WSL2](https://learn.microsoft.com/windows/wsl/install)** — install Node, your agent CLI, and ai-whisper inside the WSL2 distro and run the commands there, where everything works as-is.
65
+
64
66
  ## Safety & permissions
65
67
 
66
68
  ai-whisper launches each agent in **full-autonomy mode** so the relay can drive it unattended — `claude --dangerously-skip-permissions` and `codex --dangerously-bypass-approvals-and-sandbox`. Inside the mounted workspace the agents read, write, and run commands without prompting. Point it at code you're willing to let two agents change autonomously, watch the run on the dashboard, and remember you are the final gatekeeper — review the result before you ship it. The deeper rationale is in [Concepts](docs/concepts.md).
@@ -4386,7 +4386,7 @@ function enforceOneActiveCollabPerWorkspace(db, options = {}) {
4386
4386
 
4387
4387
  // ../broker/dist/storage/clipboard-capture-lease.js
4388
4388
  var LEASE_ID = 1;
4389
- var DEFAULT_LEASE_TTL_MS = 5e3;
4389
+ var DEFAULT_LEASE_TTL_MS = 25e3;
4390
4390
  function defaultIsPidAlive2(pid) {
4391
4391
  try {
4392
4392
  process.kill(pid, 0);
@@ -4406,7 +4406,7 @@ function enforceOneActiveCollabPerWorkspace(db, options = {}) {
4406
4406
 
4407
4407
  // ../broker/dist/storage/clipboard-capture-lease.js
4408
4408
  var LEASE_ID = 1;
4409
- var DEFAULT_LEASE_TTL_MS = 5e3;
4409
+ var DEFAULT_LEASE_TTL_MS = 25e3;
4410
4410
  function defaultIsPidAlive2(pid) {
4411
4411
  try {
4412
4412
  process.kill(pid, 0);
@@ -4376,7 +4376,7 @@ function enforceOneActiveCollabPerWorkspace(db, options = {}) {
4376
4376
 
4377
4377
  // ../broker/dist/storage/clipboard-capture-lease.js
4378
4378
  var LEASE_ID = 1;
4379
- var DEFAULT_LEASE_TTL_MS = 5e3;
4379
+ var DEFAULT_LEASE_TTL_MS = 25e3;
4380
4380
  function defaultIsPidAlive2(pid) {
4381
4381
  try {
4382
4382
  process.kill(pid, 0);
@@ -4877,7 +4877,7 @@ function acquireCaptureLease(db, collabId, pid, options = {}) {
4877
4877
  const tx = db.transaction(() => {
4878
4878
  const row = db.prepare("SELECT holder_collab_id, holder_pid, acquired_at FROM clipboard_capture_lease WHERE id = ?").get(LEASE_ID);
4879
4879
  if (row && !isStale(row, opts))
4880
- return false;
4880
+ return null;
4881
4881
  const acquiredAt = new Date(opts.now()).toISOString();
4882
4882
  db.prepare(`INSERT INTO clipboard_capture_lease (id, holder_collab_id, holder_pid, acquired_at)
4883
4883
  VALUES (?, ?, ?, ?)
@@ -4885,13 +4885,19 @@ function acquireCaptureLease(db, collabId, pid, options = {}) {
4885
4885
  holder_collab_id = excluded.holder_collab_id,
4886
4886
  holder_pid = excluded.holder_pid,
4887
4887
  acquired_at = excluded.acquired_at`).run(LEASE_ID, collabId, pid, acquiredAt);
4888
- return true;
4888
+ return acquiredAt;
4889
4889
  });
4890
4890
  return tx.immediate();
4891
4891
  }
4892
- function releaseCaptureLease(db, collabId) {
4892
+ function releaseCaptureLease(db, collabId, token) {
4893
+ const tx = db.transaction(() => {
4894
+ db.prepare("UPDATE clipboard_capture_lease SET holder_collab_id = NULL, holder_pid = NULL, acquired_at = NULL WHERE id = ? AND holder_collab_id = ? AND acquired_at = ?").run(LEASE_ID, collabId, token);
4895
+ });
4896
+ tx();
4897
+ }
4898
+ function releaseCaptureLeaseForHolderPid(db, collabId, pid) {
4893
4899
  const tx = db.transaction(() => {
4894
- 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);
4900
+ db.prepare("UPDATE clipboard_capture_lease SET holder_collab_id = NULL, holder_pid = NULL, acquired_at = NULL WHERE id = ? AND holder_collab_id = ? AND holder_pid = ?").run(LEASE_ID, collabId, pid);
4895
4901
  });
4896
4902
  tx();
4897
4903
  }
@@ -4912,7 +4918,7 @@ var init_clipboard_capture_lease = __esm({
4912
4918
  "../broker/dist/storage/clipboard-capture-lease.js"() {
4913
4919
  "use strict";
4914
4920
  LEASE_ID = 1;
4915
- DEFAULT_LEASE_TTL_MS = 5e3;
4921
+ DEFAULT_LEASE_TTL_MS = 25e3;
4916
4922
  }
4917
4923
  });
4918
4924
 
@@ -8006,6 +8012,11 @@ function lookupByCwd(db, cwd) {
8006
8012
  import { execFileSync as execFileSync2 } from "node:child_process";
8007
8013
  import { existsSync as existsSync6 } from "node:fs";
8008
8014
  function resolveCurrentTty() {
8015
+ if (process.platform === "win32") {
8016
+ throw new Error(
8017
+ "This command is not supported natively on Windows: it requires a Unix tty-backed shell to drive an interactive PTY session. Run ai-whisper inside WSL2 (Windows Subsystem for Linux) instead \u2014 install Node, your agent CLI, and ai-whisper inside your WSL2 distro and run the command there. Setup guide: https://learn.microsoft.com/windows/wsl/install"
8018
+ );
8019
+ }
8009
8020
  const stdin = process.stdin;
8010
8021
  const ttyPath = stdin.isTTY && typeof stdin.path === "string" ? stdin.path : execFileSync2("tty", [], {
8011
8022
  encoding: "utf8",
@@ -12603,8 +12614,8 @@ function compareSemver(a, b) {
12603
12614
  var EZIO_PROVENANCE = {
12604
12615
  ezioCliVersion: "0.2.0-beta.5",
12605
12616
  ezioGitSha: "a701ee7",
12606
- builtAt: "2026-06-19T11:36:33.762Z",
12607
- whisperVersion: "0.6.0"
12617
+ builtAt: "2026-06-21T14:21:01.635Z",
12618
+ whisperVersion: "0.6.1"
12608
12619
  };
12609
12620
 
12610
12621
  // src/ezio-provenance-types.ts
@@ -12904,6 +12915,22 @@ function isAutonomousHandoff(handoffId, broker) {
12904
12915
  const chain = broker.control.getRelayChain?.(meta.chainId);
12905
12916
  return wf?.status === "running" && chain?.status === "active";
12906
12917
  }
12918
+ var CAPTURE_WATCHDOG_TIMEOUT = /* @__PURE__ */ Symbol("capture-watchdog-timeout");
12919
+ async function raceCaptureAgainstWatchdog(run, watchdogMs, schedule) {
12920
+ let cancel = () => {
12921
+ };
12922
+ const watchdog = new Promise((resolve5) => {
12923
+ cancel = schedule(watchdogMs, () => resolve5(CAPTURE_WATCHDOG_TIMEOUT));
12924
+ });
12925
+ try {
12926
+ return await Promise.race([
12927
+ Promise.resolve(run()).then((r) => r ?? null),
12928
+ watchdog
12929
+ ]);
12930
+ } finally {
12931
+ cancel();
12932
+ }
12933
+ }
12907
12934
  function createMountedTurnOwnedRelay(input) {
12908
12935
  function resolveCols() {
12909
12936
  const c = input.getTerminalCols?.() ?? process.stdout.columns;
@@ -12913,6 +12940,11 @@ function createMountedTurnOwnedRelay(input) {
12913
12940
  const HAND_BACK_READY_AFTER_MS = 3e4;
12914
12941
  const autoHandbackMaxAttempts = Math.max(1, input.autoHandbackMaxAttempts ?? 3);
12915
12942
  const autoHandbackRetryMs = Math.max(0, input.autoHandbackRetryMs ?? 1e4);
12943
+ const captureWatchdogMs2 = Math.max(1, input.captureWatchdogMs ?? 2e4);
12944
+ const scheduleWatchdog = input.scheduleWatchdog ?? ((ms, onTimeout) => {
12945
+ const timer = setTimeout(onTimeout, ms);
12946
+ return () => clearTimeout(timer);
12947
+ });
12916
12948
  const autoHandbackAttempts = /* @__PURE__ */ new Map();
12917
12949
  let autoHandbackInFlight = false;
12918
12950
  let disconnectHandled = false;
@@ -13533,15 +13565,30 @@ ${hint}`,
13533
13565
  let leaseDegraded = false;
13534
13566
  let interferenceDetected = false;
13535
13567
  let cleanCaptureEmpty = false;
13568
+ let captureTimedOut = false;
13536
13569
  try {
13537
- const captureResult = await input.captureHandbackText?.(turnResult.text ?? "") ?? null;
13538
- if (typeof captureResult === "string") {
13570
+ const captureResult = await raceCaptureAgainstWatchdog(
13571
+ () => input.captureHandbackText?.(turnResult.text ?? ""),
13572
+ captureWatchdogMs2,
13573
+ scheduleWatchdog
13574
+ );
13575
+ if (captureResult === CAPTURE_WATCHDOG_TIMEOUT) {
13576
+ captureTimedOut = true;
13577
+ console.warn(
13578
+ `[ai-whisper] capture watchdog fired (${captureWatchdogMs2}ms): target=${input.currentAgent} handoff=${accepted.handoffId} \u2014 abandoning capture, will retry`
13579
+ );
13580
+ } else if (typeof captureResult === "string") {
13539
13581
  clipboardText = captureResult;
13540
13582
  } else if (captureResult !== null) {
13541
13583
  interferenceDetected = captureResult.interferenceDetected;
13542
13584
  if (captureResult.status === "captured") {
13543
13585
  clipboardText = captureResult.text;
13544
13586
  cleanCaptureEmpty = (captureResult.text ?? "").trim().length === 0;
13587
+ } else if (captureResult.status === "timed_out" || captureResult.status === "lease_unavailable") {
13588
+ captureTimedOut = true;
13589
+ console.warn(
13590
+ `[ai-whisper] capture ${captureResult.status}: target=${input.currentAgent} handoff=${accepted.handoffId} \u2014 will retry (no PTY fallback)`
13591
+ );
13545
13592
  } else {
13546
13593
  leaseDegraded = true;
13547
13594
  console.warn(
@@ -13571,7 +13618,7 @@ ${msg}`
13571
13618
  const capturedNothing = captureStatus === "no_response_captured";
13572
13619
  const staleAgainstCurrentTurn = captureStatus === "no_response_captured_confidently" && classification.jaccardScore !== null;
13573
13620
  const emptyClipConfidentMiss = captureStatus === "no_response_captured_confidently" && cleanCaptureEmpty;
13574
- if ((capturedNothing || staleAgainstCurrentTurn || emptyClipConfidentMiss) && retryState.attempts + 1 < autoHandbackMaxAttempts) {
13621
+ if ((capturedNothing || staleAgainstCurrentTurn || emptyClipConfidentMiss || captureTimedOut) && retryState.attempts + 1 < autoHandbackMaxAttempts) {
13575
13622
  retryState.attempts += 1;
13576
13623
  retryState.nextEligibleAt = Date.now() + autoHandbackRetryMs;
13577
13624
  autoHandbackAttempts.set(accepted.handoffId, retryState);
@@ -13872,25 +13919,40 @@ function createAssistantTurnCapture() {
13872
13919
 
13873
13920
  // src/runtime/clipboard-handback-capture.ts
13874
13921
  import { execFile as execFile2 } from "node:child_process";
13875
- function execFileText(command, args = []) {
13922
+ var CaptureIoTimeoutError = class extends Error {
13923
+ constructor(command) {
13924
+ super(`clipboard I/O timed out: ${command}`);
13925
+ this.name = "CaptureIoTimeoutError";
13926
+ }
13927
+ };
13928
+ function execFileText(command, args = [], timeoutMs) {
13876
13929
  return new Promise((resolve5, reject) => {
13877
- execFile2(command, args, { encoding: "utf8" }, (error, stdout) => {
13878
- if (error) {
13879
- reject(
13880
- error instanceof Error ? error : new Error("Clipboard command failed")
13881
- );
13882
- return;
13930
+ execFile2(
13931
+ command,
13932
+ args,
13933
+ { encoding: "utf8", timeout: timeoutMs ?? 0, killSignal: "SIGTERM" },
13934
+ (error, stdout) => {
13935
+ if (error) {
13936
+ if (error.killed) {
13937
+ reject(new CaptureIoTimeoutError(command));
13938
+ return;
13939
+ }
13940
+ reject(
13941
+ error instanceof Error ? error : new Error("Clipboard command failed")
13942
+ );
13943
+ return;
13944
+ }
13945
+ resolve5(stdout);
13883
13946
  }
13884
- resolve5(stdout);
13885
- });
13947
+ );
13886
13948
  });
13887
13949
  }
13888
13950
  async function captureClipboardHandback(input) {
13889
13951
  const readClipboard = input.readClipboard ?? (() => {
13890
- if (process.platform !== "darwin") {
13952
+ if ((input.platform ?? process.platform) !== "darwin") {
13891
13953
  return Promise.resolve("");
13892
13954
  }
13893
- return execFileText("pbpaste");
13955
+ return execFileText("pbpaste", [], input.clipboardTimeoutMs);
13894
13956
  });
13895
13957
  const sleep3 = input.sleep ?? ((ms) => new Promise((resolve5) => setTimeout(resolve5, ms)));
13896
13958
  const attempts = input.attempts ?? 10;
@@ -13936,7 +13998,7 @@ async function captureHandbackText(input) {
13936
13998
  const recaptureAttempts = input.recaptureAttempts ?? 2;
13937
13999
  const recaptureBackoffMs = input.recaptureBackoffMs ?? 50;
13938
14000
  const sig = input.copySignature;
13939
- let acquired = false;
14001
+ let acquired = null;
13940
14002
  const deadline = Date.now() + acquireMaxWaitMs;
13941
14003
  for (; ; ) {
13942
14004
  try {
@@ -13947,21 +14009,30 @@ async function captureHandbackText(input) {
13947
14009
  input.leaseOptions
13948
14010
  );
13949
14011
  } catch {
13950
- acquired = false;
14012
+ acquired = null;
13951
14013
  }
13952
14014
  if (acquired) break;
13953
14015
  if (Date.now() >= deadline) break;
13954
14016
  await sleep3(acquireBackoffMs);
13955
14017
  }
13956
14018
  if (!acquired) {
13957
- return { status: "degraded_pty_only", text: null, interferenceDetected: false };
14019
+ return { status: "lease_unavailable", text: null, interferenceDetected: false };
13958
14020
  }
14021
+ const leaseToken = acquired;
13959
14022
  try {
13960
14023
  let interferenceDetected = false;
13961
14024
  for (let attempt = 0; attempt <= recaptureAttempts; attempt += 1) {
13962
14025
  if (attempt > 0) await sleep3(recaptureBackoffMs);
13963
14026
  const c0 = await input.readChangeCount();
13964
- const clip = await input.runCapture();
14027
+ let clip;
14028
+ try {
14029
+ clip = await input.runCapture();
14030
+ } catch (err) {
14031
+ if (err instanceof CaptureIoTimeoutError) {
14032
+ return { status: "timed_out", text: null, interferenceDetected };
14033
+ }
14034
+ throw err;
14035
+ }
13965
14036
  const cn = await input.readChangeCount();
13966
14037
  const checkAvailable = c0 !== null && cn !== null;
13967
14038
  const delta = checkAvailable ? cn - c0 : null;
@@ -13991,7 +14062,7 @@ async function captureHandbackText(input) {
13991
14062
  }
13992
14063
  return { status: "degraded_pty_only", text: null, interferenceDetected: true };
13993
14064
  } finally {
13994
- releaseCaptureLease(input.db, input.collabId);
14065
+ releaseCaptureLease(input.db, input.collabId, leaseToken);
13995
14066
  }
13996
14067
  }
13997
14068
 
@@ -14001,15 +14072,20 @@ import { existsSync as existsSync11 } from "node:fs";
14001
14072
  import { fileURLToPath as fileURLToPath3 } from "node:url";
14002
14073
  import { dirname as dirname6, join as join20 } from "node:path";
14003
14074
  var HELPER_BIN_NAME = "clipboard-change-count";
14004
- function execFileText2(command, args = []) {
14075
+ function execFileText2(command, args = [], timeoutMs) {
14005
14076
  return new Promise((resolve5, reject) => {
14006
- execFile3(command, args, { encoding: "utf8" }, (error, stdout) => {
14007
- if (error) {
14008
- reject(error instanceof Error ? error : new Error("helper failed"));
14009
- return;
14077
+ execFile3(
14078
+ command,
14079
+ args,
14080
+ { encoding: "utf8", timeout: timeoutMs ?? 0, killSignal: "SIGTERM" },
14081
+ (error, stdout) => {
14082
+ if (error) {
14083
+ reject(error instanceof Error ? error : new Error("helper failed"));
14084
+ return;
14085
+ }
14086
+ resolve5(stdout);
14010
14087
  }
14011
- resolve5(stdout);
14012
- });
14088
+ );
14013
14089
  });
14014
14090
  }
14015
14091
  function makeChangeCountReader(deps) {
@@ -14018,7 +14094,7 @@ function makeChangeCountReader(deps) {
14018
14094
  const here = dirname6(fileURLToPath3(import.meta.url));
14019
14095
  const bin = join20(here, "..", "native", HELPER_BIN_NAME);
14020
14096
  if (!existsSync11(bin)) throw new Error("changeCount helper not built");
14021
- return execFileText2(bin);
14097
+ return execFileText2(bin, [], deps?.timeoutMs);
14022
14098
  });
14023
14099
  return async () => {
14024
14100
  if (platform !== "darwin") return null;
@@ -14133,7 +14209,13 @@ function resolvePositiveIntEnv(name) {
14133
14209
  const n = Number(process.env[name] ?? "");
14134
14210
  return Number.isFinite(n) && n > 0 ? Math.floor(n) : void 0;
14135
14211
  }
14136
- var readChangeCount = makeChangeCountReader();
14212
+ var clipboardIoTimeoutMs = resolvePositiveIntEnv("AI_WHISPER_CLIPBOARD_IO_TIMEOUT_MS") ?? 8e3;
14213
+ var captureWatchdogMs = resolvePositiveIntEnv("AI_WHISPER_CAPTURE_WATCHDOG_MS") ?? 2e4;
14214
+ var captureLeaseTtlMs = Math.max(
14215
+ captureWatchdogMs + 5e3,
14216
+ DEFAULT_LEASE_TTL_MS
14217
+ );
14218
+ var readChangeCount = makeChangeCountReader({ timeoutMs: clipboardIoTimeoutMs });
14137
14219
  function safeReapSessions(input) {
14138
14220
  const reap = input.reap ?? ((collabId, agentType, keepSessionId) => {
14139
14221
  const db = openDatabase(getSharedSqlitePath());
@@ -14245,7 +14327,11 @@ function createMountSessionRuntime(input) {
14245
14327
  agentType: input.target,
14246
14328
  attachmentKind: "mounted"
14247
14329
  });
14248
- releaseCaptureLease(db, resolvedClaim.collabId);
14330
+ releaseCaptureLeaseForHolderPid(
14331
+ db,
14332
+ resolvedClaim.collabId,
14333
+ process.pid
14334
+ );
14249
14335
  } finally {
14250
14336
  db.close();
14251
14337
  }
@@ -14453,6 +14539,7 @@ function createMountSessionRuntime(input) {
14453
14539
  turnText,
14454
14540
  readChangeCount,
14455
14541
  copySignature,
14542
+ leaseOptions: { ttlMs: captureLeaseTtlMs },
14456
14543
  runCapture: () => captureClipboardHandback({
14457
14544
  triggerCopy: () => submitInjectedInput2("/copy"),
14458
14545
  // Some provider versions show a picker after /copy (e.g. Claude
@@ -14461,7 +14548,8 @@ function createMountSessionRuntime(input) {
14461
14548
  confirmPicker: () => {
14462
14549
  writeInjectedInput("mounted-submit-picker", "\r");
14463
14550
  },
14464
- triggerDelayMs: 300
14551
+ triggerDelayMs: 300,
14552
+ clipboardTimeoutMs: clipboardIoTimeoutMs
14465
14553
  })
14466
14554
  });
14467
14555
  } finally {
@@ -14490,6 +14578,7 @@ function createMountSessionRuntime(input) {
14490
14578
  autoHandbackRetryMs: resolvePositiveIntEnv(
14491
14579
  "AI_WHISPER_AUTO_HANDBACK_RETRY_MS"
14492
14580
  ),
14581
+ captureWatchdogMs,
14493
14582
  // Protocol-native providers (ai-ezio) expose onTurnFinished; for them
14494
14583
  // the quiescence /copy auto-handback is skipped (handback comes from
14495
14584
  // the explicit idle event). Event-enabled claude/codex ALSO suppress it
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-whisper",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Terminal-first relay for paired AI coding agents (Claude + Codex), driven by structured workflows.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -53,10 +53,10 @@
53
53
  "ink-testing-library": "^4.0.0",
54
54
  "@ai-whisper/adapter-ai-ezio": "0.0.0",
55
55
  "@ai-whisper/adapter-claude": "0.0.0",
56
- "@ai-whisper/companion-core": "0.0.0",
56
+ "@ai-whisper/adapter-codex": "0.0.0",
57
57
  "@ai-whisper/broker": "0.0.0",
58
- "@ai-whisper/shared": "0.0.0",
59
- "@ai-whisper/adapter-codex": "0.0.0"
58
+ "@ai-whisper/companion-core": "0.0.0",
59
+ "@ai-whisper/shared": "0.0.0"
60
60
  },
61
61
  "files": [
62
62
  "dist",