@openclaw/voice-call 2026.7.2-beta.5 → 2026.7.33

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 (49) hide show
  1. package/README.md +3 -2
  2. package/dist/.setup/config-BBpso-fA.mjs +674 -0
  3. package/dist/.setup/config-compat-CYRE-SSo.mjs +156 -0
  4. package/dist/{guarded-json-api-Dxhe25Zi.js → .setup/guarded-json-api-BukKSVNW.mjs} +50 -24
  5. package/dist/{mock-YsvBTX-7.js → .setup/mock-DY4yTqR2.mjs} +1 -1
  6. package/dist/{plivo-B-KvPVji.js → .setup/plivo-DFHh72MH.mjs} +3 -2
  7. package/dist/{realtime-handler-hIVPEZxN.js → .setup/realtime-handler-DWLxvZuO.mjs} +197 -128
  8. package/dist/{response-generator-z6M9229t.js → .setup/response-generator-DoC6lfcm.mjs} +6 -21
  9. package/dist/{runtime-entry-CQtitvRd.js → .setup/runtime-entry-CH-CV0Sf.mjs} +384 -868
  10. package/dist/{store-path-nYL-yM0S.js → .setup/store-1UfyY3HS.mjs} +6 -13
  11. package/dist/{telnyx-BG56lL7z.js → .setup/telnyx-Bb8X-nin.mjs} +1 -1
  12. package/dist/{twilio-B3FQ_P9x.js → .setup/twilio-Bsbf3O9A.mjs} +11 -15
  13. package/dist/{webhook-replay-aaP79dI8.js → .setup/webhook-replay-g3cDA5wr.mjs} +1 -1
  14. package/dist/cli-metadata.js +1 -5
  15. package/dist/doctor-contract-api.js +14 -36
  16. package/dist/index.js +38 -28
  17. package/dist/runtime-entry.js +1 -1
  18. package/dist/setup-api.js +2 -86
  19. package/node_modules/commander/LICENSE +22 -0
  20. package/node_modules/commander/Readme.md +1172 -0
  21. package/node_modules/commander/index.js +21 -0
  22. package/node_modules/commander/lib/argument.js +147 -0
  23. package/node_modules/commander/lib/command.js +2790 -0
  24. package/node_modules/commander/lib/error.js +36 -0
  25. package/node_modules/commander/lib/help.js +731 -0
  26. package/node_modules/commander/lib/option.js +377 -0
  27. package/node_modules/commander/lib/suggestSimilar.js +99 -0
  28. package/node_modules/commander/package-support.json +19 -0
  29. package/node_modules/commander/package.json +64 -0
  30. package/node_modules/commander/typings/index.d.ts +1113 -0
  31. package/node_modules/typebox/build/compile/validator.d.mts +4 -1
  32. package/node_modules/typebox/build/guard/guard.d.mts +1 -1
  33. package/node_modules/typebox/build/guard/guard.mjs +2 -5
  34. package/node_modules/typebox/build/guard/string.mjs +4 -16
  35. package/node_modules/typebox/build/schema/engine/_functions.mjs +15 -34
  36. package/node_modules/typebox/build/type/action/readonly_object.d.mts +0 -5
  37. package/node_modules/typebox/build/type/action/readonly_object.mjs +0 -8
  38. package/node_modules/typebox/build/typebox.d.mts +1 -1
  39. package/node_modules/typebox/build/typebox.mjs +1 -1
  40. package/node_modules/typebox/package.json +1 -1
  41. package/node_modules/typebox/readme.md +32 -29
  42. package/node_modules/ws/lib/permessage-deflate.js +3 -1
  43. package/node_modules/ws/package.json +1 -1
  44. package/openclaw.plugin.json +15 -2
  45. package/package.json +8 -6
  46. package/dist/cli-output-mode.js +0 -27
  47. package/dist/twilio-region-4fkgz3UG.js +0 -25
  48. /package/dist/{realtime-transcription.runtime-CbJAs5t_.js → .setup/realtime-transcription.runtime-CbJAs5t_.mjs} +0 -0
  49. /package/dist/{realtime-voice.runtime-vtdCOWg-.js → .setup/realtime-voice.runtime-vtdCOWg-.mjs} +0 -0
@@ -1,8 +1,7 @@
1
- import { o as canonicalizeVoiceCallMediaBase64 } from "./runtime-entry-CQtitvRd.js";
2
1
  import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
3
2
  import { isFutureDateTimestampMs, resolveExpiresAtMsFromDurationMs } from "openclaw/plugin-sdk/number-runtime";
4
3
  import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env";
5
- import { REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME, buildRealtimeVoiceAgentConsultWorkingResponse, calculateMulawRms, createRealtimeVoiceSessionHarness, createSpeechThresholdGate, readRealtimeVoiceConsultQuestion, readSpeakableRealtimeVoiceToolResult } from "openclaw/plugin-sdk/realtime-voice";
4
+ import { REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME, buildRealtimeVoiceAgentConsultWorkingResponse, createRealtimeVoiceBridgeSession, createRealtimeVoiceForcedConsultCoordinator, createTalkSessionController, readRealtimeVoiceConsultQuestion, readSpeakableRealtimeVoiceToolResult, recordTalkObservabilityEvent } from "openclaw/plugin-sdk/realtime-voice";
6
5
  import { normalizeWebhookPath } from "openclaw/plugin-sdk/webhook-ingress";
7
6
  import { randomUUID } from "node:crypto";
8
7
  import { sliceUtf16Safe, truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
@@ -12,7 +11,13 @@ import WebSocket$1, { WebSocketServer } from "ws";
12
11
  const TELEPHONY_SAMPLE_RATE = 8e3;
13
12
  const TELEPHONY_CHUNK_BYTES = 160;
14
13
  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;
15
17
  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);
16
21
  /** Paces outgoing mulaw audio frames at telephony cadence. */
17
22
  var RealtimeAudioPacer = class {
18
23
  constructor(params) {
@@ -107,6 +112,52 @@ var RealtimeAudioPacer = class {
107
112
  if (this.queue.length > 0) this.timer = setTimeout(() => this.pump(), delayMs);
108
113
  }
109
114
  };
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
+ }
110
161
  //#endregion
111
162
  //#region extensions/voice-call/src/webhook/stream-frame-adapter.ts
112
163
  /** Parse numeric timestamps sent as numbers or integer strings. */
@@ -130,15 +181,22 @@ function readRecordField(record, field) {
130
181
  const value = record[field];
131
182
  return typeof value === "object" && value !== null ? value : void 0;
132
183
  }
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
+ }
133
192
  /** Parse a common provider media frame. */
134
193
  function parseMediaFrame(msg) {
135
194
  const mediaData = readRecordField(msg, "media");
136
195
  const payload = typeof mediaData?.payload === "string" ? mediaData.payload : void 0;
137
- const canonicalPayload = payload ? canonicalizeVoiceCallMediaBase64(payload) : void 0;
138
- if (!canonicalPayload) return { kind: "ignored" };
196
+ if (!payload || !isValidBase64Payload(payload)) return { kind: "ignored" };
139
197
  return {
140
198
  kind: "media",
141
- payloadBase64: canonicalPayload,
199
+ payloadBase64: payload,
142
200
  timestampMs: parseTimestampMs(mediaData?.timestamp),
143
201
  track: typeof mediaData?.track === "string" ? mediaData.track : void 0
144
202
  };
@@ -271,8 +329,8 @@ var TelnyxStreamFrameAdapter = class {
271
329
  //#region extensions/voice-call/src/webhook/realtime-handler.ts
272
330
  const STREAM_TOKEN_TTL_MS = 3e4;
273
331
  const DEFAULT_HOST = "localhost:8443";
274
- const MAX_REALTIME_MESSAGE_BYTES = 256 * 1024;
275
- const MAX_REALTIME_WS_BUFFERED_BYTES = 1024 * 1024;
332
+ const MAX_REALTIME_MESSAGE_BYTES = 262144;
333
+ const MAX_REALTIME_WS_BUFFERED_BYTES = 1048576;
276
334
  const FORCED_CONSULT_FALLBACK_DELAY_MS = 200;
277
335
  const FORCED_CONSULT_NATIVE_DEDUPE_MS = 2e3;
278
336
  const FORCED_CONSULT_RESULT_MAX_CHARS = 1800;
@@ -374,7 +432,7 @@ function buildForcedConsultSpeechPrompt(result) {
374
432
  "Internal OpenClaw consult result is ready.",
375
433
  "Do not call tools for this internal result.",
376
434
  "Speak the following answer to the caller now, briefly and naturally:",
377
- trimmed.length <= FORCED_CONSULT_RESULT_MAX_CHARS ? trimmed : `${truncateUtf16Safe(trimmed, FORCED_CONSULT_RESULT_MAX_CHARS - 16).trimEnd()} [truncated]`
435
+ trimmed.length <= FORCED_CONSULT_RESULT_MAX_CHARS ? trimmed : `${truncateUtf16Safe(trimmed, 1784).trimEnd()} [truncated]`
378
436
  ].join("\n");
379
437
  }
380
438
  function appendRecentTalkEventMetadata(call, event) {
@@ -417,6 +475,7 @@ var RealtimeCallHandler = class {
417
475
  this.partialUserTranscriptUpdatedAtByCallId = /* @__PURE__ */ new Map();
418
476
  this.recentFinalUserTranscriptsByCallId = /* @__PURE__ */ new Map();
419
477
  this.recentFinalUserTranscriptTimersByCallId = /* @__PURE__ */ new Map();
478
+ this.forcedConsultCoordinatorsByCallId = /* @__PURE__ */ new Map();
420
479
  this.forcedConsultsByCallId = /* @__PURE__ */ new Map();
421
480
  this.nativeConsultsInFlightByCallId = /* @__PURE__ */ new Map();
422
481
  this.publicOrigin = null;
@@ -602,51 +661,56 @@ var RealtimeCallHandler = class {
602
661
  }
603
662
  const { callId, instructions, initialGreetingInstructions } = registration;
604
663
  const callRecord = this.manager.getCallByProviderCallId(callSid);
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
- });
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
+ };
637
701
  const providerHandlesInputAudioBargeIn = this.realtimeProvider.capabilities?.handlesInputAudioBargeIn === true;
638
702
  const cancelOutputAudioForBargeIn = (source, interruptProvider, clearedAudioBytes = 0) => {
639
- const outputAudioActive = harness.talk.outputAudioActive;
703
+ const outputAudioActive = talk.outputAudioActive;
640
704
  const pendingTelephonyAudio = audioPacer.hasPendingAudio();
641
705
  if (source === "provider" && !outputAudioActive && !pendingTelephonyAudio && clearedAudioBytes === 0) return;
642
- const interruptedTurnId = harness.talk.activeTurnId;
706
+ const interruptedTurnId = talk.activeTurnId;
643
707
  interruptProvider?.(outputAudioActive || pendingTelephonyAudio);
644
708
  const clearedBytes = clearedAudioBytes + (source === "local" || pendingTelephonyAudio ? audioPacer.clearAudio() : 0);
645
709
  console.log(`[voice-call] realtime outbound audio cleared by ${source} barge-in callId=${callId} providerCallId=${callSid} queuedBytes=${clearedBytes}`);
646
710
  if (!outputAudioActive || !interruptedTurnId) return;
647
711
  const reason = `${source}-barge-in`;
648
- harness.finishOutputAudio(reason);
649
- harness.talk.cancelTurn({
712
+ finishOutputAudio(reason);
713
+ const cancelled = talk.cancelTurn({
650
714
  turnId: interruptedTurnId,
651
715
  payload: {
652
716
  callId,
@@ -654,8 +718,9 @@ var RealtimeCallHandler = class {
654
718
  reason
655
719
  }
656
720
  });
721
+ if (cancelled.ok) rememberTalkEvent(cancelled.event);
657
722
  };
658
- harness.emit({
723
+ emitTalkEvent({
659
724
  type: "session.started",
660
725
  payload: {
661
726
  callId,
@@ -697,13 +762,9 @@ var RealtimeCallHandler = class {
697
762
  if (ws.readyState === WebSocket$1.OPEN) ws.close(1013, "Backpressure: paced audio queue exceeded");
698
763
  }
699
764
  });
700
- const speechDetector = createSpeechThresholdGate({
701
- rmsThreshold: .035,
702
- speechFrames: BARGE_IN_REQUIRED_LOUD_CHUNKS,
703
- silenceFrames: 12
704
- });
765
+ const speechDetector = new RealtimeMulawSpeechStartDetector({ requiredLoudChunks: BARGE_IN_REQUIRED_LOUD_CHUNKS });
705
766
  const interruptResponseOnInputAudio = typeof this.providerConfig.interruptResponseOnInputAudio === "boolean" ? this.providerConfig.interruptResponseOnInputAudio : void 0;
706
- const session = harness.createBridge({
767
+ const session = createRealtimeVoiceBridgeSession({
707
768
  provider: this.realtimeProvider,
708
769
  cfg: this.coreConfig,
709
770
  providerConfig: this.providerConfig,
@@ -715,38 +776,46 @@ var RealtimeCallHandler = class {
715
776
  audioSink: {
716
777
  isOpen: () => ws.readyState === WebSocket$1.OPEN,
717
778
  sendAudio: (muLaw) => {
718
- harness.recordOutputAudio(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
+ });
719
792
  audioPacer.sendAudio(muLaw);
720
793
  },
721
794
  clearAudio: (reason) => {
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
- });
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");
731
802
  },
732
803
  sendMark: (markName) => {
733
804
  audioPacer.sendMark(markName);
734
805
  }
735
806
  },
736
807
  onTranscript: (role, text, isFinal) => {
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,
808
+ const turnId = ensureTalkTurn();
809
+ emitTalkEvent({
810
+ type: role === "assistant" ? isFinal ? "output.text.done" : "output.text.delta" : isFinal ? "transcript.done" : "transcript.delta",
745
811
  turnId,
746
- payload,
812
+ payload: role === "assistant" ? { text } : {
813
+ role,
814
+ text
815
+ },
747
816
  final: isFinal
748
817
  });
749
- if (role === "user" && isFinal) harness.emit({
818
+ if (role === "user" && isFinal) emitTalkEvent({
750
819
  type: "input.audio.committed",
751
820
  turnId,
752
821
  payload: {
@@ -782,7 +851,6 @@ var RealtimeCallHandler = class {
782
851
  };
783
852
  this.manager.processEvent(event);
784
853
  this.scheduleForcedAgentConsult({
785
- harness,
786
854
  session,
787
855
  callId,
788
856
  callSid,
@@ -804,8 +872,8 @@ var RealtimeCallHandler = class {
804
872
  });
805
873
  },
806
874
  onToolCall: (toolEvent, sessionLocal) => {
807
- const turnId = harness.ensureTurn();
808
- harness.emit({
875
+ const turnId = ensureTalkTurn();
876
+ emitTalkEvent({
809
877
  type: "tool.call",
810
878
  turnId,
811
879
  itemId: toolEvent.itemId,
@@ -816,17 +884,17 @@ var RealtimeCallHandler = class {
816
884
  }
817
885
  });
818
886
  console.log(`[voice-call] realtime tool call received callId=${callId} providerCallId=${callSid} tool=${toolEvent.name}`);
819
- return this.executeToolCall(sessionLocal, callId, toolEvent.callId || toolEvent.itemId, toolEvent.name, toolEvent.args, turnId, harness);
887
+ this.executeToolCall(sessionLocal, callId, toolEvent.callId || toolEvent.itemId, toolEvent.name, toolEvent.args, turnId, emitTalkEvent);
820
888
  },
821
889
  onEvent: (event) => {
822
890
  if (event.type === "input_audio_buffer.speech_started") {
823
- harness.ensureTurn();
891
+ ensureTalkTurn();
824
892
  return;
825
893
  }
826
894
  if (event.type === "input_audio_buffer.speech_stopped") {
827
- const turnId = harness.talk.activeTurnId;
895
+ const turnId = talk.activeTurnId;
828
896
  if (!turnId) return;
829
- harness.emit({
897
+ emitTalkEvent({
830
898
  type: "input.audio.committed",
831
899
  turnId,
832
900
  payload: {
@@ -839,18 +907,18 @@ var RealtimeCallHandler = class {
839
907
  return;
840
908
  }
841
909
  if (event.type === "response.done") {
842
- harness.finishOutputAudio("response.done");
843
- harness.endTurn("response.done");
910
+ finishOutputAudio("response.done");
911
+ endTalkTurn("response.done");
844
912
  return;
845
913
  }
846
- if (event.type === "error") harness.emit({
914
+ if (event.type === "error") emitTalkEvent({
847
915
  type: "session.error",
848
916
  payload: { message: event.detail ?? "Realtime provider error" },
849
917
  final: true
850
918
  });
851
919
  },
852
920
  onReady: () => {
853
- harness.emit({
921
+ emitTalkEvent({
854
922
  type: "session.ready",
855
923
  payload: {
856
924
  callId,
@@ -860,7 +928,7 @@ var RealtimeCallHandler = class {
860
928
  },
861
929
  onError: (error) => {
862
930
  console.error("[voice-call] realtime voice error:", error.message);
863
- harness.emit({
931
+ emitTalkEvent({
864
932
  type: "session.error",
865
933
  payload: { message: error.message },
866
934
  final: true
@@ -872,8 +940,8 @@ var RealtimeCallHandler = class {
872
940
  this.activeTelephonyClosersByCallId.delete(callId);
873
941
  this.activeTelephonyClosersByCallId.delete(callSid);
874
942
  this.clearUserTranscriptState(callId);
875
- harness.finishOutputAudio(reason);
876
- harness.emit({
943
+ finishOutputAudio(reason);
944
+ emitTalkEvent({
877
945
  type: "session.closed",
878
946
  payload: { reason },
879
947
  final: true
@@ -903,16 +971,17 @@ var RealtimeCallHandler = class {
903
971
  this.activeTelephonyClosersByCallId.set(callSid, closeTelephony);
904
972
  const sendAudioToSession = session.sendAudio.bind(session);
905
973
  session.sendAudio = (audio) => {
906
- if (speechDetector.accept({
907
- rms: calculateMulawRms(audio),
908
- peak: 0
909
- })) {
974
+ if (speechDetector.accept(audio)) {
910
975
  console.log(`[voice-call] realtime local speech detected callId=${callId} providerCallId=${callSid}`);
911
976
  if (!providerHandlesInputAudioBargeIn) cancelOutputAudioForBargeIn("local", (audioPlaybackActive) => {
912
977
  session.handleBargeIn({ audioPlaybackActive });
913
978
  });
914
979
  }
915
- harness.recordInputAudio(audio);
980
+ emitTalkEvent({
981
+ type: "input.audio.delta",
982
+ turnId: ensureTalkTurn(),
983
+ payload: { byteLength: audio.length }
984
+ });
916
985
  sendAudioToSession(audio);
917
986
  };
918
987
  const closeSession = session.close.bind(session);
@@ -928,8 +997,7 @@ var RealtimeCallHandler = class {
928
997
  this.activeTelephonyClosersByCallId.delete(callId);
929
998
  this.activeTelephonyClosersByCallId.delete(callSid);
930
999
  this.clearUserTranscriptState(callId);
931
- this.forcedConsultsByCallId.delete(callId);
932
- harness.close();
1000
+ this.clearForcedConsultState(callId);
933
1001
  audioPacer.close();
934
1002
  }
935
1003
  };
@@ -1012,6 +1080,18 @@ var RealtimeCallHandler = class {
1012
1080
  });
1013
1081
  }
1014
1082
  }
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
+ }
1015
1095
  closeTelephonyBridge(callIdOrSid, bridge, reason) {
1016
1096
  const closer = this.activeTelephonyClosersByCallId.get(callIdOrSid);
1017
1097
  if (closer) {
@@ -1028,7 +1108,7 @@ var RealtimeCallHandler = class {
1028
1108
  if (!handler) return;
1029
1109
  const existingForcedConsult = this.forcedConsultsByCallId.get(params.callId);
1030
1110
  if (existingForcedConsult && !existingForcedConsult.completedAt) return;
1031
- const coordinator = params.harness.forcedConsults;
1111
+ const coordinator = this.forcedConsultCoordinator(params.callId);
1032
1112
  if (coordinator.hasRecentNativeConsult(question, { allowUnknownQuestion: true })) return;
1033
1113
  coordinator.clearPending();
1034
1114
  const pending = coordinator.prepare(question);
@@ -1044,7 +1124,7 @@ var RealtimeCallHandler = class {
1044
1124
  });
1045
1125
  }
1046
1126
  async runForcedAgentConsult(params) {
1047
- const coordinator = params.harness.forcedConsults;
1127
+ const coordinator = this.forcedConsultCoordinator(params.callId);
1048
1128
  coordinator.markStarted(params.handle);
1049
1129
  const startedAt = Date.now();
1050
1130
  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}`);
@@ -1136,14 +1216,14 @@ var RealtimeCallHandler = class {
1136
1216
  reason
1137
1217
  });
1138
1218
  }
1139
- async executeToolCall(bridge, callId, bridgeCallId, name, args, turnId, harness) {
1219
+ async executeToolCall(bridge, callId, bridgeCallId, name, args, turnId, emitTalkEvent) {
1140
1220
  const handler = this.toolHandlers.get(name);
1141
1221
  const startedAt = Date.now();
1142
1222
  const hasResultError = (result) => {
1143
1223
  return Boolean(result && typeof result === "object" && !Array.isArray(result) && "error" in result);
1144
1224
  };
1145
1225
  const emitFinalToolEvent = (result) => {
1146
- harness.emit({
1226
+ emitTalkEvent?.({
1147
1227
  type: hasResultError(result) ? "tool.error" : "tool.result",
1148
1228
  turnId,
1149
1229
  callId: bridgeCallId,
@@ -1154,14 +1234,13 @@ var RealtimeCallHandler = class {
1154
1234
  final: true
1155
1235
  });
1156
1236
  };
1157
- const submitFinalToolResult = async (result) => {
1158
- await bridge.submitToolResult(bridgeCallId, result);
1237
+ const submitFinalToolResult = (result) => {
1238
+ bridge.submitToolResult(bridgeCallId, result);
1159
1239
  emitFinalToolEvent(result);
1160
1240
  };
1161
- const submitWorkingResponse = async () => {
1241
+ const submitWorkingResponse = () => {
1162
1242
  if (handler && name === REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME && bridge.bridge.supportsToolResultContinuation && !this.config.fastContext.enabled) {
1163
- await bridge.submitToolResult(bridgeCallId, buildRealtimeVoiceAgentConsultWorkingResponse("caller"), { willContinue: true });
1164
- harness.emit({
1243
+ emitTalkEvent?.({
1165
1244
  type: "tool.progress",
1166
1245
  turnId,
1167
1246
  callId: bridgeCallId,
@@ -1170,66 +1249,56 @@ var RealtimeCallHandler = class {
1170
1249
  status: "working"
1171
1250
  }
1172
1251
  });
1252
+ bridge.submitToolResult(bridgeCallId, buildRealtimeVoiceAgentConsultWorkingResponse("caller"), { willContinue: true });
1173
1253
  }
1174
1254
  };
1175
1255
  if (name === REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME) {
1176
- const coordinator = harness.forcedConsults;
1256
+ const coordinator = this.forcedConsultCoordinator(callId);
1177
1257
  const forcedMatch = coordinator.recordNativeConsult(args, bridgeCallId);
1178
1258
  if (forcedMatch.kind === "none") {
1179
1259
  const pending = coordinator.consumePending();
1180
1260
  if (pending) coordinator.remove(pending);
1181
1261
  }
1182
1262
  const forcedConsult = this.forcedConsultsByCallId.get(callId);
1183
- if (forcedMatch.kind === "already_delivered" && coordinator.isCancelled(forcedMatch.handle)) {
1184
- if (forcedConsult) forcedConsult.sendSpeechPrompt = false;
1185
- await submitFinalToolResult({
1186
- status: "cancelled",
1187
- message: "OpenClaw cancelled this consult before completion. Do not restart it."
1188
- });
1189
- return;
1190
- }
1191
1263
  if (forcedConsult) {
1192
1264
  if (forcedConsult.completedAt || forcedMatch.kind === "already_delivered") {
1193
- await submitFinalToolResult({
1265
+ submitFinalToolResult({
1194
1266
  status: "already_delivered",
1195
1267
  message: "OpenClaw already delivered this consult result internally. Do not repeat it."
1196
1268
  });
1197
1269
  return;
1198
1270
  }
1199
1271
  forcedConsult.sendSpeechPrompt = false;
1200
- await submitFinalToolResult(await forcedConsult.promise.catch((error) => ({ error: formatErrorMessage(error) })));
1272
+ submitFinalToolResult(await forcedConsult.promise.catch((error) => ({ error: formatErrorMessage(error) })));
1201
1273
  return;
1202
1274
  }
1203
1275
  const existingNativeConsult = this.nativeConsultsInFlightByCallId.get(callId);
1204
1276
  if (existingNativeConsult) {
1205
1277
  console.log(`[voice-call] realtime tool call sharing in-flight agent consult callId=${callId} ageMs=${Date.now() - existingNativeConsult.startedAt}`);
1206
- await submitWorkingResponse();
1207
- await submitFinalToolResult(await existingNativeConsult.promise);
1278
+ submitWorkingResponse();
1279
+ submitFinalToolResult(await existingNativeConsult.promise);
1208
1280
  return;
1209
1281
  }
1282
+ submitWorkingResponse();
1210
1283
  const state = {
1211
1284
  startedAt,
1212
1285
  promise: Promise.resolve()
1213
1286
  };
1214
- state.promise = submitWorkingResponse().then(async () => {
1215
- try {
1216
- await this.waitForConsultTranscriptSettle(callId, startedAt);
1217
- const context = { partialUserTranscript: this.resolveUserTranscriptContext(callId) };
1218
- state.partialUserTranscript = context.partialUserTranscript;
1219
- const handlerArgs = withFallbackConsultQuestion(args, context.partialUserTranscript);
1220
- console.log(`[voice-call] realtime tool call executing callId=${callId} tool=${name} hasHandler=${Boolean(handler)}`);
1221
- return !handler ? { error: `Tool "${name}" not available` } : await handler(handlerArgs, callId, context);
1222
- } catch (error) {
1223
- return { error: formatErrorMessage(error) };
1224
- }
1225
- });
1287
+ state.promise = (async () => {
1288
+ await this.waitForConsultTranscriptSettle(callId, startedAt);
1289
+ const context = { partialUserTranscript: this.resolveUserTranscriptContext(callId) };
1290
+ state.partialUserTranscript = context.partialUserTranscript;
1291
+ const handlerArgs = withFallbackConsultQuestion(args, context.partialUserTranscript);
1292
+ console.log(`[voice-call] realtime tool call executing callId=${callId} tool=${name} hasHandler=${Boolean(handler)}`);
1293
+ return !handler ? { error: `Tool "${name}" not available` } : await handler(handlerArgs, callId, context);
1294
+ })().catch((error) => ({ error: formatErrorMessage(error) }));
1226
1295
  this.nativeConsultsInFlightByCallId.set(callId, state);
1227
1296
  try {
1228
1297
  const result = await state.promise;
1229
1298
  const status = result && typeof result === "object" && !Array.isArray(result) && "error" in result ? "error" : "ok";
1230
1299
  const error = status === "error" && result && typeof result === "object" && !Array.isArray(result) ? formatErrorMessage(result.error ?? "unknown") : void 0;
1231
1300
  console.log(`[voice-call] realtime tool call completed callId=${callId} tool=${name} status=${status} elapsedMs=${Date.now() - startedAt}${error ? ` error=${error}` : ""}`);
1232
- await submitFinalToolResult(result);
1301
+ submitFinalToolResult(result);
1233
1302
  if (status === "ok") this.consumePartialUserTranscript(callId, state.partialUserTranscript);
1234
1303
  } finally {
1235
1304
  if (this.nativeConsultsInFlightByCallId.get(callId) === state) this.nativeConsultsInFlightByCallId.delete(callId);
@@ -1243,7 +1312,7 @@ var RealtimeCallHandler = class {
1243
1312
  const status = result && typeof result === "object" && !Array.isArray(result) && "error" in result ? "error" : "ok";
1244
1313
  const error = status === "error" && result && typeof result === "object" && !Array.isArray(result) ? formatErrorMessage(result.error ?? "unknown") : void 0;
1245
1314
  console.log(`[voice-call] realtime tool call completed callId=${callId} tool=${name} status=${status} elapsedMs=${Date.now() - startedAt}${error ? ` error=${error}` : ""}`);
1246
- await submitFinalToolResult(result);
1315
+ submitFinalToolResult(result);
1247
1316
  if (name === REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME && status === "ok") this.consumePartialUserTranscript(callId, context.partialUserTranscript);
1248
1317
  }
1249
1318
  };
@@ -1,7 +1,8 @@
1
- import { h as resolveCallAgentId, m as resolveVoiceResponseModel, x as resolveVoiceCallSessionKey } from "./runtime-entry-CQtitvRd.js";
1
+ import { s as resolveVoiceCallSessionKey } from "./config-BBpso-fA.mjs";
2
+ import { m as resolveCallAgentId, p as resolveVoiceResponseModel } from "./runtime-entry-CH-CV0Sf.mjs";
2
3
  import { isRecord, normalizeLowercaseStringOrEmpty, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
3
4
  import crypto from "node:crypto";
4
- import { ModelSelectionLockedError, applyModelOverrideToSessionEntry, resolvePersistedSessionRuntimeId } from "openclaw/plugin-sdk/model-session-runtime";
5
+ import { applyModelOverrideToSessionEntry } from "openclaw/plugin-sdk/model-session-runtime";
5
6
  //#region extensions/voice-call/src/response-generator.ts
6
7
  /**
7
8
  * Voice call response generator - uses the embedded OpenClaw agent for tool support.
@@ -66,11 +67,7 @@ function sanitizePlainSpokenText(text) {
66
67
  const withoutCodeFences = text.replace(/```[\s\S]*?```/g, " ").trim();
67
68
  if (!withoutCodeFences) return null;
68
69
  const paragraphs = normalizeStringEntries(withoutCodeFences.split(/\n\s*\n+/));
69
- while (paragraphs.length > 1) {
70
- const firstParagraph = paragraphs.at(0);
71
- if (!firstParagraph || !isLikelyMetaReasoningParagraph(firstParagraph)) break;
72
- paragraphs.shift();
73
- }
70
+ while (paragraphs.length > 1 && isLikelyMetaReasoningParagraph(paragraphs[0])) paragraphs.shift();
74
71
  return normalizeSpokenText(paragraphs.join(" "));
75
72
  }
76
73
  function extractSpokenTextFromPayloads(payloads) {
@@ -107,7 +104,7 @@ function resolveVoiceSandboxSessionKey(agentId, sessionKey) {
107
104
  * Uses the same agent infrastructure as messaging for consistent behavior.
108
105
  */
109
106
  async function generateVoiceResponse(params) {
110
- const { voiceConfig, callId, sessionKey, from, transcript, userMessage, coreConfig, agentRuntime, onEarlyText } = params;
107
+ const { voiceConfig, callId, sessionKey, from, senderIsOwner, transcript, userMessage, coreConfig, agentRuntime, onEarlyText } = params;
111
108
  if (!coreConfig) return {
112
109
  text: null,
113
110
  deliveredEarly: false,
@@ -145,7 +142,6 @@ async function generateVoiceResponse(params) {
145
142
  agentRuntime
146
143
  });
147
144
  let sessionEntry = existingSessionEntry;
148
- if (sessionEntry?.modelSelectionLocked === true && voiceConfig.responseModel) throw new ModelSelectionLockedError();
149
145
  if (!sessionEntry?.sessionId || voiceConfig.responseModel) sessionEntry = await agentRuntime.session.patchSessionEntry({
150
146
  storePath,
151
147
  sessionKey: resolvedSessionKey,
@@ -177,8 +173,6 @@ async function generateVoiceResponse(params) {
177
173
  error: "Voice response session could not be initialized"
178
174
  };
179
175
  const sessionId = sessionEntry.sessionId;
180
- const modelSelectionLocked = sessionEntry.modelSelectionLocked === true;
181
- const persistedRuntimeId = resolvePersistedSessionRuntimeId(sessionEntry);
182
176
  const thinkLevel = agentRuntime.resolveThinkingDefault({
183
177
  cfg,
184
178
  provider,
@@ -213,11 +207,6 @@ async function generateVoiceResponse(params) {
213
207
  prompt: userMessage,
214
208
  provider,
215
209
  model,
216
- modelSelectionLocked,
217
- ...persistedRuntimeId ? {
218
- agentHarnessId: persistedRuntimeId,
219
- agentHarnessRuntimeOverride: persistedRuntimeId
220
- } : {},
221
210
  thinkLevel,
222
211
  verboseLevel: "off",
223
212
  timeoutMs,
@@ -225,6 +214,7 @@ async function generateVoiceResponse(params) {
225
214
  lane: "voice",
226
215
  extraSystemPrompt,
227
216
  agentDir,
217
+ senderIsOwner,
228
218
  toolsAllow,
229
219
  abortSignal,
230
220
  blockReplyBreak: "text_end",
@@ -271,11 +261,6 @@ async function generateVoiceResponse(params) {
271
261
  };
272
262
  });
273
263
  } catch (err) {
274
- if (err instanceof ModelSelectionLockedError) return {
275
- text: null,
276
- deliveredEarly: false,
277
- error: err.message
278
- };
279
264
  console.error(`[voice-call] Response generation failed:`, err);
280
265
  return {
281
266
  text: null,