@openclaw/voice-call 2026.7.2-beta.7 → 2026.7.34

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-DS9sZa3C.js → .setup/guarded-json-api-BukKSVNW.mjs} +56 -42
  5. package/dist/{mock-YsvBTX-7.js → .setup/mock-DY4yTqR2.mjs} +1 -1
  6. package/dist/{plivo-vZ--Wqxv.js → .setup/plivo-DFHh72MH.mjs} +3 -2
  7. package/dist/{realtime-handler--i_OERv6.js → .setup/realtime-handler-DWLxvZuO.mjs} +198 -130
  8. package/dist/{response-generator-D-7ApVfT.js → .setup/response-generator-DoC6lfcm.mjs} +6 -21
  9. package/dist/{runtime-entry-D0UJiLfi.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-Dx6cfLvV.js → .setup/telnyx-Bb8X-nin.mjs} +1 -1
  12. package/dist/{twilio-D_D8KysU.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 +46 -48
  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-D0UJiLfi.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
- });
637
- let providerHandlesInputAudioBargeIn = this.realtimeProvider.capabilities?.handlesInputAudioBargeIn === true;
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
+ };
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
@@ -890,7 +958,6 @@ var RealtimeCallHandler = class {
890
958
  });
891
959
  }
892
960
  });
893
- providerHandlesInputAudioBargeIn = session.bridge.handlesInputAudioBargeIn ?? providerHandlesInputAudioBargeIn;
894
961
  const closeTelephony = (reason) => {
895
962
  try {
896
963
  session.close();
@@ -904,16 +971,17 @@ var RealtimeCallHandler = class {
904
971
  this.activeTelephonyClosersByCallId.set(callSid, closeTelephony);
905
972
  const sendAudioToSession = session.sendAudio.bind(session);
906
973
  session.sendAudio = (audio) => {
907
- if (speechDetector.accept({
908
- rms: calculateMulawRms(audio),
909
- peak: 0
910
- })) {
974
+ if (speechDetector.accept(audio)) {
911
975
  console.log(`[voice-call] realtime local speech detected callId=${callId} providerCallId=${callSid}`);
912
976
  if (!providerHandlesInputAudioBargeIn) cancelOutputAudioForBargeIn("local", (audioPlaybackActive) => {
913
977
  session.handleBargeIn({ audioPlaybackActive });
914
978
  });
915
979
  }
916
- harness.recordInputAudio(audio);
980
+ emitTalkEvent({
981
+ type: "input.audio.delta",
982
+ turnId: ensureTalkTurn(),
983
+ payload: { byteLength: audio.length }
984
+ });
917
985
  sendAudioToSession(audio);
918
986
  };
919
987
  const closeSession = session.close.bind(session);
@@ -929,8 +997,7 @@ var RealtimeCallHandler = class {
929
997
  this.activeTelephonyClosersByCallId.delete(callId);
930
998
  this.activeTelephonyClosersByCallId.delete(callSid);
931
999
  this.clearUserTranscriptState(callId);
932
- this.forcedConsultsByCallId.delete(callId);
933
- harness.close();
1000
+ this.clearForcedConsultState(callId);
934
1001
  audioPacer.close();
935
1002
  }
936
1003
  };
@@ -1013,6 +1080,18 @@ var RealtimeCallHandler = class {
1013
1080
  });
1014
1081
  }
1015
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
+ }
1016
1095
  closeTelephonyBridge(callIdOrSid, bridge, reason) {
1017
1096
  const closer = this.activeTelephonyClosersByCallId.get(callIdOrSid);
1018
1097
  if (closer) {
@@ -1029,7 +1108,7 @@ var RealtimeCallHandler = class {
1029
1108
  if (!handler) return;
1030
1109
  const existingForcedConsult = this.forcedConsultsByCallId.get(params.callId);
1031
1110
  if (existingForcedConsult && !existingForcedConsult.completedAt) return;
1032
- const coordinator = params.harness.forcedConsults;
1111
+ const coordinator = this.forcedConsultCoordinator(params.callId);
1033
1112
  if (coordinator.hasRecentNativeConsult(question, { allowUnknownQuestion: true })) return;
1034
1113
  coordinator.clearPending();
1035
1114
  const pending = coordinator.prepare(question);
@@ -1045,7 +1124,7 @@ var RealtimeCallHandler = class {
1045
1124
  });
1046
1125
  }
1047
1126
  async runForcedAgentConsult(params) {
1048
- const coordinator = params.harness.forcedConsults;
1127
+ const coordinator = this.forcedConsultCoordinator(params.callId);
1049
1128
  coordinator.markStarted(params.handle);
1050
1129
  const startedAt = Date.now();
1051
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}`);
@@ -1137,14 +1216,14 @@ var RealtimeCallHandler = class {
1137
1216
  reason
1138
1217
  });
1139
1218
  }
1140
- async executeToolCall(bridge, callId, bridgeCallId, name, args, turnId, harness) {
1219
+ async executeToolCall(bridge, callId, bridgeCallId, name, args, turnId, emitTalkEvent) {
1141
1220
  const handler = this.toolHandlers.get(name);
1142
1221
  const startedAt = Date.now();
1143
1222
  const hasResultError = (result) => {
1144
1223
  return Boolean(result && typeof result === "object" && !Array.isArray(result) && "error" in result);
1145
1224
  };
1146
1225
  const emitFinalToolEvent = (result) => {
1147
- harness.emit({
1226
+ emitTalkEvent?.({
1148
1227
  type: hasResultError(result) ? "tool.error" : "tool.result",
1149
1228
  turnId,
1150
1229
  callId: bridgeCallId,
@@ -1155,14 +1234,13 @@ var RealtimeCallHandler = class {
1155
1234
  final: true
1156
1235
  });
1157
1236
  };
1158
- const submitFinalToolResult = async (result) => {
1159
- await bridge.submitToolResult(bridgeCallId, result);
1237
+ const submitFinalToolResult = (result) => {
1238
+ bridge.submitToolResult(bridgeCallId, result);
1160
1239
  emitFinalToolEvent(result);
1161
1240
  };
1162
- const submitWorkingResponse = async () => {
1241
+ const submitWorkingResponse = () => {
1163
1242
  if (handler && name === REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME && bridge.bridge.supportsToolResultContinuation && !this.config.fastContext.enabled) {
1164
- await bridge.submitToolResult(bridgeCallId, buildRealtimeVoiceAgentConsultWorkingResponse("caller"), { willContinue: true });
1165
- harness.emit({
1243
+ emitTalkEvent?.({
1166
1244
  type: "tool.progress",
1167
1245
  turnId,
1168
1246
  callId: bridgeCallId,
@@ -1171,66 +1249,56 @@ var RealtimeCallHandler = class {
1171
1249
  status: "working"
1172
1250
  }
1173
1251
  });
1252
+ bridge.submitToolResult(bridgeCallId, buildRealtimeVoiceAgentConsultWorkingResponse("caller"), { willContinue: true });
1174
1253
  }
1175
1254
  };
1176
1255
  if (name === REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME) {
1177
- const coordinator = harness.forcedConsults;
1256
+ const coordinator = this.forcedConsultCoordinator(callId);
1178
1257
  const forcedMatch = coordinator.recordNativeConsult(args, bridgeCallId);
1179
1258
  if (forcedMatch.kind === "none") {
1180
1259
  const pending = coordinator.consumePending();
1181
1260
  if (pending) coordinator.remove(pending);
1182
1261
  }
1183
1262
  const forcedConsult = this.forcedConsultsByCallId.get(callId);
1184
- if (forcedMatch.kind === "already_delivered" && coordinator.isCancelled(forcedMatch.handle)) {
1185
- if (forcedConsult) forcedConsult.sendSpeechPrompt = false;
1186
- await submitFinalToolResult({
1187
- status: "cancelled",
1188
- message: "OpenClaw cancelled this consult before completion. Do not restart it."
1189
- });
1190
- return;
1191
- }
1192
1263
  if (forcedConsult) {
1193
1264
  if (forcedConsult.completedAt || forcedMatch.kind === "already_delivered") {
1194
- await submitFinalToolResult({
1265
+ submitFinalToolResult({
1195
1266
  status: "already_delivered",
1196
1267
  message: "OpenClaw already delivered this consult result internally. Do not repeat it."
1197
1268
  });
1198
1269
  return;
1199
1270
  }
1200
1271
  forcedConsult.sendSpeechPrompt = false;
1201
- await submitFinalToolResult(await forcedConsult.promise.catch((error) => ({ error: formatErrorMessage(error) })));
1272
+ submitFinalToolResult(await forcedConsult.promise.catch((error) => ({ error: formatErrorMessage(error) })));
1202
1273
  return;
1203
1274
  }
1204
1275
  const existingNativeConsult = this.nativeConsultsInFlightByCallId.get(callId);
1205
1276
  if (existingNativeConsult) {
1206
1277
  console.log(`[voice-call] realtime tool call sharing in-flight agent consult callId=${callId} ageMs=${Date.now() - existingNativeConsult.startedAt}`);
1207
- await submitWorkingResponse();
1208
- await submitFinalToolResult(await existingNativeConsult.promise);
1278
+ submitWorkingResponse();
1279
+ submitFinalToolResult(await existingNativeConsult.promise);
1209
1280
  return;
1210
1281
  }
1282
+ submitWorkingResponse();
1211
1283
  const state = {
1212
1284
  startedAt,
1213
1285
  promise: Promise.resolve()
1214
1286
  };
1215
- state.promise = submitWorkingResponse().then(async () => {
1216
- try {
1217
- await this.waitForConsultTranscriptSettle(callId, startedAt);
1218
- const context = { partialUserTranscript: this.resolveUserTranscriptContext(callId) };
1219
- state.partialUserTranscript = context.partialUserTranscript;
1220
- const handlerArgs = withFallbackConsultQuestion(args, context.partialUserTranscript);
1221
- console.log(`[voice-call] realtime tool call executing callId=${callId} tool=${name} hasHandler=${Boolean(handler)}`);
1222
- return !handler ? { error: `Tool "${name}" not available` } : await handler(handlerArgs, callId, context);
1223
- } catch (error) {
1224
- return { error: formatErrorMessage(error) };
1225
- }
1226
- });
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) }));
1227
1295
  this.nativeConsultsInFlightByCallId.set(callId, state);
1228
1296
  try {
1229
1297
  const result = await state.promise;
1230
1298
  const status = result && typeof result === "object" && !Array.isArray(result) && "error" in result ? "error" : "ok";
1231
1299
  const error = status === "error" && result && typeof result === "object" && !Array.isArray(result) ? formatErrorMessage(result.error ?? "unknown") : void 0;
1232
1300
  console.log(`[voice-call] realtime tool call completed callId=${callId} tool=${name} status=${status} elapsedMs=${Date.now() - startedAt}${error ? ` error=${error}` : ""}`);
1233
- await submitFinalToolResult(result);
1301
+ submitFinalToolResult(result);
1234
1302
  if (status === "ok") this.consumePartialUserTranscript(callId, state.partialUserTranscript);
1235
1303
  } finally {
1236
1304
  if (this.nativeConsultsInFlightByCallId.get(callId) === state) this.nativeConsultsInFlightByCallId.delete(callId);
@@ -1244,7 +1312,7 @@ var RealtimeCallHandler = class {
1244
1312
  const status = result && typeof result === "object" && !Array.isArray(result) && "error" in result ? "error" : "ok";
1245
1313
  const error = status === "error" && result && typeof result === "object" && !Array.isArray(result) ? formatErrorMessage(result.error ?? "unknown") : void 0;
1246
1314
  console.log(`[voice-call] realtime tool call completed callId=${callId} tool=${name} status=${status} elapsedMs=${Date.now() - startedAt}${error ? ` error=${error}` : ""}`);
1247
- await submitFinalToolResult(result);
1315
+ submitFinalToolResult(result);
1248
1316
  if (name === REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME && status === "ok") this.consumePartialUserTranscript(callId, context.partialUserTranscript);
1249
1317
  }
1250
1318
  };