@openclaw/voice-call 2026.9.2 → 2026.9.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{cli-D3DeAqfd.js → cli-BGdihqyA.js} +2 -2
- package/dist/doctor-contract-api.js +3 -1
- package/dist/{guarded-json-api-BhLtja0F.js → guarded-json-api-3Jy2k-eX.js} +1 -1
- package/dist/index.js +2 -2
- package/dist/{plivo-qIMf0pd2.js → plivo-BsdFT1gS.js} +2 -2
- package/dist/{realtime-handler-CHCnMJfl.js → realtime-handler-C7N4qzvf.js} +148 -23
- package/dist/{response-generator-BK43h7yV.js → response-generator-DsdC88Xg.js} +1 -1
- package/dist/{runtime-entry-DNmi-vZR.js → runtime-entry-Ln4SWaYK.js} +27 -19
- package/dist/runtime-entry.js +1 -1
- package/dist/{store-path-C_z9zKnA.js → store-path-M0GXxUfv.js} +16 -80
- package/dist/{telnyx-DCbnTTjn.js → telnyx-eCxGOUfA.js} +1 -1
- package/dist/{twilio-BwJFcA2z.js → twilio-DQKhnXXE.js} +2 -2
- package/openclaw.plugin.json +2 -1
- package/package.json +4 -4
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { sleep } from "./runtime-api.js";
|
|
2
2
|
import "./api.js";
|
|
3
3
|
import { l as resolveVoiceCallStreamExposurePaths, u as validateProviderConfig } from "./config-DQwvsxeV.js";
|
|
4
|
-
import {
|
|
5
|
-
import { b as setVoiceCallStateRuntime, d as loadActiveCallsFromStore, l as findCallInStore, o as MAX_CALL_RECORD_EVENTS, t as resolveDefaultVoiceCallStoreDir, u as getCallHistoryFromStore } from "./store-path-
|
|
4
|
+
import { d as cleanupTailscaleExposureRoute, f as getTailscaleSelfInfo, p as setupTailscaleExposureRoutes, u as resolveWebhookExposureStatus, v as resolveVoiceCallAgentId, y as resolveUserPath } from "./runtime-entry-Ln4SWaYK.js";
|
|
5
|
+
import { b as setVoiceCallStateRuntime, d as loadActiveCallsFromStore, l as findCallInStore, o as MAX_CALL_RECORD_EVENTS, t as resolveDefaultVoiceCallStoreDir, u as getCallHistoryFromStore } from "./store-path-M0GXxUfv.js";
|
|
6
6
|
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
7
7
|
import { callGatewayFromCli, isGatewayClientRequestError, isGatewayTransportError, redactSensitiveUrlLikeString } from "openclaw/plugin-sdk/gateway-runtime";
|
|
8
8
|
import { isRecord, normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as CALL_RECORD_EVENT_META_MAX_ENTRIES, c as buildVoiceCallLegacyJsonlEventKey, f as parseVoiceCallRecordLine, h as resolveVoiceCallLegacyCallLogPath, i as CALL_RECORD_EVENT_CHUNKS_NAMESPACE, m as prepareVoiceCallRecordForStorage, n as CALL_RECORD_CHUNK_MAX_ENTRIES, o as MAX_CALL_RECORD_EVENTS, r as CALL_RECORD_EVENTS_NAMESPACE, s as RAW_CALL_RECORD_CHUNK_BYTES, t as resolveDefaultVoiceCallStoreDir } from "./store-path-
|
|
1
|
+
import { a as CALL_RECORD_EVENT_META_MAX_ENTRIES, c as buildVoiceCallLegacyJsonlEventKey, f as parseVoiceCallRecordLine, h as resolveVoiceCallLegacyCallLogPath, i as CALL_RECORD_EVENT_CHUNKS_NAMESPACE, m as prepareVoiceCallRecordForStorage, n as CALL_RECORD_CHUNK_MAX_ENTRIES, o as MAX_CALL_RECORD_EVENTS, r as CALL_RECORD_EVENTS_NAMESPACE, s as RAW_CALL_RECORD_CHUNK_BYTES, t as resolveDefaultVoiceCallStoreDir } from "./store-path-M0GXxUfv.js";
|
|
2
2
|
import { normalizeAgentId } from "openclaw/plugin-sdk/routing";
|
|
3
3
|
import { asOptionalRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
4
4
|
import path from "node:path";
|
|
@@ -68,6 +68,8 @@ function describeVoiceCallSchemaMigration(migration) {
|
|
|
68
68
|
case "state-consolidation-v13": return "cron jobs and subagent runs -> canonical JSON storage";
|
|
69
69
|
case "creator-namespace-v14": return "cron creators -> explicit principal namespaces";
|
|
70
70
|
case "conversation-binding-targets-v15": return "conversation bindings -> exact target keys without agent/session projections";
|
|
71
|
+
case "skill-workshop-directory-ownership-v16": return "Skill Workshop proposals -> per-agent Workshop directory ownership";
|
|
72
|
+
case "prepared-worker-ownership-v17": return "prepared workers -> one-use capacity and fixed workspace ownership";
|
|
71
73
|
case "worker-placement-execution-mode-v8": return "cloud worker placements -> execution-mode claims";
|
|
72
74
|
case "operator-approvals-system-agent": return "operator approvals -> OpenClaw system changes";
|
|
73
75
|
case "session-watch-cursor-provenance-v4": return "session watch cursors -> provenance column";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { fetchWithSsrFGuard } from "./runtime-api.js";
|
|
2
2
|
import "./api.js";
|
|
3
|
-
import {
|
|
3
|
+
import { l as getHeader, n as normalizeProxyIp } from "./runtime-entry-Ln4SWaYK.js";
|
|
4
4
|
import { n as reserveWebhookReplay, t as createWebhookReplayCache } from "./webhook-replay-CCQSoBNF.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
|
@@ -2,7 +2,7 @@ import { definePluginEntry } from "./runtime-api.js";
|
|
|
2
2
|
import "./api.js";
|
|
3
3
|
import { VOICE_CALL_CLI_DESCRIPTOR } from "./cli-output-mode.js";
|
|
4
4
|
import { i as resolveVoiceCallConfig, t as VoiceCallConfigSchema, u as validateProviderConfig } from "./config-DQwvsxeV.js";
|
|
5
|
-
import { t as createVoiceCallRuntime } from "./runtime-entry-
|
|
5
|
+
import { t as createVoiceCallRuntime } from "./runtime-entry-Ln4SWaYK.js";
|
|
6
6
|
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
7
7
|
import { ErrorCodes, errorShape } from "openclaw/plugin-sdk/gateway-runtime";
|
|
8
8
|
import { resolveGlobalSingleton } from "openclaw/plugin-sdk/global-singleton";
|
|
@@ -514,7 +514,7 @@ var voice_call_default = definePluginEntry({
|
|
|
514
514
|
}
|
|
515
515
|
}));
|
|
516
516
|
api.registerCli(async ({ program }) => {
|
|
517
|
-
const { registerVoiceCallCli } = await import("./cli-
|
|
517
|
+
const { registerVoiceCallCli } = await import("./cli-BGdihqyA.js");
|
|
518
518
|
registerVoiceCallCli({
|
|
519
519
|
program,
|
|
520
520
|
config,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { a as reconstructWebhookUrl, o as verifyPlivoWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-
|
|
1
|
+
import { b as escapeXml, l as getHeader } from "./runtime-entry-Ln4SWaYK.js";
|
|
2
|
+
import { a as reconstructWebhookUrl, o as verifyPlivoWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-3Jy2k-eX.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,5 +1,5 @@
|
|
|
1
1
|
import { d as REALTIME_VOICE_END_CALL_TOOL_NAME, s as resolveVoiceCallPublicPathPrefix } from "./config-DQwvsxeV.js";
|
|
2
|
-
import {
|
|
2
|
+
import { c as canonicalizeVoiceCallMediaBase64, o as WebSocket, s as WebSocketServer } from "./runtime-entry-Ln4SWaYK.js";
|
|
3
3
|
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
4
4
|
import { asNullableRecord, asOptionalObjectRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
5
5
|
import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env";
|
|
@@ -9,13 +9,14 @@ import { isFutureDateTimestampMs, resolveExpiresAtMsFromDurationMs } from "openc
|
|
|
9
9
|
import { randomUUID } from "node:crypto";
|
|
10
10
|
import { sliceUtf16Safe, truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
|
11
11
|
import "node:http";
|
|
12
|
-
import WebSocket$1, { WebSocketServer } from "ws";
|
|
13
12
|
//#region extensions/voice-call/src/webhook/realtime-audio-pacer.ts
|
|
14
13
|
const TELEPHONY_SAMPLE_RATE = 8e3;
|
|
15
14
|
const TELEPHONY_CHUNK_BYTES = 160;
|
|
16
15
|
const LEAD_MS = 160;
|
|
17
16
|
const DEFAULT_MAX_QUEUED_AUDIO_BYTES = TELEPHONY_SAMPLE_RATE * 120;
|
|
18
17
|
const QUEUE_COMPACT_HEAD_THRESHOLD = 256;
|
|
18
|
+
const MAX_PLAYBACK_SEGMENTS = 128;
|
|
19
|
+
const MAX_PENDING_MARK_BOUNDARIES = 64;
|
|
19
20
|
/** Paces outgoing mulaw audio frames at telephony cadence. */
|
|
20
21
|
var RealtimeAudioPacer = class {
|
|
21
22
|
constructor(params) {
|
|
@@ -26,9 +27,18 @@ var RealtimeAudioPacer = class {
|
|
|
26
27
|
this.queuedAudioBytes = 0;
|
|
27
28
|
this.closed = false;
|
|
28
29
|
this.streamClockMs = null;
|
|
30
|
+
this.playbackSegments = [];
|
|
31
|
+
this.sentAudioMs = 0;
|
|
32
|
+
this.retiredAudioMs = 0;
|
|
33
|
+
this.confirmedPlayedMs = 0;
|
|
34
|
+
this.playbackMarkPrefix = `openclaw-playout-${randomUUID()}`;
|
|
35
|
+
this.playbackMarkSequence = 0;
|
|
36
|
+
this.lastPlaybackMarkMs = 0;
|
|
37
|
+
this.markBoundaries = [];
|
|
38
|
+
this.retiredItemOffsets = /* @__PURE__ */ new Map();
|
|
29
39
|
}
|
|
30
40
|
/** Queue mulaw audio and split it into 20ms-ish telephony chunks. */
|
|
31
|
-
sendAudio(muLaw) {
|
|
41
|
+
sendAudio(muLaw, metadata) {
|
|
32
42
|
if (this.closed || muLaw.length === 0) return;
|
|
33
43
|
const maxQueuedAudioBytes = this.params.maxQueuedAudioBytes ?? DEFAULT_MAX_QUEUED_AUDIO_BYTES;
|
|
34
44
|
for (let offset = 0; offset < muLaw.length; offset += TELEPHONY_CHUNK_BYTES) {
|
|
@@ -37,15 +47,42 @@ var RealtimeAudioPacer = class {
|
|
|
37
47
|
this.failBackpressure();
|
|
38
48
|
return;
|
|
39
49
|
}
|
|
50
|
+
const durationMs = chunk.length / 8;
|
|
51
|
+
const segment = this.extendPlaybackSegment(metadata?.itemId, durationMs);
|
|
52
|
+
if (!segment) {
|
|
53
|
+
this.failBackpressure();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
40
56
|
this.queue.push({
|
|
41
57
|
type: "audio",
|
|
42
58
|
chunk,
|
|
43
|
-
durationMs
|
|
59
|
+
durationMs,
|
|
60
|
+
segment
|
|
44
61
|
});
|
|
45
62
|
this.queuedAudioBytes += chunk.length;
|
|
46
63
|
}
|
|
47
64
|
this.ensurePump();
|
|
48
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Retained provider items in playback order with consumed playout duration.
|
|
68
|
+
* Queued audio has not reached the telephony line, so unplayed items report zero.
|
|
69
|
+
*/
|
|
70
|
+
getPlaybackState() {
|
|
71
|
+
let remaining = Math.max(0, this.confirmedPlayedMs - this.retiredAudioMs);
|
|
72
|
+
const playedByItem = /* @__PURE__ */ new Map();
|
|
73
|
+
for (const segment of this.playbackSegments) {
|
|
74
|
+
const consumed = Math.min(remaining, segment.sentMs);
|
|
75
|
+
remaining -= consumed;
|
|
76
|
+
if (segment.itemId !== void 0) {
|
|
77
|
+
const previousMs = playedByItem.get(segment.itemId) ?? this.retiredItemOffsets.get(segment.itemId) ?? 0;
|
|
78
|
+
playedByItem.set(segment.itemId, previousMs + consumed);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return Array.from(playedByItem, ([itemId, audioEndMs]) => ({
|
|
82
|
+
itemId,
|
|
83
|
+
audioEndMs: Math.floor(audioEndMs)
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
49
86
|
/** Queue a provider mark frame after prior audio frames. */
|
|
50
87
|
sendMark(name) {
|
|
51
88
|
if (this.closed || !name) return;
|
|
@@ -55,14 +92,32 @@ var RealtimeAudioPacer = class {
|
|
|
55
92
|
});
|
|
56
93
|
this.ensurePump();
|
|
57
94
|
}
|
|
95
|
+
/** Retire the playback prefix once the carrier confirms playout reached the mark. */
|
|
96
|
+
acknowledgeMark(name) {
|
|
97
|
+
if (this.closed || !name) return;
|
|
98
|
+
const boundaryIndex = this.markBoundaries.findIndex((boundary) => boundary.name === name);
|
|
99
|
+
if (boundaryIndex < 0) return;
|
|
100
|
+
const boundary = this.markBoundaries[boundaryIndex];
|
|
101
|
+
this.markBoundaries.splice(0, boundaryIndex + 1);
|
|
102
|
+
if (!boundary) return;
|
|
103
|
+
while (this.playbackSegments.length > 0) {
|
|
104
|
+
const head = this.playbackSegments[0];
|
|
105
|
+
if (!head || head.sentMs < head.totalMs || head.lastSentEndMs === void 0 || head.lastSentEndMs > boundary.sentMs) break;
|
|
106
|
+
const retired = this.playbackSegments.shift();
|
|
107
|
+
if (retired) {
|
|
108
|
+
this.retiredAudioMs += retired.sentMs;
|
|
109
|
+
if (retired.itemId !== void 0) this.retiredItemOffsets.set(retired.itemId, (this.retiredItemOffsets.get(retired.itemId) ?? 0) + retired.sentMs);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
this.confirmedPlayedMs = Math.max(this.confirmedPlayedMs, Math.min(boundary.sentMs, this.sentAudioMs));
|
|
113
|
+
}
|
|
58
114
|
/** Clear queued audio and notify the provider stream. */
|
|
59
115
|
clearAudio() {
|
|
60
116
|
if (this.closed) return 0;
|
|
61
117
|
const clearedAudioBytes = this.queuedAudioBytes;
|
|
62
118
|
this.clearTimer();
|
|
63
119
|
this.resetQueue();
|
|
64
|
-
this.
|
|
65
|
-
this.streamClockMs = null;
|
|
120
|
+
this.resetPlaybackState();
|
|
66
121
|
this.params.send(this.params.serializer.clear());
|
|
67
122
|
return clearedAudioBytes;
|
|
68
123
|
}
|
|
@@ -75,8 +130,33 @@ var RealtimeAudioPacer = class {
|
|
|
75
130
|
this.closed = true;
|
|
76
131
|
this.clearTimer();
|
|
77
132
|
this.resetQueue();
|
|
133
|
+
this.resetPlaybackState();
|
|
134
|
+
}
|
|
135
|
+
extendPlaybackSegment(itemId, durationMs) {
|
|
136
|
+
let segment = this.playbackSegments.at(-1);
|
|
137
|
+
if (!segment || segment.itemId !== itemId) {
|
|
138
|
+
if (this.playbackSegments.length >= MAX_PLAYBACK_SEGMENTS) return null;
|
|
139
|
+
segment = {
|
|
140
|
+
itemId,
|
|
141
|
+
totalMs: 0,
|
|
142
|
+
sentMs: 0
|
|
143
|
+
};
|
|
144
|
+
this.playbackSegments.push(segment);
|
|
145
|
+
}
|
|
146
|
+
segment.totalMs += durationMs;
|
|
147
|
+
return segment;
|
|
148
|
+
}
|
|
149
|
+
resetPlaybackState() {
|
|
150
|
+
this.playbackSegments = [];
|
|
78
151
|
this.queuedAudioBytes = 0;
|
|
152
|
+
this.sentAudioMs = 0;
|
|
153
|
+
this.retiredAudioMs = 0;
|
|
154
|
+
this.confirmedPlayedMs = 0;
|
|
155
|
+
this.lastPlaybackMarkMs = 0;
|
|
156
|
+
this.markBoundaries = [];
|
|
157
|
+
this.retiredItemOffsets.clear();
|
|
79
158
|
this.streamClockMs = null;
|
|
159
|
+
this.params.onPlaybackReset?.();
|
|
80
160
|
}
|
|
81
161
|
/** Clear the scheduled pump timer. */
|
|
82
162
|
clearTimer() {
|
|
@@ -124,7 +204,7 @@ var RealtimeAudioPacer = class {
|
|
|
124
204
|
while (this.pendingQueueSize > 0 && this.streamClockMs < now + LEAD_MS) {
|
|
125
205
|
const item = this.takeNextItem();
|
|
126
206
|
if (!item) break;
|
|
127
|
-
if (!(item.type === "audio" ? this.sendAudioItem(item) : this.
|
|
207
|
+
if (!(item.type === "audio" ? this.sendAudioItem(item) : this.sendMarkItem(item))) {
|
|
128
208
|
this.resetQueue();
|
|
129
209
|
this.queuedAudioBytes = 0;
|
|
130
210
|
this.streamClockMs = null;
|
|
@@ -141,7 +221,30 @@ var RealtimeAudioPacer = class {
|
|
|
141
221
|
sendAudioItem(item) {
|
|
142
222
|
this.queuedAudioBytes = Math.max(0, this.queuedAudioBytes - item.chunk.length);
|
|
143
223
|
const sent = this.params.send(this.params.serializer.media(item.chunk.toString("base64")));
|
|
224
|
+
if (sent) {
|
|
225
|
+
item.segment.sentMs += item.durationMs;
|
|
226
|
+
this.sentAudioMs += item.durationMs;
|
|
227
|
+
item.segment.lastSentEndMs = this.sentAudioMs;
|
|
228
|
+
}
|
|
144
229
|
this.streamClockMs = (this.streamClockMs ?? performance.now()) + item.durationMs;
|
|
230
|
+
if (!sent || item.segment.itemId === void 0 || this.sentAudioMs - this.lastPlaybackMarkMs < LEAD_MS) return sent;
|
|
231
|
+
this.lastPlaybackMarkMs = this.sentAudioMs;
|
|
232
|
+
this.playbackMarkSequence += 1;
|
|
233
|
+
return this.sendMarkItem({
|
|
234
|
+
type: "mark",
|
|
235
|
+
name: `${this.playbackMarkPrefix}-${this.playbackMarkSequence}`
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
/** Send a queued mark frame and bind it to the playback prefix before it. */
|
|
239
|
+
sendMarkItem(item) {
|
|
240
|
+
const sent = this.params.send(this.params.serializer.mark(item.name));
|
|
241
|
+
if (sent) {
|
|
242
|
+
this.markBoundaries.push({
|
|
243
|
+
name: item.name,
|
|
244
|
+
sentMs: this.sentAudioMs
|
|
245
|
+
});
|
|
246
|
+
if (this.markBoundaries.length > MAX_PENDING_MARK_BOUNDARIES) this.markBoundaries.shift();
|
|
247
|
+
}
|
|
145
248
|
return sent;
|
|
146
249
|
}
|
|
147
250
|
};
|
|
@@ -443,6 +546,15 @@ function appendRecentTalkEventMetadata(call, event) {
|
|
|
443
546
|
}].slice(-12);
|
|
444
547
|
call.metadata = metadata;
|
|
445
548
|
}
|
|
549
|
+
function rejectRealtimeUpgrade(socket, status) {
|
|
550
|
+
const reason = status === 401 ? "Unauthorized" : "Service Unavailable";
|
|
551
|
+
try {
|
|
552
|
+
socket.end(`HTTP/1.1 ${status} ${reason}\r\nConnection: close\r\n\r\n`, () => socket.destroy());
|
|
553
|
+
} catch (error) {
|
|
554
|
+
socket.destroy();
|
|
555
|
+
throw error;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
446
558
|
var RealtimeCallHandler = class {
|
|
447
559
|
constructor(config, manager, resolveCallRegistration, servePath, streamDisconnectLifecycle, coreConfig) {
|
|
448
560
|
this.config = config;
|
|
@@ -506,16 +618,15 @@ var RealtimeCallHandler = class {
|
|
|
506
618
|
}
|
|
507
619
|
}
|
|
508
620
|
handleWebSocketUpgrade(request, socket, head) {
|
|
621
|
+
socket.once("error", () => socket.destroy());
|
|
509
622
|
if (this.closing) {
|
|
510
|
-
socket
|
|
511
|
-
socket.destroy();
|
|
623
|
+
rejectRealtimeUpgrade(socket, 503);
|
|
512
624
|
return;
|
|
513
625
|
}
|
|
514
626
|
const token = new URL(request.url ?? "/", "wss://localhost").pathname.split("/").pop() ?? null;
|
|
515
627
|
const callerMeta = token ? this.consumeStreamToken(token) : null;
|
|
516
628
|
if (!callerMeta) {
|
|
517
|
-
socket
|
|
518
|
-
socket.destroy();
|
|
629
|
+
rejectRealtimeUpgrade(socket, 401);
|
|
519
630
|
return;
|
|
520
631
|
}
|
|
521
632
|
const adapter = (callerMeta.providerName ?? "twilio") === "telnyx" ? new TelnyxStreamFrameAdapter() : new TwilioStreamFrameAdapter();
|
|
@@ -565,7 +676,8 @@ var RealtimeCallHandler = class {
|
|
|
565
676
|
return;
|
|
566
677
|
}
|
|
567
678
|
if (frame.kind === "mark") {
|
|
568
|
-
telephonyBinding.
|
|
679
|
+
telephonyBinding.acknowledgeCarrierMark(frame.name);
|
|
680
|
+
telephonyBinding.bridge.acknowledgeMark(frame.name);
|
|
569
681
|
return;
|
|
570
682
|
}
|
|
571
683
|
if (frame.kind === "error") {
|
|
@@ -598,7 +710,7 @@ var RealtimeCallHandler = class {
|
|
|
598
710
|
this.pendingStreamTokens.clear();
|
|
599
711
|
const sockets = [...this.activeSockets];
|
|
600
712
|
this.closePromise = Promise.all([shutdownBarrier, ...sockets.map((ws) => new Promise((resolve) => {
|
|
601
|
-
if (ws.readyState === WebSocket
|
|
713
|
+
if (ws.readyState === WebSocket.CLOSED) {
|
|
602
714
|
resolve();
|
|
603
715
|
return;
|
|
604
716
|
}
|
|
@@ -790,7 +902,7 @@ var RealtimeCallHandler = class {
|
|
|
790
902
|
});
|
|
791
903
|
console.log(`[voice-call] Realtime bridge starting for call ${callId} (providerCallId=${callSid}, initialGreeting=${initialGreetingInstructions ? "queued" : "absent"})`);
|
|
792
904
|
const sendString = (message) => {
|
|
793
|
-
if (ws.readyState !== WebSocket
|
|
905
|
+
if (ws.readyState !== WebSocket.OPEN) return false;
|
|
794
906
|
if (ws.bufferedAmount > MAX_REALTIME_WS_BUFFERED_BYTES) {
|
|
795
907
|
console.warn(`[voice-call] realtime outbound websocket backpressure before send callId=${callId} providerCallId=${callSid} bufferedBytes=${ws.bufferedAmount}`);
|
|
796
908
|
ws.close(1013, "Backpressure: send buffer exceeded");
|
|
@@ -804,7 +916,9 @@ var RealtimeCallHandler = class {
|
|
|
804
916
|
}
|
|
805
917
|
return true;
|
|
806
918
|
};
|
|
919
|
+
const pendingMarkAcks = /* @__PURE__ */ new Map();
|
|
807
920
|
const audioPacer = new RealtimeAudioPacer({
|
|
921
|
+
onPlaybackReset: () => pendingMarkAcks.clear(),
|
|
808
922
|
send: sendString,
|
|
809
923
|
serializer: {
|
|
810
924
|
media: (payload) => adapter.serializeMedia(payload),
|
|
@@ -813,7 +927,7 @@ var RealtimeCallHandler = class {
|
|
|
813
927
|
},
|
|
814
928
|
onBackpressure: () => {
|
|
815
929
|
console.warn(`[voice-call] realtime paced audio backpressure callId=${callId} providerCallId=${callSid}`);
|
|
816
|
-
if (ws.readyState === WebSocket
|
|
930
|
+
if (ws.readyState === WebSocket.OPEN) ws.close(1013, "Backpressure: paced audio queue exceeded");
|
|
817
931
|
}
|
|
818
932
|
});
|
|
819
933
|
const speechDetector = createSpeechThresholdGate({
|
|
@@ -839,11 +953,12 @@ var RealtimeCallHandler = class {
|
|
|
839
953
|
initialGreetingInstructions,
|
|
840
954
|
triggerGreetingOnReady: Boolean(initialGreetingInstructions),
|
|
841
955
|
audioSink: {
|
|
842
|
-
isOpen: () => ws.readyState === WebSocket
|
|
843
|
-
sendAudio: (muLaw) => {
|
|
956
|
+
isOpen: () => ws.readyState === WebSocket.OPEN,
|
|
957
|
+
sendAudio: (muLaw, metadata) => {
|
|
844
958
|
harness.recordOutputAudio(muLaw);
|
|
845
|
-
audioPacer.sendAudio(muLaw);
|
|
959
|
+
audioPacer.sendAudio(muLaw, metadata);
|
|
846
960
|
},
|
|
961
|
+
getPlaybackState: () => audioPacer.getPlaybackState(),
|
|
847
962
|
clearAudio: (reason) => {
|
|
848
963
|
harness.flushOutput(() => {
|
|
849
964
|
const clearedBytes = audioPacer.clearAudio();
|
|
@@ -855,8 +970,9 @@ var RealtimeCallHandler = class {
|
|
|
855
970
|
harness.finishOutputAudio("clear");
|
|
856
971
|
});
|
|
857
972
|
},
|
|
858
|
-
sendMark: (markName) => {
|
|
973
|
+
sendMark: (markName, acknowledge) => {
|
|
859
974
|
audioPacer.sendMark(markName);
|
|
975
|
+
if (markName && acknowledge) pendingMarkAcks.set(markName, acknowledge);
|
|
860
976
|
}
|
|
861
977
|
},
|
|
862
978
|
onTranscript: (role, text, isFinal) => {
|
|
@@ -1035,7 +1151,7 @@ var RealtimeCallHandler = class {
|
|
|
1035
1151
|
if (ownsCallState) this.clearUserTranscriptState(callId, userTranscriptOwner);
|
|
1036
1152
|
if (reason === "completed" && (!ownsCallState || sessionClosed)) return;
|
|
1037
1153
|
this.streamDisconnectLifecycle.retire(callSid, streamSid);
|
|
1038
|
-
if (ws.readyState === WebSocket
|
|
1154
|
+
if (ws.readyState === WebSocket.OPEN) ws.close(reason === "error" ? 1011 : 1e3, "Bridge disconnected");
|
|
1039
1155
|
if (ownsCallState) emitCallEnd(reason);
|
|
1040
1156
|
}
|
|
1041
1157
|
};
|
|
@@ -1056,7 +1172,7 @@ var RealtimeCallHandler = class {
|
|
|
1056
1172
|
audioPacer.close();
|
|
1057
1173
|
const reason = provisionalCloseReason ?? "error";
|
|
1058
1174
|
if (!hadPredecessorOnAdmission || !this.activeBridgesByCallId.has(callId)) emitCallEnd(reason);
|
|
1059
|
-
if (ws.readyState === WebSocket
|
|
1175
|
+
if (ws.readyState === WebSocket.OPEN) ws.close(reason === "error" ? 1011 : 1e3, "Failed to create realtime bridge");
|
|
1060
1176
|
return null;
|
|
1061
1177
|
}
|
|
1062
1178
|
const session = candidate;
|
|
@@ -1125,10 +1241,19 @@ var RealtimeCallHandler = class {
|
|
|
1125
1241
|
};
|
|
1126
1242
|
const telephonyBinding = {
|
|
1127
1243
|
bridge: session,
|
|
1244
|
+
acknowledgeCarrierMark: (markName) => {
|
|
1245
|
+
audioPacer.acknowledgeMark(markName);
|
|
1246
|
+
if (!markName) return;
|
|
1247
|
+
const acknowledge = pendingMarkAcks.get(markName);
|
|
1248
|
+
if (acknowledge) {
|
|
1249
|
+
pendingMarkAcks.delete(markName);
|
|
1250
|
+
acknowledge();
|
|
1251
|
+
}
|
|
1252
|
+
},
|
|
1128
1253
|
close: (cause) => closeBinding(telephonyBinding, cause),
|
|
1129
1254
|
endCall: () => {
|
|
1130
1255
|
closeBinding(telephonyBinding);
|
|
1131
|
-
if (ws.readyState === WebSocket
|
|
1256
|
+
if (ws.readyState === WebSocket.OPEN) ws.close(1e3, "Call ended");
|
|
1132
1257
|
},
|
|
1133
1258
|
noteMediaActivity: () => {
|
|
1134
1259
|
if (bindingClosed || this.activeTelephonyBindingsByCallId.get(callId) !== telephonyBinding) return;
|
|
@@ -1137,7 +1262,7 @@ var RealtimeCallHandler = class {
|
|
|
1137
1262
|
console.warn(`[voice-call] Realtime media inactive callId=${callId} providerCallId=${callSid} timeoutMs=${REALTIME_MEDIA_INACTIVITY_TIMEOUT_MS} graceMs=${REALTIME_DISCONNECT_HANGUP_GRACE_MS}`);
|
|
1138
1263
|
livenessTimer = setTimeout(() => {
|
|
1139
1264
|
telephonyBinding.close("inactivity");
|
|
1140
|
-
if (ws.readyState === WebSocket
|
|
1265
|
+
if (ws.readyState === WebSocket.OPEN) ws.close(1e3, "Media inactivity");
|
|
1141
1266
|
}, REALTIME_DISCONNECT_HANGUP_GRACE_MS);
|
|
1142
1267
|
livenessTimer.unref?.();
|
|
1143
1268
|
}, REALTIME_MEDIA_INACTIVITY_TIMEOUT_MS);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { c as resolveVoiceCallSessionKey } from "./config-DQwvsxeV.js";
|
|
2
|
-
import {
|
|
2
|
+
import { _ as resolveCallAgentId, g as resolveVoiceResponseModel } from "./runtime-entry-Ln4SWaYK.js";
|
|
3
3
|
import { filterStringEntries, isRecord, 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";
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { isBlockedHostnameOrIp, isRequestBodyLimitError, readRequestBodyWithLimit, requestBodyErrorToText, sendHttpRequestRejection } from "./runtime-api.js";
|
|
2
2
|
import "./api.js";
|
|
3
3
|
import { a as resolveVoiceCallEffectiveConfig, c as resolveVoiceCallSessionKey, f as resolveVoiceCallRealtimeTools, i as resolveVoiceCallConfig, l as resolveVoiceCallStreamExposurePaths, n as normalizeVoiceCallConfig, o as resolveVoiceCallNumberRouteKeyForCall, r as resolveTwilioAuthToken, u as validateProviderConfig } from "./config-DQwvsxeV.js";
|
|
4
|
-
import { _ as releaseRejectedProviderCall, b as setVoiceCallStateRuntime, d as loadActiveCallsFromStore, g as appendCallReplayKey, l as findCallInStore, p as persistCallRecord, t as resolveDefaultVoiceCallStoreDir, u as getCallHistoryFromStore, v as rememberManagerReplayKey, x as TerminalStates, y as reserveRejectedProviderCall } from "./store-path-
|
|
4
|
+
import { _ as releaseRejectedProviderCall, b as setVoiceCallStateRuntime, d as loadActiveCallsFromStore, g as appendCallReplayKey, l as findCallInStore, p as persistCallRecord, t as resolveDefaultVoiceCallStoreDir, u as getCallHistoryFromStore, v as rememberManagerReplayKey, x as TerminalStates, y as reserveRejectedProviderCall } from "./store-path-M0GXxUfv.js";
|
|
5
|
+
import { createRequire } from "node:module";
|
|
5
6
|
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
6
7
|
import { isLoopbackHost } from "openclaw/plugin-sdk/gateway-runtime";
|
|
7
8
|
import { normalizeAgentId } from "openclaw/plugin-sdk/routing";
|
|
@@ -24,7 +25,7 @@ import { runCommandWithTimeout } from "openclaw/plugin-sdk/process-runtime";
|
|
|
24
25
|
import http from "node:http";
|
|
25
26
|
import { URL as URL$1 } from "node:url";
|
|
26
27
|
import { resolveConfiguredCapabilityProvider } from "openclaw/plugin-sdk/provider-selection-runtime";
|
|
27
|
-
import {
|
|
28
|
+
import { rejectWebSocketUpgrade } from "openclaw/plugin-sdk/websocket-runtime";
|
|
28
29
|
import { canonicalizeBase64 } from "openclaw/plugin-sdk/media-runtime";
|
|
29
30
|
//#region extensions/voice-call/src/allowlist.ts
|
|
30
31
|
/** Normalize a phone number to digits only. */
|
|
@@ -234,14 +235,14 @@ function escapeXml(text) {
|
|
|
234
235
|
/**
|
|
235
236
|
* Map of OpenAI voice names to similar Twilio Polly voices.
|
|
236
237
|
*/
|
|
237
|
-
const OPENAI_TO_POLLY_MAP =
|
|
238
|
-
alloy
|
|
239
|
-
echo
|
|
240
|
-
fable
|
|
241
|
-
onyx
|
|
242
|
-
nova
|
|
243
|
-
shimmer
|
|
244
|
-
|
|
238
|
+
const OPENAI_TO_POLLY_MAP = /* @__PURE__ */ new Map([
|
|
239
|
+
["alloy", "Polly.Joanna"],
|
|
240
|
+
["echo", "Polly.Matthew"],
|
|
241
|
+
["fable", "Polly.Amy"],
|
|
242
|
+
["onyx", "Polly.Brian"],
|
|
243
|
+
["nova", "Polly.Salli"],
|
|
244
|
+
["shimmer", "Polly.Kimberly"]
|
|
245
|
+
]);
|
|
245
246
|
/**
|
|
246
247
|
* Default Polly voice when no mapping is found.
|
|
247
248
|
*/
|
|
@@ -256,7 +257,7 @@ const DEFAULT_POLLY_VOICE = "Polly.Joanna";
|
|
|
256
257
|
function mapVoiceToPolly(voice) {
|
|
257
258
|
if (!voice) return DEFAULT_POLLY_VOICE;
|
|
258
259
|
if (voice.startsWith("Polly.") || voice.startsWith("Google.")) return voice;
|
|
259
|
-
return OPENAI_TO_POLLY_MAP
|
|
260
|
+
return OPENAI_TO_POLLY_MAP.get(normalizeLowercaseStringOrEmpty(voice)) ?? DEFAULT_POLLY_VOICE;
|
|
260
261
|
}
|
|
261
262
|
//#endregion
|
|
262
263
|
//#region extensions/voice-call/src/manager/twiml.ts
|
|
@@ -1850,6 +1851,10 @@ function canonicalizeVoiceCallMediaBase64(payloadBase64) {
|
|
|
1850
1851
|
return canonicalizeBase64(payloadBase64.replaceAll("-", "+").replaceAll("_", "/"));
|
|
1851
1852
|
}
|
|
1852
1853
|
//#endregion
|
|
1854
|
+
//#region extensions/voice-call/src/websocket.ts
|
|
1855
|
+
const require = createRequire(import.meta.url);
|
|
1856
|
+
const { WebSocket, WebSocketServer } = require(path.join(path.dirname(require.resolve("ws/package.json")), "index.js"));
|
|
1857
|
+
//#endregion
|
|
1853
1858
|
//#region extensions/voice-call/src/media-stream.ts
|
|
1854
1859
|
const DEFAULT_PRE_START_TIMEOUT_MS = 5e3;
|
|
1855
1860
|
const DEFAULT_MAX_PENDING_CONNECTIONS = 32;
|
|
@@ -2588,7 +2593,7 @@ function normalizeProviderStatus(status) {
|
|
|
2588
2593
|
/** Map terminal provider status strings to OpenClaw end reasons. */
|
|
2589
2594
|
function mapProviderStatusToEndReason(status) {
|
|
2590
2595
|
const normalized = normalizeProviderStatus(status);
|
|
2591
|
-
return TERMINAL_PROVIDER_STATUS_TO_END_REASON[normalized] ?? null;
|
|
2596
|
+
return Object.hasOwn(TERMINAL_PROVIDER_STATUS_TO_END_REASON, normalized) ? TERMINAL_PROVIDER_STATUS_TO_END_REASON[normalized] ?? null : null;
|
|
2592
2597
|
}
|
|
2593
2598
|
/** Return true when a provider status is terminal. */
|
|
2594
2599
|
function isProviderStatusTerminal(status) {
|
|
@@ -2687,7 +2692,7 @@ const MAX_WEBHOOK_BODY_BYTES = WEBHOOK_BODY_READ_DEFAULTS.preAuth.maxBytes;
|
|
|
2687
2692
|
const WEBHOOK_BODY_TIMEOUT_MS = WEBHOOK_BODY_READ_DEFAULTS.preAuth.timeoutMs;
|
|
2688
2693
|
const MISSING_REMOTE_ADDRESS_IN_FLIGHT_KEY = "__voice_call_no_remote__";
|
|
2689
2694
|
const loadRealtimeTranscriptionRuntime = createLazyRuntimeModule(() => import("./realtime-transcription.runtime-CbJAs5t_.js"));
|
|
2690
|
-
const loadResponseGeneratorModule = createLazyRuntimeModule(() => import("./response-generator-
|
|
2695
|
+
const loadResponseGeneratorModule = createLazyRuntimeModule(() => import("./response-generator-DsdC88Xg.js"));
|
|
2691
2696
|
function appendRecentTalkEventMetadata(call, event) {
|
|
2692
2697
|
const metadata = call.metadata ?? {};
|
|
2693
2698
|
const recent = Array.isArray(metadata.recentTalkEvents) ? metadata.recentTalkEvents.filter((entry) => Boolean(entry) && typeof entry === "object" && !Array.isArray(entry)) : [];
|
|
@@ -2997,7 +3002,10 @@ var VoiceCallWebhookServer = class {
|
|
|
2997
3002
|
return;
|
|
2998
3003
|
}
|
|
2999
3004
|
if (this.getUpgradePathname(request) === streamPath && this.mediaStreamHandler) this.mediaStreamHandler?.handleUpgrade(request, socket, head);
|
|
3000
|
-
else
|
|
3005
|
+
else {
|
|
3006
|
+
socket.once("error", () => {});
|
|
3007
|
+
rejectWebSocketUpgrade(socket, { status: 404 });
|
|
3008
|
+
}
|
|
3001
3009
|
});
|
|
3002
3010
|
this.server.on("error", (err) => {
|
|
3003
3011
|
this.server = null;
|
|
@@ -3413,12 +3421,12 @@ const REALTIME_VOICE_CONSULT_SYSTEM_PROMPT = [
|
|
|
3413
3421
|
"Do not print secret values or dump environment variables; only check whether required configuration is present.",
|
|
3414
3422
|
"Be accurate, brief, and speakable."
|
|
3415
3423
|
].join(" ");
|
|
3416
|
-
const loadTelnyxProvider = createLazyRuntimeModule(() => import("./telnyx-
|
|
3417
|
-
const loadTwilioProvider = createLazyRuntimeModule(() => import("./twilio-
|
|
3418
|
-
const loadPlivoProvider = createLazyRuntimeModule(() => import("./plivo-
|
|
3424
|
+
const loadTelnyxProvider = createLazyRuntimeModule(() => import("./telnyx-eCxGOUfA.js"));
|
|
3425
|
+
const loadTwilioProvider = createLazyRuntimeModule(() => import("./twilio-DQKhnXXE.js"));
|
|
3426
|
+
const loadPlivoProvider = createLazyRuntimeModule(() => import("./plivo-BsdFT1gS.js"));
|
|
3419
3427
|
const loadMockProvider = createLazyRuntimeModule(() => import("./mock-C57qj18W.js"));
|
|
3420
3428
|
const loadRealtimeVoiceRuntime = createLazyRuntimeModule(() => import("./realtime-voice.runtime-vtdCOWg-.js"));
|
|
3421
|
-
const loadRealtimeHandler = createLazyRuntimeModule(() => import("./realtime-handler-
|
|
3429
|
+
const loadRealtimeHandler = createLazyRuntimeModule(() => import("./realtime-handler-C7N4qzvf.js"));
|
|
3422
3430
|
function resolveVoiceCallConsultSessionKey(call) {
|
|
3423
3431
|
return resolveVoiceCallSessionKey({
|
|
3424
3432
|
config: call.config,
|
|
@@ -3760,4 +3768,4 @@ async function createVoiceCallRuntime(params) {
|
|
|
3760
3768
|
}
|
|
3761
3769
|
}
|
|
3762
3770
|
//#endregion
|
|
3763
|
-
export {
|
|
3771
|
+
export { resolveCallAgentId as _, normalizeProviderStatus as a, escapeXml as b, canonicalizeVoiceCallMediaBase64 as c, cleanupTailscaleExposureRoute as d, getTailscaleSelfInfo as f, resolveVoiceResponseModel as g, chunkAudio as h, mapProviderStatusToEndReason as i, getHeader as l, TELEPHONY_DEFAULT_TTS_TIMEOUT_MS as m, normalizeProxyIp as n, WebSocket as o, setupTailscaleExposureRoutes as p, isProviderStatusTerminal as r, WebSocketServer as s, createVoiceCallRuntime as t, resolveWebhookExposureStatus as u, resolveVoiceCallAgentId as v, mapVoiceToPolly as x, resolveUserPath as y };
|
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-Ln4SWaYK.js";
|
|
2
2
|
export { createVoiceCallRuntime };
|
|
@@ -10,34 +10,6 @@ const ProviderNameSchema = z.enum([
|
|
|
10
10
|
"plivo",
|
|
11
11
|
"mock"
|
|
12
12
|
]);
|
|
13
|
-
const CallStateSchema = z.enum([
|
|
14
|
-
"initiated",
|
|
15
|
-
"ringing",
|
|
16
|
-
"answered",
|
|
17
|
-
"active",
|
|
18
|
-
"speaking",
|
|
19
|
-
"listening",
|
|
20
|
-
"completed",
|
|
21
|
-
"hangup-user",
|
|
22
|
-
"hangup-bot",
|
|
23
|
-
"timeout",
|
|
24
|
-
"error",
|
|
25
|
-
"failed",
|
|
26
|
-
"no-answer",
|
|
27
|
-
"busy",
|
|
28
|
-
"voicemail"
|
|
29
|
-
]);
|
|
30
|
-
const TerminalStates = /* @__PURE__ */ new Set([
|
|
31
|
-
"completed",
|
|
32
|
-
"hangup-user",
|
|
33
|
-
"hangup-bot",
|
|
34
|
-
"timeout",
|
|
35
|
-
"error",
|
|
36
|
-
"failed",
|
|
37
|
-
"no-answer",
|
|
38
|
-
"busy",
|
|
39
|
-
"voicemail"
|
|
40
|
-
]);
|
|
41
13
|
const EndReasonSchema = z.enum([
|
|
42
14
|
"completed",
|
|
43
15
|
"hangup-user",
|
|
@@ -49,54 +21,16 @@ const EndReasonSchema = z.enum([
|
|
|
49
21
|
"busy",
|
|
50
22
|
"voicemail"
|
|
51
23
|
]);
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
from: z.string().optional(),
|
|
61
|
-
to: z.string().optional()
|
|
62
|
-
});
|
|
63
|
-
z.discriminatedUnion("type", [
|
|
64
|
-
BaseEventSchema.extend({ type: z.literal("call.initiated") }),
|
|
65
|
-
BaseEventSchema.extend({ type: z.literal("call.ringing") }),
|
|
66
|
-
BaseEventSchema.extend({ type: z.literal("call.answered") }),
|
|
67
|
-
BaseEventSchema.extend({ type: z.literal("call.active") }),
|
|
68
|
-
BaseEventSchema.extend({
|
|
69
|
-
type: z.literal("call.speaking"),
|
|
70
|
-
text: z.string()
|
|
71
|
-
}),
|
|
72
|
-
BaseEventSchema.extend({
|
|
73
|
-
type: z.literal("call.assistant-speech"),
|
|
74
|
-
transcript: z.string()
|
|
75
|
-
}),
|
|
76
|
-
BaseEventSchema.extend({
|
|
77
|
-
type: z.literal("call.speech"),
|
|
78
|
-
transcript: z.string(),
|
|
79
|
-
isFinal: z.boolean(),
|
|
80
|
-
confidence: z.number().min(0).max(1).optional()
|
|
81
|
-
}),
|
|
82
|
-
BaseEventSchema.extend({
|
|
83
|
-
type: z.literal("call.silence"),
|
|
84
|
-
durationMs: z.number()
|
|
85
|
-
}),
|
|
86
|
-
BaseEventSchema.extend({
|
|
87
|
-
type: z.literal("call.dtmf"),
|
|
88
|
-
digits: z.string()
|
|
89
|
-
}),
|
|
90
|
-
BaseEventSchema.extend({
|
|
91
|
-
type: z.literal("call.ended"),
|
|
92
|
-
reason: EndReasonSchema
|
|
93
|
-
}),
|
|
94
|
-
BaseEventSchema.extend({
|
|
95
|
-
type: z.literal("call.error"),
|
|
96
|
-
error: z.string(),
|
|
97
|
-
retryable: z.boolean().optional()
|
|
98
|
-
})
|
|
24
|
+
const CallStateSchema = z.enum([
|
|
25
|
+
"initiated",
|
|
26
|
+
"ringing",
|
|
27
|
+
"answered",
|
|
28
|
+
"active",
|
|
29
|
+
"speaking",
|
|
30
|
+
"listening",
|
|
31
|
+
...EndReasonSchema.options
|
|
99
32
|
]);
|
|
33
|
+
const TerminalStates = new Set(EndReasonSchema.options);
|
|
100
34
|
const CallDirectionSchema = z.enum(["outbound", "inbound"]);
|
|
101
35
|
const TranscriptEntrySchema = z.object({
|
|
102
36
|
timestamp: z.number(),
|
|
@@ -358,12 +292,14 @@ function pruneCallRecordEvents(stores) {
|
|
|
358
292
|
for (const row of sorted.slice(0, rows.length - MAX_CALL_RECORD_EVENTS)) deleteCallRecordEventRows(stores, row.key);
|
|
359
293
|
}
|
|
360
294
|
/** Read and reassemble one chunked call record event. */
|
|
361
|
-
function readCallRecordEvent(stores, eventKey) {
|
|
362
|
-
|
|
363
|
-
|
|
295
|
+
function readCallRecordEvent(stores, eventKey, meta) {
|
|
296
|
+
if (!Number.isSafeInteger(meta.chunkCount) || meta.chunkCount < 1 || meta.chunkCount > 48) return null;
|
|
297
|
+
const records = stores.chunks.lookupMany?.(Array.from({ length: meta.chunkCount }, (_, index) => buildChunkKey(eventKey, index)));
|
|
364
298
|
const chunks = [];
|
|
365
299
|
for (let index = 0; index < meta.chunkCount; index += 1) {
|
|
366
|
-
const
|
|
300
|
+
const result = records?.[index];
|
|
301
|
+
if (result && !result.ok) throw result.error;
|
|
302
|
+
const chunk = records ? result?.value : stores.chunks.lookup(buildChunkKey(eventKey, index));
|
|
367
303
|
if (!chunk || chunk.index !== index) return null;
|
|
368
304
|
chunks.push(Buffer.from(chunk.dataBase64, "base64"));
|
|
369
305
|
}
|
|
@@ -372,7 +308,7 @@ function readCallRecordEvent(stores, eventKey) {
|
|
|
372
308
|
/** Read all persisted call records in stable persisted order. */
|
|
373
309
|
function readCallRecordEvents(stores) {
|
|
374
310
|
return stores.events.entries().toSorted((a, b) => a.createdAt - b.createdAt || a.key.localeCompare(b.key)).map((entry) => {
|
|
375
|
-
const call = readCallRecordEvent(stores, entry.key);
|
|
311
|
+
const call = readCallRecordEvent(stores, entry.key, entry.value);
|
|
376
312
|
return call ? {
|
|
377
313
|
call,
|
|
378
314
|
persistedAt: entry.value.persistedAt ?? entry.createdAt,
|
|
@@ -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-3Jy2k-eX.js";
|
|
2
2
|
import crypto from "node:crypto";
|
|
3
3
|
//#region extensions/voice-call/src/providers/telnyx.ts
|
|
4
4
|
function normalizeTelnyxDirection(direction) {
|
|
@@ -1,8 +1,8 @@
|
|
|
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 { a as normalizeProviderStatus,
|
|
5
|
-
import { c as verifyTwilioWebhook, i as readVoiceCallProviderJsonResponse, n as cancelProviderResponseBody, r as readProviderErrorResponseSnippet, t as guardedJsonApiRequest } from "./guarded-json-api-
|
|
4
|
+
import { a as normalizeProviderStatus, b as escapeXml, h as chunkAudio, i as mapProviderStatusToEndReason, l as getHeader, r as isProviderStatusTerminal, x as mapVoiceToPolly } from "./runtime-entry-Ln4SWaYK.js";
|
|
5
|
+
import { c as verifyTwilioWebhook, i as readVoiceCallProviderJsonResponse, n as cancelProviderResponseBody, r as readProviderErrorResponseSnippet, t as guardedJsonApiRequest } from "./guarded-json-api-3Jy2k-eX.js";
|
|
6
6
|
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
7
7
|
import { sleepWithAbort } from "openclaw/plugin-sdk/runtime-env";
|
|
8
8
|
import crypto from "node:crypto";
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "voice-call",
|
|
3
|
+
"categories": ["voice", "tools"],
|
|
3
4
|
"doctorContract": {
|
|
4
5
|
"resolveSessionStoreAgentIds": true,
|
|
5
|
-
"stateMigrations":
|
|
6
|
+
"stateMigrations": [{ "id": "voice-call-calls-jsonl-to-plugin-state" }]
|
|
6
7
|
},
|
|
7
8
|
"name": "Voice Call",
|
|
8
9
|
"description": "OpenClaw voice-call plugin for Twilio, Telnyx, and Plivo phone calls.",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/voice-call",
|
|
3
|
-
"version": "2026.9.
|
|
3
|
+
"version": "2026.9.4",
|
|
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.9.
|
|
16
|
+
"openclaw": ">=2026.9.4"
|
|
17
17
|
},
|
|
18
18
|
"peerDependenciesMeta": {
|
|
19
19
|
"openclaw": {
|
|
@@ -31,10 +31,10 @@
|
|
|
31
31
|
"minHostVersion": ">=2026.4.10"
|
|
32
32
|
},
|
|
33
33
|
"compat": {
|
|
34
|
-
"pluginApi": ">=2026.9.
|
|
34
|
+
"pluginApi": ">=2026.9.4"
|
|
35
35
|
},
|
|
36
36
|
"build": {
|
|
37
|
-
"openclawVersion": "2026.9.
|
|
37
|
+
"openclawVersion": "2026.9.4"
|
|
38
38
|
},
|
|
39
39
|
"release": {
|
|
40
40
|
"publishToClawHub": true,
|