@openclaw/voice-call 2026.5.24-beta.2 → 2026.5.25-beta.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.
@@ -1,5 +1,6 @@
1
1
  import { TtsConfigSchema } from "./runtime-api.js";
2
2
  import "./api.js";
3
+ import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
3
4
  import { REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME, REALTIME_VOICE_AGENT_CONSULT_TOOL_POLICIES } from "openclaw/plugin-sdk/realtime-voice";
4
5
  import { buildSecretInputSchema, hasConfiguredSecretInput, normalizeResolvedSecretInputString } from "openclaw/plugin-sdk/secret-input";
5
6
  import { z } from "zod";
@@ -10,7 +11,7 @@ const BLOCKED_MERGE_KEYS = new Set([
10
11
  "constructor"
11
12
  ]);
12
13
  function deepMergeDefined(base, override) {
13
- if (!isPlainObject(base) || !isPlainObject(override)) return override === void 0 ? base : override;
14
+ if (!isRecord(base) || !isRecord(override)) return override === void 0 ? base : override;
14
15
  const result = { ...base };
15
16
  for (const [key, value] of Object.entries(override)) {
16
17
  if (BLOCKED_MERGE_KEYS.has(key) || value === void 0) continue;
@@ -19,9 +20,6 @@ function deepMergeDefined(base, override) {
19
20
  }
20
21
  return result;
21
22
  }
22
- function isPlainObject(value) {
23
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
24
- }
25
23
  //#endregion
26
24
  //#region extensions/voice-call/src/realtime-defaults.ts
27
25
  const DEFAULT_VOICE_CALL_REALTIME_INSTRUCTIONS = `You are OpenClaw's phone-call realtime voice interface. Keep spoken replies brief and natural. When a question needs deeper reasoning, current information, or tools, call ${REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME} before answering.`;
@@ -1,4 +1,4 @@
1
- import { t as VoiceCallConfigSchema } from "./config-CmO0pHE1.js";
1
+ import { t as VoiceCallConfigSchema } from "./config-cNGVtrwa.js";
2
2
  import { asOptionalRecord, readStringField } from "openclaw/plugin-sdk/string-coerce-runtime";
3
3
  //#region extensions/voice-call/src/config-compat.ts
4
4
  const VOICE_CALL_LEGACY_CONFIG_REMOVAL_VERSION = "2026.6.0";
@@ -1,9 +1,9 @@
1
1
  import { fetchWithSsrFGuard } from "./runtime-api.js";
2
2
  import "./api.js";
3
- import { a as getHeader } from "./runtime-entry-Ckt5k9pK.js";
3
+ import { a as getHeader } from "./runtime-entry-CxeSe0VA.js";
4
4
  import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
5
5
  import { isLoopbackHost } from "openclaw/plugin-sdk/gateway-runtime";
6
- import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
6
+ import { normalizeLowercaseStringOrEmpty, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
7
7
  import crypto from "node:crypto";
8
8
  import { safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
9
9
  //#region extensions/voice-call/src/webhook-security.ts
@@ -451,7 +451,7 @@ function validatePlivoV3Signature(params) {
451
451
  postParams: params.postParams
452
452
  })}.${params.nonce}`;
453
453
  const expected = normalizeSignatureBase64(crypto.createHmac("sha256", params.authToken).update(hmacBase).digest("base64"));
454
- const provided = params.signatureHeader.split(",").map((s) => s.trim()).filter(Boolean).map((s) => normalizeSignatureBase64(s));
454
+ const provided = normalizeStringEntries(params.signatureHeader.split(",")).map((s) => normalizeSignatureBase64(s));
455
455
  for (const sig of provided) if (timingSafeEqualString(expected, sig)) return true;
456
456
  return false;
457
457
  }
package/dist/index.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import { definePluginEntry, sleep } from "./runtime-api.js";
2
2
  import "./api.js";
3
- import { i as resolveVoiceCallConfig, s as validateProviderConfig } from "./config-CmO0pHE1.js";
4
- import { c as getTailscaleSelfInfo, l as setupTailscaleExposureRoute, o as resolveWebhookExposureStatus, p as resolveUserPath, s as cleanupTailscaleExposureRoute, t as createVoiceCallRuntime } from "./runtime-entry-Ckt5k9pK.js";
5
- import { i as parseVoiceCallPluginConfig, r as normalizeVoiceCallLegacyConfigInput, t as formatVoiceCallLegacyConfigWarnings } from "./config-compat-HORl1Cdw.js";
3
+ import { i as resolveVoiceCallConfig, s as validateProviderConfig } from "./config-cNGVtrwa.js";
4
+ import { c as getTailscaleSelfInfo, l as setupTailscaleExposureRoute, o as resolveWebhookExposureStatus, p as resolveUserPath, s as cleanupTailscaleExposureRoute, t as createVoiceCallRuntime } from "./runtime-entry-CxeSe0VA.js";
5
+ import { i as parseVoiceCallPluginConfig, r as normalizeVoiceCallLegacyConfigInput, t as formatVoiceCallLegacyConfigWarnings } from "./config-compat-4gaIelu_.js";
6
6
  import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
7
7
  import { ErrorCodes, callGatewayFromCli, errorShape } from "openclaw/plugin-sdk/gateway-runtime";
8
- import { normalizeOptionalLowercaseString, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
8
+ import { isRecord, normalizeOptionalLowercaseString, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
9
9
  import { Type } from "typebox";
10
10
  import fs from "node:fs";
11
11
  import os from "node:os";
@@ -30,9 +30,6 @@ function parseVoiceCallIntOption(raw, optionName, opts) {
30
30
  if (!Number.isInteger(parsed) || parsed < min) throw new Error(`Invalid numeric value for ${optionName}: ${raw ?? ""}`);
31
31
  return parsed;
32
32
  }
33
- function isRecord$1(value) {
34
- return Boolean(value && typeof value === "object" && !Array.isArray(value));
35
- }
36
33
  function isGatewayUnavailableForLocalFallback(err) {
37
34
  const message = formatErrorMessage(err);
38
35
  return message.includes("ECONNREFUSED") || message.includes("ECONNRESET") || message.includes("EHOSTUNREACH") || message.includes("ENOTFOUND") || message.includes("gateway closed (1006") || message.includes("gateway not connected");
@@ -65,15 +62,15 @@ function isUnknownGatewayMethod(err, method) {
65
62
  return formatErrorMessage(err).includes(`unknown method: ${method}`);
66
63
  }
67
64
  function readGatewayOperationId(payload) {
68
- if (isRecord$1(payload) && typeof payload.operationId === "string" && payload.operationId) return payload.operationId;
65
+ if (isRecord(payload) && typeof payload.operationId === "string" && payload.operationId) return payload.operationId;
69
66
  throw new Error("voicecall gateway response missing operationId");
70
67
  }
71
68
  function readGatewayPollTimeoutMs(payload, fallbackTimeoutMs) {
72
- if (isRecord$1(payload) && typeof payload.pollTimeoutMs === "number") return Math.max(1, Math.ceil(payload.pollTimeoutMs));
69
+ if (isRecord(payload) && typeof payload.pollTimeoutMs === "number") return Math.max(1, Math.ceil(payload.pollTimeoutMs));
73
70
  return fallbackTimeoutMs;
74
71
  }
75
72
  function readCompletedContinueResult(payload) {
76
- if (!isRecord$1(payload)) throw new Error("voicecall gateway response missing operation status");
73
+ if (!isRecord(payload)) throw new Error("voicecall gateway response missing operation status");
77
74
  if (payload.status === "pending") return { status: "pending" };
78
75
  if (payload.status === "failed") return {
79
76
  status: "failed",
@@ -191,11 +188,11 @@ async function initiateCallAndPrintId(params) {
191
188
  writeStdoutJson({ callId: result.callId });
192
189
  }
193
190
  function writeGatewayCallId(payload) {
194
- if (isRecord$1(payload) && typeof payload.callId === "string") {
191
+ if (isRecord(payload) && typeof payload.callId === "string") {
195
192
  writeStdoutJson({ callId: payload.callId });
196
193
  return;
197
194
  }
198
- if (isRecord$1(payload) && typeof payload.error === "string") throw new Error(payload.error);
195
+ if (isRecord(payload) && typeof payload.error === "string") throw new Error(payload.error);
199
196
  throw new Error("voicecall gateway response missing callId");
200
197
  }
201
198
  async function initiateCallViaGatewayOrRuntime(params) {
@@ -273,7 +270,7 @@ function registerVoiceCallCli(params) {
273
270
  mode
274
271
  }, { timeoutMs: resolveGatewayOperationTimeoutMs(config) });
275
272
  let callId;
276
- if (gateway.ok) callId = isRecord$1(gateway.payload) ? gateway.payload.callId : void 0;
273
+ if (gateway.ok) callId = isRecord(gateway.payload) ? gateway.payload.callId : void 0;
277
274
  else {
278
275
  const result = await (await ensureRuntime()).manager.initiateCall(options.to, void 0, {
279
276
  message: options.message,
@@ -330,7 +327,7 @@ function registerVoiceCallCli(params) {
330
327
  }, { timeoutMs: resolveGatewayContinueTimeoutMs(config) });
331
328
  }
332
329
  if (gateway.ok) {
333
- if (isRecord$1(gateway.payload) && typeof gateway.payload.operationId === "string") {
330
+ if (isRecord(gateway.payload) && typeof gateway.payload.operationId === "string") {
334
331
  writeStdoutJson(await pollVoiceCallContinueGateway({
335
332
  operationId: readGatewayOperationId(gateway.payload),
336
333
  timeoutMs: readGatewayPollTimeoutMs(gateway.payload, resolveGatewayContinueTimeoutMs(config))
@@ -383,7 +380,7 @@ function registerVoiceCallCli(params) {
383
380
  root.command("status").description("Show call status").option("--call-id <id>", "Call ID").option("--json", "Print machine-readable JSON").action(async (options) => {
384
381
  const gateway = await callVoiceCallGateway("voicecall.status", options.callId ? { callId: options.callId } : void 0);
385
382
  if (gateway.ok) {
386
- if (options.callId && isRecord$1(gateway.payload)) {
383
+ if (options.callId && isRecord(gateway.payload)) {
387
384
  if (gateway.payload.found === true && "call" in gateway.payload) {
388
385
  writeStdoutJson(gateway.payload.call);
389
386
  return;
@@ -1,5 +1,5 @@
1
- import { a as getHeader, m as escapeXml } from "./runtime-entry-Ckt5k9pK.js";
2
- import { n as reconstructWebhookUrl, r as verifyPlivoWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-BveGhYhl.js";
1
+ import { a as getHeader, m as escapeXml } from "./runtime-entry-CxeSe0VA.js";
2
+ import { n as reconstructWebhookUrl, r as verifyPlivoWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-Dkeawg_W.js";
3
3
  import { normalizeLowercaseStringOrEmpty, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
4
4
  import crypto from "node:crypto";
5
5
  //#region extensions/voice-call/src/providers/plivo.ts
@@ -1,7 +1,8 @@
1
1
  import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
2
2
  import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env";
3
- import { REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME, buildRealtimeVoiceAgentConsultWorkingResponse, createRealtimeVoiceBridgeSession, createTalkSessionController, recordTalkObservabilityEvent } from "openclaw/plugin-sdk/realtime-voice";
3
+ import { REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME, buildRealtimeVoiceAgentConsultWorkingResponse, createRealtimeVoiceBridgeSession, createRealtimeVoiceForcedConsultCoordinator, createTalkSessionController, readRealtimeVoiceConsultQuestion, readSpeakableRealtimeVoiceToolResult, recordTalkObservabilityEvent } from "openclaw/plugin-sdk/realtime-voice";
4
4
  import { randomUUID } from "node:crypto";
5
+ import "node:http";
5
6
  import WebSocket, { WebSocketServer } from "ws";
6
7
  //#region extensions/voice-call/src/webhook/realtime-audio-pacer.ts
7
8
  const TELEPHONY_SAMPLE_RATE = 8e3;
@@ -316,21 +317,13 @@ function buildGreetingInstructions(baseInstructions, greeting) {
316
317
  const intro = "Start the call by greeting the caller naturally. Include this greeting in your first spoken reply:";
317
318
  return baseInstructions ? `${baseInstructions}\n\n${intro} "${trimmedGreeting}"` : `${intro} "${trimmedGreeting}"`;
318
319
  }
319
- function readSpeakableToolResultText(result) {
320
- if (typeof result === "string") return result.trim() || void 0;
321
- if (!result || typeof result !== "object" || Array.isArray(result)) return;
322
- const text = result.text;
323
- if (typeof text === "string" && text.trim()) return text.trim();
324
- const output = result.output;
325
- return typeof output === "string" && output.trim() ? output.trim() : void 0;
326
- }
327
320
  function readConsultArgText(args, key) {
328
321
  if (!args || typeof args !== "object" || Array.isArray(args)) return;
329
322
  const value = args[key];
330
323
  return typeof value === "string" && value.trim() ? value.trim() : void 0;
331
324
  }
332
325
  function readConsultQuestionText(args) {
333
- return readConsultArgText(args, "question") ?? readConsultArgText(args, "prompt") ?? readConsultArgText(args, "query") ?? readConsultArgText(args, "task");
326
+ return readRealtimeVoiceConsultQuestion(args);
334
327
  }
335
328
  function normalizeTranscriptText(text) {
336
329
  return text.replace(/\s+/g, " ").trim();
@@ -435,10 +428,8 @@ var RealtimeCallHandler = class {
435
428
  this.partialUserTranscriptUpdatedAtByCallId = /* @__PURE__ */ new Map();
436
429
  this.recentFinalUserTranscriptsByCallId = /* @__PURE__ */ new Map();
437
430
  this.recentFinalUserTranscriptTimersByCallId = /* @__PURE__ */ new Map();
438
- this.forcedConsultTimersByCallId = /* @__PURE__ */ new Map();
439
- this.forcedConsultInFlightByCallId = /* @__PURE__ */ new Set();
431
+ this.forcedConsultCoordinatorsByCallId = /* @__PURE__ */ new Map();
440
432
  this.forcedConsultsByCallId = /* @__PURE__ */ new Map();
441
- this.lastProviderConsultAtByCallId = /* @__PURE__ */ new Map();
442
433
  this.nativeConsultsInFlightByCallId = /* @__PURE__ */ new Map();
443
434
  this.publicOrigin = null;
444
435
  this.publicPathPrefix = "";
@@ -1002,14 +993,16 @@ var RealtimeCallHandler = class {
1002
993
  }
1003
994
  }
1004
995
  clearForcedConsultState(callId) {
1005
- const timer = this.forcedConsultTimersByCallId.get(callId);
1006
- if (timer) {
1007
- clearTimeout(timer);
1008
- this.forcedConsultTimersByCallId.delete(callId);
1009
- }
1010
- this.forcedConsultInFlightByCallId.delete(callId);
996
+ this.forcedConsultCoordinatorsByCallId.get(callId)?.clear();
997
+ this.forcedConsultCoordinatorsByCallId.delete(callId);
1011
998
  this.forcedConsultsByCallId.delete(callId);
1012
- this.lastProviderConsultAtByCallId.delete(callId);
999
+ }
1000
+ forcedConsultCoordinator(callId) {
1001
+ const existing = this.forcedConsultCoordinatorsByCallId.get(callId);
1002
+ if (existing) return existing;
1003
+ const created = createRealtimeVoiceForcedConsultCoordinator();
1004
+ this.forcedConsultCoordinatorsByCallId.set(callId, created);
1005
+ return created;
1013
1006
  }
1014
1007
  closeTelephonyBridge(callIdOrSid, bridge, reason) {
1015
1008
  const closer = this.activeTelephonyClosersByCallId.get(callIdOrSid);
@@ -1025,36 +1018,43 @@ var RealtimeCallHandler = class {
1025
1018
  if (!question) return;
1026
1019
  const handler = this.toolHandlers.get(REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME);
1027
1020
  if (!handler) return;
1028
- const existingTimer = this.forcedConsultTimersByCallId.get(params.callId);
1029
- if (existingTimer) clearTimeout(existingTimer);
1030
- const timer = setTimeout(() => {
1031
- this.forcedConsultTimersByCallId.delete(params.callId);
1032
- if (this.forcedConsultInFlightByCallId.has(params.callId)) return;
1033
- const lastProviderConsultAt = this.lastProviderConsultAtByCallId.get(params.callId) ?? 0;
1034
- if (Date.now() - lastProviderConsultAt < 2e3) return;
1021
+ const existingForcedConsult = this.forcedConsultsByCallId.get(params.callId);
1022
+ if (existingForcedConsult && !existingForcedConsult.completedAt) return;
1023
+ const coordinator = this.forcedConsultCoordinator(params.callId);
1024
+ if (coordinator.hasRecentNativeConsult(question, { allowUnknownQuestion: true })) return;
1025
+ coordinator.clearPending();
1026
+ const pending = coordinator.prepare(question);
1027
+ if (!pending) return;
1028
+ coordinator.schedule(pending, FORCED_CONSULT_FALLBACK_DELAY_MS, (handle) => {
1029
+ const activeForcedConsult = this.forcedConsultsByCallId.get(params.callId);
1030
+ if (activeForcedConsult && !activeForcedConsult.completedAt) return;
1035
1031
  this.runForcedAgentConsult({
1036
1032
  ...params,
1037
- question,
1033
+ handle,
1038
1034
  handler
1039
1035
  });
1040
- }, FORCED_CONSULT_FALLBACK_DELAY_MS);
1041
- this.forcedConsultTimersByCallId.set(params.callId, timer);
1036
+ });
1042
1037
  }
1043
1038
  async runForcedAgentConsult(params) {
1044
- this.forcedConsultInFlightByCallId.add(params.callId);
1039
+ const coordinator = this.forcedConsultCoordinator(params.callId);
1040
+ coordinator.markStarted(params.handle);
1045
1041
  const startedAt = Date.now();
1046
- logger.debug(`[voice-call] realtime forced agent consult reason=${FORCED_CONSULT_REASON} consultPolicy=always callId=${params.callId} providerCallId=${params.callSid} chars=${params.question.length}`);
1047
- console.log(`[voice-call] realtime forced agent consult starting callId=${params.callId} providerCallId=${params.callSid} chars=${params.question.length}`);
1042
+ logger.debug(`[voice-call] realtime forced agent consult reason=${FORCED_CONSULT_REASON} consultPolicy=always callId=${params.callId} providerCallId=${params.callSid} chars=${params.handle.question.length}`);
1043
+ console.log(`[voice-call] realtime forced agent consult starting callId=${params.callId} providerCallId=${params.callSid} chars=${params.handle.question.length}`);
1048
1044
  params.clearAudio();
1049
1045
  const state = {
1050
1046
  sendSpeechPrompt: true,
1051
- promise: Promise.resolve().then(() => params.handler({ question: params.question }, params.callId, {}))
1047
+ promise: Promise.resolve().then(() => params.handler({ question: params.handle.question }, params.callId, {}))
1052
1048
  };
1053
1049
  this.forcedConsultsByCallId.set(params.callId, state);
1054
1050
  try {
1055
1051
  const result = await state.promise;
1056
1052
  state.completedAt = Date.now();
1057
- const text = readSpeakableToolResultText(result);
1053
+ coordinator.markDelivered(params.handle);
1054
+ const text = readSpeakableRealtimeVoiceToolResult(result, {
1055
+ keys: ["text", "output"],
1056
+ maxChars: FORCED_CONSULT_RESULT_MAX_CHARS
1057
+ });
1058
1058
  if (!text) {
1059
1059
  console.warn(`[voice-call] realtime forced agent consult returned no speakable text callId=${params.callId} providerCallId=${params.callSid}`);
1060
1060
  return;
@@ -1064,13 +1064,15 @@ var RealtimeCallHandler = class {
1064
1064
  params.session.sendUserMessage(buildForcedConsultSpeechPrompt(text));
1065
1065
  }
1066
1066
  console.log(`[voice-call] realtime forced agent consult completed callId=${params.callId} providerCallId=${params.callSid} elapsedMs=${Date.now() - startedAt}`);
1067
- this.consumePartialUserTranscript(params.callId, params.question);
1067
+ this.consumePartialUserTranscript(params.callId, params.handle.question);
1068
1068
  } catch (error) {
1069
1069
  console.warn(`[voice-call] realtime forced agent consult failed callId=${params.callId} providerCallId=${params.callSid} error=${formatErrorMessage(error)}`);
1070
1070
  } finally {
1071
- this.forcedConsultInFlightByCallId.delete(params.callId);
1072
1071
  setTimeout(() => {
1073
- if (this.forcedConsultsByCallId.get(params.callId) === state) this.forcedConsultsByCallId.delete(params.callId);
1072
+ if (this.forcedConsultsByCallId.get(params.callId) === state) {
1073
+ this.forcedConsultsByCallId.delete(params.callId);
1074
+ coordinator.remove(params.handle);
1075
+ }
1074
1076
  }, FORCED_CONSULT_NATIVE_DEDUPE_MS).unref?.();
1075
1077
  }
1076
1078
  }
@@ -1161,15 +1163,15 @@ var RealtimeCallHandler = class {
1161
1163
  }
1162
1164
  };
1163
1165
  if (name === REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME) {
1164
- this.lastProviderConsultAtByCallId.set(callId, Date.now());
1165
- const timer = this.forcedConsultTimersByCallId.get(callId);
1166
- if (timer) {
1167
- clearTimeout(timer);
1168
- this.forcedConsultTimersByCallId.delete(callId);
1166
+ const coordinator = this.forcedConsultCoordinator(callId);
1167
+ const forcedMatch = coordinator.recordNativeConsult(args, bridgeCallId);
1168
+ if (forcedMatch.kind === "none") {
1169
+ const pending = coordinator.consumePending();
1170
+ if (pending) coordinator.remove(pending);
1169
1171
  }
1170
1172
  const forcedConsult = this.forcedConsultsByCallId.get(callId);
1171
1173
  if (forcedConsult) {
1172
- if (forcedConsult.completedAt) {
1174
+ if (forcedConsult.completedAt || forcedMatch.kind === "already_delivered") {
1173
1175
  submitFinalToolResult({
1174
1176
  status: "already_delivered",
1175
1177
  message: "OpenClaw already delivered this consult result internally. Do not repeat it."
@@ -1,6 +1,6 @@
1
- import { o as resolveVoiceCallSessionKey } from "./config-CmO0pHE1.js";
2
- import { f as resolveVoiceResponseModel } from "./runtime-entry-Ckt5k9pK.js";
3
- import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
1
+ import { o as resolveVoiceCallSessionKey } from "./config-cNGVtrwa.js";
2
+ import { f as resolveVoiceResponseModel } from "./runtime-entry-CxeSe0VA.js";
3
+ import { isRecord, normalizeLowercaseStringOrEmpty, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
4
4
  import crypto from "node:crypto";
5
5
  import { applyModelOverrideToSessionEntry } from "openclaw/plugin-sdk/model-session-runtime";
6
6
  //#region extensions/voice-call/src/response-generator.ts
@@ -8,20 +8,17 @@ import { applyModelOverrideToSessionEntry } from "openclaw/plugin-sdk/model-sess
8
8
  * Voice call response generator - uses the embedded Pi agent for tool support.
9
9
  * Routes voice responses through the same agent infrastructure as messaging.
10
10
  */
11
- function isRecord$1(value) {
12
- return typeof value === "object" && value !== null && !Array.isArray(value);
13
- }
14
11
  function readExplicitToolsAllow(value) {
15
- if (!isRecord$1(value)) return;
12
+ if (!isRecord(value)) return;
16
13
  const allow = value.allow;
17
14
  if (!Array.isArray(allow)) return;
18
15
  return allow.filter((entry) => typeof entry === "string");
19
16
  }
20
17
  function resolveVoiceAgentToolsAllow(config, agentId) {
21
- const agents = isRecord$1(config.agents) ? config.agents : void 0;
22
- const agent = (Array.isArray(agents?.list) ? agents.list : []).find((entry) => isRecord$1(entry) && entry.id === agentId);
23
- if (!isRecord$1(agent)) return;
24
- return readExplicitToolsAllow(isRecord$1(agent.tools) ? agent.tools : void 0);
18
+ const agents = isRecord(config.agents) ? config.agents : void 0;
19
+ const agent = (Array.isArray(agents?.list) ? agents.list : []).find((entry) => isRecord(entry) && entry.id === agentId);
20
+ if (!isRecord(agent)) return;
21
+ return readExplicitToolsAllow(isRecord(agent.tools) ? agent.tools : void 0);
25
22
  }
26
23
  const VOICE_SPOKEN_OUTPUT_CONTRACT = [
27
24
  "Output format requirements:",
@@ -69,7 +66,7 @@ function isLikelyMetaReasoningParagraph(paragraph) {
69
66
  function sanitizePlainSpokenText(text) {
70
67
  const withoutCodeFences = text.replace(/```[\s\S]*?```/g, " ").trim();
71
68
  if (!withoutCodeFences) return null;
72
- const paragraphs = withoutCodeFences.split(/\n\s*\n+/).map((paragraph) => paragraph.trim()).filter(Boolean);
69
+ const paragraphs = normalizeStringEntries(withoutCodeFences.split(/\n\s*\n+/));
73
70
  while (paragraphs.length > 1 && isLikelyMetaReasoningParagraph(paragraphs[0])) paragraphs.shift();
74
71
  return normalizeSpokenText(paragraphs.join(" "));
75
72
  }
@@ -1,9 +1,9 @@
1
1
  import { isBlockedHostnameOrIp, isRequestBodyLimitError, readRequestBodyWithLimit, requestBodyErrorToText } from "./runtime-api.js";
2
2
  import "./api.js";
3
- import { a as resolveVoiceCallEffectiveConfig, c as deepMergeDefined, i as resolveVoiceCallConfig, n as normalizeVoiceCallConfig, o as resolveVoiceCallSessionKey, r as resolveTwilioAuthToken, s as validateProviderConfig } from "./config-CmO0pHE1.js";
3
+ import { a as resolveVoiceCallEffectiveConfig, c as deepMergeDefined, i as resolveVoiceCallConfig, n as normalizeVoiceCallConfig, o as resolveVoiceCallSessionKey, r as resolveTwilioAuthToken, s as validateProviderConfig } from "./config-cNGVtrwa.js";
4
4
  import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
5
5
  import { isLoopbackHost } from "openclaw/plugin-sdk/gateway-runtime";
6
- import { normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
6
+ import { normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, normalizeOptionalString, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
7
7
  import { REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME, buildRealtimeVoiceAgentConsultPolicyInstructions, consultRealtimeVoiceAgent, convertPcmToMulaw8k, createTalkSessionController, recordTalkObservabilityEvent, resolveRealtimeVoiceAgentConsultTools, resolveRealtimeVoiceAgentConsultToolsAllow, resolveRealtimeVoiceFastContextConsult } from "openclaw/plugin-sdk/realtime-voice";
8
8
  import { z } from "zod";
9
9
  import fs from "node:fs";
@@ -1250,15 +1250,12 @@ var CallManager = class {
1250
1250
  };
1251
1251
  //#endregion
1252
1252
  //#region extensions/voice-call/src/realtime-agent-context.ts
1253
- function normalizeString(value) {
1254
- return typeof value === "string" && value.trim() ? value.trim() : void 0;
1255
- }
1256
1253
  function readAgentEntries(cfg) {
1257
1254
  const agents = cfg.agents;
1258
1255
  return Array.isArray(agents?.list) ? agents.list.filter((entry) => Boolean(entry && typeof entry === "object")) : [];
1259
1256
  }
1260
1257
  function resolveAgentSystemPromptOverride(cfg, agentId) {
1261
- return normalizeString(readAgentEntries(cfg).find((candidate) => normalizeString(candidate.id) === agentId)?.systemPromptOverride) ?? normalizeString(cfg.agents?.defaults?.systemPromptOverride);
1258
+ return normalizeOptionalString(readAgentEntries(cfg).find((candidate) => normalizeOptionalString(candidate.id) === agentId)?.systemPromptOverride) ?? normalizeOptionalString(cfg.agents?.defaults?.systemPromptOverride);
1262
1259
  }
1263
1260
  function limitText(text, maxChars) {
1264
1261
  if (text.length <= maxChars) return text;
@@ -1296,11 +1293,11 @@ async function buildRealtimeVoiceInstructions(params) {
1296
1293
  if (contextConfig.includeIdentity) {
1297
1294
  const identity = params.agentRuntime.resolveAgentIdentity(params.coreConfig, agentId);
1298
1295
  const identityLines = [
1299
- normalizeString(identity?.name) ? `- Name: ${normalizeString(identity?.name)}` : void 0,
1300
- normalizeString(identity?.emoji) ? `- Emoji: ${normalizeString(identity?.emoji)}` : void 0,
1301
- normalizeString(identity?.vibe) ? `- Vibe: ${normalizeString(identity?.vibe)}` : void 0,
1302
- normalizeString(identity?.theme) ? `- Theme: ${normalizeString(identity?.theme)}` : void 0,
1303
- normalizeString(identity?.creature) ? `- Creature/persona: ${normalizeString(identity?.creature)}` : void 0
1296
+ normalizeOptionalString(identity?.name) ? `- Name: ${normalizeOptionalString(identity?.name)}` : void 0,
1297
+ normalizeOptionalString(identity?.emoji) ? `- Emoji: ${normalizeOptionalString(identity?.emoji)}` : void 0,
1298
+ normalizeOptionalString(identity?.vibe) ? `- Vibe: ${normalizeOptionalString(identity?.vibe)}` : void 0,
1299
+ normalizeOptionalString(identity?.theme) ? `- Theme: ${normalizeOptionalString(identity?.theme)}` : void 0,
1300
+ normalizeOptionalString(identity?.creature) ? `- Creature/persona: ${normalizeOptionalString(identity?.creature)}` : void 0
1304
1301
  ].filter(Boolean);
1305
1302
  if (identityLines.length > 0) capsule.push(`Configured identity:\n${identityLines.join("\n")}`);
1306
1303
  }
@@ -2531,7 +2528,7 @@ function loadRealtimeTranscriptionRuntime() {
2531
2528
  return realtimeTranscriptionRuntimePromise;
2532
2529
  }
2533
2530
  function loadResponseGeneratorModule() {
2534
- responseGeneratorModulePromise ??= import("./response-generator-CzGdOMuc.js");
2531
+ responseGeneratorModulePromise ??= import("./response-generator-D7HL2sFM.js");
2535
2532
  return responseGeneratorModulePromise;
2536
2533
  }
2537
2534
  function sanitizeTranscriptForLog(value) {
@@ -2572,7 +2569,7 @@ function resolveForwardedClientIp(request, trustedProxyIPs) {
2572
2569
  const normalizedTrustedProxyIps = new Set(trustedProxyIPs.map((ip) => normalizeProxyIp(ip)).filter((ip) => Boolean(ip)));
2573
2570
  const forwardedFor = getHeader(request.headers, "x-forwarded-for");
2574
2571
  if (forwardedFor) {
2575
- const forwardedIps = forwardedFor.split(",").map((part) => part.trim()).filter(Boolean);
2572
+ const forwardedIps = normalizeStringEntries(forwardedFor.split(","));
2576
2573
  if (forwardedIps.length > 0) {
2577
2574
  if (normalizedTrustedProxyIps.size === 0) return forwardedIps[0];
2578
2575
  for (let index = forwardedIps.length - 1; index >= 0; index -= 1) {
@@ -3148,15 +3145,15 @@ let mockProviderPromise;
3148
3145
  let realtimeVoiceRuntimePromise;
3149
3146
  let realtimeHandlerPromise;
3150
3147
  function loadTelnyxProvider() {
3151
- telnyxProviderPromise ??= import("./telnyx-7YcRSSf6.js");
3148
+ telnyxProviderPromise ??= import("./telnyx-CJAhbDYn.js");
3152
3149
  return telnyxProviderPromise;
3153
3150
  }
3154
3151
  function loadTwilioProvider() {
3155
- twilioProviderPromise ??= import("./twilio-BAcJzoFs.js");
3152
+ twilioProviderPromise ??= import("./twilio-Dn84Eomh.js");
3156
3153
  return twilioProviderPromise;
3157
3154
  }
3158
3155
  function loadPlivoProvider() {
3159
- plivoProviderPromise ??= import("./plivo-cga35v6d.js");
3156
+ plivoProviderPromise ??= import("./plivo-CNtzf7Do.js");
3160
3157
  return plivoProviderPromise;
3161
3158
  }
3162
3159
  function loadMockProvider() {
@@ -3168,7 +3165,7 @@ function loadRealtimeVoiceRuntime() {
3168
3165
  return realtimeVoiceRuntimePromise;
3169
3166
  }
3170
3167
  function loadRealtimeHandler() {
3171
- realtimeHandlerPromise ??= import("./realtime-handler-GzCyjKXl.js");
3168
+ realtimeHandlerPromise ??= import("./realtime-handler-NP8w71q9.js");
3172
3169
  return realtimeHandlerPromise;
3173
3170
  }
3174
3171
  function resolveVoiceCallConsultSessionKey(call) {
@@ -1,2 +1,2 @@
1
- import { t as createVoiceCallRuntime } from "./runtime-entry-Ckt5k9pK.js";
1
+ import { t as createVoiceCallRuntime } from "./runtime-entry-CxeSe0VA.js";
2
2
  export { createVoiceCallRuntime };
package/dist/setup-api.js CHANGED
@@ -1,4 +1,4 @@
1
- import { n as migrateVoiceCallLegacyConfigInput } from "./config-compat-HORl1Cdw.js";
1
+ import { n as migrateVoiceCallLegacyConfigInput } from "./config-compat-4gaIelu_.js";
2
2
  import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
3
3
  import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
4
4
  //#region extensions/voice-call/setup-api.ts
@@ -1,4 +1,4 @@
1
- import { i as verifyTelnyxWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-BveGhYhl.js";
1
+ import { i as verifyTelnyxWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-Dkeawg_W.js";
2
2
  import crypto from "node:crypto";
3
3
  //#region extensions/voice-call/src/providers/telnyx.ts
4
4
  function normalizeTelnyxDirection(direction) {
@@ -1,7 +1,7 @@
1
1
  import { fetchWithSsrFGuard } from "./runtime-api.js";
2
2
  import "./api.js";
3
- import { a as getHeader, d as chunkAudio, h as mapVoiceToPolly, i as normalizeProviderStatus, m as escapeXml, n as isProviderStatusTerminal, r as mapProviderStatusToEndReason } from "./runtime-entry-Ckt5k9pK.js";
4
- import { a as verifyTwilioWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-BveGhYhl.js";
3
+ import { a as getHeader, d as chunkAudio, h as mapVoiceToPolly, i as normalizeProviderStatus, m as escapeXml, n as isProviderStatusTerminal, r as mapProviderStatusToEndReason } from "./runtime-entry-CxeSe0VA.js";
4
+ import { a as verifyTwilioWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-Dkeawg_W.js";
5
5
  import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
6
6
  import crypto from "node:crypto";
7
7
  import { safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
@@ -40,6 +40,10 @@ class Receiver extends Writable {
40
40
  * extensions
41
41
  * @param {Boolean} [options.isServer=false] Specifies whether to operate in
42
42
  * client or server mode
43
+ * @param {Number} [options.maxBufferedChunks=0] The maximum number of
44
+ * buffered data chunks
45
+ * @param {Number} [options.maxFragments=0] The maximum number of message
46
+ * fragments
43
47
  * @param {Number} [options.maxPayload=0] The maximum allowed message length
44
48
  * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or
45
49
  * not to skip UTF-8 validation for text and close messages
@@ -54,6 +58,8 @@ class Receiver extends Writable {
54
58
  this._binaryType = options.binaryType || BINARY_TYPES[0];
55
59
  this._extensions = options.extensions || {};
56
60
  this._isServer = !!options.isServer;
61
+ this._maxBufferedChunks = options.maxBufferedChunks | 0;
62
+ this._maxFragments = options.maxFragments | 0;
57
63
  this._maxPayload = options.maxPayload | 0;
58
64
  this._skipUTF8Validation = !!options.skipUTF8Validation;
59
65
  this[kWebSocket] = undefined;
@@ -89,6 +95,22 @@ class Receiver extends Writable {
89
95
  _write(chunk, encoding, cb) {
90
96
  if (this._opcode === 0x08 && this._state == GET_INFO) return cb();
91
97
 
98
+ if (
99
+ this._maxBufferedChunks > 0 &&
100
+ this._buffers.length >= this._maxBufferedChunks
101
+ ) {
102
+ cb(
103
+ this.createError(
104
+ RangeError,
105
+ 'Too many buffered chunks',
106
+ false,
107
+ 1008,
108
+ 'WS_ERR_TOO_MANY_BUFFERED_PARTS'
109
+ )
110
+ );
111
+ return;
112
+ }
113
+
92
114
  this._bufferedBytes += chunk.length;
93
115
  this._buffers.push(chunk);
94
116
  this.startLoop(cb);
@@ -485,6 +507,22 @@ class Receiver extends Writable {
485
507
  }
486
508
 
487
509
  if (data.length) {
510
+ if (
511
+ this._maxFragments > 0 &&
512
+ this._fragments.length >= this._maxFragments
513
+ ) {
514
+ const error = this.createError(
515
+ RangeError,
516
+ 'Too many message fragments',
517
+ false,
518
+ 1008,
519
+ 'WS_ERR_TOO_MANY_BUFFERED_PARTS'
520
+ );
521
+
522
+ cb(error);
523
+ return;
524
+ }
525
+
488
526
  //
489
527
  // This message is not compressed so its length is the sum of the payload
490
528
  // length of all fragments.
@@ -524,6 +562,22 @@ class Receiver extends Writable {
524
562
  return;
525
563
  }
526
564
 
565
+ if (
566
+ this._maxFragments > 0 &&
567
+ this._fragments.length >= this._maxFragments
568
+ ) {
569
+ const error = this.createError(
570
+ RangeError,
571
+ 'Too many message fragments',
572
+ false,
573
+ 1008,
574
+ 'WS_ERR_TOO_MANY_BUFFERED_PARTS'
575
+ );
576
+
577
+ cb(error);
578
+ return;
579
+ }
580
+
527
581
  this._fragments.push(buf);
528
582
  }
529
583
 
@@ -43,6 +43,10 @@ class WebSocketServer extends EventEmitter {
43
43
  * called
44
44
  * @param {Function} [options.handleProtocols] A hook to handle protocols
45
45
  * @param {String} [options.host] The hostname where to bind the server
46
+ * @param {Number} [options.maxBufferedChunks=1048576] The maximum number of
47
+ * buffered data chunks
48
+ * @param {Number} [options.maxFragments=131072] The maximum number of message
49
+ * fragments
46
50
  * @param {Number} [options.maxPayload=104857600] The maximum allowed message
47
51
  * size
48
52
  * @param {Boolean} [options.noServer=false] Enable no server mode
@@ -65,6 +69,8 @@ class WebSocketServer extends EventEmitter {
65
69
  options = {
66
70
  allowSynchronousEvents: true,
67
71
  autoPong: true,
72
+ maxBufferedChunks: 1024 * 1024,
73
+ maxFragments: 128 * 1024,
68
74
  maxPayload: 100 * 1024 * 1024,
69
75
  skipUTF8Validation: false,
70
76
  perMessageDeflate: false,
@@ -424,6 +430,8 @@ class WebSocketServer extends EventEmitter {
424
430
 
425
431
  ws.setSocket(socket, head, {
426
432
  allowSynchronousEvents: this.options.allowSynchronousEvents,
433
+ maxBufferedChunks: this.options.maxBufferedChunks,
434
+ maxFragments: this.options.maxFragments,
427
435
  maxPayload: this.options.maxPayload,
428
436
  skipUTF8Validation: this.options.skipUTF8Validation
429
437
  });
@@ -201,6 +201,10 @@ class WebSocket extends EventEmitter {
201
201
  * multiple times in the same tick
202
202
  * @param {Function} [options.generateMask] The function used to generate the
203
203
  * masking key
204
+ * @param {Number} [options.maxBufferedChunks=0] The maximum number of
205
+ * buffered data chunks
206
+ * @param {Number} [options.maxFragments=0] The maximum number of message
207
+ * fragments
204
208
  * @param {Number} [options.maxPayload=0] The maximum allowed message size
205
209
  * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or
206
210
  * not to skip UTF-8 validation for text and close messages
@@ -212,6 +216,8 @@ class WebSocket extends EventEmitter {
212
216
  binaryType: this.binaryType,
213
217
  extensions: this._extensions,
214
218
  isServer: this._isServer,
219
+ maxBufferedChunks: options.maxBufferedChunks,
220
+ maxFragments: options.maxFragments,
215
221
  maxPayload: options.maxPayload,
216
222
  skipUTF8Validation: options.skipUTF8Validation
217
223
  });
@@ -640,6 +646,10 @@ module.exports = WebSocket;
640
646
  * masking key
641
647
  * @param {Number} [options.handshakeTimeout] Timeout in milliseconds for the
642
648
  * handshake request
649
+ * @param {Number} [options.maxBufferedChunks=1048576] The maximum number of
650
+ * buffered data chunks
651
+ * @param {Number} [options.maxFragments=131072] The maximum number of message
652
+ * fragments
643
653
  * @param {Number} [options.maxPayload=104857600] The maximum allowed message
644
654
  * size
645
655
  * @param {Number} [options.maxRedirects=10] The maximum number of redirects
@@ -660,6 +670,8 @@ function initAsClient(websocket, address, protocols, options) {
660
670
  autoPong: true,
661
671
  closeTimeout: CLOSE_TIMEOUT,
662
672
  protocolVersion: protocolVersions[1],
673
+ maxBufferedChunks: 1024 * 1024,
674
+ maxFragments: 128 * 1024,
663
675
  maxPayload: 100 * 1024 * 1024,
664
676
  skipUTF8Validation: false,
665
677
  perMessageDeflate: true,
@@ -1017,6 +1029,8 @@ function initAsClient(websocket, address, protocols, options) {
1017
1029
  websocket.setSocket(socket, head, {
1018
1030
  allowSynchronousEvents: opts.allowSynchronousEvents,
1019
1031
  generateMask: opts.generateMask,
1032
+ maxBufferedChunks: opts.maxBufferedChunks,
1033
+ maxFragments: opts.maxFragments,
1020
1034
  maxPayload: opts.maxPayload,
1021
1035
  skipUTF8Validation: opts.skipUTF8Validation
1022
1036
  });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ws",
3
- "version": "8.20.1",
3
+ "version": "8.21.0",
4
4
  "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js",
5
5
  "keywords": [
6
6
  "HyBi",
@@ -1,20 +1,20 @@
1
1
  {
2
2
  "name": "@openclaw/voice-call",
3
- "version": "2026.5.24-beta.2",
3
+ "version": "2026.5.25-beta.1",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@openclaw/voice-call",
9
- "version": "2026.5.24-beta.2",
9
+ "version": "2026.5.25-beta.1",
10
10
  "dependencies": {
11
11
  "commander": "14.0.3",
12
12
  "typebox": "1.1.38",
13
- "ws": "8.20.1",
13
+ "ws": "8.21.0",
14
14
  "zod": "4.4.3"
15
15
  },
16
16
  "peerDependencies": {
17
- "openclaw": ">=2026.5.24-beta.2"
17
+ "openclaw": ">=2026.5.25-beta.1"
18
18
  },
19
19
  "peerDependenciesMeta": {
20
20
  "openclaw": {
@@ -38,9 +38,9 @@
38
38
  "license": "MIT"
39
39
  },
40
40
  "node_modules/ws": {
41
- "version": "8.20.1",
42
- "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz",
43
- "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==",
41
+ "version": "8.21.0",
42
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
43
+ "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
44
44
  "license": "MIT",
45
45
  "engines": {
46
46
  "node": ">=10.0.0"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/voice-call",
3
- "version": "2026.5.24-beta.2",
3
+ "version": "2026.5.25-beta.1",
4
4
  "description": "OpenClaw voice-call plugin",
5
5
  "repository": {
6
6
  "type": "git",
@@ -10,11 +10,11 @@
10
10
  "dependencies": {
11
11
  "commander": "14.0.3",
12
12
  "typebox": "1.1.38",
13
- "ws": "8.20.1",
13
+ "ws": "8.21.0",
14
14
  "zod": "4.4.3"
15
15
  },
16
16
  "peerDependencies": {
17
- "openclaw": ">=2026.5.24-beta.2"
17
+ "openclaw": ">=2026.5.25-beta.1"
18
18
  },
19
19
  "peerDependenciesMeta": {
20
20
  "openclaw": {
@@ -31,10 +31,10 @@
31
31
  "minHostVersion": ">=2026.4.10"
32
32
  },
33
33
  "compat": {
34
- "pluginApi": ">=2026.5.24-beta.2"
34
+ "pluginApi": ">=2026.5.25-beta.1"
35
35
  },
36
36
  "build": {
37
- "openclawVersion": "2026.5.24-beta.2"
37
+ "openclawVersion": "2026.5.25-beta.1"
38
38
  },
39
39
  "release": {
40
40
  "publishToClawHub": true,