@openclaw/voice-call 2026.7.2-beta.7 → 2026.8.1-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -38,7 +38,7 @@ Put under `plugins.entries.voice-call.config`:
38
38
  provider: "twilio", // or "telnyx" | "plivo" | "mock"
39
39
  fromNumber: "+15550001234",
40
40
  toNumber: "+15550005678",
41
- sessionScope: "per-phone", // or "per-call"
41
+ sessionScope: "per-phone", // or "per-call" | "main"
42
42
 
43
43
  twilio: {
44
44
  accountSid: "ACxxxxxxxx",
@@ -67,7 +67,7 @@ Put under `plugins.entries.voice-call.config`:
67
67
  // Public exposure (pick one):
68
68
  // publicUrl: "https://example.ngrok.app/voice/webhook",
69
69
  // tunnel: { provider: "ngrok" },
70
- // tailscale: { mode: "funnel", path: "/voice/webhook" }
70
+ // tailscale: { mode: "funnel", port: 8443, path: "/voice/webhook" }
71
71
 
72
72
  outbound: {
73
73
  defaultMode: "notify", // or "conversation"
@@ -98,13 +98,14 @@ Put under `plugins.entries.voice-call.config`:
98
98
  Notes:
99
99
 
100
100
  - Twilio/Telnyx/Plivo require a **publicly reachable** webhook URL.
101
+ - `tailscale.port` defaults to `443` and owns the external HTTPS port for both legacy `tailscale.mode` and unified Tailscale tunnel providers. Funnel supports `443`, `8443`, or `10000`; Serve accepts any valid TCP port.
101
102
  - Twilio defaults to US1. For a non-US Region, set `twilio.region` to `ie1` or `au1` and use credentials created in that Region; see [Twilio's regional REST API guide](https://www.twilio.com/docs/global-infrastructure/using-the-twilio-rest-api-in-a-non-us-region).
102
103
  - `mock` is a local dev provider (no network calls).
103
104
  - Telnyx requires `telnyx.publicKey` (or `TELNYX_PUBLIC_KEY`) unless `skipSignatureVerification` is true.
104
105
  - Runtime accepts canonical config only. If older configs still use `provider: "log"`, `twilio.from`, or legacy `streaming.*` OpenAI keys, run `openclaw doctor --fix` to rewrite them.
105
106
  - advanced webhook, streaming, and tunnel notes: `https://docs.openclaw.ai/plugins/voice-call`
106
107
  - `responseModel` is optional. When unset, voice responses use the runtime default model.
107
- - `sessionScope` defaults to `per-phone`, preserving caller memory across calls. Use `per-call` for reception, booking, IVR, and bridge flows where each carrier call should start fresh.
108
+ - `sessionScope` defaults to `per-phone`, preserving caller memory across calls. Use `per-call` for reception, booking, IVR, and bridge flows where each carrier call should start fresh. Use `main` to share the configured agent's main session, honoring core `session.mainKey` and global scope.
108
109
  - `realtime.consultThinkingLevel` is optional. When set, it overrides the thinking level used by the model behind realtime `openclaw_agent_consult` calls.
109
110
  - `realtime.consultFastMode` is optional. When set, it toggles fast mode for realtime `openclaw_agent_consult` calls.
110
111
 
@@ -1,9 +1,10 @@
1
- import { a as CALL_RECORD_EVENT_META_MAX_ENTRIES, c as buildVoiceCallLegacyJsonlEventKey, f as parseVoiceCallRecordLine, h as resolveVoiceCallLegacyCallLogPath, i as CALL_RECORD_EVENT_CHUNKS_NAMESPACE, m as prepareVoiceCallRecordForStorage, n as CALL_RECORD_CHUNK_MAX_ENTRIES, o as MAX_CALL_RECORD_EVENTS, r as CALL_RECORD_EVENTS_NAMESPACE, s as RAW_CALL_RECORD_CHUNK_BYTES, t as resolveDefaultVoiceCallStoreDir } from "./store-path-nYL-yM0S.js";
1
+ import { a as CALL_RECORD_EVENT_META_MAX_ENTRIES, c as buildVoiceCallLegacyJsonlEventKey, f as parseVoiceCallRecordLine, h as resolveVoiceCallLegacyCallLogPath, i as CALL_RECORD_EVENT_CHUNKS_NAMESPACE, m as prepareVoiceCallRecordForStorage, n as CALL_RECORD_CHUNK_MAX_ENTRIES, o as MAX_CALL_RECORD_EVENTS, r as CALL_RECORD_EVENTS_NAMESPACE, s as RAW_CALL_RECORD_CHUNK_BYTES, t as resolveDefaultVoiceCallStoreDir } from "./store-path-51_oC_xn.js";
2
2
  import { normalizeAgentId } from "openclaw/plugin-sdk/routing";
3
+ import { asOptionalRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
3
4
  import path from "node:path";
4
5
  import os from "node:os";
5
6
  import fs from "node:fs/promises";
6
- import { archiveLegacyStateSource, detectOpenClawStateDatabaseSchemaMigrations, repairOpenClawStateDatabaseSchema } from "openclaw/plugin-sdk/runtime-doctor";
7
+ import { archiveLegacyStateSource } from "openclaw/plugin-sdk/runtime-doctor-migrations";
7
8
  //#region extensions/voice-call/doctor-contract-api.ts
8
9
  /** Resolve home from doctor env with OS fallback. */
9
10
  function resolveHome(env) {
@@ -26,21 +27,18 @@ function getVoiceCallConfigStore(config) {
26
27
  }
27
28
  return "";
28
29
  }
29
- function asRecord(value) {
30
- return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
31
- }
32
30
  /** Return Voice Call agents whose templated core session stores need migration. */
33
31
  function resolveSessionStoreAgentIds(params) {
34
32
  const agentIds = /* @__PURE__ */ new Set();
35
33
  for (const pluginId of ["voice-call", "@openclaw/voice-call"]) {
36
34
  const entry = params.cfg.plugins?.entries?.[pluginId];
37
35
  if (!entry) continue;
38
- const config = entry.config === void 0 ? {} : asRecord(entry.config);
36
+ const config = entry.config === void 0 ? {} : asOptionalRecord(entry.config);
39
37
  if (!config) continue;
40
38
  agentIds.add(normalizeAgentId(typeof config.agentId === "string" ? config.agentId : void 0));
41
- const numbers = asRecord(config.numbers);
39
+ const numbers = asOptionalRecord(config.numbers);
42
40
  for (const route of Object.values(numbers ?? {})) {
43
- const agentId = asRecord(route)?.agentId;
41
+ const agentId = asOptionalRecord(route)?.agentId;
44
42
  if (typeof agentId === "string") agentIds.add(normalizeAgentId(agentId));
45
43
  }
46
44
  }
@@ -61,7 +59,10 @@ function resolveVoiceCallStateDatabaseEnv(params) {
61
59
  function describeVoiceCallSchemaMigration(migration) {
62
60
  switch (migration.kind) {
63
61
  case "agent-databases-composite-primary-key": return "agent database registry primary key -> agent_id,path";
62
+ case "agent-databases-relative-paths-v9": return "agent database registry paths -> state-relative paths";
64
63
  case "audit-events-v2": return "audit event ledger -> versioned message lifecycle schema";
64
+ case "commitments-retirement-v7": return "retired commitments storage -> removed table and indexes";
65
+ case "worker-placement-execution-mode-v8": return "cloud worker placements -> execution-mode claims";
65
66
  case "operator-approvals-system-agent": return "operator approvals -> OpenClaw system changes";
66
67
  case "session-watch-cursor-provenance-v4": return "session watch cursors -> provenance column";
67
68
  case "strict-tables-v3": return "tables -> SQLite STRICT typing";
@@ -185,12 +186,14 @@ const stateMigrations = [{
185
186
  id: "voice-call-calls-jsonl-to-plugin-state",
186
187
  label: "Voice Call call log",
187
188
  async detectLegacyState(params) {
189
+ const { detectOpenClawStateDatabaseSchemaMigrations } = await import("openclaw/plugin-sdk/doctor-repair-runtime");
188
190
  const { entries } = await readLegacyCallRecords(resolveVoiceCallLegacyCallLogPath(resolveVoiceCallStorePath(params)));
189
191
  const schemaMigrations = detectOpenClawStateDatabaseSchemaMigrations({ env: resolveVoiceCallStateDatabaseEnv(params) });
190
192
  if (entries.length === 0 && schemaMigrations.length === 0) return null;
191
193
  return { preview: [...schemaMigrations.map((migration) => `- Voice Call SQLite schema: ${describeVoiceCallSchemaMigration(migration)}`), ...entries.length > 0 ? [`- Voice Call call log: ${entries.length} ${entries.length === 1 ? "record" : "records"} -> plugin state (${CALL_RECORD_EVENTS_NAMESPACE})`] : []] };
192
194
  },
193
195
  async migrateLegacyState(params) {
196
+ const { detectOpenClawStateDatabaseSchemaMigrations, repairOpenClawStateDatabaseSchema } = await import("openclaw/plugin-sdk/doctor-repair-runtime");
194
197
  const changes = [];
195
198
  const warnings = [];
196
199
  const filePath = resolveVoiceCallLegacyCallLogPath(resolveVoiceCallStorePath(params));
@@ -1,12 +1,12 @@
1
1
  import { fetchWithSsrFGuard } from "./runtime-api.js";
2
2
  import "./api.js";
3
- import { n as normalizeProxyIp, s as getHeader } from "./runtime-entry-D0UJiLfi.js";
4
- import { n as markWebhookReplay, t as createWebhookReplayCache } from "./webhook-replay-aaP79dI8.js";
3
+ import { n as normalizeProxyIp, s as getHeader } from "./runtime-entry-DK8kPCGC.js";
4
+ import { n as reserveWebhookReplay, t as createWebhookReplayCache } from "./webhook-replay-FQrEGVSM.js";
5
5
  import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
6
6
  import { isLoopbackHost } from "openclaw/plugin-sdk/gateway-runtime";
7
7
  import { normalizeLowercaseStringOrEmpty, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
8
8
  import crypto from "node:crypto";
9
- import { safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
9
+ import { redactSensitiveText, safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
10
10
  import { isIP } from "node:net";
11
11
  import { readResponseTextPrefix, readResponseWithLimit } from "openclaw/plugin-sdk/response-limit-runtime";
12
12
  //#region extensions/voice-call/src/webhook-security.ts
@@ -256,8 +256,7 @@ function verifyTelnyxWebhook(ctx, publicKey, options) {
256
256
  return {
257
257
  ok: true,
258
258
  reason: "verification skipped (dev mode)",
259
- isReplay: markWebhookReplay(telnyxReplayCache, replayKey),
260
- verifiedRequestKey: replayKey
259
+ ...reserveWebhookReplay(telnyxReplayCache, replayKey)
261
260
  };
262
261
  }
263
262
  if (!publicKey) return {
@@ -293,8 +292,7 @@ function verifyTelnyxWebhook(ctx, publicKey, options) {
293
292
  const replayKey = `telnyx:${sha256Hex(`${timestamp}\n${canonicalSignature}\n${ctx.rawBody}`)}`;
294
293
  return {
295
294
  ok: true,
296
- isReplay: markWebhookReplay(telnyxReplayCache, replayKey),
297
- verifiedRequestKey: replayKey
295
+ ...reserveWebhookReplay(telnyxReplayCache, replayKey)
298
296
  };
299
297
  } catch (err) {
300
298
  return {
@@ -312,8 +310,7 @@ function verifyTwilioWebhook(ctx, authToken, options) {
312
310
  return {
313
311
  ok: true,
314
312
  reason: "verification skipped (dev mode)",
315
- isReplay: markWebhookReplay(twilioReplayCache, replayKey),
316
- verifiedRequestKey: replayKey
313
+ ...reserveWebhookReplay(twilioReplayCache, replayKey)
317
314
  };
318
315
  }
319
316
  const signature = getHeader(ctx.headers, "x-twilio-signature");
@@ -339,8 +336,7 @@ function verifyTwilioWebhook(ctx, authToken, options) {
339
336
  return {
340
337
  ok: true,
341
338
  verificationUrl,
342
- isReplay: markWebhookReplay(twilioReplayCache, replayKey),
343
- verifiedRequestKey: replayKey
339
+ ...reserveWebhookReplay(twilioReplayCache, replayKey)
344
340
  };
345
341
  }
346
342
  const variants = /* @__PURE__ */ new Set();
@@ -363,8 +359,7 @@ function verifyTwilioWebhook(ctx, authToken, options) {
363
359
  return {
364
360
  ok: true,
365
361
  verificationUrl: candidateUrl,
366
- isReplay: markWebhookReplay(twilioReplayCache, replayKey),
367
- verifiedRequestKey: replayKey
362
+ ...reserveWebhookReplay(twilioReplayCache, replayKey)
368
363
  };
369
364
  }
370
365
  const isNgrokFreeTier = verificationUrl.includes(".ngrok-free.app") || verificationUrl.includes(".ngrok.io");
@@ -456,8 +451,7 @@ function verifyPlivoWebhook(ctx, authToken, options) {
456
451
  return {
457
452
  ok: true,
458
453
  reason: "verification skipped (dev mode)",
459
- isReplay: markWebhookReplay(plivoReplayCache, replayKey),
460
- verifiedRequestKey: replayKey
454
+ ...reserveWebhookReplay(plivoReplayCache, replayKey)
461
455
  };
462
456
  }
463
457
  const signatureV3 = getHeader(ctx.headers, "x-plivo-signature-v3");
@@ -507,13 +501,11 @@ function verifyPlivoWebhook(ctx, authToken, options) {
507
501
  postParams,
508
502
  nonce: nonceV3
509
503
  });
510
- const isReplay = markWebhookReplay(plivoReplayCache, replayKey);
511
504
  return {
512
505
  ok: true,
513
506
  version: "v3",
514
507
  verificationUrl,
515
- isReplay,
516
- verifiedRequestKey: replayKey
508
+ ...reserveWebhookReplay(plivoReplayCache, replayKey)
517
509
  };
518
510
  }
519
511
  if (signatureV2 && nonceV2) {
@@ -529,13 +521,11 @@ function verifyPlivoWebhook(ctx, authToken, options) {
529
521
  reason: "Invalid Plivo V2 signature"
530
522
  };
531
523
  const replayKey = createPlivoV2ReplayKey(verificationUrl, nonceV2);
532
- const isReplay = markWebhookReplay(plivoReplayCache, replayKey);
533
524
  return {
534
525
  ok: true,
535
526
  version: "v2",
536
527
  verificationUrl,
537
- isReplay,
538
- verifiedRequestKey: replayKey
528
+ ...reserveWebhookReplay(plivoReplayCache, replayKey)
539
529
  };
540
530
  }
541
531
  return {
@@ -555,26 +545,20 @@ async function cancelProviderResponseBody(response) {
555
545
  function appendTruncatedSuffix(text) {
556
546
  return `${text.trimEnd()}${TRUNCATED_SUFFIX}`;
557
547
  }
558
- async function readProviderResponseTextWithLimit(params) {
559
- if (params.truncateOnLimit) {
560
- const prefix = await readResponseTextPrefix(params.response, params.maxBytes);
561
- return prefix.truncated ? appendTruncatedSuffix(prefix.text) : prefix.text;
548
+ async function readVoiceCallProviderJsonResponse(response, malformedJsonMessage) {
549
+ const body = await readResponseWithLimit(response, PROVIDER_JSON_RESPONSE_MAX_BYTES, { onOverflow: ({ size, maxBytes }) => /* @__PURE__ */ new Error(`provider response body too large: ${size} bytes (limit: ${maxBytes} bytes)`) });
550
+ if (body.byteLength === 0) return;
551
+ try {
552
+ const text = new TextDecoder("utf-8", { fatal: true }).decode(body);
553
+ return JSON.parse(text);
554
+ } catch (cause) {
555
+ throw new Error(malformedJsonMessage, { cause });
562
556
  }
563
- const body = await readResponseWithLimit(params.response, params.maxBytes, { onOverflow: ({ size, maxBytes }) => /* @__PURE__ */ new Error(`provider response body too large: ${size} bytes (limit: ${maxBytes} bytes)`) });
564
- return new TextDecoder().decode(body);
565
- }
566
- async function readProviderJsonResponseText(response) {
567
- return await readProviderResponseTextWithLimit({
568
- response,
569
- maxBytes: PROVIDER_JSON_RESPONSE_MAX_BYTES
570
- });
571
557
  }
572
558
  async function readProviderErrorResponseSnippet(response) {
573
- return await readProviderResponseTextWithLimit({
574
- response,
575
- maxBytes: PROVIDER_ERROR_RESPONSE_MAX_BYTES,
576
- truncateOnLimit: true
577
- });
559
+ const prefix = await readResponseTextPrefix(response, PROVIDER_ERROR_RESPONSE_MAX_BYTES);
560
+ const text = redactSensitiveText(prefix.text, { mode: "tools" });
561
+ return prefix.truncated ? appendTruncatedSuffix(text) : text;
578
562
  }
579
563
  //#endregion
580
564
  //#region extensions/voice-call/src/providers/shared/guarded-json-api.ts
@@ -601,16 +585,10 @@ async function guardedJsonApiRequest(params) {
601
585
  const errorText = await readProviderErrorResponseSnippet(response);
602
586
  throw new Error(`${params.errorPrefix}: ${response.status} ${errorText}`);
603
587
  }
604
- const text = await readProviderJsonResponseText(response);
605
- if (!text) return;
606
- try {
607
- return JSON.parse(text);
608
- } catch {
609
- throw new Error(`${params.errorPrefix}: malformed JSON response`);
610
- }
588
+ return await readVoiceCallProviderJsonResponse(response, `${params.errorPrefix}: malformed JSON response`);
611
589
  } finally {
612
590
  await release();
613
591
  }
614
592
  }
615
593
  //#endregion
616
- export { reconstructWebhookUrl as a, verifyTwilioWebhook as c, readProviderJsonResponseText as i, cancelProviderResponseBody as n, verifyPlivoWebhook as o, readProviderErrorResponseSnippet as r, verifyTelnyxWebhook as s, guardedJsonApiRequest as t };
594
+ export { reconstructWebhookUrl as a, verifyTwilioWebhook as c, readVoiceCallProviderJsonResponse as i, cancelProviderResponseBody as n, verifyPlivoWebhook as o, readProviderErrorResponseSnippet as r, verifyTelnyxWebhook as s, guardedJsonApiRequest as t };