@openclaw/voice-call 2026.5.27 → 2026.5.28-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.
@@ -218,8 +218,6 @@ const VoiceCallRealtimeAgentContextConfigSchema = z.object({
218
218
  maxChars: z.number().int().positive().default(6e3),
219
219
  /** Include configured agent identity fields. */
220
220
  includeIdentity: z.boolean().default(true),
221
- /** Include agents.defaults/list systemPromptOverride when configured. */
222
- includeSystemPrompt: z.boolean().default(true),
223
221
  /** Include selected workspace files such as SOUL.md and IDENTITY.md. */
224
222
  includeWorkspaceFiles: z.boolean().default(true),
225
223
  /** Workspace-relative files to include, bounded by maxChars. */
@@ -232,7 +230,6 @@ const VoiceCallRealtimeAgentContextConfigSchema = z.object({
232
230
  enabled: false,
233
231
  maxChars: 6e3,
234
232
  includeIdentity: true,
235
- includeSystemPrompt: true,
236
233
  includeWorkspaceFiles: true,
237
234
  files: [
238
235
  "SOUL.md",
@@ -293,7 +290,6 @@ const VoiceCallRealtimeConfigSchema = z.object({
293
290
  enabled: false,
294
291
  maxChars: 6e3,
295
292
  includeIdentity: true,
296
- includeSystemPrompt: true,
297
293
  includeWorkspaceFiles: true,
298
294
  files: [
299
295
  "SOUL.md",
@@ -1,4 +1,4 @@
1
- import { t as VoiceCallConfigSchema } from "./config-cNGVtrwa.js";
1
+ import { t as VoiceCallConfigSchema } from "./config-U-rgixyY.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";
@@ -6,7 +6,7 @@ const asObject = asOptionalRecord;
6
6
  const getString = readStringField;
7
7
  function getNumber(obj, key) {
8
8
  const value = obj?.[key];
9
- return typeof value === "number" ? value : void 0;
9
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
10
10
  }
11
11
  function mergeProviderConfig(providersValue, providerId, compatValues) {
12
12
  if (Object.keys(compatValues).length === 0) return asObject(providersValue);
@@ -22,6 +22,7 @@ function mergeProviderConfig(providersValue, providerId, compatValues) {
22
22
  }
23
23
  function collectVoiceCallLegacyConfigIssues(value) {
24
24
  const raw = asObject(value) ?? {};
25
+ const realtimeAgentContext = asObject(asObject(raw.realtime)?.agentContext);
25
26
  const twilio = asObject(raw.twilio);
26
27
  const streaming = asObject(raw.streaming);
27
28
  const issues = [];
@@ -60,6 +61,11 @@ function collectVoiceCallLegacyConfigIssues(value) {
60
61
  replacement: "streaming.providers.openai.vadThreshold",
61
62
  message: "Move streaming.vadThreshold to streaming.providers.openai.vadThreshold."
62
63
  });
64
+ if (realtimeAgentContext && Object.prototype.hasOwnProperty.call(realtimeAgentContext, "includeSystemPrompt")) issues.push({
65
+ path: "realtime.agentContext.includeSystemPrompt",
66
+ replacement: "realtime.agentContext",
67
+ message: "Remove realtime.agentContext.includeSystemPrompt; realtime context now uses the generated agent prompt."
68
+ });
63
69
  return issues;
64
70
  }
65
71
  function formatVoiceCallLegacyConfigWarnings(params) {
@@ -69,6 +75,8 @@ function formatVoiceCallLegacyConfigWarnings(params) {
69
75
  }
70
76
  function migrateVoiceCallLegacyConfigInput(params) {
71
77
  const raw = asObject(params.value) ?? {};
78
+ const realtime = asObject(raw.realtime);
79
+ const realtimeAgentContext = asObject(realtime?.agentContext);
72
80
  const twilio = asObject(raw.twilio);
73
81
  const streaming = asObject(raw.streaming);
74
82
  const configPathPrefix = params.configPathPrefix ?? "plugins.entries.voice-call.config";
@@ -98,12 +106,19 @@ function migrateVoiceCallLegacyConfigInput(params) {
98
106
  }
99
107
  const normalizedTwilio = twilio ? { ...twilio } : void 0;
100
108
  if (normalizedTwilio) delete normalizedTwilio.from;
109
+ const normalizedRealtimeAgentContext = realtimeAgentContext ? { ...realtimeAgentContext } : void 0;
110
+ if (normalizedRealtimeAgentContext) delete normalizedRealtimeAgentContext.includeSystemPrompt;
111
+ const normalizedRealtime = realtime ? {
112
+ ...realtime,
113
+ agentContext: normalizedRealtimeAgentContext ?? realtime.agentContext
114
+ } : void 0;
101
115
  const config = {
102
116
  ...raw,
103
117
  provider: raw.provider === "log" ? "mock" : raw.provider,
104
118
  fromNumber: raw.fromNumber ?? (typeof twilio?.from === "string" ? twilio.from : void 0),
105
119
  twilio: normalizedTwilio,
106
- streaming: normalizedStreaming
120
+ streaming: normalizedStreaming,
121
+ realtime: normalizedRealtime
107
122
  };
108
123
  const changes = [];
109
124
  if (raw.provider === "log") changes.push(`Moved ${configPathPrefix}.provider "log" → "mock".`);
@@ -111,8 +126,11 @@ function migrateVoiceCallLegacyConfigInput(params) {
111
126
  if (typeof streaming?.sttProvider === "string") changes.push(`Moved ${configPathPrefix}.streaming.sttProvider → ${configPathPrefix}.streaming.provider.`);
112
127
  if (typeof streaming?.openaiApiKey === "string") changes.push(`Moved ${configPathPrefix}.streaming.openaiApiKey → ${configPathPrefix}.streaming.providers.openai.apiKey.`);
113
128
  if (typeof streaming?.sttModel === "string") changes.push(`Moved ${configPathPrefix}.streaming.sttModel → ${configPathPrefix}.streaming.providers.openai.model.`);
114
- if (typeof streaming?.silenceDurationMs === "number") changes.push(`Moved ${configPathPrefix}.streaming.silenceDurationMs → ${configPathPrefix}.streaming.providers.openai.silenceDurationMs.`);
115
- if (typeof streaming?.vadThreshold === "number") changes.push(`Moved ${configPathPrefix}.streaming.vadThreshold ${configPathPrefix}.streaming.providers.openai.vadThreshold.`);
129
+ if (getNumber(streaming, "silenceDurationMs") !== void 0) changes.push(`Moved ${configPathPrefix}.streaming.silenceDurationMs → ${configPathPrefix}.streaming.providers.openai.silenceDurationMs.`);
130
+ else if (typeof streaming?.silenceDurationMs === "number") changes.push(`Removed invalid ${configPathPrefix}.streaming.silenceDurationMs.`);
131
+ if (getNumber(streaming, "vadThreshold") !== void 0) changes.push(`Moved ${configPathPrefix}.streaming.vadThreshold → ${configPathPrefix}.streaming.providers.openai.vadThreshold.`);
132
+ else if (typeof streaming?.vadThreshold === "number") changes.push(`Removed invalid ${configPathPrefix}.streaming.vadThreshold.`);
133
+ if (realtimeAgentContext && Object.prototype.hasOwnProperty.call(realtimeAgentContext, "includeSystemPrompt")) changes.push(`Removed ${configPathPrefix}.realtime.agentContext.includeSystemPrompt.`);
116
134
  return {
117
135
  config,
118
136
  changes,
@@ -1,6 +1,6 @@
1
1
  import { fetchWithSsrFGuard } from "./runtime-api.js";
2
2
  import "./api.js";
3
- import { a as getHeader } from "./runtime-entry-CxeSe0VA.js";
3
+ import { a as getHeader } from "./runtime-entry-lBiyw_6t.js";
4
4
  import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
5
5
  import { isLoopbackHost } from "openclaw/plugin-sdk/gateway-runtime";
6
6
  import { normalizeLowercaseStringOrEmpty, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { definePluginEntry, sleep } from "./runtime-api.js";
2
2
  import "./api.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";
3
+ import { i as resolveVoiceCallConfig, s as validateProviderConfig } from "./config-U-rgixyY.js";
4
+ import { c as getTailscaleSelfInfo, l as setupTailscaleExposureRoute, o as resolveWebhookExposureStatus, p as resolveUserPath, s as cleanupTailscaleExposureRoute, t as createVoiceCallRuntime } from "./runtime-entry-lBiyw_6t.js";
5
+ import { i as parseVoiceCallPluginConfig, r as normalizeVoiceCallLegacyConfigInput, t as formatVoiceCallLegacyConfigWarnings } from "./config-compat-CokN3Zzr.js";
6
6
  import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
7
7
  import { ErrorCodes, callGatewayFromCli, errorShape } from "openclaw/plugin-sdk/gateway-runtime";
8
8
  import { isRecord, normalizeOptionalLowercaseString, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
@@ -17,6 +17,7 @@ const VOICE_CALL_GATEWAY_DEFAULT_TIMEOUT_MS = 5e3;
17
17
  const VOICE_CALL_GATEWAY_OPERATION_TIMEOUT_MS = 3e4;
18
18
  const VOICE_CALL_GATEWAY_TRANSCRIPT_BUFFER_MS = 1e4;
19
19
  const VOICE_CALL_GATEWAY_POLL_INTERVAL_MS = 1e3;
20
+ const DECIMAL_INTEGER_RE = /^\d+$/;
20
21
  const voiceCallCliDeps = { callGatewayFromCli };
21
22
  function writeStdoutLine(...values) {
22
23
  process.stdout.write(`${format(...values)}\n`);
@@ -26,7 +27,8 @@ function writeStdoutJson(value) {
26
27
  }
27
28
  function parseVoiceCallIntOption(raw, optionName, opts) {
28
29
  const min = opts?.min ?? 0;
29
- const parsed = Number(raw);
30
+ const value = raw?.trim() ?? "";
31
+ const parsed = DECIMAL_INTEGER_RE.test(value) ? Number(value) : NaN;
30
32
  if (!Number.isInteger(parsed) || parsed < min) throw new Error(`Invalid numeric value for ${optionName}: ${raw ?? ""}`);
31
33
  return parsed;
32
34
  }
@@ -763,7 +765,7 @@ const voiceCallConfigSchema = {
763
765
  },
764
766
  "realtime.agentContext.enabled": {
765
767
  label: "Enable Agent Voice Context",
766
- help: "Injects a compact agent identity, system prompt, and workspace context capsule into realtime voice instructions.",
768
+ help: "Injects a compact agent identity and workspace context capsule into realtime voice instructions.",
767
769
  advanced: true
768
770
  },
769
771
  "realtime.agentContext.maxChars": {
@@ -774,10 +776,6 @@ const voiceCallConfigSchema = {
774
776
  label: "Include Agent Identity",
775
777
  advanced: true
776
778
  },
777
- "realtime.agentContext.includeSystemPrompt": {
778
- label: "Include Agent System Prompt",
779
- advanced: true
780
- },
781
779
  "realtime.agentContext.includeWorkspaceFiles": {
782
780
  label: "Include Agent Workspace Files",
783
781
  advanced: true
@@ -1,5 +1,5 @@
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";
1
+ import { a as getHeader, m as escapeXml } from "./runtime-entry-lBiyw_6t.js";
2
+ import { n as reconstructWebhookUrl, r as verifyPlivoWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-BT5XfY80.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
@@ -142,9 +142,9 @@ function decodeMulawSample(value) {
142
142
  //#region extensions/voice-call/src/webhook/stream-frame-adapter.ts
143
143
  function parseTimestampMs(value) {
144
144
  if (typeof value === "number" && Number.isFinite(value)) return value;
145
- if (typeof value === "string") {
146
- const parsed = Number.parseInt(value, 10);
147
- return Number.isFinite(parsed) ? parsed : void 0;
145
+ if (typeof value === "string" && /^[+-]?\d+$/.test(value.trim())) {
146
+ const parsed = Number(value.trim());
147
+ return Number.isSafeInteger(parsed) ? parsed : void 0;
148
148
  }
149
149
  }
150
150
  function tryParseJson(rawMessage) {
@@ -1,11 +1,11 @@
1
- import { o as resolveVoiceCallSessionKey } from "./config-cNGVtrwa.js";
2
- import { f as resolveVoiceResponseModel } from "./runtime-entry-CxeSe0VA.js";
1
+ import { o as resolveVoiceCallSessionKey } from "./config-U-rgixyY.js";
2
+ import { f as resolveVoiceResponseModel } from "./runtime-entry-lBiyw_6t.js";
3
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
7
7
  /**
8
- * Voice call response generator - uses the embedded Pi agent for tool support.
8
+ * Voice call response generator - uses the embedded OpenClaw agent for tool support.
9
9
  * Routes voice responses through the same agent infrastructure as messaging.
10
10
  */
11
11
  function readExplicitToolsAllow(value) {
@@ -92,7 +92,7 @@ function resolveVoiceSandboxSessionKey(agentId, sessionKey) {
92
92
  return `agent:${agentId}:${trimmed}`;
93
93
  }
94
94
  /**
95
- * Generate a voice response using the embedded Pi agent with full tool support.
95
+ * Generate a voice response using the embedded OpenClaw agent with full tool support.
96
96
  * Uses the same agent infrastructure as messaging for consistent behavior.
97
97
  */
98
98
  async function generateVoiceResponse(params) {
@@ -168,7 +168,7 @@ async function generateVoiceResponse(params) {
168
168
  const timeoutMs = voiceConfig.responseTimeoutMs ?? agentRuntime.resolveAgentTimeoutMs({ cfg });
169
169
  const runId = `voice:${callId}:${Date.now()}`;
170
170
  try {
171
- const result = await agentRuntime.runEmbeddedPiAgent({
171
+ const result = await agentRuntime.runEmbeddedAgent({
172
172
  sessionId,
173
173
  sessionKey: resolvedSessionKey,
174
174
  sandboxSessionKey: resolveVoiceSandboxSessionKey(agentId, resolvedSessionKey),
@@ -1,6 +1,6 @@
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-cNGVtrwa.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-U-rgixyY.js";
4
4
  import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
5
5
  import { isLoopbackHost } from "openclaw/plugin-sdk/gateway-runtime";
6
6
  import { normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, normalizeOptionalString, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
@@ -1250,13 +1250,6 @@ var CallManager = class {
1250
1250
  };
1251
1251
  //#endregion
1252
1252
  //#region extensions/voice-call/src/realtime-agent-context.ts
1253
- function readAgentEntries(cfg) {
1254
- const agents = cfg.agents;
1255
- return Array.isArray(agents?.list) ? agents.list.filter((entry) => Boolean(entry && typeof entry === "object")) : [];
1256
- }
1257
- function resolveAgentSystemPromptOverride(cfg, agentId) {
1258
- return normalizeOptionalString(readAgentEntries(cfg).find((candidate) => normalizeOptionalString(candidate.id) === agentId)?.systemPromptOverride) ?? normalizeOptionalString(cfg.agents?.defaults?.systemPromptOverride);
1259
- }
1260
1253
  function limitText(text, maxChars) {
1261
1254
  if (text.length <= maxChars) return text;
1262
1255
  return `${text.slice(0, Math.max(0, maxChars - 32)).trimEnd()}\n[truncated]`;
@@ -1301,10 +1294,6 @@ async function buildRealtimeVoiceInstructions(params) {
1301
1294
  ].filter(Boolean);
1302
1295
  if (identityLines.length > 0) capsule.push(`Configured identity:\n${identityLines.join("\n")}`);
1303
1296
  }
1304
- if (contextConfig.includeSystemPrompt) {
1305
- const systemPrompt = resolveAgentSystemPromptOverride(params.coreConfig, agentId);
1306
- if (systemPrompt) capsule.push(`Configured system prompt override:\n${systemPrompt}`);
1307
- }
1308
1297
  if (contextConfig.includeWorkspaceFiles) {
1309
1298
  const fileSections = await readWorkspaceVoiceContextFiles({
1310
1299
  workspaceDir: params.agentRuntime.resolveAgentWorkspaceDir(params.coreConfig, agentId),
@@ -1464,15 +1453,58 @@ function collectTelephonyProviderConfigs(ttsConfig) {
1464
1453
  return entries;
1465
1454
  }
1466
1455
  //#endregion
1456
+ //#region extensions/voice-call/src/bounded-child-output.ts
1457
+ const DEFAULT_MAX_OUTPUT_CHARS = 16384;
1458
+ function emptyBoundedChildOutput() {
1459
+ return {
1460
+ text: "",
1461
+ truncated: false
1462
+ };
1463
+ }
1464
+ function appendBoundedChildOutput(current, chunk, maxChars = DEFAULT_MAX_OUTPUT_CHARS) {
1465
+ const appended = current.text + chunk;
1466
+ if (appended.length <= maxChars) return {
1467
+ text: appended,
1468
+ truncated: current.truncated
1469
+ };
1470
+ return {
1471
+ text: appended.slice(-maxChars),
1472
+ truncated: true
1473
+ };
1474
+ }
1475
+ function formatBoundedChildOutput(output) {
1476
+ return output.truncated ? `[output truncated]\n${output.text}` : output.text;
1477
+ }
1478
+ //#endregion
1467
1479
  //#region extensions/voice-call/src/webhook/tailscale.ts
1480
+ const TAILSCALE_COMMAND_STDOUT_MAX_BYTES = 4 * 1024 * 1024;
1481
+ function appendTailscaleCommandStdout(current, data, maxBytes = TAILSCALE_COMMAND_STDOUT_MAX_BYTES) {
1482
+ if (current.exceeded) return current;
1483
+ const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data);
1484
+ const bytes = current.bytes + buffer.byteLength;
1485
+ if (bytes > maxBytes) return {
1486
+ bytes,
1487
+ exceeded: true,
1488
+ text: ""
1489
+ };
1490
+ return {
1491
+ bytes,
1492
+ exceeded: false,
1493
+ text: `${current.text}${buffer.toString("utf8")}`
1494
+ };
1495
+ }
1468
1496
  function runTailscaleCommand(args, timeoutMs = 2500) {
1469
1497
  return new Promise((resolve) => {
1470
1498
  const proc = spawn("tailscale", args, { stdio: [
1471
1499
  "ignore",
1472
1500
  "pipe",
1473
- "pipe"
1501
+ "ignore"
1474
1502
  ] });
1475
- let stdout = "";
1503
+ let stdout = {
1504
+ bytes: 0,
1505
+ exceeded: false,
1506
+ text: ""
1507
+ };
1476
1508
  let settled = false;
1477
1509
  let timer;
1478
1510
  const finish = (result) => {
@@ -1482,7 +1514,14 @@ function runTailscaleCommand(args, timeoutMs = 2500) {
1482
1514
  resolve(result);
1483
1515
  };
1484
1516
  proc.stdout.on("data", (data) => {
1485
- stdout += data;
1517
+ stdout = appendTailscaleCommandStdout(stdout, data);
1518
+ if (stdout.exceeded) {
1519
+ proc.kill("SIGKILL");
1520
+ finish({
1521
+ code: -1,
1522
+ stdout: ""
1523
+ });
1524
+ }
1486
1525
  });
1487
1526
  timer = setTimeout(() => {
1488
1527
  proc.kill("SIGKILL");
@@ -1500,13 +1539,17 @@ function runTailscaleCommand(args, timeoutMs = 2500) {
1500
1539
  proc.on("close", (code) => {
1501
1540
  finish({
1502
1541
  code: code ?? -1,
1503
- stdout
1542
+ stdout: stdout.text
1504
1543
  });
1505
1544
  });
1506
1545
  });
1507
1546
  }
1508
1547
  async function getTailscaleSelfInfo() {
1509
- const { code, stdout } = await runTailscaleCommand(["status", "--json"]);
1548
+ const { code, stdout } = await runTailscaleCommand([
1549
+ "status",
1550
+ "--json",
1551
+ "--peers=false"
1552
+ ]);
1510
1553
  if (code !== 0) return null;
1511
1554
  try {
1512
1555
  const status = JSON.parse(stdout);
@@ -1569,6 +1612,7 @@ async function cleanupTailscaleExposure(config) {
1569
1612
  }
1570
1613
  //#endregion
1571
1614
  //#region extensions/voice-call/src/tunnel.ts
1615
+ const NGROK_LOG_BUFFER_MAX_CHARS = 16384;
1572
1616
  /**
1573
1617
  * Start an ngrok tunnel to expose the local webhook server.
1574
1618
  *
@@ -1635,9 +1679,9 @@ async function startNgrokTunnel(config) {
1635
1679
  } catch {}
1636
1680
  };
1637
1681
  proc.stdout.on("data", (data) => {
1638
- outputBuffer += data.toString();
1639
- const lines = outputBuffer.split("\n");
1682
+ const lines = (outputBuffer + data.toString()).split("\n");
1640
1683
  outputBuffer = lines.pop() || "";
1684
+ if (outputBuffer.length > NGROK_LOG_BUFFER_MAX_CHARS) outputBuffer = outputBuffer.slice(-16384);
1641
1685
  for (const line of lines) if (line.trim()) processLine(line);
1642
1686
  });
1643
1687
  proc.stderr.on("data", (data) => {
@@ -1646,7 +1690,8 @@ async function startNgrokTunnel(config) {
1646
1690
  if (!resolved) {
1647
1691
  resolved = true;
1648
1692
  clearTimeout(timeout);
1649
- reject(/* @__PURE__ */ new Error(`ngrok error: ${msg}`));
1693
+ const output = appendBoundedChildOutput(emptyBoundedChildOutput(), msg);
1694
+ reject(/* @__PURE__ */ new Error(`ngrok error: ${formatBoundedChildOutput(output)}`));
1650
1695
  }
1651
1696
  }
1652
1697
  });
@@ -1676,17 +1721,20 @@ async function runNgrokCommand(args) {
1676
1721
  "pipe",
1677
1722
  "pipe"
1678
1723
  ] });
1679
- let stdout = "";
1680
- let stderr = "";
1724
+ let stdout = emptyBoundedChildOutput();
1725
+ let stderr = emptyBoundedChildOutput();
1681
1726
  proc.stdout.on("data", (data) => {
1682
- stdout += data.toString();
1727
+ stdout = appendBoundedChildOutput(stdout, data.toString());
1683
1728
  });
1684
1729
  proc.stderr.on("data", (data) => {
1685
- stderr += data.toString();
1730
+ stderr = appendBoundedChildOutput(stderr, data.toString());
1686
1731
  });
1687
1732
  proc.on("close", (code) => {
1688
- if (code === 0) resolve(stdout);
1689
- else reject(/* @__PURE__ */ new Error(`ngrok command failed: ${stderr || stdout}`));
1733
+ if (code === 0) resolve(stdout.text);
1734
+ else {
1735
+ const output = stderr.text ? stderr : stdout;
1736
+ reject(/* @__PURE__ */ new Error(`ngrok command failed: ${formatBoundedChildOutput(output)}`));
1737
+ }
1690
1738
  });
1691
1739
  proc.on("error", reject);
1692
1740
  });
@@ -1712,10 +1760,18 @@ async function startTailscaleTunnel(config) {
1712
1760
  "pipe",
1713
1761
  "pipe"
1714
1762
  ] });
1763
+ let stdout = emptyBoundedChildOutput();
1764
+ let stderr = emptyBoundedChildOutput();
1715
1765
  const timeout = setTimeout(() => {
1716
1766
  proc.kill("SIGKILL");
1717
1767
  reject(/* @__PURE__ */ new Error(`Tailscale ${config.mode} timed out`));
1718
1768
  }, 1e4);
1769
+ proc.stdout.on("data", (data) => {
1770
+ stdout = appendBoundedChildOutput(stdout, data.toString());
1771
+ });
1772
+ proc.stderr.on("data", (data) => {
1773
+ stderr = appendBoundedChildOutput(stderr, data.toString());
1774
+ });
1719
1775
  proc.on("close", (code) => {
1720
1776
  clearTimeout(timeout);
1721
1777
  if (code === 0) {
@@ -1728,7 +1784,11 @@ async function startTailscaleTunnel(config) {
1728
1784
  await stopTailscaleTunnel(config.mode, path);
1729
1785
  }
1730
1786
  });
1731
- } else reject(/* @__PURE__ */ new Error(`Tailscale ${config.mode} failed with code ${code}`));
1787
+ } else {
1788
+ const output = stderr.text ? stderr : stdout;
1789
+ const detail = output.text ? `: ${formatBoundedChildOutput(output)}` : "";
1790
+ reject(/* @__PURE__ */ new Error(`Tailscale ${config.mode} failed with code ${code}${detail}`));
1791
+ }
1732
1792
  });
1733
1793
  proc.on("error", (err) => {
1734
1794
  clearTimeout(timeout);
@@ -2528,7 +2588,7 @@ function loadRealtimeTranscriptionRuntime() {
2528
2588
  return realtimeTranscriptionRuntimePromise;
2529
2589
  }
2530
2590
  function loadResponseGeneratorModule() {
2531
- responseGeneratorModulePromise ??= import("./response-generator-D7HL2sFM.js");
2591
+ responseGeneratorModulePromise ??= import("./response-generator-Ca_N0987.js");
2532
2592
  return responseGeneratorModulePromise;
2533
2593
  }
2534
2594
  function sanitizeTranscriptForLog(value) {
@@ -2595,6 +2655,23 @@ function buildRealtimeRejectedTwiML() {
2595
2655
  body: "<?xml version=\"1.0\" encoding=\"UTF-8\"?><Response><Reject reason=\"rejected\" /></Response>"
2596
2656
  };
2597
2657
  }
2658
+ function buildTwilioReplayTwiML() {
2659
+ return {
2660
+ statusCode: 200,
2661
+ headers: { "Content-Type": "text/xml" },
2662
+ body: "<?xml version=\"1.0\" encoding=\"UTF-8\"?><Response></Response>"
2663
+ };
2664
+ }
2665
+ const WEBHOOK_REPLAY_RESPONSE_TTL_MS = 600 * 1e3;
2666
+ const WEBHOOK_REPLAY_RESPONSE_MAX_ENTRIES = 1e4;
2667
+ const WEBHOOK_REPLAY_RESPONSE_PRUNE_INTERVAL = 64;
2668
+ function cloneWebhookResponsePayload(payload) {
2669
+ return {
2670
+ statusCode: payload.statusCode,
2671
+ headers: payload.headers ? { ...payload.headers } : void 0,
2672
+ body: payload.body
2673
+ };
2674
+ }
2598
2675
  /**
2599
2676
  * HTTP server for receiving voice call webhooks from providers.
2600
2677
  * Supports WebSocket upgrades for media streams when streaming is enabled.
@@ -2609,6 +2686,8 @@ var VoiceCallWebhookServer = class {
2609
2686
  this.mediaStreamHandler = null;
2610
2687
  this.pendingDisconnectHangups = /* @__PURE__ */ new Map();
2611
2688
  this.realtimeHandler = null;
2689
+ this.replayResponses = /* @__PURE__ */ new Map();
2690
+ this.replayResponseCacheCalls = 0;
2612
2691
  this.config = normalizeVoiceCallConfig(config);
2613
2692
  this.manager = manager;
2614
2693
  this.provider = provider;
@@ -2976,34 +3055,78 @@ var VoiceCallWebhookServer = class {
2976
3055
  body: "Unauthorized"
2977
3056
  };
2978
3057
  }
2979
- const initialTwiML = this.provider.consumeInitialTwiML?.(ctx);
2980
- if (initialTwiML !== void 0 && initialTwiML !== null) {
2981
- const params = new URLSearchParams(ctx.rawBody);
2982
- console.log(`[voice-call] Serving provider initial TwiML before realtime handling (callSid=${params.get("CallSid") ?? "unknown"}, direction=${params.get("Direction") ?? "unknown"})`);
2983
- return {
2984
- statusCode: 200,
2985
- headers: { "Content-Type": "application/xml" },
2986
- body: initialTwiML
2987
- };
3058
+ const isReplay = Boolean(verification.isReplay);
3059
+ if (isReplay) {
3060
+ console.warn("[voice-call] Replay detected; skipping event side effects");
3061
+ if (this.provider.name === "twilio") return buildTwilioReplayTwiML();
3062
+ const cachedResponse = await this.getCachedReplayResponse(verification.verifiedRequestKey);
3063
+ if (cachedResponse) return cachedResponse;
2988
3064
  }
2989
- const realtimeParams = this.getRealtimeTwimlParams(ctx);
2990
- if (realtimeParams) {
2991
- const direction = realtimeParams.get("Direction");
2992
- if ((!direction || direction === "inbound") && !this.shouldAcceptRealtimeInboundRequest(realtimeParams)) {
2993
- console.log("[voice-call] Realtime inbound call rejected before stream setup");
2994
- return buildRealtimeRejectedTwiML();
3065
+ const buildResponse = async () => {
3066
+ const initialTwiML = this.provider.consumeInitialTwiML?.(ctx);
3067
+ if (initialTwiML !== void 0 && initialTwiML !== null) {
3068
+ const params = new URLSearchParams(ctx.rawBody);
3069
+ console.log(`[voice-call] Serving provider initial TwiML before realtime handling (callSid=${params.get("CallSid") ?? "unknown"}, direction=${params.get("Direction") ?? "unknown"})`);
3070
+ return {
3071
+ statusCode: 200,
3072
+ headers: { "Content-Type": "application/xml" },
3073
+ body: initialTwiML
3074
+ };
2995
3075
  }
2996
- console.log(`[voice-call] Serving realtime TwiML for Twilio call ${realtimeParams.get("CallSid") ?? "unknown"} (direction=${direction ?? "unknown"})`);
2997
- return this.realtimeHandler.buildTwiMLPayload(req, realtimeParams);
2998
- }
2999
- const parsed = this.provider.parseWebhookEvent(ctx, { verifiedRequestKey: verification.verifiedRequestKey });
3000
- if (verification.isReplay) console.warn("[voice-call] Replay detected; skipping event side effects");
3001
- else this.processParsedEvents(parsed.events);
3002
- return normalizeWebhookResponse(parsed);
3076
+ const realtimeParams = this.getRealtimeTwimlParams(ctx);
3077
+ if (realtimeParams) {
3078
+ const direction = realtimeParams.get("Direction");
3079
+ if ((!direction || direction === "inbound") && !this.shouldAcceptRealtimeInboundRequest(realtimeParams)) {
3080
+ console.log("[voice-call] Realtime inbound call rejected before stream setup");
3081
+ return buildRealtimeRejectedTwiML();
3082
+ }
3083
+ console.log(`[voice-call] Serving realtime TwiML for Twilio call ${realtimeParams.get("CallSid") ?? "unknown"} (direction=${direction ?? "unknown"})`);
3084
+ return this.realtimeHandler.buildTwiMLPayload(req, realtimeParams);
3085
+ }
3086
+ const parsed = this.provider.parseWebhookEvent(ctx, { verifiedRequestKey: verification.verifiedRequestKey });
3087
+ if (!isReplay) this.processParsedEvents(parsed.events);
3088
+ return normalizeWebhookResponse(parsed);
3089
+ };
3090
+ if (isReplay) return await buildResponse();
3091
+ if (this.provider.name === "twilio") return await buildResponse();
3092
+ return await this.cacheReplayResponse(verification.verifiedRequestKey, buildResponse);
3003
3093
  } finally {
3004
3094
  this.webhookInFlightLimiter.release(inFlightKey);
3005
3095
  }
3006
3096
  }
3097
+ pruneReplayResponses(now) {
3098
+ for (const [key, entry] of this.replayResponses) if (entry.expiresAt <= now) this.replayResponses.delete(key);
3099
+ while (this.replayResponses.size > WEBHOOK_REPLAY_RESPONSE_MAX_ENTRIES) {
3100
+ const oldest = this.replayResponses.keys().next().value;
3101
+ if (!oldest) break;
3102
+ this.replayResponses.delete(oldest);
3103
+ }
3104
+ }
3105
+ async getCachedReplayResponse(key) {
3106
+ const now = Date.now();
3107
+ const entry = this.replayResponses.get(key);
3108
+ if (!entry) return null;
3109
+ if (entry.expiresAt <= now) {
3110
+ this.replayResponses.delete(key);
3111
+ return null;
3112
+ }
3113
+ return cloneWebhookResponsePayload(await entry.response);
3114
+ }
3115
+ async cacheReplayResponse(key, buildResponse) {
3116
+ const now = Date.now();
3117
+ this.replayResponseCacheCalls += 1;
3118
+ if (this.replayResponseCacheCalls % WEBHOOK_REPLAY_RESPONSE_PRUNE_INTERVAL === 0) this.pruneReplayResponses(now);
3119
+ const response = buildResponse().then(cloneWebhookResponsePayload).catch((err) => {
3120
+ this.replayResponses.delete(key);
3121
+ throw err;
3122
+ });
3123
+ this.replayResponses.set(key, {
3124
+ expiresAt: now + WEBHOOK_REPLAY_RESPONSE_TTL_MS,
3125
+ response
3126
+ });
3127
+ if (this.replayResponses.size > WEBHOOK_REPLAY_RESPONSE_MAX_ENTRIES) this.pruneReplayResponses(now);
3128
+ return cloneWebhookResponsePayload(await response);
3129
+ }
3007
3130
  verifyPreAuthWebhookHeaders(headers) {
3008
3131
  if (this.config.skipSignatureVerification) return { ok: true };
3009
3132
  switch (this.provider.name) {
@@ -3145,15 +3268,15 @@ let mockProviderPromise;
3145
3268
  let realtimeVoiceRuntimePromise;
3146
3269
  let realtimeHandlerPromise;
3147
3270
  function loadTelnyxProvider() {
3148
- telnyxProviderPromise ??= import("./telnyx-CJAhbDYn.js");
3271
+ telnyxProviderPromise ??= import("./telnyx-l9-ePiyj.js");
3149
3272
  return telnyxProviderPromise;
3150
3273
  }
3151
3274
  function loadTwilioProvider() {
3152
- twilioProviderPromise ??= import("./twilio-Dn84Eomh.js");
3275
+ twilioProviderPromise ??= import("./twilio-C5wUc1S2.js");
3153
3276
  return twilioProviderPromise;
3154
3277
  }
3155
3278
  function loadPlivoProvider() {
3156
- plivoProviderPromise ??= import("./plivo-CNtzf7Do.js");
3279
+ plivoProviderPromise ??= import("./plivo-D4QmQaQu.js");
3157
3280
  return plivoProviderPromise;
3158
3281
  }
3159
3282
  function loadMockProvider() {
@@ -3165,7 +3288,7 @@ function loadRealtimeVoiceRuntime() {
3165
3288
  return realtimeVoiceRuntimePromise;
3166
3289
  }
3167
3290
  function loadRealtimeHandler() {
3168
- realtimeHandlerPromise ??= import("./realtime-handler-NP8w71q9.js");
3291
+ realtimeHandlerPromise ??= import("./realtime-handler-DT0Rlj3m.js");
3169
3292
  return realtimeHandlerPromise;
3170
3293
  }
3171
3294
  function resolveVoiceCallConsultSessionKey(call) {
@@ -1,2 +1,2 @@
1
- import { t as createVoiceCallRuntime } from "./runtime-entry-CxeSe0VA.js";
1
+ import { t as createVoiceCallRuntime } from "./runtime-entry-lBiyw_6t.js";
2
2
  export { createVoiceCallRuntime };
package/dist/setup-api.js CHANGED
@@ -1,4 +1,4 @@
1
- import { n as migrateVoiceCallLegacyConfigInput } from "./config-compat-4gaIelu_.js";
1
+ import { n as migrateVoiceCallLegacyConfigInput } from "./config-compat-CokN3Zzr.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-Dkeawg_W.js";
1
+ import { i as verifyTelnyxWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-BT5XfY80.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-CxeSe0VA.js";
4
- import { a as verifyTwilioWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-Dkeawg_W.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-lBiyw_6t.js";
4
+ import { a as verifyTwilioWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-BT5XfY80.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";
@@ -314,6 +314,11 @@ var TwilioProvider = class TwilioProvider {
314
314
  if (direction === "inbound") return "inbound";
315
315
  if (direction === "outbound-api" || direction === "outbound-dial") return "outbound";
316
316
  }
317
+ static parseConfidence(value) {
318
+ const trimmed = value?.trim();
319
+ if (!trimmed || !/^\d+(?:\.\d+)?$/.test(trimmed)) return .9;
320
+ return Number(trimmed);
321
+ }
317
322
  /**
318
323
  * Convert Twilio webhook params to normalized event format.
319
324
  */
@@ -337,7 +342,7 @@ var TwilioProvider = class TwilioProvider {
337
342
  type: "call.speech",
338
343
  transcript: speechResult,
339
344
  isFinal: true,
340
- confidence: Number.parseFloat(params.get("Confidence") || "0.9")
345
+ confidence: TwilioProvider.parseConfidence(params.get("Confidence"))
341
346
  };
342
347
  const digits = params.get("Digits");
343
348
  if (digits) return {
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@openclaw/voice-call",
3
- "version": "2026.5.27",
3
+ "version": "2026.5.28-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.27",
9
+ "version": "2026.5.28-beta.1",
10
10
  "dependencies": {
11
11
  "commander": "14.0.3",
12
12
  "typebox": "1.1.38",
@@ -14,7 +14,7 @@
14
14
  "zod": "4.4.3"
15
15
  },
16
16
  "peerDependencies": {
17
- "openclaw": ">=2026.5.27"
17
+ "openclaw": ">=2026.5.28-beta.1"
18
18
  },
19
19
  "peerDependenciesMeta": {
20
20
  "openclaw": {
@@ -188,7 +188,7 @@
188
188
  },
189
189
  "realtime.agentContext.enabled": {
190
190
  "label": "Enable Agent Voice Context",
191
- "help": "Injects a compact agent identity, system prompt, and workspace context capsule into realtime voice instructions.",
191
+ "help": "Injects a compact agent identity and workspace context capsule into realtime voice instructions.",
192
192
  "advanced": true
193
193
  },
194
194
  "realtime.agentContext.maxChars": {
@@ -199,10 +199,6 @@
199
199
  "label": "Include Agent Identity",
200
200
  "advanced": true
201
201
  },
202
- "realtime.agentContext.includeSystemPrompt": {
203
- "label": "Include Agent System Prompt",
204
- "advanced": true
205
- },
206
202
  "realtime.agentContext.includeWorkspaceFiles": {
207
203
  "label": "Include Agent Workspace Files",
208
204
  "advanced": true
@@ -617,9 +613,6 @@
617
613
  "includeIdentity": {
618
614
  "type": "boolean"
619
615
  },
620
- "includeSystemPrompt": {
621
- "type": "boolean"
622
- },
623
616
  "includeWorkspaceFiles": {
624
617
  "type": "boolean"
625
618
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/voice-call",
3
- "version": "2026.5.27",
3
+ "version": "2026.5.28-beta.1",
4
4
  "description": "OpenClaw voice-call plugin for Twilio, Telnyx, and Plivo phone calls.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -14,7 +14,7 @@
14
14
  "zod": "4.4.3"
15
15
  },
16
16
  "peerDependencies": {
17
- "openclaw": ">=2026.5.27"
17
+ "openclaw": ">=2026.5.28-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.27"
34
+ "pluginApi": ">=2026.5.28-beta.1"
35
35
  },
36
36
  "build": {
37
- "openclawVersion": "2026.5.27"
37
+ "openclawVersion": "2026.5.28-beta.1"
38
38
  },
39
39
  "release": {
40
40
  "publishToClawHub": true,