@openclaw/voice-call 2026.9.8 → 2026.10.1-beta.2

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 (41) hide show
  1. package/dist/.setup/{cli-D6uwMXyS.mjs → cli-Cn1leykM.mjs} +57 -37
  2. package/dist/.setup/{config-Ds7xoZbI.mjs → config-CX9J-053.mjs} +18 -119
  3. package/dist/.setup/{guarded-json-api-Bqc6QS8A.mjs → guarded-json-api-x55yqHvW.mjs} +39 -94
  4. package/dist/.setup/{mock-ClWW6Ufu.mjs → mock-eLv_Dc4o.mjs} +3 -6
  5. package/dist/.setup/{plivo-69ObrQPk.mjs → plivo-CpPIENu4.mjs} +48 -101
  6. package/dist/.setup/{realtime-handler-BgDPcPZt.mjs → realtime-handler-Do7Sz7Gh.mjs} +123 -235
  7. package/dist/.setup/{response-generator-BzKrJMEb.mjs → response-generator-DAhbXEta.mjs} +6 -25
  8. package/dist/.setup/{runtime-entry-BNOQRXii.mjs → runtime-entry-CbSKxSKk.mjs} +554 -425
  9. package/dist/.setup/state-retention-api-ONgqE-_V.mjs +201 -0
  10. package/dist/.setup/store-path-Cfwff6j7.mjs +33 -0
  11. package/dist/.setup/{telnyx-DjFGCfy1.mjs → telnyx-DPilutCH.mjs} +10 -29
  12. package/dist/.setup/{twilio-Bp3gfmUB.mjs → twilio-BWFNGyVx.mjs} +78 -201
  13. package/dist/.setup/{twilio-region-4fkgz3UG.mjs → twilio-region-wBOx-8sz.mjs} +1 -4
  14. package/dist/.setup/voice-mapping-B_iAUkER.mjs +31 -0
  15. package/dist/.setup/{webhook-replay-CCQSoBNF.mjs → webhook-replay-B7RJNWNG.mjs} +2 -5
  16. package/dist/doctor-contract-api.js +7 -330
  17. package/dist/index.js +18 -16
  18. package/dist/runtime-entry.js +1 -1
  19. package/dist/state-retention-api.js +2 -0
  20. package/openclaw.plugin.json +1 -1
  21. package/package.json +4 -6
  22. package/dist/.setup/utils-B5pnM_Ij.mjs +0 -437
  23. package/node_modules/ws/LICENSE +0 -20
  24. package/node_modules/ws/README.md +0 -548
  25. package/node_modules/ws/browser.js +0 -8
  26. package/node_modules/ws/index.js +0 -22
  27. package/node_modules/ws/lib/buffer-util.js +0 -131
  28. package/node_modules/ws/lib/constants.js +0 -19
  29. package/node_modules/ws/lib/event-target.js +0 -292
  30. package/node_modules/ws/lib/extension.js +0 -203
  31. package/node_modules/ws/lib/limiter.js +0 -55
  32. package/node_modules/ws/lib/permessage-deflate.js +0 -530
  33. package/node_modules/ws/lib/receiver.js +0 -743
  34. package/node_modules/ws/lib/sender.js +0 -607
  35. package/node_modules/ws/lib/stream.js +0 -161
  36. package/node_modules/ws/lib/subprotocol.js +0 -62
  37. package/node_modules/ws/lib/validation.js +0 -152
  38. package/node_modules/ws/lib/websocket-server.js +0 -562
  39. package/node_modules/ws/lib/websocket.js +0 -1407
  40. package/node_modules/ws/package.json +0 -74
  41. package/node_modules/ws/wrapper.mjs +0 -21
@@ -1,13 +1,14 @@
1
1
  import { sleep } from "../api.js";
2
- import { l as resolveVoiceCallStreamExposurePaths, u as validateProviderConfig } from "./config-Ds7xoZbI.mjs";
3
- import { d as findCallInStore, f as getCallHistoryFromStore, n as resolveDefaultVoiceCallStoreDir, p as loadActiveCallsFromStore, s as MAX_CALL_RECORD_EVENTS, t as resolveUserPath, x as setVoiceCallStateRuntime } from "./utils-B5pnM_Ij.mjs";
4
- import { d as cleanupTailscaleExposureRoute, f as getTailscaleSelfInfo, p as setupTailscaleExposureRoutes, u as resolveWebhookExposureStatus, v as resolveVoiceCallAgentId } from "./runtime-entry-BNOQRXii.mjs";
2
+ import { l as resolveVoiceCallStreamExposurePaths, u as validateProviderConfig } from "./config-CX9J-053.mjs";
3
+ import { _ as MAX_CALL_RECORD_EVENTS, b as loadActiveCallsFromStore, c as resolveWebhookExposureStatus, d as setupTailscaleExposureRoutes, h as resolveVoiceCallAgentId, l as cleanupTailscaleExposureRoute, u as getTailscaleSelfInfo, v as findCallInStore, x as setVoiceCallStateRuntime, y as getCallHistoryFromStore } from "./runtime-entry-CbSKxSKk.mjs";
4
+ import { r as resolveUserPath, t as resolveDefaultVoiceCallStoreDir } from "./store-path-Cfwff6j7.mjs";
5
5
  import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
6
6
  import { callGatewayFromCli, isGatewayClientRequestError, isGatewayTransportError, redactSensitiveUrlLikeString } from "openclaw/plugin-sdk/gateway-runtime";
7
7
  import { isRecord, normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/string-coerce-runtime";
8
8
  import { MAX_TCP_PORT, MAX_TIMER_TIMEOUT_MS, addTimerTimeoutGraceMs, clampTimerTimeoutMs, parseStrictNonNegativeInteger } from "openclaw/plugin-sdk/number-runtime";
9
9
  import fs from "node:fs";
10
10
  import path from "node:path";
11
+ import { createDeferred } from "openclaw/plugin-sdk/concurrency-runtime";
11
12
  import { once } from "node:events";
12
13
  import { format } from "node:util";
13
14
  //#region extensions/voice-call/src/cli-command-io.ts
@@ -94,11 +95,6 @@ async function* readLogLines(file, last, pollMs) {
94
95
  await sleep(pollMs);
95
96
  }
96
97
  }
97
- function percentile(values, p) {
98
- if (values.length === 0) return 0;
99
- const sorted = [...values].toSorted((a, b) => a - b);
100
- return sorted[Math.min(sorted.length - 1, Math.max(0, Math.ceil(p / 100 * sorted.length) - 1))] ?? 0;
101
- }
102
98
  function summarizeSeries(values) {
103
99
  if (values.length === 0) return {
104
100
  count: 0,
@@ -108,16 +104,16 @@ function summarizeSeries(values) {
108
104
  p50Ms: 0,
109
105
  p95Ms: 0
110
106
  };
111
- const minMs = values.reduce((min, value) => value < min ? value : min);
112
- const maxMs = values.reduce((max, value) => value > max ? value : max);
107
+ const sorted = values.toSorted((a, b) => a - b);
108
+ const percentile = (fraction) => sorted[Math.ceil(fraction * sorted.length) - 1] ?? 0;
113
109
  const avgMs = values.reduce((sum, value) => sum + value, 0) / values.length;
114
110
  return {
115
111
  count: values.length,
116
- minMs,
117
- maxMs,
112
+ minMs: sorted[0] ?? 0,
113
+ maxMs: sorted[sorted.length - 1] ?? 0,
118
114
  avgMs,
119
- p50Ms: percentile(values, 50),
120
- p95Ms: percentile(values, 95)
115
+ p50Ms: percentile(.5),
116
+ p95Ms: percentile(.95)
121
117
  };
122
118
  }
123
119
  async function writeVoiceCallLatencySummary(calls) {
@@ -388,8 +384,40 @@ function writeSetupStatus(status) {
388
384
  writeCliLine("Voice Call setup: %s", status.ok ? "OK" : "needs attention");
389
385
  for (const check of status.checks) writeCliLine("%s %s: %s", check.ok ? "OK" : "FAIL", check.id, check.message);
390
386
  }
387
+ async function runWithStandaloneRuntime(createRuntime, run) {
388
+ let runtime;
389
+ const interrupted = createDeferred();
390
+ let exitSignal;
391
+ const interrupt = (signal) => {
392
+ exitSignal ??= signal;
393
+ process.exitCode = exitSignal === "SIGINT" ? 130 : 143;
394
+ interrupted.resolve();
395
+ };
396
+ const onSigint = () => interrupt("SIGINT");
397
+ const onSigterm = () => interrupt("SIGTERM");
398
+ try {
399
+ await run(async () => {
400
+ if (!runtime) {
401
+ process.on("SIGINT", onSigint);
402
+ process.on("SIGTERM", onSigterm);
403
+ runtime = createRuntime();
404
+ }
405
+ const active = await runtime;
406
+ if (exitSignal) throw new Error(`Voice call command interrupted by ${exitSignal}`);
407
+ return active;
408
+ });
409
+ if (runtime) await interrupted.promise;
410
+ } finally {
411
+ try {
412
+ await (await runtime?.catch(() => void 0))?.stop();
413
+ } finally {
414
+ process.off("SIGINT", onSigint);
415
+ process.off("SIGTERM", onSigterm);
416
+ }
417
+ }
418
+ }
391
419
  function registerVoiceCallCli(params) {
392
- const { program, config, coreConfig, ensureRuntime, stateRuntime } = params;
420
+ const { program, config, coreConfig, ensureRuntime: createRuntime, stateRuntime } = params;
393
421
  const ensureHistoryStateRuntime = () => {
394
422
  if (stateRuntime) setVoiceCallStateRuntime({ state: stateRuntime });
395
423
  };
@@ -402,7 +430,7 @@ function registerVoiceCallCli(params) {
402
430
  }
403
431
  writeSetupStatus(status);
404
432
  });
405
- root.command("smoke").description("Check Voice Call readiness and optionally place a short outbound test call").option("-t, --to <phone>", "Phone number to call for a live smoke").option("--message <text>", "Message to speak during the smoke call", "OpenClaw voice call smoke test.").option("--mode <mode>", "Call mode: notify or conversation", "notify").option("--yes", "Actually place the live outbound call").option("--json", "Print machine-readable JSON").action(async (options) => {
433
+ root.command("smoke").description("Check Voice Call readiness and optionally place a short outbound test call").option("-t, --to <phone>", "Phone number to call for a live smoke").option("--message <text>", "Message to speak during the smoke call", "OpenClaw voice call smoke test.").option("--mode <mode>", "Call mode: notify or conversation", "notify").option("--yes", "Actually place the live outbound call").option("--json", "Print machine-readable JSON").action(async (options) => runWithStandaloneRuntime(createRuntime, async (ensureRuntime) => {
406
434
  const setup = buildSetupStatus(config, coreConfig);
407
435
  if (!setup.ok) {
408
436
  if (options.json) writeCliJson({
@@ -459,28 +487,20 @@ function registerVoiceCallCli(params) {
459
487
  }
460
488
  writeSetupStatus(setup);
461
489
  writeCliLine("live-call: started %s", callId);
462
- });
463
- root.command("call").description("Initiate an outbound voice call").requiredOption("-m, --message <text>", "Message to speak when call connects").option("-t, --to <phone>", "Phone number to call (E.164 format, uses config toNumber if not set)").option("--mode <mode>", "Call mode: notify (hangup after message) or conversation (stay open)", "conversation").action(async (options) => {
464
- writeCliJson({ callId: await initiateVoiceCall({
465
- ensureRuntime,
466
- config,
467
- method: "voicecall.initiate",
468
- to: options.to,
469
- message: options.message,
470
- mode: options.mode
471
- }) });
472
- });
473
- root.command("start").description("Alias for voicecall call").requiredOption("--to <phone>", "Phone number to call").option("--message <text>", "Message to speak when call connects").option("--mode <mode>", "Call mode: notify (hangup after message) or conversation (stay open)", "conversation").action(async (options) => {
490
+ }));
491
+ const callAction = (method) => async (options) => runWithStandaloneRuntime(createRuntime, async (ensureRuntime) => {
474
492
  writeCliJson({ callId: await initiateVoiceCall({
475
493
  ensureRuntime,
476
494
  config,
477
- method: "voicecall.start",
495
+ method,
478
496
  to: options.to,
479
497
  message: options.message,
480
498
  mode: options.mode
481
499
  }) });
482
500
  });
483
- root.command("continue").description("Speak a message and wait for a response").requiredOption("--call-id <id>", "Call ID").requiredOption("--message <text>", "Message to speak").action(async (options) => {
501
+ root.command("call").description("Initiate an outbound voice call").requiredOption("-m, --message <text>", "Message to speak when call connects").option("-t, --to <phone>", "Phone number to call (E.164 format, uses config toNumber if not set)").option("--mode <mode>", "Call mode: notify (hangup after message) or conversation (stay open)", "conversation").action(callAction("voicecall.initiate"));
502
+ root.command("start").description("Alias for voicecall call").requiredOption("--to <phone>", "Phone number to call").option("--message <text>", "Message to speak when call connects").option("--mode <mode>", "Call mode: notify (hangup after message) or conversation (stay open)", "conversation").action(callAction("voicecall.start"));
503
+ root.command("continue").description("Speak a message and wait for a response").requiredOption("--call-id <id>", "Call ID").requiredOption("--message <text>", "Message to speak").action(async (options) => runWithStandaloneRuntime(createRuntime, async (ensureRuntime) => {
484
504
  const gatewayParams = {
485
505
  callId: options.callId,
486
506
  message: options.message
@@ -501,8 +521,8 @@ function registerVoiceCallCli(params) {
501
521
  managerFallback: (manager) => manager.continueCall(options.callId, options.message),
502
522
  failureLabel: "continue"
503
523
  });
504
- });
505
- root.command("speak").description("Speak a message without waiting for response").requiredOption("--call-id <id>", "Call ID").requiredOption("--message <text>", "Message to speak").action(async (options) => {
524
+ }));
525
+ root.command("speak").description("Speak a message without waiting for response").requiredOption("--call-id <id>", "Call ID").requiredOption("--message <text>", "Message to speak").action(async (options) => runWithStandaloneRuntime(createRuntime, async (ensureRuntime) => {
506
526
  await runGatewayManagerCommand({
507
527
  config,
508
528
  ensureRuntime,
@@ -513,8 +533,8 @@ function registerVoiceCallCli(params) {
513
533
  managerFallback: (manager) => manager.speak(options.callId, options.message),
514
534
  failureLabel: "speak"
515
535
  });
516
- });
517
- root.command("dtmf").description("Send DTMF digits to an active call").requiredOption("--call-id <id>", "Call ID").requiredOption("--digits <digits>", "DTMF digits").action(async (options) => {
536
+ }));
537
+ root.command("dtmf").description("Send DTMF digits to an active call").requiredOption("--call-id <id>", "Call ID").requiredOption("--digits <digits>", "DTMF digits").action(async (options) => runWithStandaloneRuntime(createRuntime, async (ensureRuntime) => {
518
538
  await runGatewayManagerCommand({
519
539
  config,
520
540
  ensureRuntime,
@@ -525,8 +545,8 @@ function registerVoiceCallCli(params) {
525
545
  managerFallback: (manager) => manager.sendDtmf(options.callId, options.digits),
526
546
  failureLabel: "dtmf"
527
547
  });
528
- });
529
- root.command("end").description("Hang up an active call").requiredOption("--call-id <id>", "Call ID").action(async (options) => {
548
+ }));
549
+ root.command("end").description("Hang up an active call").requiredOption("--call-id <id>", "Call ID").action(async (options) => runWithStandaloneRuntime(createRuntime, async (ensureRuntime) => {
530
550
  await runGatewayManagerCommand({
531
551
  config,
532
552
  ensureRuntime,
@@ -534,7 +554,7 @@ function registerVoiceCallCli(params) {
534
554
  managerFallback: (manager) => manager.endCall(options.callId),
535
555
  failureLabel: "end"
536
556
  });
537
- });
557
+ }));
538
558
  root.command("status").description("Show call status").option("--call-id <id>", "Call ID").option("--json", "Print machine-readable JSON").action(async (options) => {
539
559
  const gateway = await callVoiceCallGateway("voicecall.status", options.callId ? { callId: options.callId } : void 0);
540
560
  if (gateway.ok) {
@@ -1,5 +1,5 @@
1
1
  import { TtsConfigSchema } from "../api.js";
2
- import { t as TWILIO_REGIONS } from "./twilio-region-4fkgz3UG.mjs";
2
+ import { t as TWILIO_REGIONS } from "./twilio-region-wBOx-8sz.mjs";
3
3
  import { normalizeAgentId, parseAgentSessionKey } from "openclaw/plugin-sdk/routing";
4
4
  import { REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME, REALTIME_VOICE_AGENT_CONSULT_TOOL_POLICIES, resolveRealtimeVoiceAgentConsultTools } from "openclaw/plugin-sdk/realtime-voice";
5
5
  import { mergeDeep } from "openclaw/plugin-sdk/plugin-config-runtime";
@@ -10,18 +10,13 @@ import { normalizeWebhookPath } from "openclaw/plugin-sdk/webhook-ingress";
10
10
  import { z } from "zod";
11
11
  import { MAX_TCP_PORT } from "openclaw/plugin-sdk/number-runtime";
12
12
  //#region extensions/voice-call/src/allowlist.ts
13
- /** Normalize a phone number to digits only. */
14
13
  function normalizePhoneNumber(input) {
15
- if (!input) return "";
16
- return input.replace(/\D/g, "");
14
+ return input?.replace(/\D/g, "") ?? "";
17
15
  }
18
16
  /** Return true when the normalized caller exactly matches an allowlist entry. */
19
17
  function isAllowlistedCaller(normalizedFrom, allowFrom) {
20
18
  if (!normalizedFrom) return false;
21
- return (allowFrom ?? []).some((num) => {
22
- const normalizedAllow = normalizePhoneNumber(num);
23
- return normalizedAllow !== "" && normalizedAllow === normalizedFrom;
24
- });
19
+ return (allowFrom ?? []).some((num) => normalizePhoneNumber(num) === normalizedFrom);
25
20
  }
26
21
  //#endregion
27
22
  //#region extensions/voice-call/src/realtime-call-control.ts
@@ -71,18 +66,7 @@ function isTailscalePortAllowed(config) {
71
66
  }
72
67
  //#endregion
73
68
  //#region extensions/voice-call/src/config.ts
74
- /**
75
- * E.164 phone number format: +[country code][number]
76
- * Examples use 555 prefix (reserved for fictional numbers)
77
- */
78
69
  const E164Schema = z.string().regex(/^\+[1-9]\d{1,14}$/, "Expected E.164 format, e.g. +15550001234");
79
- /**
80
- * Controls how inbound calls are handled:
81
- * - "disabled": Block all inbound calls (outbound only)
82
- * - "allowlist": Only accept calls from numbers in allowFrom
83
- * - "pairing": Unknown callers can request pairing (future)
84
- * - "open": Accept all inbound calls (dangerous!)
85
- */
86
70
  const InboundPolicySchema = z.enum([
87
71
  "disabled",
88
72
  "allowlist",
@@ -91,47 +75,33 @@ const InboundPolicySchema = z.enum([
91
75
  ]);
92
76
  const SecretInputSchema = buildSecretInputSchema();
93
77
  const TelnyxConfigSchema = z.object({
94
- /** Telnyx API v2 key */
95
78
  apiKey: z.string().min(1).optional(),
96
- /** Telnyx connection ID (from Call Control app) */
97
79
  connectionId: z.string().min(1).optional(),
98
80
  /** Public key for webhook signature verification */
99
81
  publicKey: z.string().min(1).optional()
100
82
  }).strict();
101
83
  const TwilioConfigSchema = z.object({
102
- /** Twilio Account SID */
103
84
  accountSid: z.string().min(1).optional(),
104
- /** Twilio Auth Token */
105
85
  authToken: SecretInputSchema.optional(),
106
86
  /** Twilio processing Region (for example, ie1) */
107
87
  region: z.enum(TWILIO_REGIONS).optional()
108
88
  }).strict();
109
89
  const PlivoConfigSchema = z.object({
110
- /** Plivo Auth ID (starts with MA/SA) */
111
90
  authId: z.string().min(1).optional(),
112
- /** Plivo Auth Token */
113
91
  authToken: z.string().min(1).optional()
114
92
  }).strict();
115
93
  const VoiceCallNumberRouteConfigSchema = z.object({
116
- /** Greeting message for inbound calls to this number. */
117
94
  inboundGreeting: z.string().optional(),
118
95
  /** TTS override for inbound calls to this number. Deep-merges with global voice-call TTS. */
119
96
  tts: TtsConfigSchema,
120
- /** Agent ID to use for voice response generation for this number. */
121
97
  agentId: z.string().min(1).optional(),
122
- /** Optional model override for voice responses for this number. */
123
98
  responseModel: z.string().optional(),
124
- /** System prompt for voice responses for this number. */
125
99
  responseSystemPrompt: z.string().optional(),
126
- /** Timeout for response generation in ms for this number. */
127
100
  responseTimeoutMs: z.number().int().positive().optional()
128
101
  }).strict();
129
102
  const VoiceCallServeConfigSchema = z.object({
130
- /** Port to listen on */
131
103
  port: z.number().int().positive().default(3334),
132
- /** Bind address */
133
104
  bind: z.string().default("127.0.0.1"),
134
- /** Webhook path */
135
105
  path: z.string().min(1).default("/voice/webhook")
136
106
  }).strict().default({
137
107
  port: 3334,
@@ -139,13 +109,6 @@ const VoiceCallServeConfigSchema = z.object({
139
109
  path: "/voice/webhook"
140
110
  });
141
111
  const VoiceCallTunnelConfigSchema = z.object({
142
- /**
143
- * Tunnel provider:
144
- * - "none": No tunnel (use publicUrl if set, or manual setup)
145
- * - "ngrok": Use ngrok for public HTTPS tunnel
146
- * - "tailscale-serve": Tailscale serve (private to tailnet)
147
- * - "tailscale-funnel": Tailscale funnel (public HTTPS)
148
- */
149
112
  provider: z.enum([
150
113
  "none",
151
114
  "ngrok",
@@ -156,23 +119,14 @@ const VoiceCallTunnelConfigSchema = z.object({
156
119
  ngrokAuthToken: z.string().min(1).optional(),
157
120
  /** ngrok custom domain (paid feature, e.g., "myapp.ngrok.io") */
158
121
  ngrokDomain: z.string().min(1).optional(),
159
- /**
160
- * Allow ngrok free tier compatibility mode.
161
- * When true, forwarded headers may be trusted for loopback requests
162
- * to reconstruct the public ngrok URL used for signing.
163
- *
164
- * IMPORTANT: This does NOT bypass signature verification.
165
- */
122
+ /** Trust loopback forwarding for ngrok URL reconstruction; signatures remain mandatory. */
166
123
  allowNgrokFreeTierLoopbackBypass: z.boolean().default(false)
167
124
  }).strict().default({
168
125
  provider: "none",
169
126
  allowNgrokFreeTierLoopbackBypass: false
170
127
  });
171
128
  const VoiceCallWebhookSecurityConfigSchema = z.object({
172
- /**
173
- * Allowed hostnames for webhook URL reconstruction.
174
- * Only these hosts are accepted from forwarding headers.
175
- */
129
+ /** Only these hosts are accepted from forwarding headers. */
176
130
  allowedHosts: z.array(z.string().min(1)).default([]),
177
131
  /**
178
132
  * Trust X-Forwarded-* headers without a hostname allowlist.
@@ -189,11 +143,6 @@ const VoiceCallWebhookSecurityConfigSchema = z.object({
189
143
  trustForwardingHeaders: false,
190
144
  trustedProxyIPs: []
191
145
  });
192
- /**
193
- * Call mode determines how outbound calls behave:
194
- * - "notify": Deliver message and auto-hangup after delay (one-way notification)
195
- * - "conversation": Stay open for back-and-forth until explicit end or timeout
196
- */
197
146
  const CallModeSchema = z.enum(["notify", "conversation"]);
198
147
  const VoiceCallSessionScopeSchema = z.enum([
199
148
  "per-phone",
@@ -201,7 +150,6 @@ const VoiceCallSessionScopeSchema = z.enum([
201
150
  "main"
202
151
  ]);
203
152
  const OutboundConfigSchema = z.object({
204
- /** Default call mode for outbound calls */
205
153
  defaultMode: CallModeSchema.default("notify"),
206
154
  /** Seconds to wait after TTS before auto-hangup in notify mode */
207
155
  notifyHangupDelaySec: z.number().int().nonnegative().default(3)
@@ -250,7 +198,6 @@ const VoiceCallRealtimeAgentContextConfigSchema = z.object({
250
198
  enabled: z.boolean().default(false),
251
199
  /** Maximum number of characters in the generated profile-file block. */
252
200
  maxChars: z.number().int().positive().default(6e3),
253
- /** Include configured agent identity fields. */
254
201
  includeIdentity: z.boolean().default(true),
255
202
  /** Include selected workspace files such as SOUL.md and IDENTITY.md. */
256
203
  includeWorkspaceFiles: z.boolean().default(true),
@@ -283,7 +230,6 @@ const VoiceCallRealtimeConsultThinkingLevelSchema = z.enum([
283
230
  "ultra"
284
231
  ]);
285
232
  const VoiceCallRealtimeConfigSchema = z.object({
286
- /** Enable realtime voice-to-voice mode. */
287
233
  enabled: z.boolean().default(false),
288
234
  /** Provider id from registered realtime voice providers. */
289
235
  provider: z.string().min(1).optional(),
@@ -299,7 +245,6 @@ const VoiceCallRealtimeConfigSchema = z.object({
299
245
  consultThinkingLevel: VoiceCallRealtimeConsultThinkingLevelSchema.optional(),
300
246
  /** Optional fast mode override for the regular agent behind realtime consults. */
301
247
  consultFastMode: z.boolean().optional(),
302
- /** Tool definitions exposed to the realtime provider. */
303
248
  tools: z.array(RealtimeToolSchema).default([]),
304
249
  /** Low-latency memory/session context for the consult tool. */
305
250
  fastContext: VoiceCallRealtimeFastContextConfigSchema,
@@ -313,24 +258,8 @@ const VoiceCallRealtimeConfigSchema = z.object({
313
258
  toolPolicy: "safe-read-only",
314
259
  consultPolicy: "auto",
315
260
  tools: [],
316
- fastContext: {
317
- enabled: false,
318
- timeoutMs: 800,
319
- maxResults: 3,
320
- sources: ["memory", "sessions"],
321
- fallbackToConsult: false
322
- },
323
- agentContext: {
324
- enabled: false,
325
- maxChars: 6e3,
326
- includeIdentity: true,
327
- includeWorkspaceFiles: true,
328
- files: [
329
- "SOUL.md",
330
- "IDENTITY.md",
331
- "USER.md"
332
- ]
333
- },
261
+ fastContext: VoiceCallRealtimeFastContextConfigSchema.parse({}),
262
+ agentContext: VoiceCallRealtimeAgentContextConfigSchema.parse({}),
334
263
  providers: {}
335
264
  });
336
265
  const VoiceCallStreamingConfigSchema = z.object({
@@ -338,7 +267,6 @@ const VoiceCallStreamingConfigSchema = z.object({
338
267
  enabled: z.boolean().default(false),
339
268
  /** Provider id from registered realtime transcription providers. */
340
269
  provider: z.string().min(1).optional(),
341
- /** WebSocket path for media stream connections */
342
270
  streamPath: z.string().min(1).default("/voice/stream"),
343
271
  /** Provider-owned raw config blobs keyed by provider id. */
344
272
  providers: VoiceCallProvidersConfigSchema,
@@ -363,36 +291,25 @@ const VoiceCallStreamingConfigSchema = z.object({
363
291
  maxConnections: 128
364
292
  });
365
293
  const VoiceCallConfigSchema = z.object({
366
- /** Enable voice call functionality */
367
294
  enabled: z.boolean().default(false),
368
- /** Active provider (telnyx, twilio, plivo, or mock) */
369
295
  provider: z.enum([
370
296
  "telnyx",
371
297
  "twilio",
372
298
  "plivo",
373
299
  "mock"
374
300
  ]).optional(),
375
- /** Telnyx-specific configuration */
376
301
  telnyx: TelnyxConfigSchema.optional(),
377
- /** Twilio-specific configuration */
378
302
  twilio: TwilioConfigSchema.optional(),
379
- /** Plivo-specific configuration */
380
303
  plivo: PlivoConfigSchema.optional(),
381
- /** Phone number to call from (E.164) */
382
304
  fromNumber: E164Schema.optional(),
383
- /** Default phone number to call (E.164) */
305
+ /** Default outbound target. */
384
306
  toNumber: E164Schema.optional(),
385
- /** Inbound call policy */
386
307
  inboundPolicy: InboundPolicySchema.default("disabled"),
387
- /** Allowlist of phone numbers for inbound calls (E.164) */
388
308
  allowFrom: z.array(E164Schema).default([]),
389
- /** Greeting message for inbound calls */
390
309
  inboundGreeting: z.string().optional(),
391
310
  /** Per-dialed-number overrides for inbound calls. Keys are E.164 numbers. */
392
311
  numbers: z.record(E164Schema, VoiceCallNumberRouteConfigSchema).default({}),
393
- /** Outbound call configuration */
394
312
  outbound: OutboundConfigSchema,
395
- /** Maximum call duration in seconds */
396
313
  maxDurationSeconds: z.number().int().positive().default(300),
397
314
  /**
398
315
  * Maximum age of a call in seconds before it is automatically reaped.
@@ -402,23 +319,15 @@ const VoiceCallConfigSchema = z.object({
402
319
  staleCallReaperSeconds: z.number().int().nonnegative().default(120),
403
320
  /** Silence timeout for end-of-speech detection (ms) */
404
321
  silenceTimeoutMs: z.number().int().positive().default(800),
405
- /** Timeout for user transcript (ms) */
406
322
  transcriptTimeoutMs: z.number().int().positive().default(18e4),
407
- /** Ring timeout for outbound calls (ms) */
408
323
  ringTimeoutMs: z.number().int().positive().default(3e4),
409
- /** Maximum concurrent calls */
410
324
  maxConcurrentCalls: z.number().int().positive().default(1),
411
- /** Webhook server configuration */
412
325
  serve: VoiceCallServeConfigSchema,
413
326
  /** @deprecated Prefer tunnel config. */
414
327
  tailscale: VoiceCallTailscaleConfigSchema,
415
- /** Tunnel configuration (unified ngrok/tailscale) */
416
328
  tunnel: VoiceCallTunnelConfigSchema,
417
- /** Webhook signature reconstruction and proxy trust configuration */
418
329
  webhookSecurity: VoiceCallWebhookSecurityConfigSchema,
419
- /** Real-time audio streaming configuration */
420
330
  streaming: VoiceCallStreamingConfigSchema,
421
- /** Realtime voice-to-voice configuration */
422
331
  realtime: VoiceCallRealtimeConfigSchema,
423
332
  /** Session memory scope for voice conversations. */
424
333
  sessionScope: VoiceCallSessionScopeSchema.default("per-phone"),
@@ -428,15 +337,11 @@ const VoiceCallConfigSchema = z.object({
428
337
  skipSignatureVerification: z.boolean().default(false),
429
338
  /** TTS override (deep-merges with core tts) */
430
339
  tts: TtsConfigSchema,
431
- /** Store path for call logs */
432
340
  store: z.string().optional(),
433
341
  /** Response/session owner. Required when multiple agents have no legacy owner. */
434
342
  agentId: z.string().min(1).optional(),
435
- /** Optional model override for generating voice responses. */
436
343
  responseModel: z.string().optional(),
437
- /** System prompt for voice responses */
438
344
  responseSystemPrompt: z.string().optional(),
439
- /** Timeout for response generation in ms (default 30s) */
440
345
  responseTimeoutMs: z.number().int().positive().default(3e4)
441
346
  }).strict().refine(isTailscalePortAllowed, {
442
347
  path: ["tailscale", "port"],
@@ -626,10 +531,6 @@ function resolveVoiceCallAgentSessionKey(params) {
626
531
  sessionKey: normalizedScopedKey
627
532
  });
628
533
  }
629
- /**
630
- * Resolves the configuration by merging environment variables into missing fields.
631
- * Returns a new configuration object with environment variables applied.
632
- */
633
534
  function resolveVoiceCallConfig(config) {
634
535
  const resolved = normalizeVoiceCallConfig(config);
635
536
  if (resolved.provider === "telnyx") {
@@ -652,14 +553,9 @@ function resolveVoiceCallConfig(config) {
652
553
  resolved.tunnel.allowNgrokFreeTierLoopbackBypass = resolved.tunnel.allowNgrokFreeTierLoopbackBypass ?? false;
653
554
  resolved.tunnel.ngrokAuthToken = resolved.tunnel.ngrokAuthToken ?? resolveSpeechProviderApiKey(process.env.NGROK_AUTHTOKEN);
654
555
  resolved.tunnel.ngrokDomain = resolved.tunnel.ngrokDomain ?? resolveSpeechProviderApiKey(process.env.NGROK_DOMAIN);
655
- resolved.webhookSecurity.allowedHosts = resolved.webhookSecurity.allowedHosts ?? [];
656
556
  resolved.webhookSecurity.trustForwardingHeaders = resolved.webhookSecurity.trustForwardingHeaders ?? false;
657
- resolved.webhookSecurity.trustedProxyIPs = resolved.webhookSecurity.trustedProxyIPs ?? [];
658
557
  return normalizeVoiceCallConfig(resolved);
659
558
  }
660
- /**
661
- * Validate that the configuration has all required fields for the selected provider.
662
- */
663
559
  function validateProviderConfig(config) {
664
560
  const errors = [];
665
561
  if (!config.enabled) return {
@@ -668,18 +564,21 @@ function validateProviderConfig(config) {
668
564
  };
669
565
  if (!config.provider) errors.push("plugins.entries.voice-call.config.provider is required");
670
566
  if (!config.fromNumber && config.provider !== "mock") errors.push(config.provider === "twilio" ? "plugins.entries.voice-call.config.fromNumber is required (or set TWILIO_FROM_NUMBER env)" : "plugins.entries.voice-call.config.fromNumber is required");
567
+ const requireCredential = (field, value, envName) => {
568
+ if (!value) errors.push(`plugins.entries.voice-call.config.${config.provider}.${field} is required (or set ${envName} env)`);
569
+ };
671
570
  if (config.provider === "telnyx") {
672
- if (!config.telnyx?.apiKey) errors.push("plugins.entries.voice-call.config.telnyx.apiKey is required (or set TELNYX_API_KEY env)");
673
- if (!config.telnyx?.connectionId) errors.push("plugins.entries.voice-call.config.telnyx.connectionId is required (or set TELNYX_CONNECTION_ID env)");
674
- if (!config.skipSignatureVerification && !config.telnyx?.publicKey) errors.push("plugins.entries.voice-call.config.telnyx.publicKey is required (or set TELNYX_PUBLIC_KEY env)");
571
+ requireCredential("apiKey", config.telnyx?.apiKey, "TELNYX_API_KEY");
572
+ requireCredential("connectionId", config.telnyx?.connectionId, "TELNYX_CONNECTION_ID");
573
+ if (!config.skipSignatureVerification) requireCredential("publicKey", config.telnyx?.publicKey, "TELNYX_PUBLIC_KEY");
675
574
  }
676
575
  if (config.provider === "twilio") {
677
- if (!config.twilio?.accountSid) errors.push("plugins.entries.voice-call.config.twilio.accountSid is required (or set TWILIO_ACCOUNT_SID env)");
678
- if (!hasConfiguredSecretInput(config.twilio?.authToken)) errors.push("plugins.entries.voice-call.config.twilio.authToken is required (or set TWILIO_AUTH_TOKEN env)");
576
+ requireCredential("accountSid", config.twilio?.accountSid, "TWILIO_ACCOUNT_SID");
577
+ requireCredential("authToken", hasConfiguredSecretInput(config.twilio?.authToken), "TWILIO_AUTH_TOKEN");
679
578
  }
680
579
  if (config.provider === "plivo") {
681
- if (!config.plivo?.authId) errors.push("plugins.entries.voice-call.config.plivo.authId is required (or set PLIVO_AUTH_ID env)");
682
- if (!config.plivo?.authToken) errors.push("plugins.entries.voice-call.config.plivo.authToken is required (or set PLIVO_AUTH_TOKEN env)");
580
+ requireCredential("authId", config.plivo?.authId, "PLIVO_AUTH_ID");
581
+ requireCredential("authToken", config.plivo?.authToken, "PLIVO_AUTH_TOKEN");
683
582
  }
684
583
  if (config.realtime.enabled && config.inboundPolicy === "disabled") errors.push("plugins.entries.voice-call.config.inboundPolicy must not be \"disabled\" when realtime.enabled is true");
685
584
  if (config.realtime.enabled && config.streaming.enabled) errors.push("plugins.entries.voice-call.config.realtime.enabled and plugins.entries.voice-call.config.streaming.enabled cannot both be true");