@openclaw/voice-call 2026.7.2-beta.2 → 2026.7.2-beta.4

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 (29) hide show
  1. package/README.md +1 -1
  2. package/dist/doctor-contract-api.js +1 -0
  3. package/dist/{guarded-json-api-BKOgUU1q.js → guarded-json-api-Dxhe25Zi.js} +1 -2
  4. package/dist/index.js +10 -6
  5. package/dist/{plivo-ByAIoPFp.js → plivo-B-KvPVji.js} +2 -3
  6. package/dist/{realtime-handler-DJk-eYyf.js → realtime-handler-hIVPEZxN.js} +95 -175
  7. package/dist/{response-generator-Cdj8EuLg.js → response-generator-z6M9229t.js} +1 -1
  8. package/dist/{runtime-entry-DSgyqIwE.js → runtime-entry-CQtitvRd.js} +89 -152
  9. package/dist/runtime-entry.js +1 -1
  10. package/dist/{telnyx-DMbLYwj4.js → telnyx-BG56lL7z.js} +1 -1
  11. package/dist/{twilio-BTKKlkLT.js → twilio-B3FQ_P9x.js} +9 -5
  12. package/node_modules/typebox/build/compile/validator.d.mts +1 -4
  13. package/node_modules/typebox/build/guard/guard.d.mts +1 -1
  14. package/node_modules/typebox/build/guard/guard.mjs +5 -2
  15. package/node_modules/typebox/build/guard/string.mjs +16 -4
  16. package/node_modules/typebox/build/schema/engine/_functions.mjs +34 -15
  17. package/node_modules/typebox/build/type/action/readonly_object.d.mts +5 -0
  18. package/node_modules/typebox/build/type/action/readonly_object.mjs +8 -0
  19. package/node_modules/typebox/build/typebox.d.mts +1 -1
  20. package/node_modules/typebox/build/typebox.mjs +1 -1
  21. package/node_modules/typebox/package.json +1 -1
  22. package/node_modules/typebox/readme.md +29 -32
  23. package/node_modules/ws/lib/receiver.js +15 -32
  24. package/node_modules/ws/lib/websocket-server.js +4 -4
  25. package/node_modules/ws/lib/websocket.js +4 -4
  26. package/node_modules/ws/package.json +5 -1
  27. package/npm-shrinkwrap.json +11 -11
  28. package/openclaw.plugin.json +1 -15
  29. package/package.json +6 -6
package/README.md CHANGED
@@ -115,7 +115,7 @@ See the plugin docs for recommended ranges and production examples:
115
115
 
116
116
  ## TTS for calls
117
117
 
118
- Voice Call uses the core `messages.tts` configuration for
118
+ Voice Call uses the core `tts` configuration for
119
119
  streaming speech on calls. Override examples and provider caveats live here:
120
120
  `https://docs.openclaw.ai/plugins/voice-call#tts-for-calls`
121
121
 
@@ -63,6 +63,7 @@ function describeVoiceCallSchemaMigration(migration) {
63
63
  case "agent-databases-composite-primary-key": return "agent database registry primary key -> agent_id,path";
64
64
  case "audit-events-v2": return "audit event ledger -> versioned message lifecycle schema";
65
65
  case "operator-approvals-system-agent": return "operator approvals -> OpenClaw system changes";
66
+ case "session-watch-cursor-provenance-v4": return "session watch cursors -> provenance column";
66
67
  case "strict-tables-v3": return "tables -> SQLite STRICT typing";
67
68
  }
68
69
  return migration.kind;
@@ -1,6 +1,6 @@
1
1
  import { fetchWithSsrFGuard } from "./runtime-api.js";
2
2
  import "./api.js";
3
- import { n as normalizeProxyIp, o as getHeader } from "./runtime-entry-DSgyqIwE.js";
3
+ import { n as normalizeProxyIp, s as getHeader } from "./runtime-entry-CQtitvRd.js";
4
4
  import { n as markWebhookReplay, t as createWebhookReplayCache } from "./webhook-replay-aaP79dI8.js";
5
5
  import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
6
6
  import { isLoopbackHost } from "openclaw/plugin-sdk/gateway-runtime";
@@ -462,7 +462,6 @@ function verifyPlivoWebhook(ctx, authToken, options) {
462
462
  if (options?.publicUrl) try {
463
463
  const req = new URL(reconstructed);
464
464
  const base = new URL(options.publicUrl);
465
- base.pathname = req.pathname;
466
465
  base.search = req.search;
467
466
  verificationUrl = base.toString();
468
467
  } catch {
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { definePluginEntry, sleep } from "./runtime-api.js";
2
2
  import "./api.js";
3
- import { c as cleanupTailscaleExposureRoute, h as resolveUserPath, l as getTailscaleSelfInfo, s as resolveWebhookExposureStatus, t as createVoiceCallRuntime, u as setupTailscaleExposureRoute, v as VoiceCallConfigSchema, x as validateProviderConfig, y as resolveVoiceCallConfig } from "./runtime-entry-DSgyqIwE.js";
3
+ import { S as validateProviderConfig, b as resolveVoiceCallConfig, c as resolveWebhookExposureStatus, d as setupTailscaleExposureRoute, g as resolveUserPath, l as cleanupTailscaleExposureRoute, t as createVoiceCallRuntime, u as getTailscaleSelfInfo, y as VoiceCallConfigSchema } from "./runtime-entry-CQtitvRd.js";
4
4
  import { g as setVoiceCallStateRuntime, t as resolveDefaultVoiceCallStoreDir, u as getCallHistoryFromStore } from "./store-path-nYL-yM0S.js";
5
5
  import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
6
6
  import { ErrorCodes, callGatewayFromCli, errorShape } from "openclaw/plugin-sdk/gateway-runtime";
@@ -87,13 +87,17 @@ function readCompletedContinueResult(payload) {
87
87
  }
88
88
  async function pollVoiceCallContinueGateway(params) {
89
89
  const deadlineMs = resolveVoiceCallDeadlineMs(params.timeoutMs);
90
- while (Date.now() <= deadlineMs) {
91
- const gateway = await callVoiceCallGateway("voicecall.continue.result", { operationId: params.operationId }, { timeoutMs: VOICE_CALL_GATEWAY_DEFAULT_TIMEOUT_MS });
90
+ for (;;) {
91
+ const remainingMs = deadlineMs - Date.now();
92
+ if (remainingMs <= 0) break;
93
+ const gateway = await callVoiceCallGateway("voicecall.continue.result", { operationId: params.operationId }, { timeoutMs: Math.min(VOICE_CALL_GATEWAY_DEFAULT_TIMEOUT_MS, remainingMs) });
92
94
  if (!gateway.ok) throw new Error(`gateway unavailable while waiting for voicecall continue result: ${formatErrorMessage(gateway.error)}`);
93
95
  const result = readCompletedContinueResult(gateway.payload);
94
96
  if (result.status === "completed") return result.result;
95
97
  if (result.status === "failed") throw new Error(result.error);
96
- await sleep(Math.min(VOICE_CALL_GATEWAY_POLL_INTERVAL_MS, Math.max(1, deadlineMs - Date.now())));
98
+ const sleepMs = Math.min(VOICE_CALL_GATEWAY_POLL_INTERVAL_MS, deadlineMs - Date.now());
99
+ if (sleepMs <= 0) break;
100
+ await sleep(sleepMs);
97
101
  }
98
102
  throw new Error("voicecall continue timed out waiting for gateway operation");
99
103
  }
@@ -533,7 +537,7 @@ const VOICE_CALL_CONTINUE_OPERATION_CLEANUP_MS = 300 * 1e3;
533
537
  function createVoiceCallContinueOperationStore(params) {
534
538
  const operations = /* @__PURE__ */ new Map();
535
539
  const resolvePollTimeoutMs = (rt) => {
536
- const ttsTimeoutMs = rt.config.tts?.timeoutMs ?? params.config.tts?.timeoutMs ?? params.coreConfig.messages?.tts?.timeoutMs ?? 8e3;
540
+ const ttsTimeoutMs = rt.config.tts?.timeoutMs ?? params.config.tts?.timeoutMs ?? params.coreConfig.tts?.timeoutMs ?? 8e3;
537
541
  return resolveTimerTimeoutMs((rt.config.transcriptTimeoutMs ?? params.config.transcriptTimeoutMs) + ttsTimeoutMs + VOICE_CALL_CONTINUE_OPERATION_BUFFER_MS, VOICE_CALL_CONTINUE_OPERATION_BUFFER_MS);
538
542
  };
539
543
  const scheduleCleanup = (operationId) => {
@@ -822,7 +826,7 @@ const voiceCallConfigSchema = {
822
826
  },
823
827
  "tts.provider": {
824
828
  label: "TTS Provider Override",
825
- help: "Deep-merges with messages.tts (Microsoft is ignored for calls).",
829
+ help: "Deep-merges with tts (Microsoft is ignored for calls).",
826
830
  advanced: true
827
831
  },
828
832
  "tts.providers": {
@@ -1,5 +1,5 @@
1
- import { g as escapeXml, o as getHeader } from "./runtime-entry-DSgyqIwE.js";
2
- import { a as reconstructWebhookUrl, o as verifyPlivoWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-BKOgUU1q.js";
1
+ import { _ as escapeXml, s as getHeader } from "./runtime-entry-CQtitvRd.js";
2
+ import { a as reconstructWebhookUrl, o as verifyPlivoWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-Dxhe25Zi.js";
3
3
  import { normalizeLowercaseStringOrEmpty, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
4
4
  import crypto from "node:crypto";
5
5
  //#region extensions/voice-call/src/providers/plivo.ts
@@ -371,7 +371,6 @@ var PlivoProvider = class PlivoProvider {
371
371
  try {
372
372
  if (this.options.publicUrl) {
373
373
  const base = new URL(this.options.publicUrl);
374
- base.pathname = new URL(ctx.url).pathname;
375
374
  return `${base.origin}${base.pathname}`;
376
375
  }
377
376
  const u = new URL(reconstructWebhookUrl(ctx, {
@@ -1,7 +1,8 @@
1
+ import { o as canonicalizeVoiceCallMediaBase64 } from "./runtime-entry-CQtitvRd.js";
1
2
  import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
2
3
  import { isFutureDateTimestampMs, resolveExpiresAtMsFromDurationMs } from "openclaw/plugin-sdk/number-runtime";
3
4
  import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env";
4
- import { REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME, buildRealtimeVoiceAgentConsultWorkingResponse, createRealtimeVoiceBridgeSession, createRealtimeVoiceForcedConsultCoordinator, createTalkSessionController, readRealtimeVoiceConsultQuestion, readSpeakableRealtimeVoiceToolResult, recordTalkObservabilityEvent } from "openclaw/plugin-sdk/realtime-voice";
5
+ import { REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME, buildRealtimeVoiceAgentConsultWorkingResponse, calculateMulawRms, createRealtimeVoiceSessionHarness, createSpeechThresholdGate, readRealtimeVoiceConsultQuestion, readSpeakableRealtimeVoiceToolResult } from "openclaw/plugin-sdk/realtime-voice";
5
6
  import { normalizeWebhookPath } from "openclaw/plugin-sdk/webhook-ingress";
6
7
  import { randomUUID } from "node:crypto";
7
8
  import { sliceUtf16Safe, truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
@@ -11,13 +12,7 @@ import WebSocket$1, { WebSocketServer } from "ws";
11
12
  const TELEPHONY_SAMPLE_RATE = 8e3;
12
13
  const TELEPHONY_CHUNK_BYTES = 160;
13
14
  const TELEPHONY_CHUNK_MS = 20;
14
- const DEFAULT_SPEECH_RMS_THRESHOLD = .035;
15
- const DEFAULT_REQUIRED_LOUD_CHUNKS = 4;
16
- const DEFAULT_REQUIRED_QUIET_CHUNKS = 12;
17
15
  const DEFAULT_MAX_QUEUED_AUDIO_BYTES = TELEPHONY_SAMPLE_RATE * 120;
18
- const PCM16_MAX_AMPLITUDE = 32768;
19
- const MULAW_LINEAR_SAMPLES = /* @__PURE__ */ new Int16Array(256);
20
- for (let i = 0; i < MULAW_LINEAR_SAMPLES.length; i += 1) MULAW_LINEAR_SAMPLES[i] = decodeMulawSample(i);
21
16
  /** Paces outgoing mulaw audio frames at telephony cadence. */
22
17
  var RealtimeAudioPacer = class {
23
18
  constructor(params) {
@@ -112,52 +107,6 @@ var RealtimeAudioPacer = class {
112
107
  if (this.queue.length > 0) this.timer = setTimeout(() => this.pump(), delayMs);
113
108
  }
114
109
  };
115
- /** Calculate normalized RMS from mulaw bytes. */
116
- function calculateMulawRms(muLaw) {
117
- if (muLaw.length === 0) return 0;
118
- let sum = 0;
119
- for (const sample of muLaw) {
120
- const normalized = (MULAW_LINEAR_SAMPLES[sample] ?? 0) / PCM16_MAX_AMPLITUDE;
121
- sum += normalized * normalized;
122
- }
123
- return Math.sqrt(sum / muLaw.length);
124
- }
125
- /** Detect likely speech start from consecutive loud mulaw chunks. */
126
- var RealtimeMulawSpeechStartDetector = class {
127
- constructor(params = {}) {
128
- this.params = params;
129
- this.loudChunks = 0;
130
- this.quietChunks = DEFAULT_REQUIRED_QUIET_CHUNKS;
131
- this.speaking = false;
132
- }
133
- /** Accept one mulaw chunk and return true only on transition into speaking. */
134
- accept(muLaw) {
135
- if (calculateMulawRms(muLaw) >= (this.params.rmsThreshold ?? DEFAULT_SPEECH_RMS_THRESHOLD)) {
136
- this.quietChunks = 0;
137
- this.loudChunks += 1;
138
- const requiredLoudChunks = this.params.requiredLoudChunks ?? DEFAULT_REQUIRED_LOUD_CHUNKS;
139
- if (!this.speaking && this.loudChunks >= requiredLoudChunks) {
140
- this.speaking = true;
141
- return true;
142
- }
143
- return false;
144
- }
145
- this.loudChunks = 0;
146
- this.quietChunks += 1;
147
- const requiredQuietChunks = this.params.requiredQuietChunks ?? DEFAULT_REQUIRED_QUIET_CHUNKS;
148
- if (this.quietChunks >= requiredQuietChunks) this.speaking = false;
149
- return false;
150
- }
151
- };
152
- /** Decode one G.711 mulaw byte to a linear PCM sample. */
153
- function decodeMulawSample(value) {
154
- const muLaw = ~value & 255;
155
- const sign = muLaw & 128;
156
- const exponent = muLaw >> 4 & 7;
157
- let sample = ((muLaw & 15) << 3) + 132 << exponent;
158
- sample -= 132;
159
- return sign ? -sample : sample;
160
- }
161
110
  //#endregion
162
111
  //#region extensions/voice-call/src/webhook/stream-frame-adapter.ts
163
112
  /** Parse numeric timestamps sent as numbers or integer strings. */
@@ -181,22 +130,15 @@ function readRecordField(record, field) {
181
130
  const value = record[field];
182
131
  return typeof value === "object" && value !== null ? value : void 0;
183
132
  }
184
- /** Normalize base64/base64url padding differences for validation. */
185
- function normalizeBase64ForCompare(value) {
186
- return value.replace(/=+$/u, "").replace(/-/gu, "+").replace(/_/gu, "/");
187
- }
188
- /** Return true when a payload round-trips as base64. */
189
- function isValidBase64Payload(value) {
190
- return normalizeBase64ForCompare(Buffer.from(value, "base64").toString("base64")) === normalizeBase64ForCompare(value);
191
- }
192
133
  /** Parse a common provider media frame. */
193
134
  function parseMediaFrame(msg) {
194
135
  const mediaData = readRecordField(msg, "media");
195
136
  const payload = typeof mediaData?.payload === "string" ? mediaData.payload : void 0;
196
- if (!payload || !isValidBase64Payload(payload)) return { kind: "ignored" };
137
+ const canonicalPayload = payload ? canonicalizeVoiceCallMediaBase64(payload) : void 0;
138
+ if (!canonicalPayload) return { kind: "ignored" };
197
139
  return {
198
140
  kind: "media",
199
- payloadBase64: payload,
141
+ payloadBase64: canonicalPayload,
200
142
  timestampMs: parseTimestampMs(mediaData?.timestamp),
201
143
  track: typeof mediaData?.track === "string" ? mediaData.track : void 0
202
144
  };
@@ -475,7 +417,6 @@ var RealtimeCallHandler = class {
475
417
  this.partialUserTranscriptUpdatedAtByCallId = /* @__PURE__ */ new Map();
476
418
  this.recentFinalUserTranscriptsByCallId = /* @__PURE__ */ new Map();
477
419
  this.recentFinalUserTranscriptTimersByCallId = /* @__PURE__ */ new Map();
478
- this.forcedConsultCoordinatorsByCallId = /* @__PURE__ */ new Map();
479
420
  this.forcedConsultsByCallId = /* @__PURE__ */ new Map();
480
421
  this.nativeConsultsInFlightByCallId = /* @__PURE__ */ new Map();
481
422
  this.publicOrigin = null;
@@ -661,56 +602,51 @@ var RealtimeCallHandler = class {
661
602
  }
662
603
  const { callId, instructions, initialGreetingInstructions } = registration;
663
604
  const callRecord = this.manager.getCallByProviderCallId(callSid);
664
- const talk = createTalkSessionController({
665
- sessionId: `voice-call:${callId}:realtime`,
666
- mode: "realtime",
667
- transport: "gateway-relay",
668
- brain: "agent-consult",
669
- provider: this.realtimeProvider.id
670
- }, { onEvent: recordTalkObservabilityEvent });
671
- const rememberTalkEvent = (event) => {
672
- if (event) appendRecentTalkEventMetadata(callRecord, event);
673
- return event;
674
- };
675
- const emitTalkEvent = (input) => {
676
- return rememberTalkEvent(talk.emit(input));
677
- };
678
- const ensureTalkTurn = () => {
679
- const turn = talk.ensureTurn({ payload: {
680
- callId,
681
- providerCallId: callSid
682
- } });
683
- rememberTalkEvent(turn.event);
684
- return turn.turnId;
685
- };
686
- const endTalkTurn = (reason = "completed") => {
687
- const ended = talk.endTurn({ payload: {
688
- callId,
689
- providerCallId: callSid,
690
- reason
691
- } });
692
- if (ended.ok) rememberTalkEvent(ended.event);
693
- };
694
- const finishOutputAudio = (reason) => {
695
- rememberTalkEvent(talk.finishOutputAudio({ payload: {
696
- callId,
697
- providerCallId: callSid,
698
- reason
699
- } }));
700
- };
605
+ const harness = createRealtimeVoiceSessionHarness({
606
+ talk: {
607
+ sessionId: `voice-call:${callId}:realtime`,
608
+ mode: "realtime",
609
+ transport: "gateway-relay",
610
+ brain: "agent-consult",
611
+ provider: this.realtimeProvider.id
612
+ },
613
+ talkPayloads: {
614
+ turnStarted: () => ({
615
+ callId,
616
+ providerCallId: callSid
617
+ }),
618
+ turnEnded: (reason) => ({
619
+ callId,
620
+ providerCallId: callSid,
621
+ reason
622
+ }),
623
+ inputAudioDelta: (audio) => ({ byteLength: audio.byteLength }),
624
+ outputAudioStarted: () => ({
625
+ callId,
626
+ providerCallId: callSid
627
+ }),
628
+ outputAudioDelta: (audio) => ({ byteLength: audio.byteLength }),
629
+ outputAudioDone: (reason) => ({
630
+ callId,
631
+ providerCallId: callSid,
632
+ reason
633
+ })
634
+ },
635
+ onTalkEvent: (event) => appendRecentTalkEventMetadata(callRecord, event)
636
+ });
701
637
  const providerHandlesInputAudioBargeIn = this.realtimeProvider.capabilities?.handlesInputAudioBargeIn === true;
702
638
  const cancelOutputAudioForBargeIn = (source, interruptProvider, clearedAudioBytes = 0) => {
703
- const outputAudioActive = talk.outputAudioActive;
639
+ const outputAudioActive = harness.talk.outputAudioActive;
704
640
  const pendingTelephonyAudio = audioPacer.hasPendingAudio();
705
641
  if (source === "provider" && !outputAudioActive && !pendingTelephonyAudio && clearedAudioBytes === 0) return;
706
- const interruptedTurnId = talk.activeTurnId;
642
+ const interruptedTurnId = harness.talk.activeTurnId;
707
643
  interruptProvider?.(outputAudioActive || pendingTelephonyAudio);
708
644
  const clearedBytes = clearedAudioBytes + (source === "local" || pendingTelephonyAudio ? audioPacer.clearAudio() : 0);
709
645
  console.log(`[voice-call] realtime outbound audio cleared by ${source} barge-in callId=${callId} providerCallId=${callSid} queuedBytes=${clearedBytes}`);
710
646
  if (!outputAudioActive || !interruptedTurnId) return;
711
647
  const reason = `${source}-barge-in`;
712
- finishOutputAudio(reason);
713
- const cancelled = talk.cancelTurn({
648
+ harness.finishOutputAudio(reason);
649
+ harness.talk.cancelTurn({
714
650
  turnId: interruptedTurnId,
715
651
  payload: {
716
652
  callId,
@@ -718,9 +654,8 @@ var RealtimeCallHandler = class {
718
654
  reason
719
655
  }
720
656
  });
721
- if (cancelled.ok) rememberTalkEvent(cancelled.event);
722
657
  };
723
- emitTalkEvent({
658
+ harness.emit({
724
659
  type: "session.started",
725
660
  payload: {
726
661
  callId,
@@ -762,9 +697,13 @@ var RealtimeCallHandler = class {
762
697
  if (ws.readyState === WebSocket$1.OPEN) ws.close(1013, "Backpressure: paced audio queue exceeded");
763
698
  }
764
699
  });
765
- const speechDetector = new RealtimeMulawSpeechStartDetector({ requiredLoudChunks: BARGE_IN_REQUIRED_LOUD_CHUNKS });
700
+ const speechDetector = createSpeechThresholdGate({
701
+ rmsThreshold: .035,
702
+ speechFrames: BARGE_IN_REQUIRED_LOUD_CHUNKS,
703
+ silenceFrames: 12
704
+ });
766
705
  const interruptResponseOnInputAudio = typeof this.providerConfig.interruptResponseOnInputAudio === "boolean" ? this.providerConfig.interruptResponseOnInputAudio : void 0;
767
- const session = createRealtimeVoiceBridgeSession({
706
+ const session = harness.createBridge({
768
707
  provider: this.realtimeProvider,
769
708
  cfg: this.coreConfig,
770
709
  providerConfig: this.providerConfig,
@@ -776,46 +715,38 @@ var RealtimeCallHandler = class {
776
715
  audioSink: {
777
716
  isOpen: () => ws.readyState === WebSocket$1.OPEN,
778
717
  sendAudio: (muLaw) => {
779
- const turnId = ensureTalkTurn();
780
- rememberTalkEvent(talk.startOutputAudio({
781
- turnId,
782
- payload: {
783
- callId,
784
- providerCallId: callSid
785
- }
786
- }).event);
787
- emitTalkEvent({
788
- type: "output.audio.delta",
789
- turnId,
790
- payload: { byteLength: muLaw.length }
791
- });
718
+ harness.recordOutputAudio(muLaw);
792
719
  audioPacer.sendAudio(muLaw);
793
720
  },
794
721
  clearAudio: (reason) => {
795
- const clearedBytes = audioPacer.clearAudio();
796
- if (reason === "barge-in") {
797
- cancelOutputAudioForBargeIn("provider", void 0, clearedBytes);
798
- return;
799
- }
800
- console.log(`[voice-call] realtime outbound audio clear requested callId=${callId} providerCallId=${callSid} queuedBytes=${clearedBytes}`);
801
- finishOutputAudio("clear");
722
+ harness.flushOutput(() => {
723
+ const clearedBytes = audioPacer.clearAudio();
724
+ if (reason === "barge-in") {
725
+ cancelOutputAudioForBargeIn("provider", void 0, clearedBytes);
726
+ return;
727
+ }
728
+ console.log(`[voice-call] realtime outbound audio clear requested callId=${callId} providerCallId=${callSid} queuedBytes=${clearedBytes}`);
729
+ harness.finishOutputAudio("clear");
730
+ });
802
731
  },
803
732
  sendMark: (markName) => {
804
733
  audioPacer.sendMark(markName);
805
734
  }
806
735
  },
807
736
  onTranscript: (role, text, isFinal) => {
808
- const turnId = ensureTalkTurn();
809
- emitTalkEvent({
810
- type: role === "assistant" ? isFinal ? "output.text.done" : "output.text.delta" : isFinal ? "transcript.done" : "transcript.delta",
737
+ const turnId = harness.ensureTurn();
738
+ const eventType = role === "assistant" ? isFinal ? "output.text.done" : "output.text.delta" : isFinal ? "transcript.done" : "transcript.delta";
739
+ const payload = role === "assistant" ? { text } : {
740
+ role,
741
+ text
742
+ };
743
+ harness.emit({
744
+ type: eventType,
811
745
  turnId,
812
- payload: role === "assistant" ? { text } : {
813
- role,
814
- text
815
- },
746
+ payload,
816
747
  final: isFinal
817
748
  });
818
- if (role === "user" && isFinal) emitTalkEvent({
749
+ if (role === "user" && isFinal) harness.emit({
819
750
  type: "input.audio.committed",
820
751
  turnId,
821
752
  payload: {
@@ -851,6 +782,7 @@ var RealtimeCallHandler = class {
851
782
  };
852
783
  this.manager.processEvent(event);
853
784
  this.scheduleForcedAgentConsult({
785
+ harness,
854
786
  session,
855
787
  callId,
856
788
  callSid,
@@ -872,8 +804,8 @@ var RealtimeCallHandler = class {
872
804
  });
873
805
  },
874
806
  onToolCall: (toolEvent, sessionLocal) => {
875
- const turnId = ensureTalkTurn();
876
- emitTalkEvent({
807
+ const turnId = harness.ensureTurn();
808
+ harness.emit({
877
809
  type: "tool.call",
878
810
  turnId,
879
811
  itemId: toolEvent.itemId,
@@ -884,17 +816,17 @@ var RealtimeCallHandler = class {
884
816
  }
885
817
  });
886
818
  console.log(`[voice-call] realtime tool call received callId=${callId} providerCallId=${callSid} tool=${toolEvent.name}`);
887
- return this.executeToolCall(sessionLocal, callId, toolEvent.callId || toolEvent.itemId, toolEvent.name, toolEvent.args, turnId, emitTalkEvent);
819
+ return this.executeToolCall(sessionLocal, callId, toolEvent.callId || toolEvent.itemId, toolEvent.name, toolEvent.args, turnId, harness);
888
820
  },
889
821
  onEvent: (event) => {
890
822
  if (event.type === "input_audio_buffer.speech_started") {
891
- ensureTalkTurn();
823
+ harness.ensureTurn();
892
824
  return;
893
825
  }
894
826
  if (event.type === "input_audio_buffer.speech_stopped") {
895
- const turnId = talk.activeTurnId;
827
+ const turnId = harness.talk.activeTurnId;
896
828
  if (!turnId) return;
897
- emitTalkEvent({
829
+ harness.emit({
898
830
  type: "input.audio.committed",
899
831
  turnId,
900
832
  payload: {
@@ -907,18 +839,18 @@ var RealtimeCallHandler = class {
907
839
  return;
908
840
  }
909
841
  if (event.type === "response.done") {
910
- finishOutputAudio("response.done");
911
- endTalkTurn("response.done");
842
+ harness.finishOutputAudio("response.done");
843
+ harness.endTurn("response.done");
912
844
  return;
913
845
  }
914
- if (event.type === "error") emitTalkEvent({
846
+ if (event.type === "error") harness.emit({
915
847
  type: "session.error",
916
848
  payload: { message: event.detail ?? "Realtime provider error" },
917
849
  final: true
918
850
  });
919
851
  },
920
852
  onReady: () => {
921
- emitTalkEvent({
853
+ harness.emit({
922
854
  type: "session.ready",
923
855
  payload: {
924
856
  callId,
@@ -928,7 +860,7 @@ var RealtimeCallHandler = class {
928
860
  },
929
861
  onError: (error) => {
930
862
  console.error("[voice-call] realtime voice error:", error.message);
931
- emitTalkEvent({
863
+ harness.emit({
932
864
  type: "session.error",
933
865
  payload: { message: error.message },
934
866
  final: true
@@ -940,8 +872,8 @@ var RealtimeCallHandler = class {
940
872
  this.activeTelephonyClosersByCallId.delete(callId);
941
873
  this.activeTelephonyClosersByCallId.delete(callSid);
942
874
  this.clearUserTranscriptState(callId);
943
- finishOutputAudio(reason);
944
- emitTalkEvent({
875
+ harness.finishOutputAudio(reason);
876
+ harness.emit({
945
877
  type: "session.closed",
946
878
  payload: { reason },
947
879
  final: true
@@ -971,17 +903,16 @@ var RealtimeCallHandler = class {
971
903
  this.activeTelephonyClosersByCallId.set(callSid, closeTelephony);
972
904
  const sendAudioToSession = session.sendAudio.bind(session);
973
905
  session.sendAudio = (audio) => {
974
- if (speechDetector.accept(audio)) {
906
+ if (speechDetector.accept({
907
+ rms: calculateMulawRms(audio),
908
+ peak: 0
909
+ })) {
975
910
  console.log(`[voice-call] realtime local speech detected callId=${callId} providerCallId=${callSid}`);
976
911
  if (!providerHandlesInputAudioBargeIn) cancelOutputAudioForBargeIn("local", (audioPlaybackActive) => {
977
912
  session.handleBargeIn({ audioPlaybackActive });
978
913
  });
979
914
  }
980
- emitTalkEvent({
981
- type: "input.audio.delta",
982
- turnId: ensureTalkTurn(),
983
- payload: { byteLength: audio.length }
984
- });
915
+ harness.recordInputAudio(audio);
985
916
  sendAudioToSession(audio);
986
917
  };
987
918
  const closeSession = session.close.bind(session);
@@ -997,7 +928,8 @@ var RealtimeCallHandler = class {
997
928
  this.activeTelephonyClosersByCallId.delete(callId);
998
929
  this.activeTelephonyClosersByCallId.delete(callSid);
999
930
  this.clearUserTranscriptState(callId);
1000
- this.clearForcedConsultState(callId);
931
+ this.forcedConsultsByCallId.delete(callId);
932
+ harness.close();
1001
933
  audioPacer.close();
1002
934
  }
1003
935
  };
@@ -1080,18 +1012,6 @@ var RealtimeCallHandler = class {
1080
1012
  });
1081
1013
  }
1082
1014
  }
1083
- clearForcedConsultState(callId) {
1084
- this.forcedConsultCoordinatorsByCallId.get(callId)?.clear();
1085
- this.forcedConsultCoordinatorsByCallId.delete(callId);
1086
- this.forcedConsultsByCallId.delete(callId);
1087
- }
1088
- forcedConsultCoordinator(callId) {
1089
- const existing = this.forcedConsultCoordinatorsByCallId.get(callId);
1090
- if (existing) return existing;
1091
- const created = createRealtimeVoiceForcedConsultCoordinator();
1092
- this.forcedConsultCoordinatorsByCallId.set(callId, created);
1093
- return created;
1094
- }
1095
1015
  closeTelephonyBridge(callIdOrSid, bridge, reason) {
1096
1016
  const closer = this.activeTelephonyClosersByCallId.get(callIdOrSid);
1097
1017
  if (closer) {
@@ -1108,7 +1028,7 @@ var RealtimeCallHandler = class {
1108
1028
  if (!handler) return;
1109
1029
  const existingForcedConsult = this.forcedConsultsByCallId.get(params.callId);
1110
1030
  if (existingForcedConsult && !existingForcedConsult.completedAt) return;
1111
- const coordinator = this.forcedConsultCoordinator(params.callId);
1031
+ const coordinator = params.harness.forcedConsults;
1112
1032
  if (coordinator.hasRecentNativeConsult(question, { allowUnknownQuestion: true })) return;
1113
1033
  coordinator.clearPending();
1114
1034
  const pending = coordinator.prepare(question);
@@ -1124,7 +1044,7 @@ var RealtimeCallHandler = class {
1124
1044
  });
1125
1045
  }
1126
1046
  async runForcedAgentConsult(params) {
1127
- const coordinator = this.forcedConsultCoordinator(params.callId);
1047
+ const coordinator = params.harness.forcedConsults;
1128
1048
  coordinator.markStarted(params.handle);
1129
1049
  const startedAt = Date.now();
1130
1050
  logger.debug(`[voice-call] realtime forced agent consult reason=${FORCED_CONSULT_REASON} consultPolicy=always callId=${params.callId} providerCallId=${params.callSid} chars=${params.handle.question.length}`);
@@ -1216,14 +1136,14 @@ var RealtimeCallHandler = class {
1216
1136
  reason
1217
1137
  });
1218
1138
  }
1219
- async executeToolCall(bridge, callId, bridgeCallId, name, args, turnId, emitTalkEvent) {
1139
+ async executeToolCall(bridge, callId, bridgeCallId, name, args, turnId, harness) {
1220
1140
  const handler = this.toolHandlers.get(name);
1221
1141
  const startedAt = Date.now();
1222
1142
  const hasResultError = (result) => {
1223
1143
  return Boolean(result && typeof result === "object" && !Array.isArray(result) && "error" in result);
1224
1144
  };
1225
1145
  const emitFinalToolEvent = (result) => {
1226
- emitTalkEvent?.({
1146
+ harness.emit({
1227
1147
  type: hasResultError(result) ? "tool.error" : "tool.result",
1228
1148
  turnId,
1229
1149
  callId: bridgeCallId,
@@ -1241,7 +1161,7 @@ var RealtimeCallHandler = class {
1241
1161
  const submitWorkingResponse = async () => {
1242
1162
  if (handler && name === REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME && bridge.bridge.supportsToolResultContinuation && !this.config.fastContext.enabled) {
1243
1163
  await bridge.submitToolResult(bridgeCallId, buildRealtimeVoiceAgentConsultWorkingResponse("caller"), { willContinue: true });
1244
- emitTalkEvent?.({
1164
+ harness.emit({
1245
1165
  type: "tool.progress",
1246
1166
  turnId,
1247
1167
  callId: bridgeCallId,
@@ -1253,7 +1173,7 @@ var RealtimeCallHandler = class {
1253
1173
  }
1254
1174
  };
1255
1175
  if (name === REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME) {
1256
- const coordinator = this.forcedConsultCoordinator(callId);
1176
+ const coordinator = harness.forcedConsults;
1257
1177
  const forcedMatch = coordinator.recordNativeConsult(args, bridgeCallId);
1258
1178
  if (forcedMatch.kind === "none") {
1259
1179
  const pending = coordinator.consumePending();
@@ -1,4 +1,4 @@
1
- import { b as resolveVoiceCallSessionKey, m as resolveCallAgentId, p as resolveVoiceResponseModel } from "./runtime-entry-DSgyqIwE.js";
1
+ import { h as resolveCallAgentId, m as resolveVoiceResponseModel, x as resolveVoiceCallSessionKey } from "./runtime-entry-CQtitvRd.js";
2
2
  import { isRecord, normalizeLowercaseStringOrEmpty, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
3
3
  import crypto from "node:crypto";
4
4
  import { ModelSelectionLockedError, applyModelOverrideToSessionEntry, resolvePersistedSessionRuntimeId } from "openclaw/plugin-sdk/model-session-runtime";