@openclaw/voice-call 2026.7.2-beta.2 → 2026.7.2-beta.3
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.
- package/dist/doctor-contract-api.js +1 -0
- package/dist/{guarded-json-api-BKOgUU1q.js → guarded-json-api-Dm4NvtGo.js} +1 -1
- package/dist/index.js +1 -1
- package/dist/{plivo-ByAIoPFp.js → plivo-rISw-5A8.js} +2 -2
- package/dist/{realtime-handler-DJk-eYyf.js → realtime-handler-HSxcgwY_.js} +91 -165
- package/dist/{response-generator-Cdj8EuLg.js → response-generator-r9wHPOEl.js} +1 -1
- package/dist/{runtime-entry-DSgyqIwE.js → runtime-entry-D8Mkwy8G.js} +66 -145
- package/dist/runtime-entry.js +1 -1
- package/dist/{telnyx-DMbLYwj4.js → telnyx-DTD9jTqC.js} +1 -1
- package/dist/{twilio-BTKKlkLT.js → twilio-GzbIb5fv.js} +9 -5
- package/npm-shrinkwrap.json +3 -3
- package/package.json +4 -4
|
@@ -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-
|
|
3
|
+
import { n as normalizeProxyIp, o as getHeader } from "./runtime-entry-D8Mkwy8G.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";
|
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-
|
|
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-D8Mkwy8G.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";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { g as escapeXml, o as getHeader } from "./runtime-entry-
|
|
2
|
-
import { a as reconstructWebhookUrl, o as verifyPlivoWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-
|
|
1
|
+
import { g as escapeXml, o as getHeader } from "./runtime-entry-D8Mkwy8G.js";
|
|
2
|
+
import { a as reconstructWebhookUrl, o as verifyPlivoWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-Dm4NvtGo.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
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
2
2
|
import { isFutureDateTimestampMs, resolveExpiresAtMsFromDurationMs } from "openclaw/plugin-sdk/number-runtime";
|
|
3
3
|
import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env";
|
|
4
|
-
import { REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME, buildRealtimeVoiceAgentConsultWorkingResponse,
|
|
4
|
+
import { REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME, buildRealtimeVoiceAgentConsultWorkingResponse, calculateMulawRms, createRealtimeVoiceSessionHarness, createSpeechThresholdGate, readRealtimeVoiceConsultQuestion, readSpeakableRealtimeVoiceToolResult } from "openclaw/plugin-sdk/realtime-voice";
|
|
5
5
|
import { normalizeWebhookPath } from "openclaw/plugin-sdk/webhook-ingress";
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
7
7
|
import { sliceUtf16Safe, truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
|
@@ -11,13 +11,7 @@ import WebSocket$1, { WebSocketServer } from "ws";
|
|
|
11
11
|
const TELEPHONY_SAMPLE_RATE = 8e3;
|
|
12
12
|
const TELEPHONY_CHUNK_BYTES = 160;
|
|
13
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;
|
|
17
14
|
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
15
|
/** Paces outgoing mulaw audio frames at telephony cadence. */
|
|
22
16
|
var RealtimeAudioPacer = class {
|
|
23
17
|
constructor(params) {
|
|
@@ -112,52 +106,6 @@ var RealtimeAudioPacer = class {
|
|
|
112
106
|
if (this.queue.length > 0) this.timer = setTimeout(() => this.pump(), delayMs);
|
|
113
107
|
}
|
|
114
108
|
};
|
|
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
109
|
//#endregion
|
|
162
110
|
//#region extensions/voice-call/src/webhook/stream-frame-adapter.ts
|
|
163
111
|
/** Parse numeric timestamps sent as numbers or integer strings. */
|
|
@@ -475,7 +423,6 @@ var RealtimeCallHandler = class {
|
|
|
475
423
|
this.partialUserTranscriptUpdatedAtByCallId = /* @__PURE__ */ new Map();
|
|
476
424
|
this.recentFinalUserTranscriptsByCallId = /* @__PURE__ */ new Map();
|
|
477
425
|
this.recentFinalUserTranscriptTimersByCallId = /* @__PURE__ */ new Map();
|
|
478
|
-
this.forcedConsultCoordinatorsByCallId = /* @__PURE__ */ new Map();
|
|
479
426
|
this.forcedConsultsByCallId = /* @__PURE__ */ new Map();
|
|
480
427
|
this.nativeConsultsInFlightByCallId = /* @__PURE__ */ new Map();
|
|
481
428
|
this.publicOrigin = null;
|
|
@@ -661,56 +608,51 @@ var RealtimeCallHandler = class {
|
|
|
661
608
|
}
|
|
662
609
|
const { callId, instructions, initialGreetingInstructions } = registration;
|
|
663
610
|
const callRecord = this.manager.getCallByProviderCallId(callSid);
|
|
664
|
-
const
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
callId,
|
|
697
|
-
providerCallId: callSid,
|
|
698
|
-
reason
|
|
699
|
-
} }));
|
|
700
|
-
};
|
|
611
|
+
const harness = createRealtimeVoiceSessionHarness({
|
|
612
|
+
talk: {
|
|
613
|
+
sessionId: `voice-call:${callId}:realtime`,
|
|
614
|
+
mode: "realtime",
|
|
615
|
+
transport: "gateway-relay",
|
|
616
|
+
brain: "agent-consult",
|
|
617
|
+
provider: this.realtimeProvider.id
|
|
618
|
+
},
|
|
619
|
+
talkPayloads: {
|
|
620
|
+
turnStarted: () => ({
|
|
621
|
+
callId,
|
|
622
|
+
providerCallId: callSid
|
|
623
|
+
}),
|
|
624
|
+
turnEnded: (reason) => ({
|
|
625
|
+
callId,
|
|
626
|
+
providerCallId: callSid,
|
|
627
|
+
reason
|
|
628
|
+
}),
|
|
629
|
+
inputAudioDelta: (audio) => ({ byteLength: audio.byteLength }),
|
|
630
|
+
outputAudioStarted: () => ({
|
|
631
|
+
callId,
|
|
632
|
+
providerCallId: callSid
|
|
633
|
+
}),
|
|
634
|
+
outputAudioDelta: (audio) => ({ byteLength: audio.byteLength }),
|
|
635
|
+
outputAudioDone: (reason) => ({
|
|
636
|
+
callId,
|
|
637
|
+
providerCallId: callSid,
|
|
638
|
+
reason
|
|
639
|
+
})
|
|
640
|
+
},
|
|
641
|
+
onTalkEvent: (event) => appendRecentTalkEventMetadata(callRecord, event)
|
|
642
|
+
});
|
|
701
643
|
const providerHandlesInputAudioBargeIn = this.realtimeProvider.capabilities?.handlesInputAudioBargeIn === true;
|
|
702
644
|
const cancelOutputAudioForBargeIn = (source, interruptProvider, clearedAudioBytes = 0) => {
|
|
703
|
-
const outputAudioActive = talk.outputAudioActive;
|
|
645
|
+
const outputAudioActive = harness.talk.outputAudioActive;
|
|
704
646
|
const pendingTelephonyAudio = audioPacer.hasPendingAudio();
|
|
705
647
|
if (source === "provider" && !outputAudioActive && !pendingTelephonyAudio && clearedAudioBytes === 0) return;
|
|
706
|
-
const interruptedTurnId = talk.activeTurnId;
|
|
648
|
+
const interruptedTurnId = harness.talk.activeTurnId;
|
|
707
649
|
interruptProvider?.(outputAudioActive || pendingTelephonyAudio);
|
|
708
650
|
const clearedBytes = clearedAudioBytes + (source === "local" || pendingTelephonyAudio ? audioPacer.clearAudio() : 0);
|
|
709
651
|
console.log(`[voice-call] realtime outbound audio cleared by ${source} barge-in callId=${callId} providerCallId=${callSid} queuedBytes=${clearedBytes}`);
|
|
710
652
|
if (!outputAudioActive || !interruptedTurnId) return;
|
|
711
653
|
const reason = `${source}-barge-in`;
|
|
712
|
-
finishOutputAudio(reason);
|
|
713
|
-
|
|
654
|
+
harness.finishOutputAudio(reason);
|
|
655
|
+
harness.talk.cancelTurn({
|
|
714
656
|
turnId: interruptedTurnId,
|
|
715
657
|
payload: {
|
|
716
658
|
callId,
|
|
@@ -718,9 +660,8 @@ var RealtimeCallHandler = class {
|
|
|
718
660
|
reason
|
|
719
661
|
}
|
|
720
662
|
});
|
|
721
|
-
if (cancelled.ok) rememberTalkEvent(cancelled.event);
|
|
722
663
|
};
|
|
723
|
-
|
|
664
|
+
harness.emit({
|
|
724
665
|
type: "session.started",
|
|
725
666
|
payload: {
|
|
726
667
|
callId,
|
|
@@ -762,9 +703,13 @@ var RealtimeCallHandler = class {
|
|
|
762
703
|
if (ws.readyState === WebSocket$1.OPEN) ws.close(1013, "Backpressure: paced audio queue exceeded");
|
|
763
704
|
}
|
|
764
705
|
});
|
|
765
|
-
const speechDetector =
|
|
706
|
+
const speechDetector = createSpeechThresholdGate({
|
|
707
|
+
rmsThreshold: .035,
|
|
708
|
+
speechFrames: BARGE_IN_REQUIRED_LOUD_CHUNKS,
|
|
709
|
+
silenceFrames: 12
|
|
710
|
+
});
|
|
766
711
|
const interruptResponseOnInputAudio = typeof this.providerConfig.interruptResponseOnInputAudio === "boolean" ? this.providerConfig.interruptResponseOnInputAudio : void 0;
|
|
767
|
-
const session =
|
|
712
|
+
const session = harness.createBridge({
|
|
768
713
|
provider: this.realtimeProvider,
|
|
769
714
|
cfg: this.coreConfig,
|
|
770
715
|
providerConfig: this.providerConfig,
|
|
@@ -776,46 +721,38 @@ var RealtimeCallHandler = class {
|
|
|
776
721
|
audioSink: {
|
|
777
722
|
isOpen: () => ws.readyState === WebSocket$1.OPEN,
|
|
778
723
|
sendAudio: (muLaw) => {
|
|
779
|
-
|
|
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
|
-
});
|
|
724
|
+
harness.recordOutputAudio(muLaw);
|
|
792
725
|
audioPacer.sendAudio(muLaw);
|
|
793
726
|
},
|
|
794
727
|
clearAudio: (reason) => {
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
728
|
+
harness.flushOutput(() => {
|
|
729
|
+
const clearedBytes = audioPacer.clearAudio();
|
|
730
|
+
if (reason === "barge-in") {
|
|
731
|
+
cancelOutputAudioForBargeIn("provider", void 0, clearedBytes);
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
console.log(`[voice-call] realtime outbound audio clear requested callId=${callId} providerCallId=${callSid} queuedBytes=${clearedBytes}`);
|
|
735
|
+
harness.finishOutputAudio("clear");
|
|
736
|
+
});
|
|
802
737
|
},
|
|
803
738
|
sendMark: (markName) => {
|
|
804
739
|
audioPacer.sendMark(markName);
|
|
805
740
|
}
|
|
806
741
|
},
|
|
807
742
|
onTranscript: (role, text, isFinal) => {
|
|
808
|
-
const turnId =
|
|
809
|
-
|
|
810
|
-
|
|
743
|
+
const turnId = harness.ensureTurn();
|
|
744
|
+
const eventType = role === "assistant" ? isFinal ? "output.text.done" : "output.text.delta" : isFinal ? "transcript.done" : "transcript.delta";
|
|
745
|
+
const payload = role === "assistant" ? { text } : {
|
|
746
|
+
role,
|
|
747
|
+
text
|
|
748
|
+
};
|
|
749
|
+
harness.emit({
|
|
750
|
+
type: eventType,
|
|
811
751
|
turnId,
|
|
812
|
-
payload
|
|
813
|
-
role,
|
|
814
|
-
text
|
|
815
|
-
},
|
|
752
|
+
payload,
|
|
816
753
|
final: isFinal
|
|
817
754
|
});
|
|
818
|
-
if (role === "user" && isFinal)
|
|
755
|
+
if (role === "user" && isFinal) harness.emit({
|
|
819
756
|
type: "input.audio.committed",
|
|
820
757
|
turnId,
|
|
821
758
|
payload: {
|
|
@@ -851,6 +788,7 @@ var RealtimeCallHandler = class {
|
|
|
851
788
|
};
|
|
852
789
|
this.manager.processEvent(event);
|
|
853
790
|
this.scheduleForcedAgentConsult({
|
|
791
|
+
harness,
|
|
854
792
|
session,
|
|
855
793
|
callId,
|
|
856
794
|
callSid,
|
|
@@ -872,8 +810,8 @@ var RealtimeCallHandler = class {
|
|
|
872
810
|
});
|
|
873
811
|
},
|
|
874
812
|
onToolCall: (toolEvent, sessionLocal) => {
|
|
875
|
-
const turnId =
|
|
876
|
-
|
|
813
|
+
const turnId = harness.ensureTurn();
|
|
814
|
+
harness.emit({
|
|
877
815
|
type: "tool.call",
|
|
878
816
|
turnId,
|
|
879
817
|
itemId: toolEvent.itemId,
|
|
@@ -884,17 +822,17 @@ var RealtimeCallHandler = class {
|
|
|
884
822
|
}
|
|
885
823
|
});
|
|
886
824
|
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,
|
|
825
|
+
return this.executeToolCall(sessionLocal, callId, toolEvent.callId || toolEvent.itemId, toolEvent.name, toolEvent.args, turnId, harness);
|
|
888
826
|
},
|
|
889
827
|
onEvent: (event) => {
|
|
890
828
|
if (event.type === "input_audio_buffer.speech_started") {
|
|
891
|
-
|
|
829
|
+
harness.ensureTurn();
|
|
892
830
|
return;
|
|
893
831
|
}
|
|
894
832
|
if (event.type === "input_audio_buffer.speech_stopped") {
|
|
895
|
-
const turnId = talk.activeTurnId;
|
|
833
|
+
const turnId = harness.talk.activeTurnId;
|
|
896
834
|
if (!turnId) return;
|
|
897
|
-
|
|
835
|
+
harness.emit({
|
|
898
836
|
type: "input.audio.committed",
|
|
899
837
|
turnId,
|
|
900
838
|
payload: {
|
|
@@ -907,18 +845,18 @@ var RealtimeCallHandler = class {
|
|
|
907
845
|
return;
|
|
908
846
|
}
|
|
909
847
|
if (event.type === "response.done") {
|
|
910
|
-
finishOutputAudio("response.done");
|
|
911
|
-
|
|
848
|
+
harness.finishOutputAudio("response.done");
|
|
849
|
+
harness.endTurn("response.done");
|
|
912
850
|
return;
|
|
913
851
|
}
|
|
914
|
-
if (event.type === "error")
|
|
852
|
+
if (event.type === "error") harness.emit({
|
|
915
853
|
type: "session.error",
|
|
916
854
|
payload: { message: event.detail ?? "Realtime provider error" },
|
|
917
855
|
final: true
|
|
918
856
|
});
|
|
919
857
|
},
|
|
920
858
|
onReady: () => {
|
|
921
|
-
|
|
859
|
+
harness.emit({
|
|
922
860
|
type: "session.ready",
|
|
923
861
|
payload: {
|
|
924
862
|
callId,
|
|
@@ -928,7 +866,7 @@ var RealtimeCallHandler = class {
|
|
|
928
866
|
},
|
|
929
867
|
onError: (error) => {
|
|
930
868
|
console.error("[voice-call] realtime voice error:", error.message);
|
|
931
|
-
|
|
869
|
+
harness.emit({
|
|
932
870
|
type: "session.error",
|
|
933
871
|
payload: { message: error.message },
|
|
934
872
|
final: true
|
|
@@ -940,8 +878,8 @@ var RealtimeCallHandler = class {
|
|
|
940
878
|
this.activeTelephonyClosersByCallId.delete(callId);
|
|
941
879
|
this.activeTelephonyClosersByCallId.delete(callSid);
|
|
942
880
|
this.clearUserTranscriptState(callId);
|
|
943
|
-
finishOutputAudio(reason);
|
|
944
|
-
|
|
881
|
+
harness.finishOutputAudio(reason);
|
|
882
|
+
harness.emit({
|
|
945
883
|
type: "session.closed",
|
|
946
884
|
payload: { reason },
|
|
947
885
|
final: true
|
|
@@ -971,17 +909,16 @@ var RealtimeCallHandler = class {
|
|
|
971
909
|
this.activeTelephonyClosersByCallId.set(callSid, closeTelephony);
|
|
972
910
|
const sendAudioToSession = session.sendAudio.bind(session);
|
|
973
911
|
session.sendAudio = (audio) => {
|
|
974
|
-
if (speechDetector.accept(
|
|
912
|
+
if (speechDetector.accept({
|
|
913
|
+
rms: calculateMulawRms(audio),
|
|
914
|
+
peak: 0
|
|
915
|
+
})) {
|
|
975
916
|
console.log(`[voice-call] realtime local speech detected callId=${callId} providerCallId=${callSid}`);
|
|
976
917
|
if (!providerHandlesInputAudioBargeIn) cancelOutputAudioForBargeIn("local", (audioPlaybackActive) => {
|
|
977
918
|
session.handleBargeIn({ audioPlaybackActive });
|
|
978
919
|
});
|
|
979
920
|
}
|
|
980
|
-
|
|
981
|
-
type: "input.audio.delta",
|
|
982
|
-
turnId: ensureTalkTurn(),
|
|
983
|
-
payload: { byteLength: audio.length }
|
|
984
|
-
});
|
|
921
|
+
harness.recordInputAudio(audio);
|
|
985
922
|
sendAudioToSession(audio);
|
|
986
923
|
};
|
|
987
924
|
const closeSession = session.close.bind(session);
|
|
@@ -997,7 +934,8 @@ var RealtimeCallHandler = class {
|
|
|
997
934
|
this.activeTelephonyClosersByCallId.delete(callId);
|
|
998
935
|
this.activeTelephonyClosersByCallId.delete(callSid);
|
|
999
936
|
this.clearUserTranscriptState(callId);
|
|
1000
|
-
this.
|
|
937
|
+
this.forcedConsultsByCallId.delete(callId);
|
|
938
|
+
harness.close();
|
|
1001
939
|
audioPacer.close();
|
|
1002
940
|
}
|
|
1003
941
|
};
|
|
@@ -1080,18 +1018,6 @@ var RealtimeCallHandler = class {
|
|
|
1080
1018
|
});
|
|
1081
1019
|
}
|
|
1082
1020
|
}
|
|
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
1021
|
closeTelephonyBridge(callIdOrSid, bridge, reason) {
|
|
1096
1022
|
const closer = this.activeTelephonyClosersByCallId.get(callIdOrSid);
|
|
1097
1023
|
if (closer) {
|
|
@@ -1108,7 +1034,7 @@ var RealtimeCallHandler = class {
|
|
|
1108
1034
|
if (!handler) return;
|
|
1109
1035
|
const existingForcedConsult = this.forcedConsultsByCallId.get(params.callId);
|
|
1110
1036
|
if (existingForcedConsult && !existingForcedConsult.completedAt) return;
|
|
1111
|
-
const coordinator =
|
|
1037
|
+
const coordinator = params.harness.forcedConsults;
|
|
1112
1038
|
if (coordinator.hasRecentNativeConsult(question, { allowUnknownQuestion: true })) return;
|
|
1113
1039
|
coordinator.clearPending();
|
|
1114
1040
|
const pending = coordinator.prepare(question);
|
|
@@ -1124,7 +1050,7 @@ var RealtimeCallHandler = class {
|
|
|
1124
1050
|
});
|
|
1125
1051
|
}
|
|
1126
1052
|
async runForcedAgentConsult(params) {
|
|
1127
|
-
const coordinator =
|
|
1053
|
+
const coordinator = params.harness.forcedConsults;
|
|
1128
1054
|
coordinator.markStarted(params.handle);
|
|
1129
1055
|
const startedAt = Date.now();
|
|
1130
1056
|
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 +1142,14 @@ var RealtimeCallHandler = class {
|
|
|
1216
1142
|
reason
|
|
1217
1143
|
});
|
|
1218
1144
|
}
|
|
1219
|
-
async executeToolCall(bridge, callId, bridgeCallId, name, args, turnId,
|
|
1145
|
+
async executeToolCall(bridge, callId, bridgeCallId, name, args, turnId, harness) {
|
|
1220
1146
|
const handler = this.toolHandlers.get(name);
|
|
1221
1147
|
const startedAt = Date.now();
|
|
1222
1148
|
const hasResultError = (result) => {
|
|
1223
1149
|
return Boolean(result && typeof result === "object" && !Array.isArray(result) && "error" in result);
|
|
1224
1150
|
};
|
|
1225
1151
|
const emitFinalToolEvent = (result) => {
|
|
1226
|
-
|
|
1152
|
+
harness.emit({
|
|
1227
1153
|
type: hasResultError(result) ? "tool.error" : "tool.result",
|
|
1228
1154
|
turnId,
|
|
1229
1155
|
callId: bridgeCallId,
|
|
@@ -1241,7 +1167,7 @@ var RealtimeCallHandler = class {
|
|
|
1241
1167
|
const submitWorkingResponse = async () => {
|
|
1242
1168
|
if (handler && name === REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME && bridge.bridge.supportsToolResultContinuation && !this.config.fastContext.enabled) {
|
|
1243
1169
|
await bridge.submitToolResult(bridgeCallId, buildRealtimeVoiceAgentConsultWorkingResponse("caller"), { willContinue: true });
|
|
1244
|
-
|
|
1170
|
+
harness.emit({
|
|
1245
1171
|
type: "tool.progress",
|
|
1246
1172
|
turnId,
|
|
1247
1173
|
callId: bridgeCallId,
|
|
@@ -1253,7 +1179,7 @@ var RealtimeCallHandler = class {
|
|
|
1253
1179
|
}
|
|
1254
1180
|
};
|
|
1255
1181
|
if (name === REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME) {
|
|
1256
|
-
const coordinator =
|
|
1182
|
+
const coordinator = harness.forcedConsults;
|
|
1257
1183
|
const forcedMatch = coordinator.recordNativeConsult(args, bridgeCallId);
|
|
1258
1184
|
if (forcedMatch.kind === "none") {
|
|
1259
1185
|
const pending = coordinator.consumePending();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { b as resolveVoiceCallSessionKey, m as resolveCallAgentId, p as resolveVoiceResponseModel } from "./runtime-entry-
|
|
1
|
+
import { b as resolveVoiceCallSessionKey, m as resolveCallAgentId, p as resolveVoiceResponseModel } from "./runtime-entry-D8Mkwy8G.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";
|
|
@@ -23,7 +23,6 @@ import path from "node:path";
|
|
|
23
23
|
import os from "node:os";
|
|
24
24
|
import { root } from "openclaw/plugin-sdk/security-runtime";
|
|
25
25
|
import { sliceUtf16Safe, truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
|
26
|
-
import { parseTtsDirectives } from "openclaw/plugin-sdk/speech";
|
|
27
26
|
import { spawn } from "node:child_process";
|
|
28
27
|
import { runCommandWithTimeout } from "openclaw/plugin-sdk/process-runtime";
|
|
29
28
|
import http from "node:http";
|
|
@@ -1913,26 +1912,26 @@ function chunkAudio(audio, chunkSize = 160) {
|
|
|
1913
1912
|
/** Default timeout for one telephony synthesis request. */
|
|
1914
1913
|
const TELEPHONY_DEFAULT_TTS_TIMEOUT_MS = 8e3;
|
|
1915
1914
|
/** Create a TTS provider that honors voice-call overrides and converts PCM to mulaw. */
|
|
1916
|
-
function createTelephonyTtsProvider(params) {
|
|
1915
|
+
async function createTelephonyTtsProvider(params) {
|
|
1917
1916
|
const { coreConfig, ttsOverride, runtime, logger } = params;
|
|
1918
|
-
const
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1917
|
+
const preparedConfig = await runtime.prepareTtsRequest({
|
|
1918
|
+
cfg: coreConfig,
|
|
1919
|
+
override: ttsOverride,
|
|
1920
|
+
text: ""
|
|
1921
|
+
});
|
|
1923
1922
|
return {
|
|
1924
|
-
synthesisTimeoutMs: resolveTimerTimeoutMs(
|
|
1923
|
+
synthesisTimeoutMs: resolveTimerTimeoutMs(preparedConfig.cfg.messages?.tts?.timeoutMs, TELEPHONY_DEFAULT_TTS_TIMEOUT_MS),
|
|
1925
1924
|
synthesizeForTelephony: async (text) => {
|
|
1926
|
-
const
|
|
1927
|
-
cfg:
|
|
1928
|
-
|
|
1929
|
-
preferredProviderId: activeProvider
|
|
1925
|
+
const prepared = await runtime.prepareTtsRequest({
|
|
1926
|
+
cfg: preparedConfig.cfg,
|
|
1927
|
+
text
|
|
1930
1928
|
});
|
|
1929
|
+
const directives = prepared.directives;
|
|
1931
1930
|
if (directives.warnings.length > 0) logger?.warn?.(`[voice-call] Ignored telephony TTS directive overrides (${directives.warnings.join("; ")})`);
|
|
1932
1931
|
const cleanText = directives.hasDirective ? directives.ttsText?.trim() || directives.cleanedText.trim() : text;
|
|
1933
1932
|
const result = await runtime.textToSpeechTelephony({
|
|
1934
1933
|
text: cleanText,
|
|
1935
|
-
cfg:
|
|
1934
|
+
cfg: prepared.cfg,
|
|
1936
1935
|
overrides: directives.overrides
|
|
1937
1936
|
});
|
|
1938
1937
|
if (!result.success || !result.audioBuffer || !result.sampleRate) throw new Error(result.error ?? "TTS conversion failed");
|
|
@@ -1944,94 +1943,6 @@ function createTelephonyTtsProvider(params) {
|
|
|
1944
1943
|
}
|
|
1945
1944
|
};
|
|
1946
1945
|
}
|
|
1947
|
-
/** Apply voice-call TTS overrides to core config without mutating the original object. */
|
|
1948
|
-
function applyTtsOverride(coreConfig, override) {
|
|
1949
|
-
if (!override) return coreConfig;
|
|
1950
|
-
const base = coreConfig.messages?.tts;
|
|
1951
|
-
const merged = mergeTtsConfig(base, override);
|
|
1952
|
-
if (!merged) return coreConfig;
|
|
1953
|
-
return {
|
|
1954
|
-
...coreConfig,
|
|
1955
|
-
messages: {
|
|
1956
|
-
...coreConfig.messages,
|
|
1957
|
-
tts: merged
|
|
1958
|
-
}
|
|
1959
|
-
};
|
|
1960
|
-
}
|
|
1961
|
-
/** Merge core and voice-call TTS config, keeping undefined override fields out. */
|
|
1962
|
-
function mergeTtsConfig(base, override) {
|
|
1963
|
-
if (!base && !override) return;
|
|
1964
|
-
if (!override) return base;
|
|
1965
|
-
if (!base) return override;
|
|
1966
|
-
return mergeDeep(base, override);
|
|
1967
|
-
}
|
|
1968
|
-
/** Resolve directive override policy for telephony synthesis. */
|
|
1969
|
-
function resolveTelephonyModelOverridePolicy(overrides) {
|
|
1970
|
-
if (!(overrides?.enabled ?? true)) return {
|
|
1971
|
-
enabled: false,
|
|
1972
|
-
allowText: false,
|
|
1973
|
-
allowProvider: false,
|
|
1974
|
-
allowVoice: false,
|
|
1975
|
-
allowModelId: false,
|
|
1976
|
-
allowVoiceSettings: false,
|
|
1977
|
-
allowNormalization: false,
|
|
1978
|
-
allowSeed: false
|
|
1979
|
-
};
|
|
1980
|
-
const allow = (value, defaultValue = true) => value ?? defaultValue;
|
|
1981
|
-
return {
|
|
1982
|
-
enabled: true,
|
|
1983
|
-
allowText: allow(overrides?.allowText),
|
|
1984
|
-
allowProvider: allow(overrides?.allowProvider, false),
|
|
1985
|
-
allowVoice: allow(overrides?.allowVoice),
|
|
1986
|
-
allowModelId: allow(overrides?.allowModelId),
|
|
1987
|
-
allowVoiceSettings: allow(overrides?.allowVoiceSettings),
|
|
1988
|
-
allowNormalization: allow(overrides?.allowNormalization),
|
|
1989
|
-
allowSeed: allow(overrides?.allowSeed)
|
|
1990
|
-
};
|
|
1991
|
-
}
|
|
1992
|
-
/** Read model override policy from TTS config when present. */
|
|
1993
|
-
function readTelephonyModelOverrides(ttsConfig) {
|
|
1994
|
-
const value = ttsConfig?.modelOverrides;
|
|
1995
|
-
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
1996
|
-
}
|
|
1997
|
-
/** Normalize provider ids for config lookup. */
|
|
1998
|
-
function normalizeProviderId(value) {
|
|
1999
|
-
return typeof value === "string" ? value.trim().toLowerCase() || void 0 : void 0;
|
|
2000
|
-
}
|
|
2001
|
-
/** Coerce provider config objects while rejecting arrays and primitives. */
|
|
2002
|
-
function asProviderConfig(value) {
|
|
2003
|
-
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
2004
|
-
}
|
|
2005
|
-
/** Collect named provider configs from canonical and legacy TTS config shapes. */
|
|
2006
|
-
function collectTelephonyProviderConfigs(ttsConfig) {
|
|
2007
|
-
if (!ttsConfig) return {};
|
|
2008
|
-
const entries = {};
|
|
2009
|
-
const rawProviders = ttsConfig.providers && typeof ttsConfig.providers === "object" && !Array.isArray(ttsConfig.providers) ? ttsConfig.providers : {};
|
|
2010
|
-
for (const [providerId, value] of Object.entries(rawProviders)) {
|
|
2011
|
-
const normalized = normalizeProviderId(providerId) ?? providerId;
|
|
2012
|
-
entries[normalized] = asProviderConfig(value);
|
|
2013
|
-
}
|
|
2014
|
-
const reservedKeys = /* @__PURE__ */ new Set([
|
|
2015
|
-
"auto",
|
|
2016
|
-
"enabled",
|
|
2017
|
-
"maxTextLength",
|
|
2018
|
-
"mode",
|
|
2019
|
-
"modelOverrides",
|
|
2020
|
-
"persona",
|
|
2021
|
-
"personas",
|
|
2022
|
-
"prefsPath",
|
|
2023
|
-
"provider",
|
|
2024
|
-
"providers",
|
|
2025
|
-
"summaryModel",
|
|
2026
|
-
"timeoutMs"
|
|
2027
|
-
]);
|
|
2028
|
-
for (const [key, value] of Object.entries(ttsConfig)) {
|
|
2029
|
-
if (reservedKeys.has(key) || typeof value !== "object" || value === null || Array.isArray(value)) continue;
|
|
2030
|
-
const normalized = normalizeProviderId(key) ?? key;
|
|
2031
|
-
entries[normalized] ??= asProviderConfig(value);
|
|
2032
|
-
}
|
|
2033
|
-
return entries;
|
|
2034
|
-
}
|
|
2035
1946
|
//#endregion
|
|
2036
1947
|
//#region extensions/voice-call/src/bounded-child-output.ts
|
|
2037
1948
|
const DEFAULT_MAX_OUTPUT_CHARS = 16384;
|
|
@@ -2159,6 +2070,30 @@ async function cleanupTailscaleExposure(config) {
|
|
|
2159
2070
|
const NGROK_LOG_BUFFER_MAX_CHARS = 16384;
|
|
2160
2071
|
const NGROK_ERROR_MARKER = "ERR_NGROK";
|
|
2161
2072
|
const TUNNEL_COMMAND_OUTPUT_MAX_BYTES = 16384;
|
|
2073
|
+
const NGROK_STOP_GRACE_MS = 2e3;
|
|
2074
|
+
const NGROK_FORCE_KILL_WAIT_MS = 1e3;
|
|
2075
|
+
async function terminateNgrokProcess(proc, isClosed) {
|
|
2076
|
+
if (isClosed()) return;
|
|
2077
|
+
await new Promise((resolve) => {
|
|
2078
|
+
let finished = false;
|
|
2079
|
+
let forceKillWaitTimer;
|
|
2080
|
+
const finish = () => {
|
|
2081
|
+
if (finished) return;
|
|
2082
|
+
finished = true;
|
|
2083
|
+
if (forceKillTimer) clearTimeout(forceKillTimer);
|
|
2084
|
+
if (forceKillWaitTimer) clearTimeout(forceKillWaitTimer);
|
|
2085
|
+
proc.off("close", finish);
|
|
2086
|
+
resolve();
|
|
2087
|
+
};
|
|
2088
|
+
proc.once("close", finish);
|
|
2089
|
+
const forceKillTimer = setTimeout(() => {
|
|
2090
|
+
forceKillWaitTimer = setTimeout(finish, NGROK_FORCE_KILL_WAIT_MS);
|
|
2091
|
+
if (!isClosed()) proc.kill("SIGKILL");
|
|
2092
|
+
}, NGROK_STOP_GRACE_MS);
|
|
2093
|
+
proc.kill("SIGTERM");
|
|
2094
|
+
if (isClosed()) finish();
|
|
2095
|
+
});
|
|
2096
|
+
}
|
|
2162
2097
|
function listenForChildStreamErrors(proc, onError) {
|
|
2163
2098
|
proc.stdout.on("error", (error) => onError("stdout", error));
|
|
2164
2099
|
proc.stderr.on("error", (error) => onError("stderr", error));
|
|
@@ -2194,23 +2129,25 @@ async function startNgrokTunnel(config) {
|
|
|
2194
2129
|
"pipe",
|
|
2195
2130
|
"pipe"
|
|
2196
2131
|
] });
|
|
2197
|
-
let
|
|
2198
|
-
let
|
|
2132
|
+
let startupSettled = false;
|
|
2133
|
+
let childClosed = false;
|
|
2199
2134
|
let publicUrl = null;
|
|
2200
2135
|
let outputBuffer = "";
|
|
2201
2136
|
let stderrTail = "";
|
|
2202
2137
|
const timeout = setTimeout(() => {
|
|
2203
|
-
if (!
|
|
2204
|
-
|
|
2205
|
-
proc.
|
|
2206
|
-
|
|
2138
|
+
if (!startupSettled) {
|
|
2139
|
+
startupSettled = true;
|
|
2140
|
+
terminateNgrokProcess(proc, () => childClosed).then(() => {
|
|
2141
|
+
reject(/* @__PURE__ */ new Error("ngrok startup timed out (30s)"));
|
|
2142
|
+
});
|
|
2207
2143
|
}
|
|
2208
2144
|
}, 3e4);
|
|
2145
|
+
timeout.unref();
|
|
2209
2146
|
const rejectIfPending = (message, kill = false) => {
|
|
2210
|
-
if (!
|
|
2211
|
-
|
|
2147
|
+
if (!startupSettled) {
|
|
2148
|
+
startupSettled = true;
|
|
2212
2149
|
clearTimeout(timeout);
|
|
2213
|
-
if (kill && !
|
|
2150
|
+
if (kill && !childClosed) proc.kill("SIGKILL");
|
|
2214
2151
|
reject(new Error(message));
|
|
2215
2152
|
}
|
|
2216
2153
|
};
|
|
@@ -2219,8 +2156,8 @@ async function startNgrokTunnel(config) {
|
|
|
2219
2156
|
const log = JSON.parse(line);
|
|
2220
2157
|
if (log.msg === "started tunnel" && log.url) publicUrl = log.url;
|
|
2221
2158
|
if (log.addr && log.url && !publicUrl) publicUrl = log.url;
|
|
2222
|
-
if (publicUrl && !
|
|
2223
|
-
|
|
2159
|
+
if (publicUrl && !startupSettled) {
|
|
2160
|
+
startupSettled = true;
|
|
2224
2161
|
clearTimeout(timeout);
|
|
2225
2162
|
const fullUrl = publicUrl + config.path;
|
|
2226
2163
|
console.log(`[voice-call] ngrok tunnel active: ${fullUrl}`);
|
|
@@ -2228,38 +2165,22 @@ async function startNgrokTunnel(config) {
|
|
|
2228
2165
|
publicUrl: fullUrl,
|
|
2229
2166
|
provider: "ngrok",
|
|
2230
2167
|
stop: async () => {
|
|
2231
|
-
|
|
2232
|
-
await new Promise((res) => {
|
|
2233
|
-
let finished = false;
|
|
2234
|
-
const finish = () => {
|
|
2235
|
-
if (finished) return;
|
|
2236
|
-
finished = true;
|
|
2237
|
-
clearTimeout(fallback);
|
|
2238
|
-
proc.off("close", finish);
|
|
2239
|
-
res();
|
|
2240
|
-
};
|
|
2241
|
-
if (closed) {
|
|
2242
|
-
res();
|
|
2243
|
-
return;
|
|
2244
|
-
}
|
|
2245
|
-
proc.once("close", finish);
|
|
2246
|
-
const fallback = setTimeout(finish, 2e3);
|
|
2247
|
-
proc.kill("SIGTERM");
|
|
2248
|
-
if (closed) finish();
|
|
2249
|
-
});
|
|
2168
|
+
await terminateNgrokProcess(proc, () => childClosed);
|
|
2250
2169
|
}
|
|
2251
2170
|
});
|
|
2252
2171
|
}
|
|
2253
2172
|
} catch {}
|
|
2254
2173
|
};
|
|
2255
|
-
proc.stdout.
|
|
2256
|
-
|
|
2174
|
+
proc.stdout.setEncoding("utf8");
|
|
2175
|
+
proc.stderr.setEncoding("utf8");
|
|
2176
|
+
proc.stdout.on("data", (chunk) => {
|
|
2177
|
+
const lines = (outputBuffer + chunk).split("\n");
|
|
2257
2178
|
outputBuffer = lines.pop() || "";
|
|
2258
2179
|
if (outputBuffer.length > NGROK_LOG_BUFFER_MAX_CHARS) outputBuffer = sliceUtf16Safe(outputBuffer, -16384);
|
|
2259
2180
|
for (const line of lines) if (line.trim()) processLine(line);
|
|
2260
2181
|
});
|
|
2261
|
-
proc.stderr.on("data", (
|
|
2262
|
-
const combined = stderrTail +
|
|
2182
|
+
proc.stderr.on("data", (chunk) => {
|
|
2183
|
+
const combined = stderrTail + chunk;
|
|
2263
2184
|
if (combined.includes(NGROK_ERROR_MARKER)) rejectIfPending(`ngrok error: ${formatBoundedChildOutput(appendBoundedChildOutput(emptyBoundedChildOutput(), combined))}`, true);
|
|
2264
2185
|
stderrTail = sliceUtf16Safe(combined, -8);
|
|
2265
2186
|
});
|
|
@@ -2270,9 +2191,9 @@ async function startNgrokTunnel(config) {
|
|
|
2270
2191
|
rejectIfPending(`Failed to start ngrok: ${err.message}`);
|
|
2271
2192
|
});
|
|
2272
2193
|
proc.on("close", (code) => {
|
|
2273
|
-
|
|
2274
|
-
if (!
|
|
2275
|
-
|
|
2194
|
+
childClosed = true;
|
|
2195
|
+
if (!startupSettled) {
|
|
2196
|
+
startupSettled = true;
|
|
2276
2197
|
clearTimeout(timeout);
|
|
2277
2198
|
reject(/* @__PURE__ */ new Error(`ngrok exited unexpectedly with code ${code}`));
|
|
2278
2199
|
}
|
|
@@ -3141,7 +3062,7 @@ const WEBHOOK_BODY_TIMEOUT_MS = WEBHOOK_BODY_READ_DEFAULTS.preAuth.timeoutMs;
|
|
|
3141
3062
|
const MISSING_REMOTE_ADDRESS_IN_FLIGHT_KEY = "__voice_call_no_remote__";
|
|
3142
3063
|
const STREAM_DISCONNECT_HANGUP_GRACE_MS = 2e3;
|
|
3143
3064
|
const loadRealtimeTranscriptionRuntime = createLazyRuntimeModule(() => import("./realtime-transcription.runtime-CbJAs5t_.js"));
|
|
3144
|
-
const loadResponseGeneratorModule = createLazyRuntimeModule(() => import("./response-generator-
|
|
3065
|
+
const loadResponseGeneratorModule = createLazyRuntimeModule(() => import("./response-generator-r9wHPOEl.js"));
|
|
3145
3066
|
function appendRecentTalkEventMetadata(call, event) {
|
|
3146
3067
|
const metadata = call.metadata ?? {};
|
|
3147
3068
|
const recent = Array.isArray(metadata.recentTalkEvents) ? metadata.recentTalkEvents.filter((entry) => Boolean(entry) && typeof entry === "object" && !Array.isArray(entry)) : [];
|
|
@@ -3816,12 +3737,12 @@ const REALTIME_VOICE_CONSULT_SYSTEM_PROMPT = [
|
|
|
3816
3737
|
"Do not print secret values or dump environment variables; only check whether required configuration is present.",
|
|
3817
3738
|
"Be accurate, brief, and speakable."
|
|
3818
3739
|
].join(" ");
|
|
3819
|
-
const loadTelnyxProvider = createLazyRuntimeModule(() => import("./telnyx-
|
|
3820
|
-
const loadTwilioProvider = createLazyRuntimeModule(() => import("./twilio-
|
|
3821
|
-
const loadPlivoProvider = createLazyRuntimeModule(() => import("./plivo-
|
|
3740
|
+
const loadTelnyxProvider = createLazyRuntimeModule(() => import("./telnyx-DTD9jTqC.js"));
|
|
3741
|
+
const loadTwilioProvider = createLazyRuntimeModule(() => import("./twilio-GzbIb5fv.js"));
|
|
3742
|
+
const loadPlivoProvider = createLazyRuntimeModule(() => import("./plivo-rISw-5A8.js"));
|
|
3822
3743
|
const loadMockProvider = createLazyRuntimeModule(() => import("./mock-YsvBTX-7.js"));
|
|
3823
3744
|
const loadRealtimeVoiceRuntime = createLazyRuntimeModule(() => import("./realtime-voice.runtime-vtdCOWg-.js"));
|
|
3824
|
-
const loadRealtimeHandler = createLazyRuntimeModule(() => import("./realtime-handler-
|
|
3745
|
+
const loadRealtimeHandler = createLazyRuntimeModule(() => import("./realtime-handler-HSxcgwY_.js"));
|
|
3825
3746
|
function resolveVoiceCallConsultSessionKey(call) {
|
|
3826
3747
|
return resolveVoiceCallSessionKey({
|
|
3827
3748
|
config: call.config,
|
|
@@ -4101,8 +4022,8 @@ async function createVoiceCallRuntime(params) {
|
|
|
4101
4022
|
if (provider.name === "twilio" && config.streaming?.enabled) {
|
|
4102
4023
|
const twilioProvider = provider;
|
|
4103
4024
|
if (ttsRuntime?.textToSpeechTelephony) try {
|
|
4104
|
-
const ttsProvider = createTelephonyTtsProvider({
|
|
4105
|
-
coreConfig,
|
|
4025
|
+
const ttsProvider = await createTelephonyTtsProvider({
|
|
4026
|
+
coreConfig: cfg,
|
|
4106
4027
|
ttsOverride: config.tts,
|
|
4107
4028
|
runtime: ttsRuntime,
|
|
4108
4029
|
logger: log
|
package/dist/runtime-entry.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as createVoiceCallRuntime } from "./runtime-entry-
|
|
1
|
+
import { t as createVoiceCallRuntime } from "./runtime-entry-D8Mkwy8G.js";
|
|
2
2
|
export { createVoiceCallRuntime };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { s as verifyTelnyxWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-
|
|
1
|
+
import { s as verifyTelnyxWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-Dm4NvtGo.js";
|
|
2
2
|
import crypto from "node:crypto";
|
|
3
3
|
//#region extensions/voice-call/src/providers/telnyx.ts
|
|
4
4
|
function normalizeTelnyxDirection(direction) {
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { fetchWithSsrFGuard } from "./runtime-api.js";
|
|
2
2
|
import "./api.js";
|
|
3
3
|
import { n as requireSupportedTwilioApiHostname, r as resolveTwilioApiBaseUrl } from "./twilio-region-4fkgz3UG.js";
|
|
4
|
-
import { _ as mapVoiceToPolly, a as normalizeProviderStatus, f as chunkAudio, g as escapeXml, i as mapProviderStatusToEndReason, o as getHeader, r as isProviderStatusTerminal } from "./runtime-entry-
|
|
5
|
-
import { c as verifyTwilioWebhook, i as readProviderJsonResponseText, n as cancelProviderResponseBody, r as readProviderErrorResponseSnippet, t as guardedJsonApiRequest } from "./guarded-json-api-
|
|
4
|
+
import { _ as mapVoiceToPolly, a as normalizeProviderStatus, f as chunkAudio, g as escapeXml, i as mapProviderStatusToEndReason, o as getHeader, r as isProviderStatusTerminal } from "./runtime-entry-D8Mkwy8G.js";
|
|
5
|
+
import { c as verifyTwilioWebhook, i as readProviderJsonResponseText, n as cancelProviderResponseBody, r as readProviderErrorResponseSnippet, t as guardedJsonApiRequest } from "./guarded-json-api-Dm4NvtGo.js";
|
|
6
6
|
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
7
|
+
import { sleepWithAbort } from "openclaw/plugin-sdk/runtime-env";
|
|
7
8
|
import crypto from "node:crypto";
|
|
8
9
|
import { safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
|
|
9
10
|
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
@@ -628,9 +629,12 @@ var TwilioProvider = class TwilioProvider {
|
|
|
628
629
|
chunkAttempts += 1;
|
|
629
630
|
if (sendAudioChunk(chunk).sent) chunkDelivered += 1;
|
|
630
631
|
const waitMs = nextChunkDueAt - Date.now();
|
|
631
|
-
if (waitMs > 0)
|
|
632
|
-
|
|
633
|
-
})
|
|
632
|
+
if (waitMs > 0) try {
|
|
633
|
+
await sleepWithAbort(Math.ceil(waitMs), signal);
|
|
634
|
+
} catch (error) {
|
|
635
|
+
if (!signal.aborted) throw error;
|
|
636
|
+
break;
|
|
637
|
+
}
|
|
634
638
|
nextChunkDueAt += CHUNK_DELAY_MS;
|
|
635
639
|
if (signal.aborted) break;
|
|
636
640
|
}
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/voice-call",
|
|
3
|
-
"version": "2026.7.2-beta.
|
|
3
|
+
"version": "2026.7.2-beta.3",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@openclaw/voice-call",
|
|
9
|
-
"version": "2026.7.2-beta.
|
|
9
|
+
"version": "2026.7.2-beta.3",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"typebox": "1.3.3",
|
|
12
12
|
"ws": "8.21.0",
|
|
13
13
|
"zod": "4.4.3"
|
|
14
14
|
},
|
|
15
15
|
"peerDependencies": {
|
|
16
|
-
"openclaw": ">=2026.7.2-beta.
|
|
16
|
+
"openclaw": ">=2026.7.2-beta.3"
|
|
17
17
|
},
|
|
18
18
|
"peerDependenciesMeta": {
|
|
19
19
|
"openclaw": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/voice-call",
|
|
3
|
-
"version": "2026.7.2-beta.
|
|
3
|
+
"version": "2026.7.2-beta.3",
|
|
4
4
|
"description": "OpenClaw voice-call plugin for Twilio, Telnyx, and Plivo phone calls.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"zod": "4.4.3"
|
|
14
14
|
},
|
|
15
15
|
"peerDependencies": {
|
|
16
|
-
"openclaw": ">=2026.7.2-beta.
|
|
16
|
+
"openclaw": ">=2026.7.2-beta.3"
|
|
17
17
|
},
|
|
18
18
|
"peerDependenciesMeta": {
|
|
19
19
|
"openclaw": {
|
|
@@ -30,10 +30,10 @@
|
|
|
30
30
|
"minHostVersion": ">=2026.4.10"
|
|
31
31
|
},
|
|
32
32
|
"compat": {
|
|
33
|
-
"pluginApi": ">=2026.7.2-beta.
|
|
33
|
+
"pluginApi": ">=2026.7.2-beta.3"
|
|
34
34
|
},
|
|
35
35
|
"build": {
|
|
36
|
-
"openclawVersion": "2026.7.2-beta.
|
|
36
|
+
"openclawVersion": "2026.7.2-beta.3"
|
|
37
37
|
},
|
|
38
38
|
"release": {
|
|
39
39
|
"publishToClawHub": true,
|