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