@openclaw/voice-call 2026.9.7 → 2026.10.1-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,5 +1,5 @@
1
- import { d as REALTIME_VOICE_END_CALL_TOOL_NAME, s as resolveVoiceCallPublicPathPrefix } from "./config-Ds7xoZbI.mjs";
2
- import { c as canonicalizeVoiceCallMediaBase64, o as WebSocket, s as WebSocketServer } from "./runtime-entry-BNOQRXii.mjs";
1
+ import { d as REALTIME_VOICE_END_CALL_TOOL_NAME, s as resolveVoiceCallPublicPathPrefix } from "./config-CX9J-053.mjs";
2
+ import { o as canonicalizeVoiceCallMediaBase64 } from "./runtime-entry-CbSKxSKk.mjs";
3
3
  import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
4
4
  import { asNullableRecord, asOptionalObjectRecord, asOptionalRecord, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
5
5
  import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env";
@@ -7,8 +7,10 @@ import { REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME, REALTIME_VOICE_AUDIO_FORMAT_G71
7
7
  import { normalizeWebhookPath } from "openclaw/plugin-sdk/webhook-ingress";
8
8
  import { isFutureDateTimestampMs, resolveExpiresAtMsFromDurationMs } from "openclaw/plugin-sdk/number-runtime";
9
9
  import { randomUUID } from "node:crypto";
10
- import { sliceUtf16Safe, truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
10
+ import { safeParseJson, sliceUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
11
11
  import "node:http";
12
+ import { WebSocket, WebSocketServer, rejectWebSocketUpgrade } from "openclaw/plugin-sdk/websocket-runtime";
13
+ import { createDeferred } from "openclaw/plugin-sdk/extension-shared";
12
14
  //#region extensions/voice-call/src/webhook/realtime-audio-pacer.ts
13
15
  const TELEPHONY_SAMPLE_RATE = 8e3;
14
16
  const TELEPHONY_CHUNK_BYTES = 160;
@@ -17,7 +19,6 @@ const DEFAULT_MAX_QUEUED_AUDIO_BYTES = TELEPHONY_SAMPLE_RATE * 120;
17
19
  const QUEUE_COMPACT_HEAD_THRESHOLD = 256;
18
20
  const MAX_PLAYBACK_SEGMENTS = 128;
19
21
  const MAX_PENDING_MARK_BOUNDARIES = 64;
20
- /** Paces outgoing mulaw audio frames at telephony cadence. */
21
22
  var RealtimeAudioPacer = class {
22
23
  constructor(params) {
23
24
  this.params = params;
@@ -103,11 +104,9 @@ var RealtimeAudioPacer = class {
103
104
  while (this.playbackSegments.length > 0) {
104
105
  const head = this.playbackSegments[0];
105
106
  if (!head || head.sentMs < head.totalMs || head.lastSentEndMs === void 0 || head.lastSentEndMs > boundary.sentMs) break;
106
- const retired = this.playbackSegments.shift();
107
- if (retired) {
108
- this.retiredAudioMs += retired.sentMs;
109
- if (retired.itemId !== void 0) this.retiredItemOffsets.set(retired.itemId, (this.retiredItemOffsets.get(retired.itemId) ?? 0) + retired.sentMs);
110
- }
107
+ this.playbackSegments.shift();
108
+ this.retiredAudioMs += head.sentMs;
109
+ if (head.itemId !== void 0) this.retiredItemOffsets.set(head.itemId, (this.retiredItemOffsets.get(head.itemId) ?? 0) + head.sentMs);
111
110
  }
112
111
  this.confirmedPlayedMs = Math.max(this.confirmedPlayedMs, Math.min(boundary.sentMs, this.sentAudioMs));
113
112
  }
@@ -118,14 +117,13 @@ var RealtimeAudioPacer = class {
118
117
  this.clearTimer();
119
118
  this.resetQueue();
120
119
  this.resetPlaybackState();
121
- this.params.send(this.params.serializer.clear());
120
+ this.params.send(this.params.serializer.serializeClear());
122
121
  return clearedAudioBytes;
123
122
  }
124
123
  /** True while queued audio or a paced send timer can still reach the telephony stream. */
125
124
  hasPendingAudio() {
126
125
  return !this.closed && (this.queuedAudioBytes > 0 || this.timer !== null);
127
126
  }
128
- /** Stop sending and discard queued frames. */
129
127
  close() {
130
128
  this.closed = true;
131
129
  this.clearTimer();
@@ -158,17 +156,14 @@ var RealtimeAudioPacer = class {
158
156
  this.streamClockMs = null;
159
157
  this.params.onPlaybackReset?.();
160
158
  }
161
- /** Clear the scheduled pump timer. */
162
159
  clearTimer() {
163
160
  if (!this.timer) return;
164
161
  clearTimeout(this.timer);
165
162
  this.timer = null;
166
163
  }
167
- /** Start the pump when queued work exists and no timer is active. */
168
164
  ensurePump() {
169
165
  if (!this.timer) this.pump();
170
166
  }
171
- /** Close the pacer and notify the caller about queued-audio backpressure. */
172
167
  failBackpressure() {
173
168
  this.close();
174
169
  this.params.onBackpressure?.();
@@ -220,7 +215,7 @@ var RealtimeAudioPacer = class {
220
215
  }
221
216
  sendAudioItem(item) {
222
217
  this.queuedAudioBytes = Math.max(0, this.queuedAudioBytes - item.chunk.length);
223
- const sent = this.params.send(this.params.serializer.media(item.chunk.toString("base64")));
218
+ const sent = this.params.send(this.params.serializer.serializeMedia(item.chunk.toString("base64")));
224
219
  if (sent) {
225
220
  item.segment.sentMs += item.durationMs;
226
221
  this.sentAudioMs += item.durationMs;
@@ -237,7 +232,7 @@ var RealtimeAudioPacer = class {
237
232
  }
238
233
  /** Send a queued mark frame and bind it to the playback prefix before it. */
239
234
  sendMarkItem(item) {
240
- const sent = this.params.send(this.params.serializer.mark(item.name));
235
+ const sent = this.params.send(this.params.serializer.serializeMark(item.name));
241
236
  if (sent) {
242
237
  this.markBoundaries.push({
243
238
  name: item.name,
@@ -258,144 +253,77 @@ function parseTimestampMs(value) {
258
253
  return Number.isSafeInteger(parsed) ? parsed : void 0;
259
254
  }
260
255
  }
261
- /** Parse a JSON object frame, returning null for invalid or non-object payloads. */
262
- function tryParseJson(rawMessage) {
263
- try {
264
- const parsed = JSON.parse(rawMessage);
265
- return asNullableRecord(parsed);
266
- } catch {}
267
- return null;
268
- }
269
- /** Parse a common provider media frame. */
270
- function parseMediaFrame(msg) {
271
- const mediaData = asOptionalObjectRecord(msg.media);
272
- const payload = typeof mediaData?.payload === "string" ? mediaData.payload : void 0;
273
- const canonicalPayload = payload ? canonicalizeVoiceCallMediaBase64(payload) : void 0;
274
- if (!canonicalPayload) return { kind: "ignored" };
275
- return {
276
- kind: "media",
277
- payloadBase64: canonicalPayload,
278
- timestampMs: parseTimestampMs(mediaData?.timestamp),
279
- track: typeof mediaData?.track === "string" ? mediaData.track : void 0
280
- };
281
- }
282
- /** Parse a common provider mark frame. */
283
- function parseMarkFrame(msg) {
284
- const markData = asOptionalObjectRecord(msg.mark);
285
- return {
286
- kind: "mark",
287
- name: typeof markData?.name === "string" ? markData.name : void 0
288
- };
289
- }
290
- /** Parse one provider frame with provider-specific start/error hooks. */
291
- function parseProviderInboundFrame(rawMessage, parseStartFrame, parseExtraFrame) {
292
- const msg = tryParseJson(rawMessage);
293
- if (!msg) return { kind: "ignored" };
294
- const event = msg.event;
295
- switch (event) {
296
- case "start": return parseStartFrame(msg) ?? { kind: "ignored" };
297
- case "media": return parseMediaFrame(msg);
298
- case "mark": return parseMarkFrame(msg);
299
- case "stop": return { kind: "stop" };
300
- default: return parseExtraFrame?.(event, msg) ?? { kind: "ignored" };
301
- }
302
- }
303
- /** Serialize a provider media frame. */
304
- function serializeMediaFrame(payloadBase64, streamSid) {
305
- return JSON.stringify({
306
- event: "media",
307
- streamSid,
308
- media: { payload: payloadBase64 }
309
- });
310
- }
311
- /** Serialize a provider clear frame. */
312
- function serializeClearFrame(streamSid) {
313
- return JSON.stringify({
314
- event: "clear",
315
- streamSid
316
- });
317
- }
318
- /** Serialize a provider mark frame. */
319
- function serializeMarkFrame(name, streamSid) {
320
- return JSON.stringify({
321
- event: "mark",
322
- streamSid,
323
- mark: { name }
324
- });
325
- }
326
- /** Twilio media stream adapter, retaining streamSid for outbound frames. */
327
- var TwilioStreamFrameAdapter = class {
328
- constructor() {
329
- this.providerName = "twilio";
330
- this.streamSid = "";
256
+ var StreamFrameAdapter = class {
257
+ constructor(provider) {
258
+ this.provider = provider;
259
+ this.streamSid = provider === "twilio" ? "" : void 0;
331
260
  }
332
- /** Parse one Twilio websocket message into a normalized frame. */
333
261
  parseInbound(rawMessage) {
334
- return parseProviderInboundFrame(rawMessage, (msg) => {
335
- const startData = asOptionalObjectRecord(msg.start);
336
- const streamSid = typeof startData?.streamSid === "string" ? startData.streamSid : "";
337
- const callSid = typeof startData?.callSid === "string" ? startData.callSid : "";
338
- if (!streamSid || !callSid) return;
339
- this.streamSid = streamSid;
340
- return {
341
- kind: "start",
342
- streamId: streamSid,
343
- providerCallId: callSid
344
- };
345
- });
262
+ const msg = asNullableRecord(safeParseJson(rawMessage));
263
+ if (!msg) return { kind: "ignored" };
264
+ switch (msg.event) {
265
+ case "start": {
266
+ const start = asOptionalObjectRecord(msg.start);
267
+ const streamId = this.provider === "twilio" ? start?.streamSid : msg.stream_id;
268
+ const providerCallId = this.provider === "twilio" ? start?.callSid : start?.call_control_id;
269
+ if (typeof streamId !== "string" || !streamId || typeof providerCallId !== "string" || !providerCallId) return { kind: "ignored" };
270
+ if (this.provider === "twilio") this.streamSid = streamId;
271
+ return {
272
+ kind: "start",
273
+ streamId,
274
+ providerCallId
275
+ };
276
+ }
277
+ case "media": {
278
+ const media = asOptionalObjectRecord(msg.media);
279
+ const payload = typeof media?.payload === "string" ? media.payload : void 0;
280
+ const payloadBase64 = payload ? canonicalizeVoiceCallMediaBase64(payload) : void 0;
281
+ return payloadBase64 ? {
282
+ kind: "media",
283
+ payloadBase64,
284
+ timestampMs: parseTimestampMs(media?.timestamp)
285
+ } : { kind: "ignored" };
286
+ }
287
+ case "mark": {
288
+ const mark = asOptionalObjectRecord(msg.mark);
289
+ return {
290
+ kind: "mark",
291
+ name: typeof mark?.name === "string" ? mark.name : void 0
292
+ };
293
+ }
294
+ case "stop": return { kind: "stop" };
295
+ case "error": {
296
+ if (this.provider !== "telnyx") return { kind: "ignored" };
297
+ const error = asOptionalObjectRecord(msg.payload);
298
+ return {
299
+ kind: "error",
300
+ code: typeof error?.code === "string" || typeof error?.code === "number" ? String(error.code) : void 0,
301
+ title: typeof error?.title === "string" ? error.title : void 0,
302
+ detail: typeof error?.detail === "string" ? error.detail : void 0
303
+ };
304
+ }
305
+ default: return { kind: "ignored" };
306
+ }
346
307
  }
347
- /** Serialize Twilio media with the active streamSid. */
348
308
  serializeMedia(payloadBase64) {
349
- return serializeMediaFrame(payloadBase64, this.streamSid);
350
- }
351
- /** Serialize Twilio clear with the active streamSid. */
352
- serializeClear() {
353
- return serializeClearFrame(this.streamSid);
354
- }
355
- /** Serialize Twilio mark with the active streamSid. */
356
- serializeMark(name) {
357
- return serializeMarkFrame(name, this.streamSid);
358
- }
359
- };
360
- /** Telnyx media stream adapter. */
361
- var TelnyxStreamFrameAdapter = class {
362
- constructor() {
363
- this.providerName = "telnyx";
364
- }
365
- /** Parse one Telnyx websocket message into a normalized frame. */
366
- parseInbound(rawMessage) {
367
- return parseProviderInboundFrame(rawMessage, (msg) => {
368
- const topLevelStreamId = typeof msg.stream_id === "string" && msg.stream_id ? msg.stream_id : void 0;
369
- const startData = asOptionalObjectRecord(msg.start);
370
- const providerCallId = typeof startData?.call_control_id === "string" && startData.call_control_id ? startData.call_control_id : void 0;
371
- if (!topLevelStreamId || !providerCallId) return;
372
- return {
373
- kind: "start",
374
- streamId: topLevelStreamId,
375
- providerCallId
376
- };
377
- }, (event, msg) => {
378
- if (event !== "error") return;
379
- const errorData = asOptionalObjectRecord(msg.payload);
380
- return {
381
- kind: "error",
382
- code: typeof errorData?.code === "string" || typeof errorData?.code === "number" ? String(errorData.code) : void 0,
383
- title: typeof errorData?.title === "string" ? errorData.title : void 0,
384
- detail: typeof errorData?.detail === "string" ? errorData.detail : void 0
385
- };
309
+ return JSON.stringify({
310
+ event: "media",
311
+ streamSid: this.streamSid,
312
+ media: { payload: payloadBase64 }
386
313
  });
387
314
  }
388
- /** Serialize Telnyx media. */
389
- serializeMedia(payloadBase64) {
390
- return serializeMediaFrame(payloadBase64);
391
- }
392
- /** Serialize Telnyx clear. */
393
315
  serializeClear() {
394
- return serializeClearFrame();
316
+ return JSON.stringify({
317
+ event: "clear",
318
+ streamSid: this.streamSid
319
+ });
395
320
  }
396
- /** Serialize Telnyx mark. */
397
321
  serializeMark(name) {
398
- return serializeMarkFrame(name);
322
+ return JSON.stringify({
323
+ event: "mark",
324
+ streamSid: this.streamSid,
325
+ mark: { name }
326
+ });
399
327
  }
400
328
  };
401
329
  //#endregion
@@ -430,12 +358,6 @@ function findTextOverlap(base, next) {
430
358
  for (let size = max; size > 0; size -= 1) if (base.slice(-size) === next.slice(0, size)) return size;
431
359
  return 0;
432
360
  }
433
- function shouldInsertTranscriptSpace(base, next) {
434
- if (!base || !next) return false;
435
- const last = base.at(-1);
436
- if (/\s$/.test(base) || last === "(" || last === "[" || last === "{" || last === "\"" || last === "'" || /^[\s,.;:!?)]/.test(next)) return false;
437
- return true;
438
- }
439
361
  function appendTranscriptText(base, fragment) {
440
362
  const next = normalizeTranscriptText(fragment);
441
363
  if (!next) return base ?? "";
@@ -447,7 +369,7 @@ function appendTranscriptText(base, fragment) {
447
369
  if (nextLower.startsWith(currentLower)) return next;
448
370
  const overlap = findTextOverlap(currentLower, nextLower);
449
371
  if (overlap >= 6 || overlap >= 3 && next.length <= 12) return `${current}${next.slice(overlap)}`.trim();
450
- return `${current}${shouldInsertTranscriptSpace(current, next) ? " " : ""}${next}`.trim();
372
+ return `${current}${/[([{"']$/.test(current) || /^[,.;:!?)]/.test(next) ? "" : " "}${next}`.trim();
451
373
  }
452
374
  function resolveFinalTranscriptText(params) {
453
375
  const final = normalizeTranscriptText(params.final);
@@ -474,32 +396,29 @@ function withFallbackConsultQuestion(args, fallback) {
474
396
  const question = fallback?.trim();
475
397
  if (providerQuestion) {
476
398
  if (question && providerQuestion.length <= 40 && question.length >= providerQuestion.length + 8) {
477
- const context = normalizeOptionalString(asOptionalRecord(args)?.context);
399
+ const record = asOptionalRecord(args);
400
+ const context = normalizeOptionalString(record?.context);
478
401
  const fallbackContext = `Realtime provider supplied a shorter consult question: ${providerQuestion}`;
479
- return args && typeof args === "object" && !Array.isArray(args) ? {
480
- ...args,
402
+ return {
403
+ ...record,
481
404
  question,
482
405
  context: context ? `${context}\n\n${fallbackContext}` : fallbackContext
483
- } : {
484
- question,
485
- context: fallbackContext
486
406
  };
487
407
  }
488
408
  return args;
489
409
  }
490
410
  if (!question) return args;
491
- return args && typeof args === "object" && !Array.isArray(args) ? {
492
- ...args,
411
+ return {
412
+ ...asOptionalRecord(args),
493
413
  question
494
- } : { question };
414
+ };
495
415
  }
496
416
  function buildForcedConsultSpeechPrompt(result) {
497
- const trimmed = result.trim();
498
417
  return [
499
418
  "Internal OpenClaw consult result is ready.",
500
419
  "Do not call tools for this internal result.",
501
420
  "Speak the following answer to the caller now, briefly and naturally:",
502
- trimmed.length <= FORCED_CONSULT_RESULT_MAX_CHARS ? trimmed : `${truncateUtf16Safe(trimmed, 1784).trimEnd()} [truncated]`
421
+ result
503
422
  ].join("\n");
504
423
  }
505
424
  async function waitForNativeConsult(state) {
@@ -530,15 +449,6 @@ function appendRecentTalkEventMetadata(metadata, event) {
530
449
  }].slice(-12)
531
450
  };
532
451
  }
533
- function rejectRealtimeUpgrade(socket, status) {
534
- const reason = status === 401 ? "Unauthorized" : "Service Unavailable";
535
- try {
536
- socket.end(`HTTP/1.1 ${status} ${reason}\r\nConnection: close\r\n\r\n`, () => socket.destroy());
537
- } catch (error) {
538
- socket.destroy();
539
- throw error;
540
- }
541
- }
542
452
  var RealtimeCallHandler = class {
543
453
  constructor(config, manager, resolveCallRegistration, servePath, streamDisconnectLifecycle, coreConfig) {
544
454
  this.config = config;
@@ -605,16 +515,16 @@ var RealtimeCallHandler = class {
605
515
  handleWebSocketUpgrade(request, socket, head) {
606
516
  socket.once("error", () => socket.destroy());
607
517
  if (this.closing) {
608
- rejectRealtimeUpgrade(socket, 503);
518
+ rejectWebSocketUpgrade(socket, { status: 503 });
609
519
  return;
610
520
  }
611
521
  const token = new URL(request.url ?? "/", "wss://localhost").pathname.split("/").pop() ?? null;
612
522
  const callerMeta = token ? this.consumeStreamToken(token) : null;
613
523
  if (!callerMeta) {
614
- rejectRealtimeUpgrade(socket, 401);
524
+ rejectWebSocketUpgrade(socket, { status: 401 });
615
525
  return;
616
526
  }
617
- const adapter = (callerMeta.providerName ?? "twilio") === "telnyx" ? new TelnyxStreamFrameAdapter() : new TwilioStreamFrameAdapter();
527
+ const adapter = new StreamFrameAdapter(callerMeta.providerName ?? "twilio");
618
528
  new WebSocketServer({
619
529
  noServer: true,
620
530
  maxPayload: MAX_REALTIME_MESSAGE_BYTES
@@ -775,19 +685,13 @@ var RealtimeCallHandler = class {
775
685
  }
776
686
  }
777
687
  issueStreamSession(request = {}) {
778
- const token = this.issueStreamToken({
688
+ const meta = {
779
689
  providerName: request.providerName ?? "twilio",
780
690
  callId: request.callId,
781
691
  from: request.from,
782
692
  to: request.to,
783
693
  direction: request.direction
784
- });
785
- return {
786
- token,
787
- streamUrl: `wss://${this.publicOrigin || DEFAULT_HOST}${this.getStreamPathPattern()}/${token}`
788
694
  };
789
- }
790
- issueStreamToken(meta = {}) {
791
695
  const token = randomUUID();
792
696
  const expiry = resolveExpiresAtMsFromDurationMs(STREAM_TOKEN_TTL_MS, { nowMs: Date.now() });
793
697
  if (expiry !== void 0) {
@@ -807,20 +711,18 @@ var RealtimeCallHandler = class {
807
711
  console.warn(`[voice-call] Realtime stream WebSocket never connected within ${STREAM_TOKEN_TTL_MS / 1e3}s${call}${participants} — the provider could not reach wss://${host}${streamPathPattern}/<token>. Verify the stream path is exposed (tailscale serve/funnel --set-path).`);
808
712
  }, STREAM_TOKEN_TTL_MS).unref?.();
809
713
  }
810
- return token;
714
+ return {
715
+ token,
716
+ streamUrl: `wss://${this.publicOrigin || DEFAULT_HOST}${this.getStreamPathPattern()}/${token}`
717
+ };
811
718
  }
812
719
  consumeStreamToken(token) {
813
720
  const entry = this.pendingStreamTokens.get(token);
814
721
  if (!entry) return null;
815
722
  this.pendingStreamTokens.delete(token);
816
723
  if (!isFutureDateTimestampMs(entry.expiry)) return null;
817
- return {
818
- from: entry.from,
819
- to: entry.to,
820
- direction: entry.direction,
821
- providerName: entry.providerName,
822
- callId: entry.callId
823
- };
724
+ const { expiry: _expiry, ...request } = entry;
725
+ return request;
824
726
  }
825
727
  async handleCall(streamSid, callSid, ws, callerMeta, adapter) {
826
728
  const preparedCall = await this.prepareCallInManager(callSid, callerMeta);
@@ -992,11 +894,7 @@ var RealtimeCallHandler = class {
992
894
  const audioPacer = new RealtimeAudioPacer({
993
895
  onPlaybackReset: () => pendingMarkAcks.clear(),
994
896
  send: sendString,
995
- serializer: {
996
- media: (payload) => adapter.serializeMedia(payload),
997
- clear: () => adapter.serializeClear(),
998
- mark: (name) => adapter.serializeMark(name)
999
- },
897
+ serializer: adapter,
1000
898
  onBackpressure: () => {
1001
899
  console.warn(`[voice-call] realtime paced audio backpressure callId=${callId} providerCallId=${callSid}`);
1002
900
  if (ws.readyState === WebSocket.OPEN) ws.close(1013, "Backpressure: paced audio queue exceeded");
@@ -1509,7 +1407,7 @@ var RealtimeCallHandler = class {
1509
1407
  const state = this.getUserTranscriptState(callId, owner);
1510
1408
  if (!state) return;
1511
1409
  if (state.recentFinalTimer) clearTimeout(state.recentFinalTimer);
1512
- if (this.userTranscriptStatesByCallId.get(callId) === state) this.userTranscriptStatesByCallId.delete(callId);
1410
+ this.userTranscriptStatesByCallId.delete(callId);
1513
1411
  }
1514
1412
  cancelNativeConsult(callId, owner) {
1515
1413
  const state = this.nativeConsultsInFlightByCallId.get(callId);
@@ -1525,24 +1423,19 @@ var RealtimeCallHandler = class {
1525
1423
  state.cancel();
1526
1424
  this.forcedConsultsByCallId.delete(callId);
1527
1425
  }
1528
- resetConsultSession(callId, owner) {
1529
- const session = this.consultSessionsByCallId.get(callId);
1530
- if (!session || session.owner !== owner) return false;
1531
- session.coordinator.clearPending();
1532
- this.cancelForcedConsult(callId, owner);
1533
- this.cancelNativeConsult(callId, owner);
1534
- return true;
1535
- }
1536
1426
  resetConsultSessionForContinuity(callId, owner) {
1537
1427
  const session = this.consultSessionsByCallId.get(callId);
1538
- if (!session || session.owner !== owner) return false;
1428
+ if (!session || session.owner !== owner) return;
1539
1429
  this.cancelForcedConsult(callId, owner);
1540
1430
  this.cancelNativeConsult(callId, owner);
1541
1431
  session.coordinator.clear();
1542
- return true;
1543
1432
  }
1544
1433
  cancelConsultSession(callId, owner) {
1545
- if (!owner || !this.resetConsultSession(callId, owner)) return;
1434
+ const session = this.consultSessionsByCallId.get(callId);
1435
+ if (!session || session.owner !== owner) return;
1436
+ session.coordinator.clearPending();
1437
+ this.cancelForcedConsult(callId, owner);
1438
+ this.cancelNativeConsult(callId, owner);
1546
1439
  this.consultSessionsByCallId.delete(callId);
1547
1440
  }
1548
1441
  isActiveBridgeOwner(callId, owner) {
@@ -1759,7 +1652,8 @@ var RealtimeCallHandler = class {
1759
1652
  const hasResultError = (result) => {
1760
1653
  return result !== null && typeof result === "object" && !Array.isArray(result) && "error" in result;
1761
1654
  };
1762
- const emitFinalToolEvent = (result) => {
1655
+ const submitFinalToolResult = async (result) => {
1656
+ if (!delegation) await bridge.submitToolResult(bridgeCallId, result);
1763
1657
  harness.emit({
1764
1658
  type: hasResultError(result) ? "tool.error" : "tool.result",
1765
1659
  turnId,
@@ -1770,12 +1664,22 @@ var RealtimeCallHandler = class {
1770
1664
  },
1771
1665
  final: true
1772
1666
  });
1773
- };
1774
- const submitFinalToolResult = async (result) => {
1775
- if (!delegation) await bridge.submitToolResult(bridgeCallId, result);
1776
- emitFinalToolEvent(result);
1777
1667
  return result;
1778
1668
  };
1669
+ const invokeHandler = async (handlerArgs, context) => {
1670
+ console.log(`[voice-call] realtime tool call executing callId=${callId} tool=${name} hasHandler=${Boolean(handler)}`);
1671
+ try {
1672
+ return handler ? await handler(handlerArgs, callId, context) : { error: `Tool "${name}" not available` };
1673
+ } catch (error) {
1674
+ return buildRealtimeVoiceAgentErrorProviderResult(error);
1675
+ }
1676
+ };
1677
+ const logResult = (result) => {
1678
+ const failed = hasResultError(result);
1679
+ const error = failed ? formatErrorMessage(result.error ?? "unknown") : void 0;
1680
+ console.log(`[voice-call] realtime tool call completed callId=${callId} tool=${name} status=${failed ? "error" : "ok"} elapsedMs=${Date.now() - startedAt}${error ? ` error=${error}` : ""}`);
1681
+ return failed;
1682
+ };
1779
1683
  const submitWorkingResponse = async () => {
1780
1684
  if (!delegation && handler && name === REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME && bridge.bridge.supportsToolResultContinuation && !this.config.fastContext.enabled) {
1781
1685
  await bridge.submitToolResult(bridgeCallId, buildRealtimeVoiceAgentConsultWorkingResponse("caller"), { willContinue: true });
@@ -1826,17 +1730,12 @@ var RealtimeCallHandler = class {
1826
1730
  return await submitFinalToolResult(outcome.result);
1827
1731
  }
1828
1732
  const abortController = new AbortController();
1829
- let releaseCancellation = () => {};
1830
- const cancellation = new Promise((resolve) => {
1831
- releaseCancellation = resolve;
1832
- });
1833
- let completeConsult = (_result) => {};
1733
+ const { promise: cancellation, resolve: releaseCancellation } = createDeferred();
1734
+ const { promise: consult, resolve: completeConsult } = createDeferred();
1834
1735
  const state = {
1835
1736
  owner: bridge,
1836
1737
  startedAt,
1837
- promise: new Promise((resolve) => {
1838
- completeConsult = resolve;
1839
- }),
1738
+ promise: consult,
1840
1739
  cancellation,
1841
1740
  get cancelled() {
1842
1741
  return abortController.signal.aborted;
@@ -1861,8 +1760,7 @@ var RealtimeCallHandler = class {
1861
1760
  };
1862
1761
  state.partialUserTranscript = context.partialUserTranscript;
1863
1762
  const handlerArgs = withFallbackConsultQuestion(args, context.partialUserTranscript);
1864
- console.log(`[voice-call] realtime tool call executing callId=${callId} tool=${name} hasHandler=${Boolean(handler)}`);
1865
- return !handler ? { error: `Tool "${name}" not available` } : await handler(handlerArgs, callId, context);
1763
+ return await invokeHandler(handlerArgs, context);
1866
1764
  } catch (error) {
1867
1765
  return buildRealtimeVoiceAgentErrorProviderResult(error);
1868
1766
  }
@@ -1871,9 +1769,7 @@ var RealtimeCallHandler = class {
1871
1769
  const outcome = await waitForNativeConsult(state);
1872
1770
  if (outcome.kind === "cancelled") return;
1873
1771
  const result = outcome.result;
1874
- const failed = hasResultError(result);
1875
- const error = failed ? formatErrorMessage(result.error ?? "unknown") : void 0;
1876
- console.log(`[voice-call] realtime tool call completed callId=${callId} tool=${name} status=${failed ? "error" : "ok"} elapsedMs=${Date.now() - startedAt}${error ? ` error=${error}` : ""}`);
1772
+ const failed = logResult(result);
1877
1773
  await submitFinalToolResult(result);
1878
1774
  if (!failed) this.consumePartialUserTranscript(callId, userTranscriptOwner, state.partialUserTranscript);
1879
1775
  return result;
@@ -1882,16 +1778,8 @@ var RealtimeCallHandler = class {
1882
1778
  if (this.nativeConsultsInFlightByCallId.get(callId) === state) this.nativeConsultsInFlightByCallId.delete(callId);
1883
1779
  }
1884
1780
  }
1885
- console.log(`[voice-call] realtime tool call executing callId=${callId} tool=${name} hasHandler=${Boolean(handler)}`);
1886
- const context = { partialUserTranscript: this.resolveUserTranscriptContext(callId, userTranscriptOwner) };
1887
- let result;
1888
- try {
1889
- result = !handler ? { error: `Tool "${name}" not available` } : await handler(args, callId, context);
1890
- } catch (error) {
1891
- result = buildRealtimeVoiceAgentErrorProviderResult(error);
1892
- }
1893
- const error = hasResultError(result) ? formatErrorMessage(result.error ?? "unknown") : void 0;
1894
- console.log(`[voice-call] realtime tool call completed callId=${callId} tool=${name} status=${error === void 0 ? "ok" : "error"} elapsedMs=${Date.now() - startedAt}${error ? ` error=${error}` : ""}`);
1781
+ const result = await invokeHandler(args, { partialUserTranscript: this.resolveUserTranscriptContext(callId, userTranscriptOwner) });
1782
+ logResult(result);
1895
1783
  return await submitFinalToolResult(result);
1896
1784
  }
1897
1785
  };
@@ -1,6 +1,6 @@
1
- import { c as resolveVoiceCallSessionKey } from "./config-Ds7xoZbI.mjs";
2
- import { _ as resolveCallAgentId, g as resolveVoiceResponseModel } from "./runtime-entry-BNOQRXii.mjs";
3
- import { filterStringEntries, isRecord, normalizeLowercaseStringOrEmpty, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
1
+ import { c as resolveVoiceCallSessionKey } from "./config-CX9J-053.mjs";
2
+ import { m as resolveCallAgentId, p as resolveVoiceResponseModel } from "./runtime-entry-CbSKxSKk.mjs";
3
+ import { normalizeLowercaseStringOrEmpty, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
4
4
  import { resolveAgentConfig } from "openclaw/plugin-sdk/agent-scope-runtime";
5
5
  import { isValidAgentHarnessSessionStoreEntry } from "openclaw/plugin-sdk/session-store-runtime";
6
6
  import crypto from "node:crypto";
@@ -12,15 +12,6 @@ import { ModelSelectionLockedError, applyModelOverrideWithAuthProfileCompatibili
12
12
  * Voice call response generator - uses the embedded OpenClaw agent for tool support.
13
13
  * Routes voice responses through the same agent infrastructure as messaging.
14
14
  */
15
- function readExplicitToolsAllow(value) {
16
- if (!isRecord(value)) return;
17
- const allow = value.allow;
18
- if (!Array.isArray(allow)) return;
19
- return filterStringEntries(allow);
20
- }
21
- function resolveVoiceAgentToolsAllow(config, agentId) {
22
- return readExplicitToolsAllow(resolveAgentConfig(config, agentId)?.tools);
23
- }
24
15
  const VOICE_SPOKEN_OUTPUT_CONTRACT = [
25
16
  "Output format requirements:",
26
17
  "- Return only valid JSON in this exact shape: {\"spoken\":\"...\"}",
@@ -93,12 +84,7 @@ function tryParseSpokenJson(text) {
93
84
  }
94
85
  function isLikelyMetaReasoningParagraph(paragraph) {
95
86
  const lower = normalizeLowercaseStringOrEmpty(paragraph);
96
- if (!lower) return false;
97
- if (lower.startsWith("thinking process")) return true;
98
- if (lower.startsWith("reasoning:") || lower.startsWith("analysis:")) return true;
99
- if (lower.startsWith("the user ") && (lower.includes("i should") || lower.includes("i need to") || lower.includes("i will"))) return true;
100
- if (lower.includes("this is a natural continuation of the conversation") || lower.includes("keep the conversation flowing")) return true;
101
- return false;
87
+ return lower.startsWith("thinking process") || lower.startsWith("reasoning:") || lower.startsWith("analysis:") || lower.startsWith("the user ") && (lower.includes("i should") || lower.includes("i need to") || lower.includes("i will")) || lower.includes("this is a natural continuation of the conversation") || lower.includes("keep the conversation flowing");
102
88
  }
103
89
  function sanitizePlainSpokenText(text) {
104
90
  const withoutCodeFences = text.replace(/```[\s\S]*?```/g, " ").trim();
@@ -146,13 +132,8 @@ function resolveVoiceSandboxSessionKey(agentId, sessionKey) {
146
132
  */
147
133
  async function generateVoiceResponse(params) {
148
134
  const { voiceConfig, callId, sessionKey, from, senderIsOwner, transcript, userMessage, coreConfig, agentRuntime, onEarlyText } = params;
149
- if (!coreConfig) return {
150
- text: null,
151
- deliveredEarly: false,
152
- error: "Core config unavailable for voice response"
153
- };
154
135
  const cfg = coreConfig;
155
- const agentId = resolveCallAgentId({ agentId: params.agentId }, voiceConfig);
136
+ const agentId = resolveCallAgentId(params);
156
137
  const resolvedSessionKey = resolveVoiceCallSessionKey({
157
138
  config: {
158
139
  ...voiceConfig,
@@ -163,7 +144,7 @@ async function generateVoiceResponse(params) {
163
144
  explicitSessionKey: sessionKey,
164
145
  coreSession: coreConfig.session
165
146
  });
166
- const toolsAllow = resolveVoiceAgentToolsAllow(cfg, agentId);
147
+ const toolsAllow = resolveAgentConfig(cfg, agentId)?.tools?.allow;
167
148
  const storePath = agentRuntime.session.resolveStorePath(cfg.session?.store, { agentId });
168
149
  try {
169
150
  return await agentRuntime.session.runWithWorkAdmission({