open-agents-ai 0.103.82 → 0.103.85

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.
Files changed (2) hide show
  1. package/dist/index.js +293 -89
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1343,10 +1343,22 @@ var init_shell = __esm({
1343
1343
  workingDir;
1344
1344
  defaultTimeout;
1345
1345
  _sudoPassword = null;
1346
+ /** Active child processes — killed on abort for instant /stop */
1347
+ _activeChildren = /* @__PURE__ */ new Set();
1346
1348
  constructor(workingDir, defaultTimeout = 3e4) {
1347
1349
  this.workingDir = workingDir;
1348
1350
  this.defaultTimeout = defaultTimeout;
1349
1351
  }
1352
+ /** Kill all active child processes immediately (called by /stop) */
1353
+ killAll() {
1354
+ for (const child of this._activeChildren) {
1355
+ try {
1356
+ child.kill("SIGKILL");
1357
+ } catch {
1358
+ }
1359
+ }
1360
+ this._activeChildren.clear();
1361
+ }
1350
1362
  /** Set a cached sudo password for the session. The shell tool will
1351
1363
  * automatically retry permission-denied commands with `sudo -S`. */
1352
1364
  setSudoPassword(password) {
@@ -1397,17 +1409,14 @@ ${stdinInput ?? ""}`);
1397
1409
  cwd: this.workingDir,
1398
1410
  env: {
1399
1411
  ...process.env,
1400
- // Non-interactive mode: libraries like prompts, inquirer,
1401
- // create-next-app, and many Node CLI tools check CI to skip
1402
- // interactive prompts and use defaults instead.
1403
1412
  CI: "true",
1404
1413
  NONINTERACTIVE: "1",
1405
- // Disable color in most tools to get clean output
1406
1414
  NO_COLOR: "1",
1407
1415
  FORCE_COLOR: "0"
1408
1416
  },
1409
1417
  stdio: ["pipe", "pipe", "pipe"]
1410
1418
  });
1419
+ this._activeChildren.add(child);
1411
1420
  let stdout = "";
1412
1421
  let stderr = "";
1413
1422
  let killed = false;
@@ -1450,6 +1459,7 @@ ${stdinInput ?? ""}`);
1450
1459
  }
1451
1460
  child.stdin.end();
1452
1461
  child.on("exit", (code) => {
1462
+ this._activeChildren.delete(child);
1453
1463
  exitFlushTimer = setTimeout(() => {
1454
1464
  const durationMs = performance.now() - start;
1455
1465
  if (killed) {
@@ -4703,6 +4713,24 @@ Process error: ${err.message}`;
4703
4713
  entry.finishedAt = Date.now();
4704
4714
  return true;
4705
4715
  }
4716
+ /** Kill ALL running tasks immediately (for /stop and /quit authority) */
4717
+ stopAll() {
4718
+ let killed = 0;
4719
+ for (const [, entry] of this.tasks) {
4720
+ if (entry.status !== "running")
4721
+ continue;
4722
+ if (entry.child) {
4723
+ try {
4724
+ entry.child.kill("SIGKILL");
4725
+ } catch {
4726
+ }
4727
+ }
4728
+ entry.status = "stopped";
4729
+ entry.finishedAt = Date.now();
4730
+ killed++;
4731
+ }
4732
+ return killed;
4733
+ }
4706
4734
  list() {
4707
4735
  return Array.from(this.tasks.values()).map(toInfo);
4708
4736
  }
@@ -12432,8 +12460,8 @@ async function handleCmd(cmd) {
12432
12460
  // succeeds in <1s. Racing them eliminates the wait.
12433
12461
  dlog('remote_infer: invoking ' + riCapName + ' on peer ' + riTargetPeer.slice(0, 20) + ' (libp2p' + (_natsConn ? ' + NATS parallel' : '') + ')');
12434
12462
  try {
12435
- var RI_TIMEOUT = 300000;
12436
- var riInvokeP = nexus.invokeCapability(riTargetPeer, riCapName, riData, { stream: false, maxDurationMs: 240000 });
12463
+ var RI_TIMEOUT = 60000; // 60s outer timeout \u2014 fail fast for interactive use
12464
+ var riInvokeP = nexus.invokeCapability(riTargetPeer, riCapName, riData, { stream: false, maxDurationMs: 45000 });
12437
12465
  var riTimeoutP = new Promise(function(_, reject) { setTimeout(function() { reject(new Error('remote_infer timed out after ' + (RI_TIMEOUT / 1000) + 's')); }, RI_TIMEOUT); });
12438
12466
 
12439
12467
  // Build race candidates \u2014 libp2p + timeout always present
@@ -12444,7 +12472,7 @@ async function handleCmd(cmd) {
12444
12472
  var _riNatsRaceP = (async function() {
12445
12473
  var _rSubject = 'nexus.invoke.' + riTargetPeer;
12446
12474
  var _rPayload = JSON.stringify({ capability: riCapName, input: riData, from: nexus.peerId || '' });
12447
- var _rResp = await _natsConn.request(_rSubject, _natsCodec.encode(_rPayload), { timeout: 240000 });
12475
+ var _rResp = await _natsConn.request(_rSubject, _natsCodec.encode(_rPayload), { timeout: 45000 });
12448
12476
  var _rResult = JSON.parse(_natsCodec.decode(_rResp.data));
12449
12477
  if (_rResult && _rResult.error) throw new Error('NATS: ' + _rResult.error);
12450
12478
  dlog('remote_infer: NATS won the race');
@@ -12483,8 +12511,8 @@ async function handleCmd(cmd) {
12483
12511
  var rPid = riCandidates[ri];
12484
12512
  dlog('remote_infer: trying peer ' + rPid.slice(0, 20) + '...');
12485
12513
  try {
12486
- var PEER_INVOKE_TIMEOUT = 180000; // 3 min per peer \u2014 remote API inference can be slow
12487
- var specInvoke = nexus.invokeCapability(rPid, riCapName, riData, { stream: false, maxDurationMs: 150000 });
12514
+ var PEER_INVOKE_TIMEOUT = 30000; // 30s per peer in discovery \u2014 fail fast, try next
12515
+ var specInvoke = nexus.invokeCapability(rPid, riCapName, riData, { stream: false, maxDurationMs: 25000 });
12488
12516
  var specTimeout = new Promise(function(_, reject) {
12489
12517
  setTimeout(function() { reject(new Error('peer invoke timeout')); }, PEER_INVOKE_TIMEOUT);
12490
12518
  });
@@ -12513,7 +12541,7 @@ async function handleCmd(cmd) {
12513
12541
  try {
12514
12542
  var _dpSubject = 'nexus.invoke.' + _dpPid;
12515
12543
  var _dpPayload = JSON.stringify({ capability: riCapName, input: riData, from: nexus.peerId || '' });
12516
- var _dpResp = await _natsConn.request(_dpSubject, _natsCodec.encode(_dpPayload), { timeout: 180000 });
12544
+ var _dpResp = await _natsConn.request(_dpSubject, _natsCodec.encode(_dpPayload), { timeout: 30000 });
12517
12545
  var _dpResult = JSON.parse(_natsCodec.decode(_dpResp.data));
12518
12546
  if (_dpResult && !_dpResult.error) {
12519
12547
  riResult = _dpResult;
@@ -18628,6 +18656,7 @@ var init_agenticRunner = __esm({
18628
18656
  handlers = [];
18629
18657
  pendingUserMessages = [];
18630
18658
  aborted = false;
18659
+ _abortController = new AbortController();
18631
18660
  _paused = false;
18632
18661
  _pauseResolve = null;
18633
18662
  _sudoPassword = null;
@@ -18837,14 +18866,30 @@ ${this.options.dynamicContext}`,
18837
18866
  injectUserMessage(content) {
18838
18867
  this.pendingUserMessages.push(content);
18839
18868
  }
18840
- /** Abort the current task run */
18869
+ /** Retract the last pending user message (Esc cancel).
18870
+ * Returns the retracted text, or null if queue is empty or already consumed. */
18871
+ retractLastPendingMessage() {
18872
+ if (this.pendingUserMessages.length === 0)
18873
+ return null;
18874
+ return this.pendingUserMessages.pop();
18875
+ }
18876
+ /** Abort the current task run — cancels in-flight requests and kills child processes */
18841
18877
  abort() {
18842
18878
  this.aborted = true;
18879
+ this._abortController.abort();
18880
+ const shellTool = this.tools.get("shell");
18881
+ if (shellTool && typeof shellTool.killAll === "function") {
18882
+ shellTool.killAll();
18883
+ }
18843
18884
  if (this._pauseResolve) {
18844
18885
  this._pauseResolve();
18845
18886
  this._pauseResolve = null;
18846
18887
  }
18847
18888
  }
18889
+ /** Get the current abort signal for passing to fetch/spawn */
18890
+ get abortSignal() {
18891
+ return this._abortController.signal;
18892
+ }
18848
18893
  /**
18849
18894
  * Pause the current task gracefully. The run loop will suspend at the next
18850
18895
  * turn boundary (between tool calls / model requests) without destroying state.
@@ -18955,6 +19000,11 @@ Respond with your assessment, then take action.`;
18955
19000
  }
18956
19001
  /** Run a task through the agentic loop */
18957
19002
  async run(task, context) {
19003
+ this.aborted = false;
19004
+ this._abortController = new AbortController();
19005
+ if (typeof this.backend.setAbortSignal === "function") {
19006
+ this.backend.setAbortSignal(this._abortController.signal);
19007
+ }
18958
19008
  const start = Date.now();
18959
19009
  const taskTimeoutMs = this.options.taskTimeoutMs;
18960
19010
  const selfEvalInterval = taskTimeoutMs;
@@ -19101,6 +19151,10 @@ If you're stuck, try a completely different approach. Do NOT repeat what failed
19101
19151
  try {
19102
19152
  response = this.options.streamEnabled && this.hasStreamingSupport() ? await this.streamingRequest(chatRequest, turn) : await this.backend.chatCompletion(chatRequest);
19103
19153
  } catch (reqErr) {
19154
+ if (reqErr instanceof Error && reqErr.fatal) {
19155
+ this.emit({ type: "error", content: `Backend error: ${reqErr.message}`, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
19156
+ break;
19157
+ }
19104
19158
  const recovered = await this.retryOnTransient(reqErr, chatRequest, turn);
19105
19159
  if (!recovered) {
19106
19160
  const errMsg = reqErr instanceof Error ? reqErr.message : String(reqErr);
@@ -19615,6 +19669,10 @@ Integrate this guidance into your current approach. Continue working on the task
19615
19669
  try {
19616
19670
  response = this.options.streamEnabled && this.hasStreamingSupport() ? await this.streamingRequest(chatRequest, turn) : await this.backend.chatCompletion(chatRequest);
19617
19671
  } catch (reqErr) {
19672
+ if (this.aborted || reqErr instanceof Error && reqErr.name === "AbortError") {
19673
+ this.emit({ type: "error", content: "Task aborted by user", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
19674
+ break;
19675
+ }
19618
19676
  const recovered = await this.retryOnTransient(reqErr, chatRequest, turn);
19619
19677
  if (!recovered) {
19620
19678
  const errMsg2 = reqErr instanceof Error ? reqErr.message : String(reqErr);
@@ -20857,6 +20915,8 @@ ${transcript}`
20857
20915
  // -------------------------------------------------------------------------
20858
20916
  /** Detect whether an error is transient (worth retrying) */
20859
20917
  isTransientError(err) {
20918
+ if (err instanceof Error && err.fatal)
20919
+ return false;
20860
20920
  const msg = err instanceof Error ? err.message : String(err);
20861
20921
  if (/Backend HTTP (502|503|504)/i.test(msg))
20862
20922
  return true;
@@ -20866,6 +20926,10 @@ ${transcript}`
20866
20926
  return true;
20867
20927
  if (/model is loading|server busy|overloaded/i.test(msg))
20868
20928
  return true;
20929
+ if (/multiaddrs failed|all multiaddrs failed|dial to peer failed|stream was reset|Cannot reach peer/i.test(msg))
20930
+ return true;
20931
+ if (/NATS.*timeout|relay.*timeout|invoke.*timeout/i.test(msg))
20932
+ return true;
20869
20933
  return false;
20870
20934
  }
20871
20935
  /**
@@ -21004,12 +21068,18 @@ ${transcript}`
21004
21068
  model;
21005
21069
  apiKey;
21006
21070
  thinking;
21071
+ /** Abort signal — set by the runner so /stop can cancel in-flight requests */
21072
+ _abortSignal = null;
21007
21073
  constructor(baseUrl, model, apiKey, thinking) {
21008
21074
  this.baseUrl = normalizeBaseUrl(baseUrl);
21009
21075
  this.model = model;
21010
21076
  this.apiKey = apiKey ?? "";
21011
21077
  this.thinking = thinking ?? true;
21012
21078
  }
21079
+ /** Set the abort signal from the runner (called at run start) */
21080
+ setAbortSignal(signal) {
21081
+ this._abortSignal = signal;
21082
+ }
21013
21083
  /** Build auth headers — all providers use standard Bearer token auth. */
21014
21084
  authHeaders() {
21015
21085
  const headers = { "Content-Type": "application/json" };
@@ -21027,11 +21097,14 @@ ${transcript}`
21027
21097
  max_tokens: request.maxTokens,
21028
21098
  think: this.thinking
21029
21099
  };
21030
- const resp = await fetch(`${this.baseUrl}/v1/chat/completions`, {
21100
+ const fetchOpts = {
21031
21101
  method: "POST",
21032
21102
  headers: this.authHeaders(),
21033
21103
  body: JSON.stringify(body)
21034
- });
21104
+ };
21105
+ if (this._abortSignal)
21106
+ fetchOpts.signal = this._abortSignal;
21107
+ const resp = await fetch(`${this.baseUrl}/v1/chat/completions`, fetchOpts);
21035
21108
  if (!resp.ok) {
21036
21109
  const text = await resp.text().catch(() => "");
21037
21110
  const isHtml = text.trimStart().startsWith("<!") || text.trimStart().startsWith("<html");
@@ -21089,11 +21162,14 @@ ${transcript}`
21089
21162
  stream_options: { include_usage: true },
21090
21163
  think: this.thinking
21091
21164
  };
21092
- const resp = await fetch(`${this.baseUrl}/v1/chat/completions`, {
21165
+ const streamFetchOpts = {
21093
21166
  method: "POST",
21094
21167
  headers: this.authHeaders(),
21095
21168
  body: JSON.stringify(body)
21096
- });
21169
+ };
21170
+ if (this._abortSignal)
21171
+ streamFetchOpts.signal = this._abortSignal;
21172
+ const resp = await fetch(`${this.baseUrl}/v1/chat/completions`, streamFetchOpts);
21097
21173
  if (!resp.ok) {
21098
21174
  const text = await resp.text().catch(() => "");
21099
21175
  const isHtml = text.trimStart().startsWith("<!") || text.trimStart().startsWith("<html");
@@ -21181,6 +21257,15 @@ var init_nexusBackend = __esm({
21181
21257
  this.authKey = authKey || "";
21182
21258
  this.thinking = thinking ?? true;
21183
21259
  }
21260
+ /** Reset the consecutive failure counter (called on endpoint switch / reconnect) */
21261
+ resetFailures() {
21262
+ this.consecutiveFailures = 0;
21263
+ }
21264
+ /** Update target peer (allows switching peers without recreating the backend) */
21265
+ setTargetPeer(peerId) {
21266
+ this.targetPeer = peerId;
21267
+ this.consecutiveFailures = 0;
21268
+ }
21184
21269
  async chatCompletion(request) {
21185
21270
  if (this.consecutiveFailures >= _NexusAgenticBackend.MAX_CONSECUTIVE_FAILURES) {
21186
21271
  const err = new Error(`Remote peer unreachable after ${this.consecutiveFailures} attempts. Use /endpoint to switch to a local model or reconnect.`);
@@ -21217,7 +21302,10 @@ var init_nexusBackend = __esm({
21217
21302
  }
21218
21303
  if (parsed.success === false) {
21219
21304
  this.consecutiveFailures++;
21220
- throw new Error(`Remote inference failed: ${parsed.output || rawResult}`);
21305
+ const output = String(parsed.output || rawResult);
21306
+ const isDialErr = /multiaddrs failed|Cannot reach peer|dial to peer|timeout|ECONNREFUSED/i.test(output);
21307
+ const hint = isDialErr ? ` (${this.consecutiveFailures}/${_NexusAgenticBackend.MAX_CONSECUTIVE_FAILURES} failures \u2014 will stop after ${_NexusAgenticBackend.MAX_CONSECUTIVE_FAILURES})` : "";
21308
+ throw new Error(`Remote inference failed: ${output}${hint}`);
21221
21309
  }
21222
21310
  this.consecutiveFailures = 0;
21223
21311
  const invokeResult = parsed.result;
@@ -38670,6 +38758,8 @@ var init_voice = __esm({
38670
38758
  await this.ensureLuxtts();
38671
38759
  this.luxttsActive = true;
38672
38760
  this.mlxActive = false;
38761
+ this.ensureLuxttsDaemon().catch(() => {
38762
+ });
38673
38763
  } else if (isMlx) {
38674
38764
  await this.ensureMlxAudio();
38675
38765
  this.mlxActive = true;
@@ -38722,6 +38812,8 @@ var init_voice = __esm({
38722
38812
  await this.ensureLuxtts();
38723
38813
  this.luxttsActive = true;
38724
38814
  this.mlxActive = false;
38815
+ this.ensureLuxttsDaemon().catch(() => {
38816
+ });
38725
38817
  } else if (isMlx) {
38726
38818
  await this.ensureMlxAudio();
38727
38819
  this.mlxActive = true;
@@ -39308,7 +39400,7 @@ var init_voice = __esm({
39308
39400
  isMlxCapable() {
39309
39401
  return platform3() === "darwin" && process.arch === "arm64";
39310
39402
  }
39311
- /** Resolve python3 binary path */
39403
+ /** Resolve python3 binary path (cached after first call) */
39312
39404
  findPython3() {
39313
39405
  if (this.python3Path)
39314
39406
  return this.python3Path;
@@ -39324,6 +39416,52 @@ var init_voice = __esm({
39324
39416
  }
39325
39417
  return null;
39326
39418
  }
39419
+ /** Async version of findPython3 — non-blocking */
39420
+ async findPython3Async() {
39421
+ if (this.python3Path)
39422
+ return this.python3Path;
39423
+ for (const bin of ["python3", "python"]) {
39424
+ try {
39425
+ const path = await this.asyncShell(`which ${bin}`, 5e3);
39426
+ if (path) {
39427
+ this.python3Path = path;
39428
+ return path;
39429
+ }
39430
+ } catch {
39431
+ }
39432
+ }
39433
+ return null;
39434
+ }
39435
+ /** Non-blocking shell execution — async alternative to execSync.
39436
+ * Returns stdout string on exit 0, rejects otherwise. */
39437
+ asyncShell(command, timeoutMs = 3e4) {
39438
+ return new Promise((resolve31, reject) => {
39439
+ const proc = nodeSpawn("sh", ["-c", command], {
39440
+ stdio: ["ignore", "pipe", "pipe"],
39441
+ cwd: tmpdir6()
39442
+ });
39443
+ let stdout = "";
39444
+ let stderr = "";
39445
+ proc.stdout.on("data", (d) => {
39446
+ stdout += d.toString();
39447
+ });
39448
+ proc.stderr.on("data", (d) => {
39449
+ stderr += d.toString();
39450
+ });
39451
+ proc.on("error", reject);
39452
+ const timer = setTimeout(() => {
39453
+ proc.kill("SIGKILL");
39454
+ reject(new Error(`Timeout after ${timeoutMs}ms`));
39455
+ }, timeoutMs);
39456
+ proc.on("close", (code) => {
39457
+ clearTimeout(timer);
39458
+ if (code === 0)
39459
+ resolve31(stdout.trim());
39460
+ else
39461
+ reject(new Error(stderr.slice(0, 300) || `Exit code ${code}`));
39462
+ });
39463
+ });
39464
+ }
39327
39465
  /** Check if mlx-audio is installed */
39328
39466
  checkMlxInstalled() {
39329
39467
  if (this.mlxInstalled !== null)
@@ -39496,9 +39634,10 @@ Error: ${err2 instanceof Error ? err2.message : String(err2)}`);
39496
39634
  * Ensure LuxTTS venv is created with all dependencies.
39497
39635
  * On first run: creates venv, installs PyTorch + LuxTTS + deps (~5-15 min).
39498
39636
  * Subsequent runs: checks venv exists and import works.
39637
+ * All shell commands use asyncShell — never blocks the event loop.
39499
39638
  */
39500
39639
  async ensureLuxtts() {
39501
- const py = this.findPython3();
39640
+ const py = await this.findPython3Async();
39502
39641
  if (!py) {
39503
39642
  throw new Error("python3 not found. LuxTTS requires Python 3.10+. Try: apt install python3 / brew install python3");
39504
39643
  }
@@ -39506,23 +39645,25 @@ Error: ${err2 instanceof Error ? err2.message : String(err2)}`);
39506
39645
  const venvPy = luxttsVenvPy();
39507
39646
  if (existsSync34(venvPy)) {
39508
39647
  try {
39509
- execSync27(`${venvPy} -c "import sys; sys.path.insert(0, '${luxttsRepoDir()}'); from zipvoice.luxvoice import LuxTTS; print('ok')"`, { stdio: "pipe", timeout: 3e4 });
39648
+ await this.asyncShell(`${venvPy} -c "import sys; sys.path.insert(0, '${luxttsRepoDir()}'); from zipvoice.luxvoice import LuxTTS; print('ok')"`, 3e4);
39510
39649
  let hasCudaSys = false;
39511
39650
  try {
39512
- execSync27("nvidia-smi", { stdio: "pipe", timeout: 5e3 });
39651
+ await this.asyncShell("nvidia-smi", 5e3);
39513
39652
  hasCudaSys = true;
39514
39653
  } catch {
39515
39654
  }
39516
39655
  if (hasCudaSys) {
39517
- try {
39518
- const torchCheck = execSync27(`${venvPy} -c "import torch; print('cuda' if torch.cuda.is_available() else 'cpu')"`, { stdio: "pipe", timeout: 15e3 }).toString().trim();
39656
+ this.asyncShell(`${venvPy} -c "import torch; print('cuda' if torch.cuda.is_available() else 'cpu')"`, 15e3).then(async (torchCheck) => {
39519
39657
  if (torchCheck === "cpu") {
39520
- renderWarning("GPU detected but PyTorch is CPU-only. Reinstalling with CUDA support...");
39521
- execSync27(`${JSON.stringify(venvPy)} -m pip install --quiet --force-reinstall torch torchaudio --index-url https://download.pytorch.org/whl/cu124`, { stdio: "pipe", timeout: 6e5 });
39522
- renderInfo("PyTorch reinstalled with CUDA GPU support.");
39658
+ renderWarning("GPU detected but PyTorch is CPU-only. Reinstalling with CUDA support in background...");
39659
+ try {
39660
+ await this.asyncShell(`${JSON.stringify(venvPy)} -m pip install --quiet --force-reinstall torch torchaudio --index-url https://download.pytorch.org/whl/cu124`, 6e5);
39661
+ renderInfo("PyTorch reinstalled with CUDA GPU support.");
39662
+ } catch {
39663
+ }
39523
39664
  }
39524
- } catch {
39525
- }
39665
+ }).catch(() => {
39666
+ });
39526
39667
  }
39527
39668
  this.writeLuxttsInferScript();
39528
39669
  this.autoDetectCloneRef();
@@ -39535,27 +39676,24 @@ Error: ${err2 instanceof Error ? err2.message : String(err2)}`);
39535
39676
  if (!existsSync34(venvDir)) {
39536
39677
  renderInfo(" Creating Python virtual environment...");
39537
39678
  try {
39538
- execSync27(`${py} -m venv ${JSON.stringify(venvDir)}`, {
39539
- stdio: "pipe",
39540
- timeout: 6e4
39541
- });
39679
+ await this.asyncShell(`${py} -m venv ${JSON.stringify(venvDir)}`, 6e4);
39542
39680
  } catch (err) {
39543
39681
  throw new Error(`Failed to create venv: ${err instanceof Error ? err.message : String(err)}`);
39544
39682
  }
39545
39683
  }
39546
39684
  let hasCuda = false;
39547
39685
  try {
39548
- execSync27("nvidia-smi", { stdio: "pipe", timeout: 5e3 });
39686
+ await this.asyncShell("nvidia-smi", 5e3);
39549
39687
  hasCuda = true;
39550
39688
  } catch {
39551
39689
  }
39552
39690
  if (hasCuda) {
39553
39691
  renderInfo(" Installing PyTorch (CUDA GPU)...");
39554
39692
  try {
39555
- execSync27(`${JSON.stringify(venvPy)} -m pip install --quiet torch torchaudio --index-url https://download.pytorch.org/whl/cu124`, { stdio: "pipe", timeout: 6e5 });
39693
+ await this.asyncShell(`${JSON.stringify(venvPy)} -m pip install --quiet torch torchaudio --index-url https://download.pytorch.org/whl/cu124`, 6e5);
39556
39694
  } catch {
39557
39695
  try {
39558
- execSync27(`${JSON.stringify(venvPy)} -m pip install --quiet torch torchaudio`, { stdio: "pipe", timeout: 6e5 });
39696
+ await this.asyncShell(`${JSON.stringify(venvPy)} -m pip install --quiet torch torchaudio`, 6e5);
39559
39697
  } catch (err2) {
39560
39698
  throw new Error(`Failed to install PyTorch (CUDA): ${err2 instanceof Error ? err2.message : String(err2)}`);
39561
39699
  }
@@ -39563,10 +39701,10 @@ Error: ${err2 instanceof Error ? err2.message : String(err2)}`);
39563
39701
  } else {
39564
39702
  renderInfo(" Installing PyTorch (CPU \u2014 no NVIDIA GPU detected)...");
39565
39703
  try {
39566
- execSync27(`${JSON.stringify(venvPy)} -m pip install --quiet torch torchaudio --index-url https://download.pytorch.org/whl/cpu`, { stdio: "pipe", timeout: 6e5 });
39704
+ await this.asyncShell(`${JSON.stringify(venvPy)} -m pip install --quiet torch torchaudio --index-url https://download.pytorch.org/whl/cpu`, 6e5);
39567
39705
  } catch {
39568
39706
  try {
39569
- execSync27(`${JSON.stringify(venvPy)} -m pip install --quiet torch torchaudio`, { stdio: "pipe", timeout: 6e5 });
39707
+ await this.asyncShell(`${JSON.stringify(venvPy)} -m pip install --quiet torch torchaudio`, 6e5);
39570
39708
  } catch (err2) {
39571
39709
  throw new Error(`Failed to install PyTorch: ${err2 instanceof Error ? err2.message : String(err2)}`);
39572
39710
  }
@@ -39577,9 +39715,9 @@ Error: ${err2 instanceof Error ? err2.message : String(err2)}`);
39577
39715
  renderInfo(" Cloning LuxTTS repository...");
39578
39716
  try {
39579
39717
  if (existsSync34(repoDir)) {
39580
- execSync27(`rm -rf ${JSON.stringify(repoDir)}`, { stdio: "pipe" });
39718
+ await this.asyncShell(`rm -rf ${JSON.stringify(repoDir)}`, 3e4);
39581
39719
  }
39582
- execSync27(`git clone --depth 1 https://github.com/ysharma3501/LuxTTS.git ${JSON.stringify(repoDir)}`, { stdio: "pipe", timeout: 12e4 });
39720
+ await this.asyncShell(`git clone --depth 1 https://github.com/ysharma3501/LuxTTS.git ${JSON.stringify(repoDir)}`, 12e4);
39583
39721
  } catch (err) {
39584
39722
  throw new Error(`Failed to clone LuxTTS: ${err instanceof Error ? err.message : String(err)}`);
39585
39723
  }
@@ -39587,19 +39725,15 @@ Error: ${err2 instanceof Error ? err2.message : String(err2)}`);
39587
39725
  renderInfo(" Installing LuxTTS dependencies...");
39588
39726
  const pipCmd = JSON.stringify(venvPy);
39589
39727
  const installCmds = [
39590
- // Core deps from requirements.txt
39591
39728
  `${pipCmd} -m pip install --quiet lhotse huggingface_hub safetensors pydub onnxruntime librosa "transformers<=4.57.6" inflect numpy vocos "setuptools<81"`,
39592
- // Tokenization deps (piper_phonemize from custom index, plus CJK support)
39593
39729
  `${pipCmd} -m pip install --quiet piper-phonemize --find-links https://k2-fsa.github.io/icefall/piper_phonemize.html`,
39594
39730
  `${pipCmd} -m pip install --quiet jieba pypinyin cn2an`,
39595
- // LinaCodec custom vocoder (required by Zipvoice)
39596
39731
  `${pipCmd} -m pip install --quiet "git+https://github.com/ysharma3501/LinaCodec.git"`,
39597
- // Zipvoice package from the cloned repo (installs as 'zipvoice')
39598
39732
  `${pipCmd} -m pip install --quiet -e ${JSON.stringify(repoDir)}`
39599
39733
  ];
39600
39734
  for (const cmd of installCmds) {
39601
39735
  try {
39602
- execSync27(cmd, { stdio: "pipe", timeout: 3e5 });
39736
+ await this.asyncShell(cmd, 3e5);
39603
39737
  } catch (err) {
39604
39738
  const msg = err instanceof Error ? err.message : String(err);
39605
39739
  if (msg.includes("piper") || msg.includes("LinaCodec")) {
@@ -39610,7 +39744,7 @@ Error: ${err2 instanceof Error ? err2.message : String(err2)}`);
39610
39744
  }
39611
39745
  }
39612
39746
  try {
39613
- execSync27(`${venvPy} -c "import sys; sys.path.insert(0, '${repoDir}'); from zipvoice.luxvoice import LuxTTS; print('ok')"`, { stdio: "pipe", timeout: 3e4 });
39747
+ await this.asyncShell(`${venvPy} -c "import sys; sys.path.insert(0, '${repoDir}'); from zipvoice.luxvoice import LuxTTS; print('ok')"`, 3e4);
39614
39748
  } catch (err) {
39615
39749
  throw new Error(`LuxTTS installed but import failed: ${err instanceof Error ? err.message : String(err)}`);
39616
39750
  }
@@ -45505,8 +45639,11 @@ var init_status_bar = __esm({
45505
45639
  active = false;
45506
45640
  scrollRegionTop = 1;
45507
45641
  stdinHooked = false;
45508
- /** Track previous terminal rows to clear ghost separators on resize */
45642
+ /** Track previous terminal dimensions to clear ghost separators on resize */
45509
45643
  _prevTermRows = 0;
45644
+ _prevTermCols = 0;
45645
+ /** Debounce timer for resize events — prevents separator stacking during drag */
45646
+ _resizeTimer = null;
45510
45647
  /**
45511
45648
  * Depth-counted content write guard. Incremented by beginContentWrite(),
45512
45649
  * decremented by endContentWrite(). Footer is only redrawn and cursor
@@ -45953,6 +46090,7 @@ var init_status_bar = __esm({
45953
46090
  this.scrollRegionTop = scrollRegionTop ?? 1;
45954
46091
  this.active = true;
45955
46092
  this._prevTermRows = process.stdout.rows ?? 24;
46093
+ this._prevTermCols = process.stdout.columns ?? 80;
45956
46094
  this.applyScrollRegion();
45957
46095
  this.renderFooterAndPositionInput();
45958
46096
  this.hookStdin();
@@ -45990,24 +46128,42 @@ var init_status_bar = __esm({
45990
46128
  get reservedRows() {
45991
46129
  return this._currentFooterHeight;
45992
46130
  }
45993
- /** Handle terminal resize — reapply scroll region and redraw footer */
46131
+ /** Handle terminal resize — debounced to prevent separator stacking during drag */
45994
46132
  handleResize() {
45995
46133
  if (!this.active)
45996
46134
  return;
46135
+ if (this._resizeTimer)
46136
+ clearTimeout(this._resizeTimer);
46137
+ this._resizeTimer = setTimeout(() => {
46138
+ this._resizeTimer = null;
46139
+ this._handleResizeImmediate();
46140
+ }, 50);
46141
+ }
46142
+ /** Actual resize handler — called after debounce settles */
46143
+ _handleResizeImmediate() {
46144
+ if (!this.active)
46145
+ return;
46146
+ const prevRows = this._prevTermRows;
46147
+ const prevCols = this._prevTermCols;
45997
46148
  this.updateFooterHeight();
45998
46149
  const rows = process.stdout.rows ?? 24;
45999
- const prevRows = this._prevTermRows;
46150
+ const cols = process.stdout.columns ?? 80;
46000
46151
  this._prevTermRows = rows;
46152
+ this._prevTermCols = cols;
46001
46153
  const pos = this.rowPositions(rows);
46002
46154
  const w = getTermWidth();
46003
46155
  const sep = c2.dim("\u2500".repeat(w));
46004
- let clearGhosts = "";
46005
- if (prevRows > 0 && rows < prevRows) {
46006
- clearGhosts = "\x1B[1;" + rows + `r\x1B[${pos.bufferRow};1H\x1B[J`;
46156
+ let clearBuf = "";
46157
+ if (prevRows > 0 && (rows !== prevRows || cols !== prevCols)) {
46158
+ const wrapFactor = cols > 0 ? Math.ceil(prevCols / cols) : 1;
46159
+ const extraWrappedRows = Math.max(0, (wrapFactor - 1) * 3);
46160
+ const safetyMargin = extraWrappedRows + 2;
46161
+ const clearFrom = Math.max(1, pos.bufferRow - safetyMargin);
46162
+ clearBuf = "\x1B[1;" + rows + `r\x1B[${clearFrom};1H\x1B[J`;
46007
46163
  }
46008
46164
  if (this.writeDepth > 0) {
46009
46165
  const inputWrap = this.wrapInput(w);
46010
- let buf = clearGhosts + `\x1B[${this.scrollRegionTop};${pos.scrollEnd}r\x1B[?25l\x1B[?7l\x1B[${pos.bufferRow};1H\x1B[2K${this.buildBufferContent(w)}\x1B[${pos.topSepRow};1H\x1B[2K${sep}`;
46166
+ let buf = clearBuf + `\x1B[${this.scrollRegionTop};${pos.scrollEnd}r\x1B[?25l\x1B[?7l\x1B[${pos.bufferRow};1H\x1B[2K${this.buildBufferContent(w)}\x1B[${pos.topSepRow};1H\x1B[2K${sep}`;
46011
46167
  for (let i = 0; i < inputWrap.lines.length; i++) {
46012
46168
  const row = pos.inputStartRow + i;
46013
46169
  const prefix = i === 0 ? this.promptText : " ".repeat(this.promptWidth);
@@ -46016,8 +46172,8 @@ var init_status_bar = __esm({
46016
46172
  buf += `\x1B[${pos.bottomSepRow};1H\x1B[2K${sep}\x1B[${pos.metricsRow};1H\x1B[2K${this.buildMetricsLine()}\x1B[?7h\x1B[${pos.scrollEnd};1H`;
46017
46173
  process.stdout.write(buf);
46018
46174
  } else {
46019
- if (clearGhosts)
46020
- process.stdout.write(clearGhosts);
46175
+ if (clearBuf)
46176
+ process.stdout.write(clearBuf);
46021
46177
  this.applyScrollRegion();
46022
46178
  this.renderFooterAndPositionInput();
46023
46179
  }
@@ -47869,8 +48025,9 @@ Rationale: ${proposal.rationale}${provenanceNote}`;
47869
48025
  let currentTaskType;
47870
48026
  let sessionFilesTouched = [];
47871
48027
  let sessionToolCallCount = 0;
48028
+ let lastSteeringInput = "";
48029
+ let lastSteeringRetracted = false;
47872
48030
  let restoredSessionContext = null;
47873
- let pendingSessionRestore = false;
47874
48031
  let sessionSudoPassword = null;
47875
48032
  let sudoPromptPending = false;
47876
48033
  const idlePrompt = `${c2.bold(c2.white("\u276F "))}`;
@@ -49130,7 +49287,8 @@ Respond concisely and safely. Remember: you are talking to the general public.`;
49130
49287
  if (!activeTask)
49131
49288
  return false;
49132
49289
  activeTask.runner.abort();
49133
- writeContent(() => renderInfo("Task aborted."));
49290
+ const bgKilled = taskManager.stopAll();
49291
+ writeContent(() => renderInfo(`Task aborted.${bgKilled > 0 ? ` ${bgKilled} background task(s) killed.` : ""}`));
49134
49292
  return true;
49135
49293
  },
49136
49294
  pauseTask() {
@@ -49222,28 +49380,40 @@ Respond concisely and safely. Remember: you are talking to the general public.`;
49222
49380
  const lastTime = lastEntry.savedAt ? new Date(lastEntry.savedAt) : null;
49223
49381
  const timeAgo = lastTime ? formatTimeAgo(lastTime) : "unknown";
49224
49382
  const lastTask = lastEntry.task?.slice(0, 80) || "unknown";
49225
- setTimeout(() => {
49383
+ setTimeout(async () => {
49226
49384
  writeContent(() => {
49227
49385
  renderInfo(`Previous session found (${savedCtx.entries.length} entries, last active ${timeAgo})`);
49228
49386
  renderInfo(`Last task: ${lastTask}${lastEntry.task && lastEntry.task.length > 80 ? "..." : ""}`);
49229
- renderInfo(`Restore previous context? (y/n) [auto-restores in 15s]`);
49230
49387
  });
49231
- showPrompt();
49232
- pendingSessionRestore = true;
49233
- setTimeout(() => {
49234
- if (pendingSessionRestore) {
49235
- pendingSessionRestore = false;
49236
- const prompt = buildContextRestorePrompt(repoRoot);
49237
- if (prompt) {
49238
- restoredSessionContext = prompt;
49239
- const info = loadSessionContext(repoRoot);
49240
- writeContent(() => renderInfo(`Context auto-restored from ${info?.entries.length ?? 0} session(s).`));
49241
- } else {
49242
- writeContent(() => renderInfo("No context to restore. Starting fresh."));
49243
- }
49244
- showPrompt();
49245
- }
49388
+ let autoRestoreTimer = setTimeout(() => {
49389
+ autoRestoreTimer = null;
49246
49390
  }, 15e3);
49391
+ const result = await tuiSelect({
49392
+ items: [
49393
+ { key: "restore", label: "Restore previous context" },
49394
+ { key: "fresh", label: "Start fresh" }
49395
+ ],
49396
+ activeKey: "restore",
49397
+ title: "Restore previous session context?",
49398
+ rl
49399
+ });
49400
+ if (autoRestoreTimer)
49401
+ clearTimeout(autoRestoreTimer);
49402
+ if (result.confirmed && result.key === "restore" || !result.confirmed && !autoRestoreTimer) {
49403
+ const prompt = buildContextRestorePrompt(repoRoot);
49404
+ if (prompt) {
49405
+ restoredSessionContext = prompt;
49406
+ const info = loadSessionContext(repoRoot);
49407
+ writeContent(() => renderInfo(`Context restored from ${info?.entries.length ?? 0} session(s).`));
49408
+ } else {
49409
+ writeContent(() => renderInfo("No context to restore. Starting fresh."));
49410
+ }
49411
+ } else if (result.confirmed && result.key === "fresh") {
49412
+ writeContent(() => renderInfo("Starting fresh."));
49413
+ } else {
49414
+ writeContent(() => renderInfo("Starting fresh."));
49415
+ }
49416
+ showPrompt();
49247
49417
  }, 150);
49248
49418
  }
49249
49419
  }
@@ -49366,22 +49536,6 @@ ${result.content.slice(0, 2e3)}${result.content.length > 2e3 ? "\n[truncated]" :
49366
49536
  rl.on("line", (line) => {
49367
49537
  persistHistoryLine(line);
49368
49538
  const input = line.trim();
49369
- if (pendingSessionRestore) {
49370
- pendingSessionRestore = false;
49371
- const answer = input.toLowerCase();
49372
- if (answer === "y" || answer === "yes") {
49373
- const prompt = buildContextRestorePrompt(repoRoot);
49374
- if (prompt) {
49375
- restoredSessionContext = prompt;
49376
- const info = loadSessionContext(repoRoot);
49377
- writeContent(() => renderInfo(`Context restored from ${info?.entries.length ?? 0} session(s). Will be injected into your next task.`));
49378
- }
49379
- } else {
49380
- writeContent(() => renderInfo("Starting fresh."));
49381
- }
49382
- showPrompt();
49383
- return;
49384
- }
49385
49539
  if (!input) {
49386
49540
  if (pasteBuffer.length > 0) {
49387
49541
  flushPasteBuffer();
@@ -49436,6 +49590,7 @@ ${result.content.slice(0, 2e3)}${result.content.length > 2e3 ? "\n[truncated]" :
49436
49590
  if (activeTask) {
49437
49591
  activeTask.runner.abort();
49438
49592
  }
49593
+ taskManager.stopAll();
49439
49594
  if (blessEngine?.isActive)
49440
49595
  blessEngine.stop();
49441
49596
  if (telegramBridge?.isActive)
@@ -49505,6 +49660,10 @@ Execute this skill now. Follow the behavioral guidance above.`;
49505
49660
  const isImage = isImagePath(cleanPath) && existsSync40(resolve28(repoRoot, cleanPath));
49506
49661
  const isMedia = !isImage && isTranscribablePath(cleanPath) && existsSync40(resolve28(repoRoot, cleanPath));
49507
49662
  if (activeTask) {
49663
+ if (activeTask.runner.isPaused) {
49664
+ activeTask.runner.resume();
49665
+ statusBar.setProcessing(true);
49666
+ }
49508
49667
  if (isImage) {
49509
49668
  try {
49510
49669
  const imgPath = resolve28(repoRoot, cleanPath);
@@ -49532,8 +49691,16 @@ ${result.text}`;
49532
49691
  writeContent(() => renderUserInterrupt(`[Media: ${cleanPath}]`));
49533
49692
  }
49534
49693
  } else {
49694
+ const isReplacement = lastSteeringRetracted;
49695
+ lastSteeringInput = input;
49696
+ lastSteeringRetracted = false;
49535
49697
  const lineCount = input.split("\n").length;
49536
- if (lineCount > 1) {
49698
+ if (isReplacement) {
49699
+ const displayText2 = lineCount > 1 ? `[pasted ${lineCount} lines]` : input;
49700
+ writeContent(() => process.stdout.write(`
49701
+ ${c2.green("\u21BB")} ${c2.bold("Context replaced:")} ${displayText2}
49702
+ `));
49703
+ } else if (lineCount > 1) {
49537
49704
  writeContent(() => renderUserInterrupt(`[pasted ${lineCount} lines]`));
49538
49705
  } else {
49539
49706
  writeContent(() => renderUserInterrupt(input));
@@ -49904,6 +50071,7 @@ ${c2.dim("(paste cancelled)")}
49904
50071
  }
49905
50072
  if (activeTask) {
49906
50073
  activeTask.runner.abort();
50074
+ taskManager.stopAll();
49907
50075
  writeContent(() => renderTaskAborted());
49908
50076
  } else {
49909
50077
  writeContent(() => process.stdout.write(`
@@ -49912,6 +50080,42 @@ ${c2.dim("(Use /quit to exit)")}
49912
50080
  }
49913
50081
  showPrompt();
49914
50082
  });
50083
+ if (process.stdin.isTTY) {
50084
+ readline2.emitKeypressEvents(process.stdin, rl);
50085
+ process.stdin.on("keypress", (_str, key) => {
50086
+ if (!key)
50087
+ return;
50088
+ if (key.name === "escape" && activeTask) {
50089
+ if (!activeTask.runner.isPaused) {
50090
+ activeTask.runner.pause();
50091
+ statusBar.setProcessing(false);
50092
+ }
50093
+ const retracted = activeTask.runner.retractLastPendingMessage();
50094
+ if (retracted) {
50095
+ lastSteeringInput = retracted;
50096
+ lastSteeringRetracted = true;
50097
+ writeContent(() => process.stdout.write(` ${c2.yellow("\u23F8")} ${c2.dim("\x1B[9m" + retracted.slice(0, 120) + "\x1B[29m")} ${c2.dim("\u2190 retracted, edit below and press Enter")}
50098
+ `));
50099
+ rl.line = retracted;
50100
+ rl.cursor = retracted.length;
50101
+ if (statusBar.isActive)
50102
+ statusBar.renderFooterPreserveCursor?.();
50103
+ } else if (lastSteeringInput) {
50104
+ lastSteeringRetracted = true;
50105
+ writeContent(() => process.stdout.write(` ${c2.yellow("\u23F8")} ${c2.dim("\x1B[9m" + lastSteeringInput.slice(0, 120) + "\x1B[29m")} ${c2.dim("\u2190 already sent, re-type below to override")}
50106
+ `));
50107
+ rl.line = lastSteeringInput;
50108
+ rl.cursor = lastSteeringInput.length;
50109
+ if (statusBar.isActive)
50110
+ statusBar.renderFooterPreserveCursor?.();
50111
+ } else {
50112
+ writeContent(() => process.stdout.write(` ${c2.yellow("\u23F8")} ${c2.dim("Paused. /resume to continue, /stop to kill.")}
50113
+ `));
50114
+ }
50115
+ showPrompt();
50116
+ }
50117
+ });
50118
+ }
49915
50119
  }
49916
50120
  async function runWithTUI(task, config, repoPath) {
49917
50121
  const repoRoot = resolve28(repoPath ?? cwd());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-agents-ai",
3
- "version": "0.103.82",
3
+ "version": "0.103.85",
4
4
  "description": "AI coding agent powered by open-source models (Ollama/vLLM) — interactive TUI with agentic tool-calling loop",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",