@openclaw/voice-call 2026.8.1-beta.2 → 2026.8.1-beta.3

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.
@@ -1,2 +1,2 @@
1
- import { t as createVoiceCallRuntime } from "./runtime-entry-F13eJ4pp.js";
1
+ import { t as createVoiceCallRuntime } from "./runtime-entry-DK8kPCGC.js";
2
2
  export { createVoiceCallRuntime };
@@ -1,4 +1,4 @@
1
- import { s as verifyTelnyxWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-DEDwgf4J.js";
1
+ import { s as verifyTelnyxWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-j4OEG0fR.js";
2
2
  import crypto from "node:crypto";
3
3
  //#region extensions/voice-call/src/providers/telnyx.ts
4
4
  function normalizeTelnyxDirection(direction) {
@@ -122,13 +122,17 @@ var TelnyxProvider = class {
122
122
  type: "call.speaking",
123
123
  text: data.payload?.text || ""
124
124
  };
125
- case "call.transcription": return {
126
- ...baseEvent,
127
- type: "call.speech",
128
- transcript: data.payload?.transcription_data?.transcript ?? data.payload?.transcription ?? "",
129
- isFinal: data.payload?.transcription_data?.is_final ?? data.payload?.is_final ?? true,
130
- confidence: data.payload?.transcription_data?.confidence ?? data.payload?.confidence
131
- };
125
+ case "call.transcription": {
126
+ const transcript = data.payload?.transcription_data?.transcript ?? data.payload?.transcription ?? "";
127
+ if (!transcript.trim()) return null;
128
+ return {
129
+ ...baseEvent,
130
+ type: "call.speech",
131
+ transcript,
132
+ isFinal: data.payload?.transcription_data?.is_final ?? data.payload?.is_final ?? true,
133
+ confidence: data.payload?.transcription_data?.confidence ?? data.payload?.confidence
134
+ };
135
+ }
132
136
  case "call.hangup": return {
133
137
  ...baseEvent,
134
138
  type: "call.ended",
@@ -1,13 +1,13 @@
1
1
  import { fetchWithSsrFGuard } from "./runtime-api.js";
2
2
  import "./api.js";
3
3
  import { n as requireSupportedTwilioApiHostname, r as resolveTwilioApiBaseUrl } from "./twilio-region-4fkgz3UG.js";
4
- import { _ as escapeXml, a as normalizeProviderStatus, i as mapProviderStatusToEndReason, p as chunkAudio, r as isProviderStatusTerminal, s as getHeader, v as mapVoiceToPolly } from "./runtime-entry-F13eJ4pp.js";
5
- import { c as verifyTwilioWebhook, i as readVoiceCallProviderJsonResponse, n as cancelProviderResponseBody, r as readProviderErrorResponseSnippet, t as guardedJsonApiRequest } from "./guarded-json-api-DEDwgf4J.js";
4
+ import { _ as escapeXml, a as normalizeProviderStatus, i as mapProviderStatusToEndReason, p as chunkAudio, r as isProviderStatusTerminal, s as getHeader, v as mapVoiceToPolly } from "./runtime-entry-DK8kPCGC.js";
5
+ import { c as verifyTwilioWebhook, i as readVoiceCallProviderJsonResponse, n as cancelProviderResponseBody, r as readProviderErrorResponseSnippet, t as guardedJsonApiRequest } from "./guarded-json-api-j4OEG0fR.js";
6
6
  import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
7
7
  import { sleepWithAbort } from "openclaw/plugin-sdk/runtime-env";
8
8
  import crypto from "node:crypto";
9
9
  import { safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
10
- import { setTimeout as setTimeout$1 } from "node:timers/promises";
10
+ import { retryAsync } from "openclaw/plugin-sdk/retry-runtime";
11
11
  //#region extensions/voice-call/src/providers/twilio/api.ts
12
12
  const TWILIO_API_TIMEOUT_MS = 3e4;
13
13
  /** Parse Twilio JSON error responses without trusting response shape. */
@@ -188,6 +188,7 @@ var TwilioProvider = class TwilioProvider {
188
188
  this.currentPublicUrl = null;
189
189
  this.ttsProvider = null;
190
190
  this.mediaStreamHandler = null;
191
+ this.playbackMarkSequence = 0;
191
192
  this.callStreamMap = /* @__PURE__ */ new Map();
192
193
  this.streamAuthTokens = /* @__PURE__ */ new Map();
193
194
  this.twimlStorage = /* @__PURE__ */ new Map();
@@ -261,15 +262,16 @@ var TwilioProvider = class TwilioProvider {
261
262
  });
262
263
  }
263
264
  async updateLiveCallTwiml(providerCallId, twiml, operation) {
264
- for (const retryDelayMs of TWILIO_CALL_UPDATE_RETRY_DELAYS_MS) try {
265
- await this.apiRequest(`/Calls/${providerCallId}.json`, { Twiml: twiml });
266
- return;
267
- } catch (err) {
268
- if (!isTwilioCallNotInProgressError(err)) throw err;
269
- console.warn(`[voice-call] Twilio ${operation} update hit call state race (21220); retrying in ${retryDelayMs}ms`);
270
- await setTimeout$1(retryDelayMs);
271
- }
272
- await this.apiRequest(`/Calls/${providerCallId}.json`, { Twiml: twiml });
265
+ await retryAsync(() => this.apiRequest(`/Calls/${providerCallId}.json`, { Twiml: twiml }), {
266
+ attempts: TWILIO_CALL_UPDATE_RETRY_DELAYS_MS.length + 1,
267
+ minDelayMs: 0,
268
+ shouldRetry: isTwilioCallNotInProgressError,
269
+ delayMs: ({ attempt }) => TWILIO_CALL_UPDATE_RETRY_DELAYS_MS[attempt - 1] ?? 0,
270
+ onRetry: ({ delayMs }) => {
271
+ console.warn(`[voice-call] Twilio ${operation} update hit call state race (21220); retrying in ${delayMs}ms`);
272
+ },
273
+ sleep: (delayMs) => sleepWithAbort(delayMs)
274
+ });
273
275
  }
274
276
  /**
275
277
  * Verify Twilio webhook signature using HMAC-SHA1.
@@ -301,6 +303,7 @@ var TwilioProvider = class TwilioProvider {
301
303
  dedupeKey,
302
304
  turnToken: turnTokenFromQuery
303
305
  });
306
+ if (event?.direction === "inbound" && event.type !== "call.ended" && event.providerCallId && this.currentPublicUrl && !this.callWebhookUrls.has(event.providerCallId)) this.callWebhookUrls.set(event.providerCallId, this.currentPublicUrl);
304
307
  const twiml = this.generateTwimlResponse(ctx);
305
308
  return {
306
309
  events: event ? [event] : [],
@@ -345,7 +348,7 @@ var TwilioProvider = class TwilioProvider {
345
348
  to: params.get("To") || void 0
346
349
  };
347
350
  const speechResult = params.get("SpeechResult");
348
- if (speechResult) return {
351
+ if (speechResult?.trim()) return {
349
352
  ...baseEvent,
350
353
  type: "call.speech",
351
354
  transcript: speechResult,
@@ -587,10 +590,6 @@ var TwilioProvider = class TwilioProvider {
587
590
  const raw = handler.sendAudio(streamSid, audio);
588
591
  return normalizeSendResult(raw);
589
592
  };
590
- const sendPlaybackMark = (name) => {
591
- const raw = handler.sendMark(streamSid, name);
592
- return normalizeSendResult(raw);
593
- };
594
593
  await handler.queueTts(streamSid, async (signal) => {
595
594
  const sendKeepAlive = () => {
596
595
  sendAudioChunk(SILENCE_CHUNK);
@@ -601,6 +600,7 @@ var TwilioProvider = class TwilioProvider {
601
600
  }, CHUNK_DELAY_MS);
602
601
  let muLawAudio;
603
602
  let synthTimeout = null;
603
+ let removeAbortListener = () => {};
604
604
  const synthTimeoutMs = ttsProvider.synthesisTimeoutMs;
605
605
  try {
606
606
  const synthPromise = ttsProvider.synthesizeForTelephony(text);
@@ -609,10 +609,23 @@ var TwilioProvider = class TwilioProvider {
609
609
  reject(/* @__PURE__ */ new Error(`Telephony TTS synthesis timed out after ${synthTimeoutMs}ms`));
610
610
  }, synthTimeoutMs);
611
611
  });
612
- muLawAudio = await Promise.race([synthPromise, timeoutPromise]);
612
+ const abortPromise = new Promise((_, reject) => {
613
+ const onAbort = () => {
614
+ reject(signal.reason instanceof Error ? signal.reason : /* @__PURE__ */ new Error("Telephony TTS synthesis aborted"));
615
+ };
616
+ signal.addEventListener("abort", onAbort, { once: true });
617
+ removeAbortListener = () => signal.removeEventListener("abort", onAbort);
618
+ if (signal.aborted) onAbort();
619
+ });
620
+ muLawAudio = await Promise.race([
621
+ synthPromise,
622
+ timeoutPromise,
623
+ abortPromise
624
+ ]);
613
625
  } finally {
614
626
  if (synthTimeout) clearTimeout(synthTimeout);
615
627
  clearInterval(keepAlive);
628
+ removeAbortListener();
616
629
  }
617
630
  if (muLawAudio.length === 0) throw new Error("Telephony TTS produced no audio");
618
631
  let chunkAttempts = 0;
@@ -621,7 +634,11 @@ var TwilioProvider = class TwilioProvider {
621
634
  for (const chunk of chunkAudio(muLawAudio, CHUNK_SIZE)) {
622
635
  if (signal.aborted) break;
623
636
  chunkAttempts += 1;
624
- if (sendAudioChunk(chunk).sent) chunkDelivered += 1;
637
+ if (!sendAudioChunk(chunk).sent) {
638
+ handler.clearAudio(streamSid);
639
+ throw new Error(`Telephony stream playback failed: audio chunk ${chunkAttempts} not delivered`);
640
+ }
641
+ chunkDelivered += 1;
625
642
  const waitMs = nextChunkDueAt - Date.now();
626
643
  if (waitMs > 0) try {
627
644
  await sleepWithAbort(Math.ceil(waitMs), signal);
@@ -632,14 +649,10 @@ var TwilioProvider = class TwilioProvider {
632
649
  nextChunkDueAt += CHUNK_DELAY_MS;
633
650
  if (signal.aborted) break;
634
651
  }
635
- let markSent = true;
636
- if (!signal.aborted) markSent = sendPlaybackMark(`tts-${Date.now()}`).sent;
637
- if (!signal.aborted && chunkAttempts > 0 && (chunkDelivered === 0 || !markSent)) {
638
- const failures = [];
639
- if (chunkDelivered === 0) failures.push("no audio chunks delivered");
640
- if (!markSent) failures.push("completion mark not delivered");
641
- throw new Error(`Telephony stream playback failed: ${failures.join("; ")}`);
642
- }
652
+ if (signal.aborted) return;
653
+ if (chunkAttempts === 0 || chunkDelivered !== chunkAttempts) throw new Error("Telephony stream playback failed: incomplete audio delivery");
654
+ const markName = `tts-${Date.now()}-${++this.playbackMarkSequence}`;
655
+ await handler.sendMarkAndWait(streamSid, markName, muLawAudio.length / 8, signal);
643
656
  });
644
657
  }
645
658
  /**
@@ -6,6 +6,13 @@
6
6
  },
7
7
  "name": "Voice Call",
8
8
  "description": "OpenClaw voice-call plugin for Twilio, Telnyx, and Plivo phone calls.",
9
+ "cliCommands": [
10
+ {
11
+ "name": "voicecall",
12
+ "description": "Voice call utilities",
13
+ "hasSubcommands": true
14
+ }
15
+ ],
9
16
  "skills": ["./skills"],
10
17
  "commandAliases": [{ "name": "voicecall" }],
11
18
  "activation": {
@@ -85,6 +92,11 @@
85
92
  "label": "Tailscale Mode",
86
93
  "advanced": true
87
94
  },
95
+ "tailscale.port": {
96
+ "label": "Tailscale HTTPS Port",
97
+ "help": "External HTTPS port. Funnel supports 443, 8443, or 10000; Serve accepts any valid port.",
98
+ "advanced": true
99
+ },
88
100
  "tailscale.path": {
89
101
  "label": "Tailscale Path",
90
102
  "advanced": true
@@ -235,7 +247,7 @@
235
247
  },
236
248
  "sessionScope": {
237
249
  "label": "Session Scope",
238
- "help": "Use per-phone to preserve caller memory across calls, or per-call to isolate every call into a fresh voice session."
250
+ "help": "Use per-phone to preserve caller memory, per-call to isolate every call, or main to share the configured agent's main session."
239
251
  },
240
252
  "responseModel": {
241
253
  "label": "Response Model",
@@ -421,6 +433,12 @@
421
433
  "type": "string",
422
434
  "enum": ["off", "serve", "funnel"]
423
435
  },
436
+ "port": {
437
+ "type": "integer",
438
+ "minimum": 1,
439
+ "maximum": 65535,
440
+ "default": 443
441
+ },
424
442
  "path": {
425
443
  "type": "string"
426
444
  }
@@ -926,7 +944,7 @@
926
944
  },
927
945
  "sessionScope": {
928
946
  "type": "string",
929
- "enum": ["per-phone", "per-call"]
947
+ "enum": ["per-phone", "per-call", "main"]
930
948
  },
931
949
  "responseModel": {
932
950
  "type": "string"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/voice-call",
3
- "version": "2026.8.1-beta.2",
3
+ "version": "2026.8.1-beta.3",
4
4
  "description": "OpenClaw voice-call plugin for Twilio, Telnyx, and Plivo phone calls.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -13,7 +13,7 @@
13
13
  "zod": "4.4.3"
14
14
  },
15
15
  "peerDependencies": {
16
- "openclaw": ">=2026.8.1-beta.2"
16
+ "openclaw": ">=2026.8.1-beta.3"
17
17
  },
18
18
  "peerDependenciesMeta": {
19
19
  "openclaw": {
@@ -30,10 +30,10 @@
30
30
  "minHostVersion": ">=2026.4.10"
31
31
  },
32
32
  "compat": {
33
- "pluginApi": ">=2026.8.1-beta.2"
33
+ "pluginApi": ">=2026.8.1-beta.3"
34
34
  },
35
35
  "build": {
36
- "openclawVersion": "2026.8.1-beta.2"
36
+ "openclawVersion": "2026.8.1-beta.3"
37
37
  },
38
38
  "release": {
39
39
  "publishToClawHub": true,