@openclaw/voice-call 2026.10.2 → 2026.10.5-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,6 +1,23 @@
1
- import { a as verifyTelnyxWebhook, n as readProviderCallStatus, t as guardedJsonApiRequest } from "./guarded-json-api-x55yqHvW.mjs";
1
+ import { i as verifyTelnyxWebhook, t as createCarrierApi } from "./carrier-api-BYKw_fsQ.mjs";
2
2
  import crypto from "node:crypto";
3
3
  //#region extensions/voice-call/src/providers/telnyx.ts
4
+ const HANGUP_REASONS = /* @__PURE__ */ new Map([
5
+ ["normal_clearing", "completed"],
6
+ ["normal_unspecified", "completed"],
7
+ ["originator_cancel", "hangup-bot"],
8
+ ["call_rejected", "busy"],
9
+ ["user_busy", "busy"],
10
+ ["no_answer", "no-answer"],
11
+ ["no_user_response", "no-answer"],
12
+ ["destination_out_of_order", "failed"],
13
+ ["network_out_of_order", "failed"],
14
+ ["service_unavailable", "failed"],
15
+ ["recovery_on_timer_expire", "failed"],
16
+ ["machine_detected", "voicemail"],
17
+ ["fax_detected", "voicemail"],
18
+ ["user_hangup", "hangup-user"],
19
+ ["subscriber_absent", "hangup-user"]
20
+ ]);
4
21
  function normalizeTelnyxDirection(direction) {
5
22
  if (direction === "incoming" || direction === "inbound") return "inbound";
6
23
  if (direction === "outgoing" || direction === "outbound") return "outbound";
@@ -16,29 +33,12 @@ function decodeClientStateBase64(value) {
16
33
  var TelnyxProvider = class {
17
34
  constructor(config, options = {}) {
18
35
  this.name = "telnyx";
19
- this.baseUrl = "https://api.telnyx.com/v2";
20
- this.apiHost = "api.telnyx.com";
21
36
  if (!config.apiKey) throw new Error("Telnyx API key is required");
22
37
  if (!config.connectionId) throw new Error("Telnyx connection ID is required");
23
- this.apiKey = config.apiKey;
24
38
  this.connectionId = config.connectionId;
25
39
  this.publicKey = config.publicKey;
26
40
  this.options = options;
27
- }
28
- async apiRequest(endpoint, body, options) {
29
- return await guardedJsonApiRequest({
30
- url: `${this.baseUrl}${endpoint}`,
31
- method: "POST",
32
- headers: {
33
- Authorization: `Bearer ${this.apiKey}`,
34
- "Content-Type": "application/json"
35
- },
36
- body,
37
- allowNotFound: options?.allowNotFound,
38
- allowedHostnames: [this.apiHost],
39
- auditContext: "voice-call.telnyx.api",
40
- errorPrefix: "Telnyx API error"
41
- });
41
+ this.api = createCarrierApi("Telnyx", "https://api.telnyx.com/v2", `Bearer ${config.apiKey}`, { statusContentType: "application/json" });
42
42
  }
43
43
  verifyWebhook(ctx) {
44
44
  return verifyTelnyxWebhook(ctx, this.publicKey, { skipVerification: this.options.skipVerification });
@@ -116,31 +116,11 @@ var TelnyxProvider = class {
116
116
  default: return null;
117
117
  }
118
118
  }
119
- /**
120
- * Map Telnyx hangup cause to normalized end reason.
121
- * @see https://developers.telnyx.com/docs/api/v2/call-control/Call-Commands#hangup-causes
122
- */
123
119
  mapHangupCause(cause) {
124
- switch (cause) {
125
- case "normal_clearing":
126
- case "normal_unspecified": return "completed";
127
- case "originator_cancel": return "hangup-bot";
128
- case "call_rejected":
129
- case "user_busy": return "busy";
130
- case "no_answer":
131
- case "no_user_response": return "no-answer";
132
- case "destination_out_of_order":
133
- case "network_out_of_order":
134
- case "service_unavailable":
135
- case "recovery_on_timer_expire": return "failed";
136
- case "machine_detected":
137
- case "fax_detected": return "voicemail";
138
- case "user_hangup":
139
- case "subscriber_absent": return "hangup-user";
140
- default:
141
- if (cause) console.warn(`[telnyx] Unknown hangup cause: ${cause}`);
142
- return "completed";
143
- }
120
+ const reason = cause && HANGUP_REASONS.get(cause);
121
+ if (reason) return reason;
122
+ if (cause) console.warn(`[telnyx] Unknown hangup cause: ${cause}`);
123
+ return "completed";
144
124
  }
145
125
  async initiateCall(input) {
146
126
  const body = {
@@ -154,22 +134,22 @@ var TelnyxProvider = class {
154
134
  ...buildTelnyxStreamingFields(input.streamUrl, input.streamAuthToken)
155
135
  };
156
136
  return {
157
- providerCallId: (await this.apiRequest("/calls", body)).data.call_control_id,
137
+ providerCallId: (await this.api.request("/calls", body)).data.call_control_id,
158
138
  status: "initiated"
159
139
  };
160
140
  }
161
141
  async hangupCall(input) {
162
- await this.apiRequest(`/calls/${input.providerCallId}/actions/hangup`, { command_id: crypto.randomUUID() }, { allowNotFound: true });
142
+ await this.api.request(`/calls/${input.providerCallId}/actions/hangup`, { command_id: crypto.randomUUID() }, { allowNotFound: true });
163
143
  }
164
144
  async answerCall(input) {
165
145
  const body = {
166
146
  command_id: `openclaw-answer-${input.callId}`,
167
147
  ...buildTelnyxStreamingFields(input.streamUrl, input.streamAuthToken)
168
148
  };
169
- await this.apiRequest(`/calls/${input.providerCallId}/actions/answer`, body);
149
+ await this.api.request(`/calls/${input.providerCallId}/actions/answer`, body);
170
150
  }
171
151
  async playTts(input) {
172
- await this.apiRequest(`/calls/${input.providerCallId}/actions/speak`, {
152
+ await this.api.request(`/calls/${input.providerCallId}/actions/speak`, {
173
153
  command_id: crypto.randomUUID(),
174
154
  payload: input.text,
175
155
  voice: input.voice || "female",
@@ -177,27 +157,16 @@ var TelnyxProvider = class {
177
157
  });
178
158
  }
179
159
  async startListening(input) {
180
- await this.apiRequest(`/calls/${input.providerCallId}/actions/transcription_start`, {
160
+ await this.api.request(`/calls/${input.providerCallId}/actions/transcription_start`, {
181
161
  command_id: crypto.randomUUID(),
182
162
  language: input.language || "en"
183
163
  });
184
164
  }
185
165
  async stopListening(input) {
186
- await this.apiRequest(`/calls/${input.providerCallId}/actions/transcription_stop`, { command_id: crypto.randomUUID() }, { allowNotFound: true });
166
+ await this.api.request(`/calls/${input.providerCallId}/actions/transcription_stop`, { command_id: crypto.randomUUID() }, { allowNotFound: true });
187
167
  }
188
168
  async getCallStatus(input) {
189
- return readProviderCallStatus(() => guardedJsonApiRequest({
190
- url: `${this.baseUrl}/calls/${input.providerCallId}`,
191
- method: "GET",
192
- headers: {
193
- Authorization: `Bearer ${this.apiKey}`,
194
- "Content-Type": "application/json"
195
- },
196
- allowNotFound: true,
197
- allowedHostnames: [this.apiHost],
198
- auditContext: "telnyx-get-call-status",
199
- errorPrefix: "Telnyx get call status error"
200
- }), (data) => {
169
+ return this.api.getCallStatus(`/calls/${input.providerCallId}`, (data) => {
201
170
  const status = data.data?.state ?? "unknown";
202
171
  const isAlive = data.data?.is_alive;
203
172
  return isAlive === void 0 ? {
@@ -1,13 +1,14 @@
1
+ import { a as mapProviderStatusToEndReason, c as getHeader, h as generateDtmfRedirectTwiml, i as isProviderStatusTerminal, o as normalizeProviderStatus } from "./runtime-entry-STjFmi_B.mjs";
1
2
  import { n as requireSupportedTwilioApiHostname, r as resolveTwilioApiBaseUrl } from "./twilio-region-wBOx-8sz.mjs";
2
- import { a as normalizeProviderStatus, g as generateDtmfRedirectTwiml, i as mapProviderStatusToEndReason, r as isProviderStatusTerminal, s as getHeader } from "./runtime-entry-CbSKxSKk.mjs";
3
- import { n as mapVoiceToPolly, t as escapeXml } from "./voice-mapping-B_iAUkER.mjs";
4
- import { n as readProviderCallStatus, o as verifyTwilioWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-x55yqHvW.mjs";
3
+ import { n as mapVoiceToPolly, t as escapeXml } from "./voice-mapping-1lzFmzOF.mjs";
4
+ import { a as verifyTwilioWebhook, t as createCarrierApi } from "./carrier-api-BYKw_fsQ.mjs";
5
5
  import { asNullableObjectRecord, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
6
6
  import { sleepWithAbort } from "openclaw/plugin-sdk/runtime-env";
7
7
  import crypto from "node:crypto";
8
8
  import { safeParseJson } from "openclaw/plugin-sdk/text-utility-runtime";
9
9
  import { safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
10
10
  import { retryAsync } from "openclaw/plugin-sdk/retry-runtime";
11
+ import { raceWithTimeout } from "openclaw/plugin-sdk/time-runtime";
11
12
  //#region extensions/voice-call/src/providers/twilio/api.ts
12
13
  var TwilioApiError = class extends Error {
13
14
  constructor(httpStatus, responseText) {
@@ -20,28 +21,27 @@ var TwilioApiError = class extends Error {
20
21
  this.twilioCode = typeof parsed?.code === "number" ? parsed.code : void 0;
21
22
  }
22
23
  };
23
- /** POST a form-encoded Twilio REST API request through the SSRF guard. */
24
- async function twilioApiRequest(params) {
25
- const bodyParams = params.body instanceof URLSearchParams ? params.body : Object.entries(params.body).reduce((acc, [key, value]) => {
26
- if (Array.isArray(value)) for (const entry of value) acc.append(key, entry);
27
- else if (typeof value === "string") acc.append(key, value);
28
- return acc;
29
- }, new URLSearchParams());
30
- return guardedJsonApiRequest({
31
- url: `${params.baseUrl}${params.endpoint}`,
32
- method: "POST",
33
- headers: {
34
- Authorization: `Basic ${Buffer.from(`${params.accountSid}:${params.authToken}`).toString("base64")}`,
35
- "Content-Type": "application/x-www-form-urlencoded"
36
- },
37
- body: bodyParams,
38
- allowNotFound: params.allowNotFound,
39
- allowedHostnames: [requireSupportedTwilioApiHostname(params.baseUrl)],
40
- auditContext: "voice-call.twilio.api",
41
- errorPrefix: "Twilio API error",
24
+ function createTwilioApi(params) {
25
+ requireSupportedTwilioApiHostname(params.baseUrl);
26
+ const api = createCarrierApi("Twilio", params.baseUrl, `Basic ${Buffer.from(`${params.accountSid}:${params.authToken}`).toString("base64")}`, {
27
+ contentType: "application/x-www-form-urlencoded",
42
28
  malformedJsonMessage: "Twilio API returned malformed JSON.",
43
29
  createError: (status, text) => new TwilioApiError(status, text)
44
30
  });
31
+ return {
32
+ request: (endpoint, body, options) => {
33
+ const form = body instanceof URLSearchParams ? body : new URLSearchParams();
34
+ if (!(body instanceof URLSearchParams)) for (const [key, value] of Object.entries(body)) for (const entry of Array.isArray(value) ? value : [value]) form.append(key, entry);
35
+ return api.request(endpoint, form, options);
36
+ },
37
+ getCallStatus: ({ providerCallId }) => api.getCallStatus(`/Calls/${providerCallId}.json`, (data) => {
38
+ const status = normalizeProviderStatus(data.status);
39
+ return {
40
+ status,
41
+ isTerminal: isProviderStatusTerminal(status)
42
+ };
43
+ })
44
+ };
45
45
  }
46
46
  //#endregion
47
47
  //#region extensions/voice-call/src/providers/twilio.ts
@@ -87,12 +87,17 @@ var TwilioProvider = class TwilioProvider {
87
87
  this.twimlStorage = /* @__PURE__ */ new Map();
88
88
  if (!config.accountSid) throw new Error("Twilio Account SID is required");
89
89
  if (!config.authToken) throw new Error("Twilio Auth Token is required");
90
- this.accountSid = config.accountSid;
91
90
  this.authToken = config.authToken;
92
- this.baseUrl = resolveTwilioApiBaseUrl({
93
- accountSid: this.accountSid,
94
- region: config.region
91
+ const api = createTwilioApi({
92
+ accountSid: config.accountSid,
93
+ authToken: config.authToken,
94
+ baseUrl: resolveTwilioApiBaseUrl({
95
+ accountSid: config.accountSid,
96
+ region: config.region
97
+ })
95
98
  });
99
+ this.apiRequest = api.request;
100
+ this.getCallStatus = api.getCallStatus;
96
101
  this.options = options;
97
102
  if (options.publicUrl) this.currentPublicUrl = options.publicUrl;
98
103
  }
@@ -131,16 +136,6 @@ var TwilioProvider = class TwilioProvider {
131
136
  if (!streamSid || !this.mediaStreamHandler) return;
132
137
  this.mediaStreamHandler.clearTtsQueue(streamSid, reason);
133
138
  }
134
- async apiRequest(endpoint, params, options) {
135
- return await twilioApiRequest({
136
- baseUrl: this.baseUrl,
137
- accountSid: this.accountSid,
138
- authToken: this.authToken,
139
- endpoint,
140
- body: params,
141
- allowNotFound: options?.allowNotFound
142
- });
143
- }
144
139
  async updateLiveCallTwiml(providerCallId, twiml, operation) {
145
140
  await retryAsync(() => this.apiRequest(`/Calls/${providerCallId}.json`, { Twiml: twiml }), {
146
141
  attempts: TWILIO_CALL_UPDATE_RETRY_DELAYS_MS.length + 1,
@@ -181,7 +176,8 @@ var TwilioProvider = class TwilioProvider {
181
176
  const event = this.normalizeEvent(params, {
182
177
  callIdOverride: callIdFromQuery,
183
178
  dedupeKey,
184
- turnToken: turnTokenFromQuery
179
+ turnToken: turnTokenFromQuery,
180
+ amdCallback: normalizeOptionalString(ctx.query?.type) === "amd"
185
181
  });
186
182
  if (event?.direction === "inbound" && event.type !== "call.ended" && event.providerCallId && this.currentPublicUrl && !this.callWebhookUrls.has(event.providerCallId)) this.callWebhookUrls.set(event.providerCallId, this.currentPublicUrl);
187
183
  return {
@@ -209,17 +205,25 @@ var TwilioProvider = class TwilioProvider {
209
205
  normalizeEvent(params, options) {
210
206
  const callSid = params.get("CallSid") || "";
211
207
  const callIdOverride = options?.callIdOverride;
208
+ const answeredBy = params.get("AnsweredBy")?.trim().toLowerCase();
212
209
  const baseEvent = {
213
210
  id: crypto.randomUUID(),
214
211
  dedupeKey: options?.dedupeKey,
215
212
  callId: callIdOverride || callSid,
216
213
  providerCallId: callSid,
217
214
  timestamp: Date.now(),
215
+ ...answeredBy ? { answeredBy } : {},
218
216
  turnToken: options?.turnToken,
219
217
  direction: TwilioProvider.parseDirection(params.get("Direction")),
220
218
  from: params.get("From") || void 0,
221
219
  to: params.get("To") || void 0
222
220
  };
221
+ if (answeredBy) console.log(`[voice-call] AMD classification callId=${baseEvent.callId} answeredBy=${answeredBy} at=${baseEvent.timestamp}`);
222
+ if (options?.amdCallback && answeredBy) return {
223
+ ...baseEvent,
224
+ type: "call.amd",
225
+ answeredBy
226
+ };
223
227
  const speechResult = params.get("SpeechResult");
224
228
  if (speechResult?.trim()) return {
225
229
  ...baseEvent,
@@ -234,7 +238,8 @@ var TwilioProvider = class TwilioProvider {
234
238
  type: "call.dtmf",
235
239
  digits
236
240
  };
237
- const callStatus = normalizeProviderStatus(params.get("CallStatus"));
241
+ const rawCallStatus = params.get("CallStatus");
242
+ const callStatus = normalizeProviderStatus(rawCallStatus);
238
243
  if (callStatus === "initiated") return {
239
244
  ...baseEvent,
240
245
  type: "call.initiated"
@@ -257,6 +262,10 @@ var TwilioProvider = class TwilioProvider {
257
262
  this.releaseCallState(callSid, callIdOverride);
258
263
  return event;
259
264
  }
265
+ if (callSid && baseEvent.direction === "inbound" && rawCallStatus === null && !params.has("SpeechResult") && !params.has("Digits")) return {
266
+ ...baseEvent,
267
+ type: "call.initiated"
268
+ };
260
269
  return null;
261
270
  }
262
271
  static {
@@ -277,7 +286,8 @@ var TwilioProvider = class TwilioProvider {
277
286
  }
278
287
  generateTwimlResponse(ctx) {
279
288
  const params = new URLSearchParams(ctx.rawBody);
280
- const isStatusCallback = normalizeOptionalString(ctx.query?.type) === "status";
289
+ const callbackType = normalizeOptionalString(ctx.query?.type);
290
+ const isStatusCallback = callbackType === "status" || callbackType === "amd";
281
291
  const callId = normalizeOptionalString(ctx.query?.callId);
282
292
  const callStatus = params.get("CallStatus");
283
293
  const direction = params.get("Direction");
@@ -299,7 +309,8 @@ var TwilioProvider = class TwilioProvider {
299
309
  }
300
310
  consumeInitialTwiML(ctx) {
301
311
  const params = new URLSearchParams(ctx.rawBody);
302
- const isStatusCallback = normalizeOptionalString(ctx.query?.type) === "status";
312
+ const callbackType = normalizeOptionalString(ctx.query?.type);
313
+ const isStatusCallback = callbackType === "status" || callbackType === "amd";
303
314
  const callId = normalizeOptionalString(ctx.query?.callId);
304
315
  const callSid = params.get("CallSid") || void 0;
305
316
  if (!callId || isStatusCallback) return null;
@@ -361,6 +372,18 @@ var TwilioProvider = class TwilioProvider {
361
372
  ],
362
373
  Timeout: "30"
363
374
  };
375
+ if (input.voicemail?.detection === "twilio") {
376
+ const amdUrl = new URL(url.toString());
377
+ amdUrl.searchParams.set("type", "amd");
378
+ params.MachineDetection = input.voicemail.onMachine === "leave-message" ? "DetectMessageEnd" : "Enable";
379
+ params.AsyncAmd = "true";
380
+ params.AsyncAmdStatusCallback = amdUrl.toString();
381
+ params.AsyncAmdStatusCallbackMethod = "POST";
382
+ params.MachineDetectionSpeechThreshold = String(input.voicemail.machineDetectionSpeechThresholdMs);
383
+ params.MachineDetectionSpeechEndThreshold = String(input.voicemail.machineDetectionSpeechEndThresholdMs);
384
+ params.MachineDetectionSilenceTimeout = String(input.voicemail.machineDetectionSilenceTimeoutMs);
385
+ params.MachineDetectionTimeout = String(input.voicemail.machineDetectionTimeoutMs / 1e3);
386
+ }
364
387
  if (input.inlineTwiml) {
365
388
  params.Twiml = input.inlineTwiml;
366
389
  console.log(`[voice-call] Sending direct Twilio initial TwiML for call ${input.callId} (kind=notify)`);
@@ -372,6 +395,9 @@ var TwilioProvider = class TwilioProvider {
372
395
  status: result.status === "queued" ? "queued" : "initiated"
373
396
  };
374
397
  }
398
+ async playMessageAndHangup(input) {
399
+ await this.apiRequest(`/Calls/${input.providerCallId}.json`, { Twiml: `<?xml version="1.0" encoding="UTF-8"?><Response><Say voice="${escapeXml(input.voice || "alice")}">${escapeXml(input.text)}</Say><Hangup/></Response>` });
400
+ }
375
401
  async hangupCall(input) {
376
402
  await this.apiRequest(`/Calls/${input.providerCallId}.json`, { Status: "completed" }, { allowNotFound: true });
377
403
  this.releaseCallState(input.providerCallId, input.callId);
@@ -410,11 +436,6 @@ var TwilioProvider = class TwilioProvider {
410
436
  async sendDtmf(input) {
411
437
  await this.updateLiveCallTwiml(input.providerCallId, generateDtmfRedirectTwiml(input.digits, this.requireCallWebhookUrl(input.providerCallId)), "sendDtmf");
412
438
  }
413
- /**
414
- * Play TTS via core TTS and Twilio Media Streams.
415
- * Generates audio with core TTS, converts to mu-law, and streams via WebSocket.
416
- * Uses a queue to serialize playback and prevent overlapping audio.
417
- */
418
439
  async playTtsViaStream(text, streamSid) {
419
440
  if (!this.ttsProvider || !this.mediaStreamHandler) throw new Error("TTS provider and media stream handler required");
420
441
  const CHUNK_SIZE = 160;
@@ -423,41 +444,23 @@ var TwilioProvider = class TwilioProvider {
423
444
  const handler = this.mediaStreamHandler;
424
445
  const ttsProvider = this.ttsProvider;
425
446
  await handler.queueTts(streamSid, async (signal) => {
426
- const sendKeepAlive = () => {
427
- handler.sendAudio(streamSid, SILENCE_CHUNK);
428
- };
429
- sendKeepAlive();
447
+ handler.sendAudio(streamSid, SILENCE_CHUNK);
430
448
  const keepAlive = setInterval(() => {
431
- if (!signal.aborted) sendKeepAlive();
449
+ if (!signal.aborted) handler.sendAudio(streamSid, SILENCE_CHUNK);
432
450
  }, CHUNK_DELAY_MS);
433
451
  let muLawAudio;
434
- let synthTimeout = null;
435
- let removeAbortListener = () => {};
436
452
  const synthTimeoutMs = ttsProvider.synthesisTimeoutMs;
437
453
  try {
438
- const synthPromise = ttsProvider.synthesizeForTelephony(text);
439
- const timeoutPromise = new Promise((_, reject) => {
440
- synthTimeout = setTimeout(() => {
441
- reject(/* @__PURE__ */ new Error(`Telephony TTS synthesis timed out after ${synthTimeoutMs}ms`));
442
- }, synthTimeoutMs);
443
- });
444
- const abortPromise = new Promise((_, reject) => {
445
- const onAbort = () => {
446
- reject(signal.reason instanceof Error ? signal.reason : /* @__PURE__ */ new Error("Telephony TTS synthesis aborted"));
447
- };
448
- signal.addEventListener("abort", onAbort, { once: true });
449
- removeAbortListener = () => signal.removeEventListener("abort", onAbort);
450
- if (signal.aborted) onAbort();
454
+ muLawAudio = await raceWithTimeout(ttsProvider.synthesizeForTelephony(text), synthTimeoutMs, () => {
455
+ throw new Error(`Telephony TTS synthesis timed out after ${synthTimeoutMs}ms`);
456
+ }, {
457
+ signal,
458
+ onAbort: () => {
459
+ throw signal.reason instanceof Error ? signal.reason : /* @__PURE__ */ new Error("Telephony TTS synthesis aborted");
460
+ }
451
461
  });
452
- muLawAudio = await Promise.race([
453
- synthPromise,
454
- timeoutPromise,
455
- abortPromise
456
- ]);
457
462
  } finally {
458
- if (synthTimeout) clearTimeout(synthTimeout);
459
463
  clearInterval(keepAlive);
460
- removeAbortListener();
461
464
  }
462
465
  if (muLawAudio.length === 0) throw new Error("Telephony TTS produced no audio");
463
466
  let chunkAttempts = 0;
@@ -477,7 +480,6 @@ var TwilioProvider = class TwilioProvider {
477
480
  break;
478
481
  }
479
482
  nextChunkDueAt += CHUNK_DELAY_MS;
480
- if (signal.aborted) break;
481
483
  }
482
484
  if (signal.aborted) return;
483
485
  const markName = `tts-${Date.now()}-${++this.playbackMarkSequence}`;
@@ -500,23 +502,6 @@ var TwilioProvider = class TwilioProvider {
500
502
  return webhookUrl;
501
503
  }
502
504
  async stopListening(_input) {}
503
- async getCallStatus(input) {
504
- return readProviderCallStatus(() => guardedJsonApiRequest({
505
- url: `${this.baseUrl}/Calls/${input.providerCallId}.json`,
506
- method: "GET",
507
- headers: { Authorization: `Basic ${Buffer.from(`${this.accountSid}:${this.authToken}`).toString("base64")}` },
508
- allowNotFound: true,
509
- allowedHostnames: [new URL(this.baseUrl).hostname],
510
- auditContext: "twilio-get-call-status",
511
- errorPrefix: "Twilio get call status error"
512
- }), (data) => {
513
- const status = normalizeProviderStatus(data.status);
514
- return {
515
- status,
516
- isTerminal: isProviderStatusTerminal(status)
517
- };
518
- });
519
- }
520
505
  };
521
506
  //#endregion
522
507
  export { TwilioProvider };
@@ -1,8 +1,5 @@
1
1
  import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
2
2
  //#region extensions/voice-call/src/voice-mapping.ts
3
- /**
4
- * Escape XML special characters for TwiML and other XML responses.
5
- */
6
3
  function escapeXml(text) {
7
4
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
8
5
  }
@@ -15,13 +12,6 @@ const OPENAI_TO_POLLY_MAP = /* @__PURE__ */ new Map([
15
12
  ["shimmer", "Polly.Kimberly"]
16
13
  ]);
17
14
  const DEFAULT_POLLY_VOICE = "Polly.Joanna";
18
- /**
19
- * Map OpenAI voice names to Twilio Polly equivalents.
20
- * Falls through if already a valid Polly/Google voice.
21
- *
22
- * @param voice - OpenAI voice name (alloy, echo, etc.) or Polly voice name
23
- * @returns Polly voice name suitable for Twilio TwiML
24
- */
25
15
  function mapVoiceToPolly(voice) {
26
16
  if (!voice) return DEFAULT_POLLY_VOICE;
27
17
  if (voice.startsWith("Polly.") || voice.startsWith("Google.")) return voice;
@@ -9,6 +9,7 @@ const MACHINE_OUTPUT_COMMANDS = /* @__PURE__ */ new Set([
9
9
  "latency",
10
10
  "speak",
11
11
  "start",
12
+ "steer",
12
13
  "status",
13
14
  "tail"
14
15
  ]);
@@ -27,28 +27,24 @@ function resolveVoiceCallStateDatabaseEnv(params) {
27
27
  OPENCLAW_STATE_DIR: resolveVoiceCallStorePath(params)
28
28
  };
29
29
  }
30
- function describeVoiceCallSchemaMigration(migration) {
31
- switch (migration.kind) {
32
- case "agent-databases-composite-primary-key": return "agent database registry primary key -> agent_id,path";
33
- case "agent-databases-relative-paths-v9": return "agent database registry paths -> state-relative paths";
34
- case "audit-events-v2": return "audit event ledger -> versioned message lifecycle schema";
35
- case "commitments-retirement-v7": return "retired commitments storage -> discarded rows, table, and indexes";
36
- case "state-table-retirement-v10": return "retired shared-state tables -> removed tables and indexes";
37
- case "state-table-retirement-v11": return "retired skill curator tables -> removed tables and indexes";
38
- case "singleton-state-foldin-v12": return "singleton state tables -> shared configuration state";
39
- case "state-consolidation-v13": return "cron jobs and subagent runs -> canonical JSON storage";
40
- case "creator-namespace-v14": return "cron creators -> explicit principal namespaces";
41
- case "conversation-binding-targets-v15": return "conversation bindings -> exact target keys without agent/session projections";
42
- case "skill-workshop-directory-ownership-v16": return "Skill Workshop proposals -> per-agent Workshop directory ownership";
43
- case "prepared-worker-ownership-v17": return "prepared workers -> one-use capacity and fixed workspace ownership";
44
- case "github-publication-requester-authority-v18": return "GitHub publication receipts -> original requesting authority";
45
- case "worker-placement-execution-mode-v8": return "cloud worker placements -> execution-mode claims";
46
- case "operator-approvals-system-agent": return "operator approvals -> OpenClaw system changes";
47
- case "session-watch-cursor-provenance-v4": return "session watch cursors -> provenance column";
48
- case "strict-tables-v3": return "tables -> SQLite STRICT typing";
49
- }
50
- return migration.kind;
51
- }
30
+ const schemaMigrationDescriptions = {
31
+ "agent-databases-composite-primary-key": "agent database registry primary key -> agent_id,path",
32
+ "agent-databases-relative-paths-v9": "agent database registry paths -> state-relative paths",
33
+ "audit-events-v2": "audit event ledger -> versioned message lifecycle schema",
34
+ "commitments-retirement-v7": "retired commitments storage -> discarded rows, table, and indexes",
35
+ "state-table-retirement-v10": "retired shared-state tables -> removed tables and indexes",
36
+ "state-table-retirement-v11": "retired skill curator tables -> removed tables and indexes",
37
+ "singleton-state-foldin-v12": "singleton state tables -> shared configuration state",
38
+ "state-consolidation-v13": "cron jobs and subagent runs -> canonical JSON storage",
39
+ "creator-namespace-v14": "cron creators -> explicit principal namespaces",
40
+ "conversation-binding-targets-v15": "conversation bindings -> exact target keys without agent/session projections",
41
+ "prepared-worker-ownership-v17": "prepared workers -> one-use capacity and fixed workspace ownership",
42
+ "github-publication-requester-authority-v18": "GitHub publication receipts -> original requesting authority",
43
+ "worker-placement-execution-mode-v8": "cloud worker placements -> execution-mode claims",
44
+ "operator-approvals-system-agent": "operator approvals -> OpenClaw system changes",
45
+ "session-watch-cursor-provenance-v4": "session watch cursors -> provenance column",
46
+ "strict-tables-v3": "tables -> SQLite STRICT typing"
47
+ };
52
48
  /** Doctor migrations owned by the voice-call plugin. */
53
49
  const stateMigrations = [...stateMigrations$1, {
54
50
  id: "voice-call-sqlite-schema",
@@ -59,7 +55,7 @@ const stateMigrations = [...stateMigrations$1, {
59
55
  const { detectOpenClawStateDatabaseSchemaMigrations } = await import("openclaw/plugin-sdk/doctor-repair-runtime");
60
56
  const schemaMigrations = detectOpenClawStateDatabaseSchemaMigrations({ env: resolveVoiceCallStateDatabaseEnv(params) });
61
57
  if (schemaMigrations.length === 0) return null;
62
- return { preview: schemaMigrations.map((migration) => `- Voice Call SQLite schema: ${describeVoiceCallSchemaMigration(migration)}`) };
58
+ return { preview: schemaMigrations.map((migration) => `- Voice Call SQLite schema: ${schemaMigrationDescriptions[migration.kind]}`) };
63
59
  },
64
60
  async migrateLegacyState(params) {
65
61
  const changes = [];