@openclaw/voice-call 2026.7.2-beta.7 → 2026.8.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.
@@ -1,2 +1,2 @@
1
- import { t as createVoiceCallRuntime } from "./runtime-entry-D0UJiLfi.js";
1
+ import { t as createVoiceCallRuntime } from "./runtime-entry-F13eJ4pp.js";
2
2
  export { createVoiceCallRuntime };
package/dist/setup-api.js CHANGED
@@ -1,18 +1,11 @@
1
- import { asOptionalRecord, isRecord, readStringField } from "openclaw/plugin-sdk/string-coerce-runtime";
1
+ import { asFiniteNumber, asOptionalRecord, isRecord, readStringField } from "openclaw/plugin-sdk/string-coerce-runtime";
2
2
  import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
3
3
  //#region extensions/voice-call/src/config-migration.ts
4
- const asObject = asOptionalRecord;
5
- const getString = readStringField;
6
- /** Read finite numeric config values. */
7
- function getNumber(obj, key) {
8
- const value = obj?.[key];
9
- return typeof value === "number" && Number.isFinite(value) ? value : void 0;
10
- }
11
4
  /** Merge legacy provider-specific values into the canonical providers map. */
12
5
  function mergeProviderConfig(providersValue, providerId, compatValues) {
13
- if (Object.keys(compatValues).length === 0) return asObject(providersValue);
14
- const providers = asObject(providersValue) ?? {};
15
- const existing = asObject(providers[providerId]) ?? {};
6
+ if (Object.keys(compatValues).length === 0) return asOptionalRecord(providersValue);
7
+ const providers = asOptionalRecord(providersValue) ?? {};
8
+ const existing = asOptionalRecord(providers[providerId]) ?? {};
16
9
  return {
17
10
  ...providers,
18
11
  [providerId]: {
@@ -23,23 +16,23 @@ function mergeProviderConfig(providersValue, providerId, compatValues) {
23
16
  }
24
17
  /** Migrate legacy voice-call config input to the current canonical shape. */
25
18
  function migrateVoiceCallLegacyConfigInput(params) {
26
- const raw = asObject(params.value) ?? {};
27
- const realtime = asObject(raw.realtime);
28
- const realtimeAgentContext = asObject(realtime?.agentContext);
29
- const twilio = asObject(raw.twilio);
30
- const streaming = asObject(raw.streaming);
19
+ const raw = asOptionalRecord(params.value) ?? {};
20
+ const realtime = asOptionalRecord(raw.realtime);
21
+ const realtimeAgentContext = asOptionalRecord(realtime?.agentContext);
22
+ const twilio = asOptionalRecord(raw.twilio);
23
+ const streaming = asOptionalRecord(raw.streaming);
31
24
  const configPathPrefix = params.configPathPrefix ?? "plugins.entries.voice-call.config";
32
25
  const legacyStreamingOpenAICompat = {};
33
- const streamingOpenAIApiKey = getString(streaming, "openaiApiKey");
26
+ const streamingOpenAIApiKey = readStringField(streaming, "openaiApiKey");
34
27
  if (streamingOpenAIApiKey) legacyStreamingOpenAICompat.apiKey = streamingOpenAIApiKey;
35
- const streamingSttModel = getString(streaming, "sttModel");
28
+ const streamingSttModel = readStringField(streaming, "sttModel");
36
29
  if (streamingSttModel) legacyStreamingOpenAICompat.model = streamingSttModel;
37
- const streamingSilenceDurationMs = getNumber(streaming, "silenceDurationMs");
30
+ const streamingSilenceDurationMs = asFiniteNumber(streaming?.silenceDurationMs);
38
31
  if (streamingSilenceDurationMs !== void 0) legacyStreamingOpenAICompat.silenceDurationMs = streamingSilenceDurationMs;
39
- const streamingVadThreshold = getNumber(streaming, "vadThreshold");
32
+ const streamingVadThreshold = asFiniteNumber(streaming?.vadThreshold);
40
33
  if (streamingVadThreshold !== void 0) legacyStreamingOpenAICompat.vadThreshold = streamingVadThreshold;
41
- const streamingProvider = getString(streaming, "provider");
42
- const legacyStreamingProvider = getString(streaming, "sttProvider");
34
+ const streamingProvider = readStringField(streaming, "provider");
35
+ const legacyStreamingProvider = readStringField(streaming, "sttProvider");
43
36
  const normalizedStreaming = streaming ? {
44
37
  ...streaming,
45
38
  provider: streamingProvider ?? legacyStreamingProvider,
@@ -74,9 +67,9 @@ function migrateVoiceCallLegacyConfigInput(params) {
74
67
  if (typeof streaming?.sttProvider === "string") changes.push(`Moved ${configPathPrefix}.streaming.sttProvider → ${configPathPrefix}.streaming.provider.`);
75
68
  if (typeof streaming?.openaiApiKey === "string") changes.push(`Moved ${configPathPrefix}.streaming.openaiApiKey → ${configPathPrefix}.streaming.providers.openai.apiKey.`);
76
69
  if (typeof streaming?.sttModel === "string") changes.push(`Moved ${configPathPrefix}.streaming.sttModel → ${configPathPrefix}.streaming.providers.openai.model.`);
77
- if (getNumber(streaming, "silenceDurationMs") !== void 0) changes.push(`Moved ${configPathPrefix}.streaming.silenceDurationMs → ${configPathPrefix}.streaming.providers.openai.silenceDurationMs.`);
70
+ if (asFiniteNumber(streaming?.silenceDurationMs) !== void 0) changes.push(`Moved ${configPathPrefix}.streaming.silenceDurationMs → ${configPathPrefix}.streaming.providers.openai.silenceDurationMs.`);
78
71
  else if (typeof streaming?.silenceDurationMs === "number") changes.push(`Removed invalid ${configPathPrefix}.streaming.silenceDurationMs.`);
79
- if (getNumber(streaming, "vadThreshold") !== void 0) changes.push(`Moved ${configPathPrefix}.streaming.vadThreshold → ${configPathPrefix}.streaming.providers.openai.vadThreshold.`);
72
+ if (asFiniteNumber(streaming?.vadThreshold) !== void 0) changes.push(`Moved ${configPathPrefix}.streaming.vadThreshold → ${configPathPrefix}.streaming.providers.openai.vadThreshold.`);
80
73
  else if (typeof streaming?.vadThreshold === "number") changes.push(`Removed invalid ${configPathPrefix}.streaming.vadThreshold.`);
81
74
  if (realtimeAgentContext && Object.hasOwn(realtimeAgentContext, "includeSystemPrompt")) changes.push(`Removed ${configPathPrefix}.realtime.agentContext.includeSystemPrompt.`);
82
75
  return {
@@ -130,6 +130,54 @@ const { setRuntime: setVoiceCallStateRuntime, tryGetRuntime: getOptionalVoiceCal
130
130
  errorMessage: "Voice Call state runtime not initialized"
131
131
  });
132
132
  //#endregion
133
+ //#region extensions/voice-call/src/manager/replay-keys.ts
134
+ /** Match the existing voice-call webhook replay-cache cardinality. */
135
+ const MAX_MANAGER_REPLAY_KEYS = 1e4;
136
+ function pruneOldestSetEntries(keys, maxEntries) {
137
+ while (keys.size > maxEntries) {
138
+ const oldest = keys.values().next().value;
139
+ if (oldest === void 0) break;
140
+ keys.delete(oldest);
141
+ }
142
+ }
143
+ function pruneOldestMapEntries(keys, maxEntries) {
144
+ while (keys.size > maxEntries) {
145
+ const oldest = keys.keys().next().value;
146
+ if (oldest === void 0) break;
147
+ keys.delete(oldest);
148
+ }
149
+ }
150
+ /** Remember one manager replay key without refreshing duplicate insertion order. */
151
+ function rememberManagerReplayKey(keys, key, maxEntries = MAX_MANAGER_REPLAY_KEYS) {
152
+ keys.add(key);
153
+ pruneOldestSetEntries(keys, maxEntries);
154
+ }
155
+ /** Append one per-call replay key and retain only the newest bounded suffix. */
156
+ function appendCallReplayKey(keys, key, maxEntries = 500) {
157
+ keys.push(key);
158
+ trimCallReplayKeys(keys, maxEntries);
159
+ }
160
+ /** Normalize restored or externally constructed call replay history in place. */
161
+ function trimCallReplayKeys(keys, maxEntries = 500) {
162
+ const overflow = keys.length - maxEntries;
163
+ if (overflow > 0) keys.splice(0, overflow);
164
+ }
165
+ /**
166
+ * Reserve one rejected provider call. The token prevents a stale failed
167
+ * hangup from deleting a newer reservation after bounded eviction and reuse.
168
+ */
169
+ function reserveRejectedProviderCall(calls, providerCallId, maxEntries = MAX_MANAGER_REPLAY_KEYS) {
170
+ if (calls.has(providerCallId)) return;
171
+ const reservation = Symbol(providerCallId);
172
+ calls.set(providerCallId, reservation);
173
+ pruneOldestMapEntries(calls, maxEntries);
174
+ return reservation;
175
+ }
176
+ /** Release a rejected-call reservation only when the failing attempt still owns it. */
177
+ function releaseRejectedProviderCall(calls, providerCallId, reservation) {
178
+ if (calls.get(providerCallId) === reservation) calls.delete(providerCallId);
179
+ }
180
+ //#endregion
133
181
  //#region extensions/voice-call/src/manager/store.ts
134
182
  /** Plugin state namespace for call record event metadata. */
135
183
  const CALL_RECORD_EVENTS_NAMESPACE = "call-record-events";
@@ -237,10 +285,18 @@ function countCallRecordChunks(call) {
237
285
  }
238
286
  /** Truncate oversized call records to fit the bounded plugin state chunk budget. */
239
287
  function prepareVoiceCallRecordForStorage(call) {
240
- if (countCallRecordChunks(call) <= 48) return call;
241
- const transcriptEntries = call.transcript.length;
288
+ let boundedCall = call;
289
+ if (call.processedEventIds.length > 500) {
290
+ boundedCall = {
291
+ ...call,
292
+ processedEventIds: [...call.processedEventIds]
293
+ };
294
+ trimCallReplayKeys(boundedCall.processedEventIds);
295
+ }
296
+ if (countCallRecordChunks(boundedCall) <= 48) return boundedCall;
297
+ const transcriptEntries = boundedCall.transcript.length;
242
298
  const metadata = {
243
- ...call.metadata,
299
+ ...boundedCall.metadata,
244
300
  voiceCallPersistence: {
245
301
  transcriptTruncated: true,
246
302
  originalTranscriptEntries: transcriptEntries
@@ -266,12 +322,12 @@ function prepareVoiceCallRecordForStorage(call) {
266
322
  ];
267
323
  for (const candidateInput of candidateInputs) {
268
324
  const candidate = CallRecordSchema.parse({
269
- ...call,
325
+ ...boundedCall,
270
326
  ...candidateInput
271
327
  });
272
328
  if (countCallRecordChunks(candidate) <= 48) return candidate;
273
329
  }
274
- return call;
330
+ return boundedCall;
275
331
  }
276
332
  /** Register a serialized call record event and its chunks, then prune old events. */
277
333
  function registerCallRecordEvent(stores, eventKey, call, order) {
@@ -356,17 +412,19 @@ function loadActiveCallsFromStore(storePath) {
356
412
  if (calls.length === 0) return {
357
413
  activeCalls: /* @__PURE__ */ new Map(),
358
414
  providerCallIdMap: /* @__PURE__ */ new Map(),
359
- processedEventIds: /* @__PURE__ */ new Set(),
360
- rejectedProviderCallIds: /* @__PURE__ */ new Set()
415
+ processedEventIds: /* @__PURE__ */ new Set()
361
416
  };
362
417
  const callMap = /* @__PURE__ */ new Map();
363
- for (const call of calls) callMap.set(call.callId, call);
418
+ for (const call of calls) {
419
+ callMap.delete(call.callId);
420
+ callMap.set(call.callId, call);
421
+ }
364
422
  const activeCalls = /* @__PURE__ */ new Map();
365
423
  const providerCallIdMap = /* @__PURE__ */ new Map();
366
424
  const processedEventIds = /* @__PURE__ */ new Set();
367
- const rejectedProviderCallIds = /* @__PURE__ */ new Set();
368
425
  for (const [callId, call] of callMap) {
369
- for (const eventId of call.processedEventIds) processedEventIds.add(eventId);
426
+ trimCallReplayKeys(call.processedEventIds);
427
+ for (const eventId of call.processedEventIds) rememberManagerReplayKey(processedEventIds, eventId);
370
428
  if (TerminalStates.has(call.state)) continue;
371
429
  activeCalls.set(callId, call);
372
430
  if (call.providerCallId) providerCallIdMap.set(call.providerCallId, callId);
@@ -374,8 +432,7 @@ function loadActiveCallsFromStore(storePath) {
374
432
  return {
375
433
  activeCalls,
376
434
  providerCallIdMap,
377
- processedEventIds,
378
- rejectedProviderCallIds
435
+ processedEventIds
379
436
  };
380
437
  }
381
438
  function readCallHistoryFromStore(storePath) {
@@ -407,4 +464,4 @@ function resolveDefaultVoiceCallStoreDir(env = process.env) {
407
464
  return path.join(resolveStateDir(env), "voice-calls");
408
465
  }
409
466
  //#endregion
410
- export { TerminalStates as _, CALL_RECORD_EVENT_META_MAX_ENTRIES as a, buildVoiceCallLegacyJsonlEventKey as c, loadActiveCallsFromStore as d, parseVoiceCallRecordLine as f, setVoiceCallStateRuntime as g, resolveVoiceCallLegacyCallLogPath as h, CALL_RECORD_EVENT_CHUNKS_NAMESPACE as i, findCallMatchesInStore as l, prepareVoiceCallRecordForStorage as m, CALL_RECORD_CHUNK_MAX_ENTRIES as n, MAX_CALL_RECORD_EVENTS as o, persistCallRecord as p, CALL_RECORD_EVENTS_NAMESPACE as r, RAW_CALL_RECORD_CHUNK_BYTES as s, resolveDefaultVoiceCallStoreDir as t, getCallHistoryFromStore as u };
467
+ export { releaseRejectedProviderCall as _, CALL_RECORD_EVENT_META_MAX_ENTRIES as a, setVoiceCallStateRuntime as b, buildVoiceCallLegacyJsonlEventKey as c, loadActiveCallsFromStore as d, parseVoiceCallRecordLine as f, appendCallReplayKey as g, resolveVoiceCallLegacyCallLogPath as h, CALL_RECORD_EVENT_CHUNKS_NAMESPACE as i, findCallMatchesInStore as l, prepareVoiceCallRecordForStorage as m, CALL_RECORD_CHUNK_MAX_ENTRIES as n, MAX_CALL_RECORD_EVENTS as o, persistCallRecord as p, CALL_RECORD_EVENTS_NAMESPACE as r, RAW_CALL_RECORD_CHUNK_BYTES as s, resolveDefaultVoiceCallStoreDir as t, getCallHistoryFromStore as u, rememberManagerReplayKey as v, TerminalStates as x, reserveRejectedProviderCall as y };
@@ -1,4 +1,4 @@
1
- import { s as verifyTelnyxWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-DS9sZa3C.js";
1
+ import { s as verifyTelnyxWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-DEDwgf4J.js";
2
2
  import crypto from "node:crypto";
3
3
  //#region extensions/voice-call/src/providers/telnyx.ts
4
4
  function normalizeTelnyxDirection(direction) {
@@ -57,7 +57,8 @@ var TelnyxProvider = class {
57
57
  ok: result.ok,
58
58
  reason: result.reason,
59
59
  isReplay: result.isReplay,
60
- verifiedRequestKey: result.verifiedRequestKey
60
+ verifiedRequestKey: result.verifiedRequestKey,
61
+ releaseReplay: result.releaseReplay
61
62
  };
62
63
  }
63
64
  /**
@@ -1,8 +1,8 @@
1
1
  import { fetchWithSsrFGuard } from "./runtime-api.js";
2
2
  import "./api.js";
3
3
  import { n as requireSupportedTwilioApiHostname, r as resolveTwilioApiBaseUrl } from "./twilio-region-4fkgz3UG.js";
4
- import { _ as escapeXml, a as normalizeProviderStatus, i as mapProviderStatusToEndReason, p as chunkAudio, r as isProviderStatusTerminal, s as getHeader, v as mapVoiceToPolly } from "./runtime-entry-D0UJiLfi.js";
5
- import { c as verifyTwilioWebhook, i as readProviderJsonResponseText, n as cancelProviderResponseBody, r as readProviderErrorResponseSnippet, t as guardedJsonApiRequest } from "./guarded-json-api-DS9sZa3C.js";
4
+ import { _ as escapeXml, a as normalizeProviderStatus, i as mapProviderStatusToEndReason, p as chunkAudio, r as isProviderStatusTerminal, s as getHeader, v as mapVoiceToPolly } from "./runtime-entry-F13eJ4pp.js";
5
+ import { c as verifyTwilioWebhook, i as readVoiceCallProviderJsonResponse, n as cancelProviderResponseBody, r as readProviderErrorResponseSnippet, t as guardedJsonApiRequest } from "./guarded-json-api-DEDwgf4J.js";
6
6
  import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
7
7
  import { sleepWithAbort } from "openclaw/plugin-sdk/runtime-env";
8
8
  import crypto from "node:crypto";
@@ -68,13 +68,7 @@ async function twilioApiRequest(params) {
68
68
  const errorText = await readProviderErrorResponseSnippet(response);
69
69
  throw new TwilioApiError(response.status, errorText);
70
70
  }
71
- const text = await readProviderJsonResponseText(response);
72
- if (!text) return;
73
- try {
74
- return JSON.parse(text);
75
- } catch {
76
- throw new Error("Twilio API returned malformed JSON.");
77
- }
71
+ return await readVoiceCallProviderJsonResponse(response, "Twilio API returned malformed JSON.");
78
72
  } finally {
79
73
  await release();
80
74
  }
@@ -138,7 +132,8 @@ function verifyTwilioProviderWebhook(params) {
138
132
  ok: result.ok,
139
133
  reason: result.reason,
140
134
  isReplay: result.isReplay,
141
- verifiedRequestKey: result.verifiedRequestKey
135
+ verifiedRequestKey: result.verifiedRequestKey,
136
+ releaseReplay: result.releaseReplay
142
137
  };
143
138
  }
144
139
  //#endregion
@@ -172,17 +167,20 @@ var TwilioProvider = class TwilioProvider {
172
167
  this.notifyCalls.delete(callId);
173
168
  }
174
169
  /**
175
- * Delete stored TwiML for a call, addressed by Twilio's provider call SID.
176
- *
177
- * This is used when we only have `providerCallId` (e.g. hangup).
170
+ * Release all process-local metadata owned by one Twilio call.
171
+ * Terminal webhooks can be replayed, so this must stay idempotent.
178
172
  */
179
- deleteStoredTwimlForProviderCall(providerCallId) {
173
+ releaseCallState(providerCallId, callId) {
180
174
  const webhookUrl = this.callWebhookUrls.get(providerCallId);
181
- if (!webhookUrl) return;
182
- const callId = webhookUrl.match(/callId=([^&]+)/)?.[1];
183
- if (!callId) return;
184
- this.deleteStoredTwiml(callId);
175
+ let resolvedCallId = callId;
176
+ if (!resolvedCallId && webhookUrl) try {
177
+ resolvedCallId = new URL(webhookUrl).searchParams.get("callId") || void 0;
178
+ } catch {}
179
+ if (resolvedCallId) this.deleteStoredTwiml(resolvedCallId);
180
+ this.callWebhookUrls.delete(providerCallId);
181
+ this.callStreamMap.delete(providerCallId);
185
182
  this.streamAuthTokens.delete(providerCallId);
183
+ this.activeStreamCalls.delete(providerCallId);
186
184
  }
187
185
  constructor(config, options = {}) {
188
186
  this.name = "twilio";
@@ -375,14 +373,13 @@ var TwilioProvider = class TwilioProvider {
375
373
  };
376
374
  const endReason = mapProviderStatusToEndReason(callStatus);
377
375
  if (endReason) {
378
- this.streamAuthTokens.delete(callSid);
379
- this.activeStreamCalls.delete(callSid);
380
- if (callIdOverride) this.deleteStoredTwiml(callIdOverride);
381
- return {
376
+ const event = {
382
377
  ...baseEvent,
383
378
  type: "call.ended",
384
379
  reason: endReason
385
380
  };
381
+ this.releaseCallState(callSid, callIdOverride);
382
+ return event;
386
383
  }
387
384
  return null;
388
385
  }
@@ -524,11 +521,8 @@ var TwilioProvider = class TwilioProvider {
524
521
  * Hang up a call via Twilio API.
525
522
  */
526
523
  async hangupCall(input) {
527
- this.deleteStoredTwimlForProviderCall(input.providerCallId);
528
- this.callWebhookUrls.delete(input.providerCallId);
529
- this.streamAuthTokens.delete(input.providerCallId);
530
- this.activeStreamCalls.delete(input.providerCallId);
531
524
  await this.apiRequest(`/Calls/${input.providerCallId}.json`, { Status: "completed" }, { allowNotFound: true });
525
+ this.releaseCallState(input.providerCallId, input.callId);
532
526
  }
533
527
  /**
534
528
  * Play TTS audio via Twilio.
@@ -10,23 +10,37 @@ function createWebhookReplayCache() {
10
10
  };
11
11
  }
12
12
  function pruneWebhookReplayCache(cache, now) {
13
- for (const [key, expiresAt] of cache.seenUntil) if (!isFutureDateTimestampMs(expiresAt, { nowMs: now })) cache.seenUntil.delete(key);
13
+ for (const [key, reservation] of cache.seenUntil) if (!isFutureDateTimestampMs(reservation.expiresAt, { nowMs: now })) cache.seenUntil.delete(key);
14
14
  while (cache.seenUntil.size > REPLAY_CACHE_MAX_ENTRIES) {
15
15
  const oldest = cache.seenUntil.keys().next().value;
16
16
  if (!oldest) break;
17
17
  cache.seenUntil.delete(oldest);
18
18
  }
19
19
  }
20
- function markWebhookReplay(cache, replayKey) {
20
+ function reserveWebhookReplay(cache, replayKey) {
21
21
  const now = Date.now();
22
22
  cache.calls += 1;
23
23
  if (cache.calls % REPLAY_CACHE_PRUNE_INTERVAL === 0) pruneWebhookReplayCache(cache, now);
24
24
  const existing = cache.seenUntil.get(replayKey);
25
- if (existing !== void 0 && isFutureDateTimestampMs(existing, { nowMs: now })) return true;
25
+ if (existing !== void 0 && isFutureDateTimestampMs(existing.expiresAt, { nowMs: now })) return {
26
+ isReplay: true,
27
+ verifiedRequestKey: replayKey
28
+ };
26
29
  const expiresAt = resolveExpiresAtMsFromDurationMs(REPLAY_WINDOW_MS, { nowMs: now });
27
- if (expiresAt !== void 0) cache.seenUntil.set(replayKey, expiresAt);
30
+ if (expiresAt === void 0) return {
31
+ isReplay: false,
32
+ verifiedRequestKey: replayKey
33
+ };
34
+ const reservation = { expiresAt };
35
+ cache.seenUntil.set(replayKey, reservation);
28
36
  if (cache.seenUntil.size > REPLAY_CACHE_MAX_ENTRIES) pruneWebhookReplayCache(cache, now);
29
- return false;
37
+ return {
38
+ isReplay: false,
39
+ verifiedRequestKey: replayKey,
40
+ releaseReplay: () => {
41
+ if (cache.seenUntil.get(replayKey) === reservation) cache.seenUntil.delete(replayKey);
42
+ }
43
+ };
30
44
  }
31
45
  //#endregion
32
- export { markWebhookReplay as n, createWebhookReplayCache as t };
46
+ export { reserveWebhookReplay as n, createWebhookReplayCache as t };
@@ -1,5 +1,9 @@
1
1
  {
2
2
  "id": "voice-call",
3
+ "doctorContract": {
4
+ "resolveSessionStoreAgentIds": true,
5
+ "stateMigrations": true
6
+ },
3
7
  "name": "Voice Call",
4
8
  "description": "OpenClaw voice-call plugin for Twilio, Telnyx, and Plivo phone calls.",
5
9
  "skills": ["./skills"],
@@ -224,6 +228,11 @@
224
228
  "label": "Call Log Store Path",
225
229
  "advanced": true
226
230
  },
231
+ "agentId": {
232
+ "label": "Response Agent ID",
233
+ "help": "Agent workspace used for voice response generation. Defaults to \"main\".",
234
+ "advanced": true
235
+ },
227
236
  "sessionScope": {
228
237
  "label": "Session Scope",
229
238
  "help": "Use per-phone to preserve caller memory across calls, or per-call to isolate every call into a fresh voice session."
@@ -703,9 +712,17 @@
703
712
  "model": {
704
713
  "type": "string"
705
714
  },
706
- "voice": {
715
+ "speakerVoice": {
707
716
  "type": "string"
708
717
  },
718
+ "voice": {
719
+ "type": "string",
720
+ "description": "Legacy alias for speakerVoice."
721
+ },
722
+ "voiceId": {
723
+ "type": "string",
724
+ "description": "Legacy alias for speakerVoice."
725
+ },
709
726
  "speed": {
710
727
  "type": "number",
711
728
  "minimum": 0.25,
@@ -726,9 +743,17 @@
726
743
  "baseUrl": {
727
744
  "type": "string"
728
745
  },
729
- "voiceId": {
746
+ "speakerVoiceId": {
730
747
  "type": "string"
731
748
  },
749
+ "voice": {
750
+ "type": "string",
751
+ "description": "Legacy alias for speakerVoiceId."
752
+ },
753
+ "voiceId": {
754
+ "type": "string",
755
+ "description": "Legacy alias for speakerVoiceId."
756
+ },
732
757
  "modelId": {
733
758
  "type": "string"
734
759
  },
@@ -782,9 +807,17 @@
782
807
  "enabled": {
783
808
  "type": "boolean"
784
809
  },
785
- "voice": {
810
+ "speakerVoice": {
786
811
  "type": "string"
787
812
  },
813
+ "voice": {
814
+ "type": "string",
815
+ "description": "Legacy alias for speakerVoice."
816
+ },
817
+ "voiceId": {
818
+ "type": "string",
819
+ "description": "Legacy alias for speakerVoice."
820
+ },
788
821
  "lang": {
789
822
  "type": "string"
790
823
  },
@@ -820,9 +853,17 @@
820
853
  "enabled": {
821
854
  "type": "boolean"
822
855
  },
823
- "voice": {
856
+ "speakerVoice": {
824
857
  "type": "string"
825
858
  },
859
+ "voice": {
860
+ "type": "string",
861
+ "description": "Legacy alias for speakerVoice."
862
+ },
863
+ "voiceId": {
864
+ "type": "string",
865
+ "description": "Legacy alias for speakerVoice."
866
+ },
826
867
  "lang": {
827
868
  "type": "string"
828
869
  },
@@ -879,6 +920,10 @@
879
920
  "store": {
880
921
  "type": "string"
881
922
  },
923
+ "agentId": {
924
+ "type": "string",
925
+ "minLength": 1
926
+ },
882
927
  "sessionScope": {
883
928
  "type": "string",
884
929
  "enum": ["per-phone", "per-call"]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/voice-call",
3
- "version": "2026.7.2-beta.7",
3
+ "version": "2026.8.1-beta.2",
4
4
  "description": "OpenClaw voice-call plugin for Twilio, Telnyx, and Plivo phone calls.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -13,7 +13,7 @@
13
13
  "zod": "4.4.3"
14
14
  },
15
15
  "peerDependencies": {
16
- "openclaw": ">=2026.7.2-beta.7"
16
+ "openclaw": ">=2026.8.1-beta.2"
17
17
  },
18
18
  "peerDependenciesMeta": {
19
19
  "openclaw": {
@@ -30,10 +30,10 @@
30
30
  "minHostVersion": ">=2026.4.10"
31
31
  },
32
32
  "compat": {
33
- "pluginApi": ">=2026.7.2-beta.7"
33
+ "pluginApi": ">=2026.8.1-beta.2"
34
34
  },
35
35
  "build": {
36
- "openclawVersion": "2026.7.2-beta.7"
36
+ "openclawVersion": "2026.8.1-beta.2"
37
37
  },
38
38
  "release": {
39
39
  "publishToClawHub": true,