open-agents-ai 0.54.0 → 0.56.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +269 -14
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -16726,6 +16726,62 @@ transcribe-cli error: ${transcribeCliError}` : "";
16726
16726
  this.pendingText = "";
16727
16727
  return text;
16728
16728
  }
16729
+ /**
16730
+ * Create a standalone transcriber for call sessions.
16731
+ * No microphone capture — just an ASR engine you feed PCM chunks to.
16732
+ * Returns an EventEmitter with .write(pcm: Buffer) and 'transcript' events.
16733
+ * Caller is responsible for calling .stop() when done.
16734
+ */
16735
+ async createCallTranscriber() {
16736
+ let tc = await this.loadTranscribeCli();
16737
+ if (!tc && _bgInstallPromise) {
16738
+ await _bgInstallPromise;
16739
+ this.transcribeCliAvailable = null;
16740
+ tc = await this.loadTranscribeCli();
16741
+ }
16742
+ if (!tc) {
16743
+ try {
16744
+ execSync18("npm i -g transcribe-cli", { stdio: "pipe", timeout: 18e4 });
16745
+ this.transcribeCliAvailable = null;
16746
+ tc = await this.loadTranscribeCli();
16747
+ } catch {
16748
+ }
16749
+ }
16750
+ if (tc?.TranscribeLive) {
16751
+ try {
16752
+ const transcriber = new tc.TranscribeLive({
16753
+ model: this.config.model,
16754
+ sampleRate: 16e3,
16755
+ channels: 1,
16756
+ sampleWidth: 2,
16757
+ chunkDuration: 3
16758
+ });
16759
+ await new Promise((resolve28, reject) => {
16760
+ const timeout = setTimeout(() => reject(new Error("Model load timeout (60s)")), 6e4);
16761
+ transcriber.on("ready", () => {
16762
+ clearTimeout(timeout);
16763
+ resolve28();
16764
+ });
16765
+ transcriber.on("error", (err) => {
16766
+ clearTimeout(timeout);
16767
+ reject(err);
16768
+ });
16769
+ });
16770
+ return transcriber;
16771
+ } catch {
16772
+ }
16773
+ }
16774
+ const scriptPath2 = findLiveWhisperScript();
16775
+ if (!scriptPath2)
16776
+ return null;
16777
+ try {
16778
+ const fallback = new WhisperFallbackTranscriber(this.config.model, scriptPath2);
16779
+ await fallback.start();
16780
+ return fallback;
16781
+ } catch {
16782
+ return null;
16783
+ }
16784
+ }
16729
16785
  /**
16730
16786
  * Transcribe a file (audio or video) using transcribe-cli.
16731
16787
  * Returns the transcription result.
@@ -17284,8 +17340,8 @@ function renderSlashHelp() {
17284
17340
  ["/listen manual", "Manual mode \u2014 press Enter to submit (turns off auto)"],
17285
17341
  ["/listen auto", "Auto-submit after 3s silence (blinking \u25CF indicator)"],
17286
17342
  ["/listen stop", "Stop listening"],
17287
- ["/call", "Start voice call session (cloudflared tunnel + PCM frontend)"],
17288
- ["/call stop", "Stop active call session"],
17343
+ ["/call", "Start voice call session (cloudflared tunnel + ASR/TTS)"],
17344
+ ["/hangup", "End active call session"],
17289
17345
  ["/cost", "Show session token cost breakdown"],
17290
17346
  ["/evaluate", "Evaluate last completed task (LLM quality scoring)"],
17291
17347
  ["/task-type", "Set task type (code, document, analysis, plan, general, auto)"],
@@ -17675,6 +17731,7 @@ var init_render = __esm({
17675
17731
  "/voice",
17676
17732
  "/listen",
17677
17733
  "/call",
17734
+ "/hangup",
17678
17735
  "/stream",
17679
17736
  "/verbose",
17680
17737
  "/dream",
@@ -17692,6 +17749,7 @@ var init_render = __esm({
17692
17749
  // packages/cli/dist/tui/voice-session.js
17693
17750
  import { createServer } from "node:http";
17694
17751
  import { spawn as spawn10, execSync as execSync19 } from "node:child_process";
17752
+ import { createHash } from "node:crypto";
17695
17753
  import { EventEmitter as EventEmitter2 } from "node:events";
17696
17754
  function parseWebSocketFrame(buf) {
17697
17755
  if (buf.length < 2)
@@ -17846,26 +17904,42 @@ let scriptProcessor = null;
17846
17904
  let micActive = false;
17847
17905
  let playbackQueue = [];
17848
17906
  let isPlaying = false;
17907
+ let reconnectDelay = 1000;
17908
+ let reconnectTimer = null;
17849
17909
 
17850
17910
  // Connect WebSocket
17851
17911
  function connect() {
17912
+ if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
17852
17913
  const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
17853
17914
  ws = new WebSocket(proto + '//' + location.host + '/ws');
17854
17915
  ws.binaryType = 'arraybuffer';
17855
17916
 
17856
17917
  ws.onopen = () => {
17857
17918
  statusEl.innerHTML = '<span class="connected">Connected</span>';
17919
+ reconnectDelay = 1000; // Reset backoff on successful connection
17920
+ };
17921
+
17922
+ ws.onerror = () => {
17923
+ // Error fires before close \u2014 just update status, close handler does reconnect
17924
+ statusEl.innerHTML = '<span class="disconnected">Connection error</span>';
17858
17925
  };
17859
17926
 
17860
17927
  ws.onclose = () => {
17861
17928
  statusEl.innerHTML = '<span class="disconnected">Disconnected \u2014 reconnecting...</span>';
17862
- setTimeout(connect, 2000);
17929
+ // Exponential backoff: 1s, 2s, 4s, max 10s
17930
+ reconnectTimer = setTimeout(connect, reconnectDelay);
17931
+ reconnectDelay = Math.min(reconnectDelay * 2, 10000);
17863
17932
  };
17864
17933
 
17865
17934
  ws.onmessage = (evt) => {
17866
17935
  if (typeof evt.data === 'string') {
17867
17936
  try {
17868
17937
  const msg = JSON.parse(evt.data);
17938
+ if (msg.type === 'keepalive') {
17939
+ // Respond to keep connection alive through proxies
17940
+ if (ws.readyState === 1) ws.send(JSON.stringify({ type: 'pong' }));
17941
+ return;
17942
+ }
17869
17943
  if (msg.type === 'transcript') {
17870
17944
  addTranscript(msg.speaker, msg.text);
17871
17945
  } else if (msg.type === 'speaking_start') {
@@ -17990,11 +18064,11 @@ function renderVoiceSessionStart(tunnelUrl) {
17990
18064
  process.stdout.write(`
17991
18065
  ${c2.cyan("\u2601")} ${c2.bold("Live Voice Session")}
17992
18066
  `);
17993
- process.stdout.write(` ${c2.dim("\u23BF")} URL: ${c2.cyan(tunnelUrl)}
18067
+ process.stdout.write(` ${c2.dim("\u23BF")} ${c2.cyan(tunnelUrl)}
17994
18068
  `);
17995
18069
  process.stdout.write(` ${c2.dim("\u23BF")} Bidirectional PCM audio + live transcription
17996
18070
  `);
17997
- process.stdout.write(` ${c2.dim("\u23BF")} Use /listen stop to end session
18071
+ process.stdout.write(` ${c2.dim("\u23BF")} /hangup to end session (auto-closes after 1 min idle)
17998
18072
 
17999
18073
  `);
18000
18074
  }
@@ -18011,6 +18085,12 @@ function renderVoiceSessionUser(action, username) {
18011
18085
  process.stdout.write(` ${c2.dim("\u23BF")} ${c2.cyan("\u2601")} ${icon} ${username} ${action}
18012
18086
  `);
18013
18087
  }
18088
+ function renderVoiceSessionTranscript(speaker, text) {
18089
+ const label = speaker === "user" ? c2.yellow("user") : c2.cyan("agent");
18090
+ const preview = text.length > 80 ? text.slice(0, 77) + "..." : text;
18091
+ process.stdout.write(` ${c2.dim("\u23BF")} ${c2.cyan("\u2601")} [${label}] ${preview}
18092
+ `);
18093
+ }
18014
18094
  var VoiceSession;
18015
18095
  var init_voice_session = __esm({
18016
18096
  "packages/cli/dist/tui/voice-session.js"() {
@@ -18022,7 +18102,10 @@ var init_voice_session = __esm({
18022
18102
  cloudflaredProcess = null;
18023
18103
  wsClients = /* @__PURE__ */ new Map();
18024
18104
  runtimeTimer = null;
18105
+ idleTimer = null;
18025
18106
  ttsSpeaking = false;
18107
+ /** Idle timeout before auto-closing (ms). Default 60s — no users connected → session ends. */
18108
+ idleTimeoutMs = 6e4;
18026
18109
  /** Callback invoked when user audio chunk arrives (PCM 16kHz 16-bit mono) */
18027
18110
  onUserAudio = null;
18028
18111
  /** Callback invoked when user speech is transcribed */
@@ -18056,6 +18139,8 @@ var init_voice_session = __esm({
18056
18139
  const port = await this.findFreePort();
18057
18140
  this.state.localPort = port;
18058
18141
  this.server = createServer((req, res) => this.handleHTTP(req, res));
18142
+ this.server.timeout = 0;
18143
+ this.server.keepAliveTimeout = 0;
18059
18144
  this.server.on("upgrade", (req, socket, head) => {
18060
18145
  if (req.url === "/ws") {
18061
18146
  this.handleWebSocketUpgrade(req, socket, head);
@@ -18081,7 +18166,8 @@ var init_voice_session = __esm({
18081
18166
  this.emit("tick", this.runtime);
18082
18167
  }, 1e3);
18083
18168
  this.emit("started", this.state.tunnelUrl);
18084
- return `Voice session started: ${this.state.tunnelUrl}`;
18169
+ this.resetIdleTimer();
18170
+ return this.state.tunnelUrl;
18085
18171
  }
18086
18172
  /**
18087
18173
  * Stop the voice session.
@@ -18094,6 +18180,10 @@ var init_voice_session = __esm({
18094
18180
  clearInterval(this.runtimeTimer);
18095
18181
  this.runtimeTimer = null;
18096
18182
  }
18183
+ if (this.idleTimer) {
18184
+ clearTimeout(this.idleTimer);
18185
+ this.idleTimer = null;
18186
+ }
18097
18187
  for (const [id, socket] of this.wsClients) {
18098
18188
  try {
18099
18189
  const closeFrame = createWebSocketFrame(8, Buffer.alloc(0));
@@ -18184,9 +18274,11 @@ var init_voice_session = __esm({
18184
18274
  socket.destroy();
18185
18275
  return;
18186
18276
  }
18187
- const crypto2 = __require("node:crypto");
18188
18277
  const magic = "258EAFA5-E914-47DA-95CA-5AB9DC085B62";
18189
- const accept = crypto2.createHash("sha1").update(key + magic).digest("base64");
18278
+ const accept = createHash("sha1").update(key + magic).digest("base64");
18279
+ socket.setNoDelay(true);
18280
+ socket.setKeepAlive(true, 1e4);
18281
+ socket.setTimeout(0);
18190
18282
  socket.write(`HTTP/1.1 101 Switching Protocols\r
18191
18283
  Upgrade: websocket\r
18192
18284
  Connection: Upgrade\r
@@ -18197,6 +18289,23 @@ Sec-WebSocket-Accept: ${accept}\r
18197
18289
  this.wsClients.set(clientId, socket);
18198
18290
  this.state.connectedUsers.set(clientId, { username: "web-user", connectedAt: Date.now() });
18199
18291
  this.emit("userConnected", clientId, "web-user");
18292
+ this.resetIdleTimer();
18293
+ try {
18294
+ socket.write(createWebSocketFrame(9, Buffer.from("keepalive")));
18295
+ } catch {
18296
+ }
18297
+ const pingInterval = setInterval(() => {
18298
+ if (socket.destroyed) {
18299
+ clearInterval(pingInterval);
18300
+ return;
18301
+ }
18302
+ try {
18303
+ socket.write(createWebSocketFrame(9, Buffer.from("keepalive")));
18304
+ socket.write(createWebSocketFrame(1, Buffer.from(JSON.stringify({ type: "keepalive" }))));
18305
+ } catch {
18306
+ clearInterval(pingInterval);
18307
+ }
18308
+ }, 5e3);
18200
18309
  let frameBuffer = Buffer.alloc(0);
18201
18310
  socket.on("data", (data) => {
18202
18311
  frameBuffer = Buffer.concat([frameBuffer, data]);
@@ -18218,10 +18327,15 @@ Sec-WebSocket-Accept: ${accept}\r
18218
18327
  const consumed = headerLen + (masked ? 4 : 0) + payloadLen;
18219
18328
  frameBuffer = frameBuffer.subarray(consumed);
18220
18329
  if (frame.opcode === 8) {
18330
+ try {
18331
+ socket.write(createWebSocketFrame(8, Buffer.alloc(0)));
18332
+ } catch {
18333
+ }
18221
18334
  socket.end();
18222
18335
  return;
18223
18336
  } else if (frame.opcode === 9) {
18224
18337
  socket.write(createWebSocketFrame(10, frame.payload));
18338
+ } else if (frame.opcode === 10) {
18225
18339
  } else if (frame.opcode === 1) {
18226
18340
  try {
18227
18341
  const msg = JSON.parse(frame.payload.toString());
@@ -18229,7 +18343,6 @@ Sec-WebSocket-Accept: ${accept}\r
18229
18343
  const entry = this.state.connectedUsers.get(clientId);
18230
18344
  if (entry)
18231
18345
  entry.username = msg.username;
18232
- this.emit("userConnected", clientId, msg.username);
18233
18346
  }
18234
18347
  } catch {
18235
18348
  }
@@ -18241,14 +18354,17 @@ Sec-WebSocket-Accept: ${accept}\r
18241
18354
  }
18242
18355
  });
18243
18356
  socket.on("close", () => {
18357
+ clearInterval(pingInterval);
18244
18358
  this.wsClients.delete(clientId);
18245
- const user = this.state.connectedUsers.get(clientId);
18246
18359
  this.state.connectedUsers.delete(clientId);
18247
18360
  this.emit("userDisconnected", clientId);
18361
+ this.resetIdleTimer();
18248
18362
  });
18249
18363
  socket.on("error", () => {
18364
+ clearInterval(pingInterval);
18250
18365
  this.wsClients.delete(clientId);
18251
18366
  this.state.connectedUsers.delete(clientId);
18367
+ this.resetIdleTimer();
18252
18368
  });
18253
18369
  if (head.length > 0) {
18254
18370
  socket.emit("data", head);
@@ -18291,6 +18407,27 @@ Sec-WebSocket-Accept: ${accept}\r
18291
18407
  });
18292
18408
  });
18293
18409
  }
18410
+ // ── Idle timer ───────────────────────────────────────────────────────
18411
+ /**
18412
+ * Reset idle timer. Called when users connect/disconnect.
18413
+ * When no users are connected, starts a countdown to auto-close.
18414
+ * When a user connects, cancels any pending auto-close.
18415
+ */
18416
+ resetIdleTimer() {
18417
+ if (this.idleTimer) {
18418
+ clearTimeout(this.idleTimer);
18419
+ this.idleTimer = null;
18420
+ }
18421
+ if (this.state.connectedUsers.size === 0) {
18422
+ this.idleTimer = setTimeout(() => {
18423
+ if (this.state.active && this.state.connectedUsers.size === 0) {
18424
+ this.emit("idle_timeout");
18425
+ this.stop().catch(() => {
18426
+ });
18427
+ }
18428
+ }, this.idleTimeoutMs);
18429
+ }
18430
+ }
18294
18431
  // ── Helpers ───────────────────────────────────────────────────────────
18295
18432
  findFreePort() {
18296
18433
  return new Promise((resolve28, reject) => {
@@ -20749,6 +20886,19 @@ async function handleSlashCommand(input, ctx) {
20749
20886
  }
20750
20887
  return "handled";
20751
20888
  }
20889
+ case "hangup": {
20890
+ if (!ctx.callStop) {
20891
+ renderWarning("Call mode not available in this context.");
20892
+ return "handled";
20893
+ }
20894
+ if (ctx.isCallActive?.()) {
20895
+ await ctx.callStop();
20896
+ renderInfo("Call session ended.");
20897
+ } else {
20898
+ renderWarning("No active call session.");
20899
+ }
20900
+ return "handled";
20901
+ }
20752
20902
  case "bruteforce":
20753
20903
  case "brute": {
20754
20904
  const isOn = ctx.bruteForceToggle();
@@ -23135,6 +23285,12 @@ var init_voice = __esm({
23135
23285
  enabled = false;
23136
23286
  modelId = "glados";
23137
23287
  ready = false;
23288
+ /**
23289
+ * Callback fired with PCM Int16 data whenever TTS synthesizes audio.
23290
+ * Used by VoiceSession to stream TTS output to WebSocket clients.
23291
+ * Set this to wire TTS → call session broadcast.
23292
+ */
23293
+ onPCMOutput = null;
23138
23294
  session = null;
23139
23295
  // ort.InferenceSession
23140
23296
  ort = null;
@@ -23366,6 +23522,14 @@ var init_voice = __esm({
23366
23522
  const audioData = result["output"].data;
23367
23523
  if (audioData.length === 0)
23368
23524
  return;
23525
+ if (this.onPCMOutput) {
23526
+ const int16 = new Int16Array(audioData.length);
23527
+ for (let i = 0; i < audioData.length; i++) {
23528
+ const s = Math.max(-1, Math.min(1, audioData[i]));
23529
+ int16[i] = s < 0 ? s * 32768 : s * 32767;
23530
+ }
23531
+ this.onPCMOutput(Buffer.from(int16.buffer), this.config.audio.sample_rate);
23532
+ }
23369
23533
  const wavPath = join34(tmpdir6(), `oa-voice-${Date.now()}.wav`);
23370
23534
  this.writeWav(audioData, this.config.audio.sample_rate, wavPath);
23371
23535
  await this.playWav(wavPath);
@@ -27257,6 +27421,10 @@ function convertMarkdownToTelegramHTML(md) {
27257
27421
  return html;
27258
27422
  }
27259
27423
  function formatIntermediateState(event) {
27424
+ if (event.type === "tool_call" && event.toolName === "task_complete")
27425
+ return null;
27426
+ if (event.type === "tool_result" && event.toolName === "task_complete")
27427
+ return null;
27260
27428
  if (event.type === "tool_call") {
27261
27429
  const argsPreview = event.toolArgs ? JSON.stringify(event.toolArgs).slice(0, 60) : "";
27262
27430
  return `\u{1F527} <code>${event.toolName || "tool"}</code>(${argsPreview.length > 57 ? argsPreview.slice(0, 57) + "..." : argsPreview})`;
@@ -27456,6 +27624,10 @@ with summary "no_reply" to silently skip without responding.
27456
27624
  commandHandler = null;
27457
27625
  /** Callback to get active call session URL (wired from interactive.ts) */
27458
27626
  callUrlGetter = null;
27627
+ /** Callback to start a call session and return the URL (wired from interactive.ts) */
27628
+ callStarter = null;
27629
+ /** Callback to stop a call session (wired from interactive.ts) */
27630
+ callStopper = null;
27459
27631
  /** Callback to write content into the scrollable TUI waterfall area (wired from interactive.ts) */
27460
27632
  writeContent = null;
27461
27633
  /** Media cache — fileUniqueId → cache entry */
@@ -27495,6 +27667,14 @@ with summary "no_reply" to silently skip without responding.
27495
27667
  setCallUrlGetter(getter) {
27496
27668
  this.callUrlGetter = getter;
27497
27669
  }
27670
+ /** Register callback to start a call session (returns URL or null) */
27671
+ setCallStarter(starter) {
27672
+ this.callStarter = starter;
27673
+ }
27674
+ /** Register callback to stop a call session */
27675
+ setCallStopper(stopper) {
27676
+ this.callStopper = stopper;
27677
+ }
27498
27678
  /** Register callback to write content into the scrollable TUI area (status bar guard) */
27499
27679
  setWriteContent(fn) {
27500
27680
  this.writeContent = fn;
@@ -27655,6 +27835,16 @@ with summary "no_reply" to silently skip without responding.
27655
27835
  const isAdmin = this.isAdminUser(msg);
27656
27836
  const toolContext = this.resolveToolContext(msg, isAdmin);
27657
27837
  const isAdminDM = toolContext === "telegram-admin-dm";
27838
+ if (msg.text.trim().toLowerCase() === "/hangup" && isAdmin && this.callStopper) {
27839
+ const callUrl = this.callUrlGetter?.();
27840
+ if (callUrl) {
27841
+ await this.callStopper();
27842
+ await this.sendMessage(msg.chatId, "Call session ended.");
27843
+ } else {
27844
+ await this.sendMessage(msg.chatId, "No active call session.");
27845
+ }
27846
+ return;
27847
+ }
27658
27848
  if (msg.text.trim().toLowerCase() === "/call" && this.callUrlGetter) {
27659
27849
  const callUrl = this.callUrlGetter();
27660
27850
  if (callUrl) {
@@ -27665,6 +27855,20 @@ with summary "no_reply" to silently skip without responding.
27665
27855
  await this.sendMessage(msg.chatId, "No active call session. Ask the admin to start one with /call.");
27666
27856
  return;
27667
27857
  }
27858
+ if (this.callStarter) {
27859
+ try {
27860
+ const newUrl = await this.callStarter();
27861
+ if (newUrl) {
27862
+ await this.sendCallButton(msg.chatId, newUrl);
27863
+ } else {
27864
+ await this.sendMessage(msg.chatId, "Failed to start call session.");
27865
+ }
27866
+ } catch (err) {
27867
+ await this.sendMessage(msg.chatId, `Call error: ${err instanceof Error ? err.message : String(err)}`).catch(() => {
27868
+ });
27869
+ }
27870
+ return;
27871
+ }
27668
27872
  }
27669
27873
  if (isAdminDM && msg.text.startsWith("/") && this.commandHandler) {
27670
27874
  const cmdName = msg.text.split(/\s+/)[0].slice(1).toLowerCase();
@@ -30796,6 +31000,8 @@ Respond concisely and safely. Remember: you are talking to the general public.`;
30796
31000
  telegramBridge.voiceEnabled = true;
30797
31001
  }
30798
31002
  telegramBridge.setCallUrlGetter(() => voiceSession?.tunnelUrl ?? null);
31003
+ telegramBridge.setCallStarter(async () => commandCtx.callStart());
31004
+ telegramBridge.setCallStopper(async () => commandCtx.callStop());
30799
31005
  telegramBridge.setCommandHandler(async (input) => {
30800
31006
  const captured = [];
30801
31007
  const origWrite = process.stdout.write;
@@ -30963,20 +31169,66 @@ Respond concisely and safely. Remember: you are talking to the general public.`;
30963
31169
  return text ? `Stopped. Last transcript: "${text}"` : "Stopped listening.";
30964
31170
  },
30965
31171
  // Voice call session — standalone cloudflared tunnel for /call
31172
+ // Wires: WebSocket audio → ASR transcription → agent input → TTS → WebSocket broadcast
30966
31173
  async callStart() {
30967
31174
  if (voiceSession?.isActive) {
30968
31175
  return voiceSession.tunnelUrl;
30969
31176
  }
30970
31177
  voiceSession = new VoiceSession();
31178
+ const callState = { transcriber: null };
31179
+ const engine = getListenEngine();
31180
+ voiceSession.onUserAudio = (pcmChunk, userId) => {
31181
+ if (callState.transcriber)
31182
+ callState.transcriber.write(pcmChunk);
31183
+ };
30971
31184
  voiceSession.on("userConnected", (id, username) => {
30972
31185
  writeContent(() => renderVoiceSessionUser("connected", username));
31186
+ if (!callState.transcriber) {
31187
+ engine.createCallTranscriber().then((t) => {
31188
+ if (!t || !voiceSession?.isActive)
31189
+ return;
31190
+ callState.transcriber = t;
31191
+ callState.transcriber.on("transcript", (evt) => {
31192
+ if (!evt.text?.trim() || !voiceSession?.isActive)
31193
+ return;
31194
+ const text = evt.text.trim();
31195
+ writeContent(() => renderVoiceSessionTranscript("user", text));
31196
+ voiceSession?.sendTranscript("user", text);
31197
+ if (evt.isFinal) {
31198
+ rl.write(text + "\n");
31199
+ }
31200
+ });
31201
+ writeContent(() => renderInfo("Call ASR ready \u2014 listening for speech"));
31202
+ }).catch((err) => {
31203
+ writeContent(() => renderWarning(`Call ASR unavailable: ${err instanceof Error ? err.message : String(err)}`));
31204
+ });
31205
+ }
30973
31206
  });
30974
31207
  voiceSession.on("userDisconnected", (id) => {
30975
31208
  writeContent(() => renderVoiceSessionUser("disconnected", id));
30976
31209
  });
31210
+ voiceSession.on("idle_timeout", () => {
31211
+ writeContent(() => renderWarning("Call session auto-closed (1 min idle \u2014 no users connected)"));
31212
+ if (callState.transcriber) {
31213
+ callState.transcriber.stop();
31214
+ callState.transcriber = null;
31215
+ }
31216
+ voiceSession = null;
31217
+ });
30977
31218
  try {
30978
31219
  const tunnelUrl = await voiceSession.start();
30979
31220
  writeContent(() => renderVoiceSessionStart(tunnelUrl));
31221
+ if (voiceEngine.enabled && voiceEngine.ready) {
31222
+ const session = voiceSession;
31223
+ voiceEngine.onPCMOutput = (pcm, sampleRate) => {
31224
+ if (session?.isActive) {
31225
+ session.sendSpeakingState(true);
31226
+ session.sendAudioToClients(pcm);
31227
+ const durationMs = pcm.length / 2 / sampleRate * 1e3;
31228
+ setTimeout(() => session.sendSpeakingState(false), durationMs);
31229
+ }
31230
+ };
31231
+ }
30980
31232
  if (telegramBridge?.isActive && savedSettings.telegramAdmin) {
30981
31233
  const adminChatId = parseInt(savedSettings.telegramAdmin, 10);
30982
31234
  if (!isNaN(adminChatId)) {
@@ -30987,6 +31239,8 @@ Respond concisely and safely. Remember: you are talking to the general public.`;
30987
31239
  return tunnelUrl;
30988
31240
  } catch (err) {
30989
31241
  writeContent(() => renderWarning(`Voice session failed: ${err instanceof Error ? err.message : String(err)}`));
31242
+ callState.transcriber?.stop();
31243
+ callState.transcriber = null;
30990
31244
  voiceSession = null;
30991
31245
  return null;
30992
31246
  }
@@ -30997,6 +31251,7 @@ Respond concisely and safely. Remember: you are talking to the general public.`;
30997
31251
  await voiceSession.stop();
30998
31252
  writeContent(() => renderVoiceSessionStop(runtime));
30999
31253
  voiceSession = null;
31254
+ voiceEngine.onPCMOutput = null;
31000
31255
  }
31001
31256
  },
31002
31257
  isCallActive() {
@@ -31472,13 +31727,13 @@ NEW TASK: ${fullInput}`;
31472
31727
  writeContent(() => renderError(errMsg));
31473
31728
  if (failureStore) {
31474
31729
  try {
31475
- const { createHash: createHash2 } = await import("node:crypto");
31730
+ const { createHash: createHash3 } = await import("node:crypto");
31476
31731
  failureStore.insert({
31477
31732
  taskId: "",
31478
31733
  sessionId: `${Date.now()}`,
31479
31734
  repoRoot,
31480
31735
  failureType: "runtime-error",
31481
- fingerprint: createHash2("sha256").update(errMsg.slice(0, 200)).digest("hex").slice(0, 16),
31736
+ fingerprint: createHash3("sha256").update(errMsg.slice(0, 200)).digest("hex").slice(0, 16),
31482
31737
  filePath: null,
31483
31738
  errorMessage: errMsg.slice(0, 500),
31484
31739
  context: null,
@@ -31739,7 +31994,7 @@ var init_run = __esm({
31739
31994
  import { glob } from "glob";
31740
31995
  import ignore from "ignore";
31741
31996
  import { readFile as readFile14, stat as stat4 } from "node:fs/promises";
31742
- import { createHash } from "node:crypto";
31997
+ import { createHash as createHash2 } from "node:crypto";
31743
31998
  import { join as join41, relative as relative3, extname as extname10, basename as basename15 } from "node:path";
31744
31999
  var DEFAULT_EXCLUDE, LANGUAGE_MAP, CodebaseIndexer;
31745
32000
  var init_codebase_indexer = __esm({
@@ -31805,7 +32060,7 @@ var init_codebase_indexer = __esm({
31805
32060
  if (fileStat.size > this.config.maxFileSize)
31806
32061
  continue;
31807
32062
  const content = await readFile14(fullPath);
31808
- const hash = createHash("sha256").update(content).digest("hex");
32063
+ const hash = createHash2("sha256").update(content).digest("hex");
31809
32064
  const ext = extname10(relativePath);
31810
32065
  indexed.push({
31811
32066
  path: fullPath,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-agents-ai",
3
- "version": "0.54.0",
3
+ "version": "0.56.0",
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",