@openclaw/voice-call 2026.8.1-beta.2 → 2026.8.1
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/README.md +4 -3
- package/dist/doctor-contract-api.js +10 -5
- package/dist/{guarded-json-api-DEDwgf4J.js → guarded-json-api-CQXCe4aR.js} +1 -1
- package/dist/index.js +390 -338
- package/dist/{mock-BCq0q36l.js → mock-Dv7KxYGt.js} +3 -1
- package/dist/{plivo-DbI17uAc.js → plivo-Ct5BT5LQ.js} +2 -2
- package/dist/{realtime-handler-uXR0QgVa.js → realtime-handler-CSK1A1Gg.js} +270 -144
- package/dist/{response-generator-D-hkzo5Y.js → response-generator-xbT37lYx.js} +59 -12
- package/dist/{runtime-entry-F13eJ4pp.js → runtime-entry-q02qkX8F.js} +435 -231
- package/dist/runtime-entry.js +1 -1
- package/dist/{store-path-51_oC_xn.js → store-path-Cf6k3F2U.js} +3 -10
- package/dist/{telnyx-sIS_WgnY.js → telnyx-C9Yizvox.js} +12 -8
- package/dist/{twilio-CbeVrVAl.js → twilio-BLjVv246.js} +40 -27
- package/node_modules/typebox/build/format/date.d.mts +1 -1
- package/node_modules/typebox/build/format/date.mjs +1 -1
- package/node_modules/typebox/build/format/date_time.d.mts +2 -2
- package/node_modules/typebox/build/format/date_time.mjs +3 -3
- package/node_modules/typebox/build/format/email.d.mts +1 -1
- package/node_modules/typebox/build/format/email.mjs +2 -2
- package/node_modules/typebox/build/format/hostname.mjs +2 -10
- package/node_modules/typebox/build/format/idn_email.d.mts +1 -1
- package/node_modules/typebox/build/format/idn_email.mjs +3 -3
- package/node_modules/typebox/build/format/idn_hostname.d.mts +1 -0
- package/node_modules/typebox/build/format/idn_hostname.mjs +3 -11
- package/node_modules/typebox/build/format/idna/format/bidi.d.mts +3 -0
- package/node_modules/typebox/build/format/idna/format/bidi.mjs +98 -0
- package/node_modules/typebox/build/format/idna/format/puny.d.mts +3 -0
- package/node_modules/typebox/build/format/idna/format/puny.mjs +144 -0
- package/node_modules/typebox/build/format/idna/hostname.d.mts +1 -0
- package/node_modules/typebox/build/format/idna/hostname.mjs +16 -0
- package/node_modules/typebox/build/format/idna/idn-hostname.d.mts +1 -0
- package/node_modules/typebox/build/format/idna/idn-hostname.mjs +23 -0
- package/node_modules/typebox/build/format/idna/index.d.mts +2 -0
- package/node_modules/typebox/build/format/idna/index.mjs +2 -0
- package/node_modules/typebox/build/format/idna/label/ascii.d.mts +1 -0
- package/node_modules/typebox/build/format/idna/label/ascii.mjs +9 -0
- package/node_modules/typebox/build/format/idna/label/puny.d.mts +1 -0
- package/node_modules/typebox/build/format/idna/label/puny.mjs +28 -0
- package/node_modules/typebox/build/format/idna/label/unicode.d.mts +1 -0
- package/node_modules/typebox/build/format/idna/label/unicode.mjs +106 -0
- package/node_modules/typebox/build/format/idna/pattern/pattern.d.mts +30 -0
- package/node_modules/typebox/build/format/idna/pattern/pattern.mjs +70 -0
- package/node_modules/typebox/build/format/ipv4.d.mts +0 -1
- package/node_modules/typebox/build/format/ipv4.mjs +2 -32
- package/node_modules/typebox/build/format/ipv6.mjs +2 -61
- package/node_modules/typebox/build/format/iri.d.mts +2 -2
- package/node_modules/typebox/build/format/iri.mjs +26 -8
- package/node_modules/typebox/build/format/iri_reference.d.mts +2 -2
- package/node_modules/typebox/build/format/iri_reference.mjs +9 -55
- package/node_modules/typebox/build/format/regex.d.mts +1 -2
- package/node_modules/typebox/build/format/regex.mjs +2 -6
- package/node_modules/typebox/build/format/time.d.mts +2 -2
- package/node_modules/typebox/build/format/time.mjs +19 -14
- package/node_modules/typebox/build/format/uri.d.mts +1 -1
- package/node_modules/typebox/build/format/uri.mjs +3 -130
- package/node_modules/typebox/build/format/uri_reference.d.mts +1 -1
- package/node_modules/typebox/build/format/uri_reference.mjs +2 -3
- package/node_modules/typebox/build/format/uri_template.d.mts +2 -3
- package/node_modules/typebox/build/format/uri_template.mjs +4 -5
- package/node_modules/typebox/build/format/url.d.mts +3 -3
- package/node_modules/typebox/build/format/url.mjs +4 -5
- package/node_modules/typebox/build/guard/emit.d.mts +3 -1
- package/node_modules/typebox/build/guard/emit.mjs +12 -5
- package/node_modules/typebox/build/guard/guard.d.mts +8 -2
- package/node_modules/typebox/build/guard/guard.mjs +25 -4
- package/node_modules/typebox/build/guard/string.d.mts +2 -2
- package/node_modules/typebox/build/guard/string.mjs +7 -7
- package/node_modules/typebox/build/schema/engine/_context.mjs +3 -3
- package/node_modules/typebox/build/schema/engine/_functions.mjs +1 -2
- package/node_modules/typebox/build/schema/engine/_regexp.d.mts +2 -0
- package/node_modules/typebox/build/schema/engine/_regexp.mjs +4 -0
- package/node_modules/typebox/build/schema/engine/_stack.mjs +12 -13
- package/node_modules/typebox/build/schema/engine/additionalItems.mjs +17 -7
- package/node_modules/typebox/build/schema/engine/additionalProperties.mjs +4 -3
- package/node_modules/typebox/build/schema/engine/contains.mjs +18 -6
- package/node_modules/typebox/build/schema/engine/enum.mjs +1 -1
- package/node_modules/typebox/build/schema/engine/items.mjs +26 -6
- package/node_modules/typebox/build/schema/engine/maxContains.mjs +3 -3
- package/node_modules/typebox/build/schema/engine/minContains.mjs +14 -4
- package/node_modules/typebox/build/schema/engine/not.mjs +4 -2
- package/node_modules/typebox/build/schema/engine/oneOf.mjs +7 -6
- package/node_modules/typebox/build/schema/engine/pattern.mjs +3 -2
- package/node_modules/typebox/build/schema/engine/patternProperties.mjs +4 -3
- package/node_modules/typebox/build/schema/engine/ref.mjs +1 -7
- package/node_modules/typebox/build/schema/engine/schema.mjs +13 -4
- package/node_modules/typebox/build/schema/engine/type.mjs +1 -1
- package/node_modules/typebox/build/schema/resolve/resolve.d.mts +4 -1
- package/node_modules/typebox/build/schema/resolve/resolve.mjs +202 -1
- package/node_modules/typebox/build/schema/static/propertyNames.d.mts +5 -0
- package/node_modules/typebox/build/schema/static/propertyNames.mjs +3 -0
- package/node_modules/typebox/build/schema/static/schema.d.mts +3 -0
- package/node_modules/typebox/build/schema/types/index.d.mts +0 -2
- package/node_modules/typebox/build/schema/types/index.mjs +0 -2
- package/node_modules/typebox/build/system/memory/clone.mjs +14 -11
- package/node_modules/typebox/build/system/memory/discard.mjs +6 -5
- package/node_modules/typebox/build/system/settings/settings.d.mts +9 -0
- package/node_modules/typebox/build/system/settings/settings.mjs +2 -0
- package/node_modules/typebox/build/type/engine/call/distribute_arguments.d.mts +6 -1
- package/node_modules/typebox/build/type/engine/call/distribute_arguments.mjs +16 -3
- package/node_modules/typebox/build/type/engine/call/instantiate.mjs +34 -4
- package/node_modules/typebox/build/type/engine/evaluate/broaden.d.mts +5 -5
- package/node_modules/typebox/build/type/engine/evaluate/broaden.mjs +20 -26
- package/node_modules/typebox/build/type/engine/evaluate/compare.d.mts +6 -10
- package/node_modules/typebox/build/type/engine/evaluate/compare.mjs +9 -13
- package/node_modules/typebox/build/type/engine/evaluate/composite.d.mts +7 -4
- package/node_modules/typebox/build/type/engine/evaluate/composite.mjs +12 -19
- package/node_modules/typebox/build/type/engine/evaluate/distribute.d.mts +4 -20
- package/node_modules/typebox/build/type/engine/evaluate/distribute.mjs +8 -20
- package/node_modules/typebox/build/type/engine/evaluate/evaluate.d.mts +4 -4
- package/node_modules/typebox/build/type/engine/evaluate/evaluate.mjs +11 -8
- package/node_modules/typebox/build/type/engine/evaluate/flatten.d.mts +1 -1
- package/node_modules/typebox/build/type/engine/evaluate/flatten.mjs +3 -4
- package/node_modules/typebox/build/type/engine/evaluate/narrow.d.mts +17 -2
- package/node_modules/typebox/build/type/engine/evaluate/narrow.mjs +26 -6
- package/node_modules/typebox/build/type/engine/exclude/operation.mjs +3 -4
- package/node_modules/typebox/build/type/engine/extract/operation.d.mts +1 -1
- package/node_modules/typebox/build/type/engine/extract/operation.mjs +3 -4
- package/node_modules/typebox/build/type/engine/priority/priority.d.mts +2 -2
- package/node_modules/typebox/build/type/engine/priority/priority.mjs +3 -4
- package/node_modules/typebox/build/type/script/mapping.d.mts +32 -29
- package/node_modules/typebox/build/type/script/mapping.mjs +18 -25
- package/node_modules/typebox/build/type/script/parser.d.mts +19 -25
- package/node_modules/typebox/build/type/script/parser.mjs +27 -23
- package/node_modules/typebox/build/value/errors/errors.d.mts +2 -2
- package/node_modules/typebox/build/value/errors/errors.mjs +1 -1
- package/node_modules/typebox/package.json +27 -27
- package/node_modules/typebox/readme.md +79 -82
- package/node_modules/ws/lib/permessage-deflate.js +3 -1
- package/node_modules/ws/package.json +1 -1
- package/openclaw.plugin.json +20 -2
- package/package.json +6 -6
- package/node_modules/typebox/build/format/_idna.d.mts +0 -2
- package/node_modules/typebox/build/format/_idna.mjs +0 -246
- package/node_modules/typebox/build/format/_puny.d.mts +0 -1
- package/node_modules/typebox/build/format/_puny.mjs +0 -77
- package/node_modules/typebox/build/schema/resolve/ref.d.mts +0 -3
- package/node_modules/typebox/build/schema/resolve/ref.mjs +0 -146
- package/node_modules/typebox/build/schema/types/contentEncoding.d.mts +0 -9
- package/node_modules/typebox/build/schema/types/contentEncoding.mjs +0 -13
- package/node_modules/typebox/build/schema/types/contentMediaType.d.mts +0 -9
- package/node_modules/typebox/build/schema/types/contentMediaType.mjs +0 -13
|
@@ -1,17 +1,18 @@
|
|
|
1
|
-
import { o as canonicalizeVoiceCallMediaBase64 } from "./runtime-entry-
|
|
1
|
+
import { T as REALTIME_VOICE_END_CALL_TOOL_NAME, o as canonicalizeVoiceCallMediaBase64, x as resolveVoiceCallPublicPathPrefix } from "./runtime-entry-q02qkX8F.js";
|
|
2
2
|
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
3
|
+
import { asNullableRecord, asOptionalObjectRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
3
4
|
import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env";
|
|
4
|
-
import { REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME, buildRealtimeVoiceAgentConsultWorkingResponse, calculateMulawRms, createRealtimeVoiceSessionHarness, createSpeechThresholdGate, readRealtimeVoiceConsultQuestion, readSpeakableRealtimeVoiceToolResult } from "openclaw/plugin-sdk/realtime-voice";
|
|
5
|
+
import { REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME, REALTIME_VOICE_AUDIO_FORMAT_G711_ULAW_8KHZ, buildRealtimeVoiceAgentConsultWorkingResponse, calculateMulawRms, createRealtimeVoiceSessionHarness, createSpeechThresholdGate, readRealtimeVoiceConsultQuestion, readSpeakableRealtimeVoiceToolResult } from "openclaw/plugin-sdk/realtime-voice";
|
|
5
6
|
import { normalizeWebhookPath } from "openclaw/plugin-sdk/webhook-ingress";
|
|
6
|
-
import { randomUUID } from "node:crypto";
|
|
7
7
|
import { isFutureDateTimestampMs, resolveExpiresAtMsFromDurationMs } from "openclaw/plugin-sdk/number-runtime";
|
|
8
|
+
import { randomUUID } from "node:crypto";
|
|
8
9
|
import { sliceUtf16Safe, truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
|
9
10
|
import "node:http";
|
|
10
11
|
import WebSocket$1, { WebSocketServer } from "ws";
|
|
11
12
|
//#region extensions/voice-call/src/webhook/realtime-audio-pacer.ts
|
|
12
13
|
const TELEPHONY_SAMPLE_RATE = 8e3;
|
|
13
14
|
const TELEPHONY_CHUNK_BYTES = 160;
|
|
14
|
-
const
|
|
15
|
+
const LEAD_MS = 160;
|
|
15
16
|
const DEFAULT_MAX_QUEUED_AUDIO_BYTES = TELEPHONY_SAMPLE_RATE * 120;
|
|
16
17
|
const QUEUE_COMPACT_HEAD_THRESHOLD = 256;
|
|
17
18
|
/** Paces outgoing mulaw audio frames at telephony cadence. */
|
|
@@ -23,6 +24,7 @@ var RealtimeAudioPacer = class {
|
|
|
23
24
|
this.timer = null;
|
|
24
25
|
this.queuedAudioBytes = 0;
|
|
25
26
|
this.closed = false;
|
|
27
|
+
this.streamClockMs = null;
|
|
26
28
|
}
|
|
27
29
|
/** Queue mulaw audio and split it into 20ms-ish telephony chunks. */
|
|
28
30
|
sendAudio(muLaw) {
|
|
@@ -37,7 +39,7 @@ var RealtimeAudioPacer = class {
|
|
|
37
39
|
this.queue.push({
|
|
38
40
|
type: "audio",
|
|
39
41
|
chunk,
|
|
40
|
-
durationMs:
|
|
42
|
+
durationMs: chunk.length / 8
|
|
41
43
|
});
|
|
42
44
|
this.queuedAudioBytes += chunk.length;
|
|
43
45
|
}
|
|
@@ -59,6 +61,7 @@ var RealtimeAudioPacer = class {
|
|
|
59
61
|
this.clearTimer();
|
|
60
62
|
this.resetQueue();
|
|
61
63
|
this.queuedAudioBytes = 0;
|
|
64
|
+
this.streamClockMs = null;
|
|
62
65
|
this.params.send(this.params.serializer.clear());
|
|
63
66
|
return clearedAudioBytes;
|
|
64
67
|
}
|
|
@@ -72,6 +75,7 @@ var RealtimeAudioPacer = class {
|
|
|
72
75
|
this.clearTimer();
|
|
73
76
|
this.resetQueue();
|
|
74
77
|
this.queuedAudioBytes = 0;
|
|
78
|
+
this.streamClockMs = null;
|
|
75
79
|
}
|
|
76
80
|
/** Clear the scheduled pump timer. */
|
|
77
81
|
clearTimer() {
|
|
@@ -110,25 +114,34 @@ var RealtimeAudioPacer = class {
|
|
|
110
114
|
this.queue.length = 0;
|
|
111
115
|
this.queueHead = 0;
|
|
112
116
|
}
|
|
113
|
-
/**
|
|
117
|
+
/** Fill the provider playout cushion, then wake at the next timeline boundary. */
|
|
114
118
|
pump() {
|
|
115
119
|
this.timer = null;
|
|
116
120
|
if (this.closed) return;
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
121
|
+
const now = performance.now();
|
|
122
|
+
this.streamClockMs ??= now;
|
|
123
|
+
while (this.pendingQueueSize > 0 && this.streamClockMs < now + LEAD_MS) {
|
|
124
|
+
const item = this.takeNextItem();
|
|
125
|
+
if (!item) break;
|
|
126
|
+
if (!(item.type === "audio" ? this.sendAudioItem(item) : this.params.send(this.params.serializer.mark(item.name)))) {
|
|
127
|
+
this.resetQueue();
|
|
128
|
+
this.queuedAudioBytes = 0;
|
|
129
|
+
this.streamClockMs = null;
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (this.pendingQueueSize === 0) {
|
|
134
|
+
this.streamClockMs = null;
|
|
129
135
|
return;
|
|
130
136
|
}
|
|
131
|
-
|
|
137
|
+
const delayMs = Math.max(1, this.streamClockMs - LEAD_MS - performance.now());
|
|
138
|
+
this.timer = setTimeout(() => this.pump(), delayMs);
|
|
139
|
+
}
|
|
140
|
+
sendAudioItem(item) {
|
|
141
|
+
this.queuedAudioBytes = Math.max(0, this.queuedAudioBytes - item.chunk.length);
|
|
142
|
+
const sent = this.params.send(this.params.serializer.media(item.chunk.toString("base64")));
|
|
143
|
+
this.streamClockMs = (this.streamClockMs ?? performance.now()) + item.durationMs;
|
|
144
|
+
return sent;
|
|
132
145
|
}
|
|
133
146
|
};
|
|
134
147
|
//#endregion
|
|
@@ -144,15 +157,13 @@ function parseTimestampMs(value) {
|
|
|
144
157
|
/** Parse a JSON object frame, returning null for invalid or non-object payloads. */
|
|
145
158
|
function tryParseJson(rawMessage) {
|
|
146
159
|
try {
|
|
147
|
-
|
|
148
|
-
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
|
|
160
|
+
return asNullableRecord(JSON.parse(rawMessage));
|
|
149
161
|
} catch {}
|
|
150
162
|
return null;
|
|
151
163
|
}
|
|
152
164
|
/** Read an object-valued field from a parsed frame. */
|
|
153
165
|
function readRecordField(record, field) {
|
|
154
|
-
|
|
155
|
-
return typeof value === "object" && value !== null ? value : void 0;
|
|
166
|
+
return asOptionalObjectRecord(record[field]);
|
|
156
167
|
}
|
|
157
168
|
/** Parse a common provider media frame. */
|
|
158
169
|
function parseMediaFrame(msg) {
|
|
@@ -297,6 +308,8 @@ const STREAM_TOKEN_TTL_MS = 3e4;
|
|
|
297
308
|
const DEFAULT_HOST = "localhost:8443";
|
|
298
309
|
const MAX_REALTIME_MESSAGE_BYTES = 256 * 1024;
|
|
299
310
|
const MAX_REALTIME_WS_BUFFERED_BYTES = 1024 * 1024;
|
|
311
|
+
const REALTIME_MEDIA_INACTIVITY_TIMEOUT_MS = 3e4;
|
|
312
|
+
const REALTIME_DISCONNECT_HANGUP_GRACE_MS = 2e3;
|
|
300
313
|
const FORCED_CONSULT_FALLBACK_DELAY_MS = 200;
|
|
301
314
|
const FORCED_CONSULT_NATIVE_DEDUPE_MS = 2e3;
|
|
302
315
|
const FORCED_CONSULT_RESULT_MAX_CHARS = 1800;
|
|
@@ -429,25 +442,24 @@ function appendRecentTalkEventMetadata(call, event) {
|
|
|
429
442
|
call.metadata = metadata;
|
|
430
443
|
}
|
|
431
444
|
var RealtimeCallHandler = class {
|
|
432
|
-
constructor(config, manager,
|
|
445
|
+
constructor(config, manager, resolveCallRegistration, servePath, streamDisconnectLifecycle, coreConfig) {
|
|
433
446
|
this.config = config;
|
|
434
447
|
this.manager = manager;
|
|
435
|
-
this.
|
|
436
|
-
this.realtimeProvider = realtimeProvider;
|
|
437
|
-
this.providerConfig = providerConfig;
|
|
448
|
+
this.resolveCallRegistration = resolveCallRegistration;
|
|
438
449
|
this.servePath = servePath;
|
|
450
|
+
this.streamDisconnectLifecycle = streamDisconnectLifecycle;
|
|
439
451
|
this.coreConfig = coreConfig;
|
|
440
|
-
this.resolveInstructions = resolveInstructions;
|
|
441
452
|
this.toolHandlers = /* @__PURE__ */ new Map();
|
|
442
453
|
this.pendingStreamTokens = /* @__PURE__ */ new Map();
|
|
443
454
|
this.activeSockets = /* @__PURE__ */ new Set();
|
|
444
455
|
this.serverClosingSockets = /* @__PURE__ */ new WeakSet();
|
|
445
456
|
this.activeBridgesByCallId = /* @__PURE__ */ new Map();
|
|
446
|
-
this.
|
|
457
|
+
this.activeTelephonyBindingsByCallId = /* @__PURE__ */ new Map();
|
|
447
458
|
this.userTranscriptStatesByCallId = /* @__PURE__ */ new Map();
|
|
448
459
|
this.forcedConsultsByCallId = /* @__PURE__ */ new Map();
|
|
449
460
|
this.consultSessionsByCallId = /* @__PURE__ */ new Map();
|
|
450
461
|
this.nativeConsultsInFlightByCallId = /* @__PURE__ */ new Map();
|
|
462
|
+
this.terminationAttempts = /* @__PURE__ */ new Set();
|
|
451
463
|
this.closePromise = null;
|
|
452
464
|
this.closing = false;
|
|
453
465
|
this.publicOrigin = null;
|
|
@@ -457,10 +469,7 @@ var RealtimeCallHandler = class {
|
|
|
457
469
|
try {
|
|
458
470
|
const parsed = new URL(url);
|
|
459
471
|
this.publicOrigin = parsed.host;
|
|
460
|
-
|
|
461
|
-
const normalizedPublicPath = normalizeWebhookPath(parsed.pathname);
|
|
462
|
-
const idx = normalizedPublicPath.indexOf(normalizedServePath);
|
|
463
|
-
this.publicPathPrefix = idx > 0 ? normalizedPublicPath.slice(0, idx) : "";
|
|
472
|
+
this.publicPathPrefix = resolveVoiceCallPublicPathPrefix(parsed.pathname, this.servePath);
|
|
464
473
|
} catch {
|
|
465
474
|
this.publicOrigin = null;
|
|
466
475
|
this.publicPathPrefix = "";
|
|
@@ -513,10 +522,10 @@ var RealtimeCallHandler = class {
|
|
|
513
522
|
maxPayload: MAX_REALTIME_MESSAGE_BYTES
|
|
514
523
|
}).handleUpgrade(request, socket, head, (ws) => {
|
|
515
524
|
this.activeSockets.add(ws);
|
|
516
|
-
let
|
|
525
|
+
let telephonyBinding = null;
|
|
517
526
|
let initialized = false;
|
|
518
527
|
let activeCallSid = "unknown";
|
|
519
|
-
let
|
|
528
|
+
let activeStreamSid = "unknown";
|
|
520
529
|
let lastMediaTimestamp;
|
|
521
530
|
let lastMediaGapWarnAt = 0;
|
|
522
531
|
ws.on("message", (data) => {
|
|
@@ -527,15 +536,18 @@ var RealtimeCallHandler = class {
|
|
|
527
536
|
if (initialized) return;
|
|
528
537
|
initialized = true;
|
|
529
538
|
activeCallSid = frame.providerCallId;
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
539
|
+
activeStreamSid = frame.streamId;
|
|
540
|
+
const nextBinding = this.handleCall(frame.streamId, frame.providerCallId, ws, callerMeta, adapter);
|
|
541
|
+
if (!nextBinding) return;
|
|
542
|
+
telephonyBinding = nextBinding;
|
|
543
|
+
this.streamDisconnectLifecycle.connect(activeCallSid, activeStreamSid);
|
|
533
544
|
return;
|
|
534
545
|
}
|
|
535
|
-
if (!
|
|
546
|
+
if (!telephonyBinding) return;
|
|
536
547
|
if (frame.kind === "media") {
|
|
537
548
|
const audio = Buffer.from(frame.payloadBase64, "base64");
|
|
538
|
-
|
|
549
|
+
telephonyBinding.noteMediaActivity();
|
|
550
|
+
telephonyBinding.bridge.sendAudio(audio);
|
|
539
551
|
if (frame.timestampMs !== void 0) {
|
|
540
552
|
if (lastMediaTimestamp !== void 0) {
|
|
541
553
|
const gapMs = frame.timestampMs - lastMediaTimestamp;
|
|
@@ -546,33 +558,31 @@ var RealtimeCallHandler = class {
|
|
|
546
558
|
}
|
|
547
559
|
}
|
|
548
560
|
lastMediaTimestamp = frame.timestampMs;
|
|
549
|
-
bridge.setMediaTimestamp(frame.timestampMs);
|
|
561
|
+
telephonyBinding.bridge.setMediaTimestamp(frame.timestampMs);
|
|
550
562
|
}
|
|
551
563
|
return;
|
|
552
564
|
}
|
|
553
565
|
if (frame.kind === "mark") {
|
|
554
|
-
bridge.acknowledgeMark();
|
|
566
|
+
telephonyBinding.bridge.acknowledgeMark();
|
|
555
567
|
return;
|
|
556
568
|
}
|
|
557
569
|
if (frame.kind === "error") {
|
|
558
570
|
console.error(`[voice-call] realtime WS error frame providerCallId=${activeCallSid} code=${frame.code ?? "?"} title=${frame.title ?? ""} detail=${frame.detail ?? ""}`);
|
|
559
571
|
return;
|
|
560
572
|
}
|
|
561
|
-
if (frame.kind === "stop")
|
|
562
|
-
stopReceived = true;
|
|
563
|
-
this.closeTelephonyBridge(activeCallSid, bridge, "completed");
|
|
564
|
-
}
|
|
573
|
+
if (frame.kind === "stop") telephonyBinding.close("disconnect");
|
|
565
574
|
} catch (error) {
|
|
566
575
|
console.error("[voice-call] realtime WS parse failed:", error);
|
|
567
576
|
}
|
|
568
577
|
});
|
|
569
|
-
ws.on("close", (
|
|
578
|
+
ws.on("close", () => {
|
|
570
579
|
this.activeSockets.delete(ws);
|
|
571
|
-
const reason = this.serverClosingSockets.has(ws)
|
|
572
|
-
|
|
580
|
+
const reason = this.serverClosingSockets.has(ws) ? "shutdown" : "disconnect";
|
|
581
|
+
if (telephonyBinding) telephonyBinding.close(reason);
|
|
573
582
|
});
|
|
574
583
|
ws.on("error", (error) => {
|
|
575
584
|
console.error("[voice-call] realtime WS error:", error);
|
|
585
|
+
ws.terminate();
|
|
576
586
|
});
|
|
577
587
|
if (this.closing) {
|
|
578
588
|
this.serverClosingSockets.add(ws);
|
|
@@ -593,7 +603,8 @@ var RealtimeCallHandler = class {
|
|
|
593
603
|
this.serverClosingSockets.add(ws);
|
|
594
604
|
ws.once("close", () => resolve());
|
|
595
605
|
ws.terminate();
|
|
596
|
-
}))]).then(() => {
|
|
606
|
+
}))]).then(async () => {
|
|
607
|
+
await Promise.all(this.terminationAttempts);
|
|
597
608
|
this.pendingStreamTokens.clear();
|
|
598
609
|
}).finally(() => {
|
|
599
610
|
this.closing = false;
|
|
@@ -635,13 +646,24 @@ var RealtimeCallHandler = class {
|
|
|
635
646
|
}
|
|
636
647
|
issueStreamToken(meta = {}) {
|
|
637
648
|
const token = randomUUID();
|
|
638
|
-
const
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
649
|
+
const expiry = resolveExpiresAtMsFromDurationMs(STREAM_TOKEN_TTL_MS, { nowMs: Date.now() });
|
|
650
|
+
if (expiry !== void 0) {
|
|
651
|
+
this.pendingStreamTokens.set(token, {
|
|
652
|
+
expiry,
|
|
653
|
+
...meta
|
|
654
|
+
});
|
|
655
|
+
const host = this.publicOrigin || DEFAULT_HOST;
|
|
656
|
+
const streamPathPattern = this.getStreamPathPattern();
|
|
657
|
+
setTimeout(() => {
|
|
658
|
+
if (!this.pendingStreamTokens.has(token)) return;
|
|
659
|
+
this.pendingStreamTokens.delete(token);
|
|
660
|
+
if (this.closing) return;
|
|
661
|
+
const call = meta.callId ? ` for call ${meta.callId}` : "";
|
|
662
|
+
const endpoints = [meta.from ? `from ${meta.from}` : "", meta.to ? `to ${meta.to}` : ""].filter(Boolean).join(" ");
|
|
663
|
+
const participants = endpoints ? ` (${endpoints})` : "";
|
|
664
|
+
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).`);
|
|
665
|
+
}, STREAM_TOKEN_TTL_MS).unref?.();
|
|
666
|
+
}
|
|
645
667
|
return token;
|
|
646
668
|
}
|
|
647
669
|
consumeStreamToken(token) {
|
|
@@ -658,20 +680,58 @@ var RealtimeCallHandler = class {
|
|
|
658
680
|
};
|
|
659
681
|
}
|
|
660
682
|
handleCall(streamSid, callSid, ws, callerMeta, adapter) {
|
|
661
|
-
const
|
|
662
|
-
if (!
|
|
683
|
+
const preparedCall = this.prepareCallInManager(callSid, callerMeta);
|
|
684
|
+
if (!preparedCall) {
|
|
663
685
|
ws.close(1008, "Caller rejected by policy");
|
|
664
686
|
return null;
|
|
665
687
|
}
|
|
666
|
-
const {
|
|
667
|
-
const
|
|
688
|
+
const { callRecord } = preparedCall;
|
|
689
|
+
const callId = callRecord.callId;
|
|
690
|
+
const hadPredecessorOnAdmission = this.activeBridgesByCallId.has(callId);
|
|
691
|
+
const previousTelephonyBinding = this.activeTelephonyBindingsByCallId.get(callId);
|
|
692
|
+
let callEndPromise;
|
|
693
|
+
const emitCallEnd = (cause) => {
|
|
694
|
+
if (callEndPromise) return callEndPromise;
|
|
695
|
+
const reason = cause === "error" ? "error" : cause === "inactivity" ? "timeout" : "completed";
|
|
696
|
+
const attempt = this.manager.endCall(callId, { reason }).then((result) => {
|
|
697
|
+
if (!result.success) {
|
|
698
|
+
console.warn(`[voice-call] Failed to end realtime call callId=${callId} providerCallId=${callSid} reason=${reason}: ${result.error ?? "unknown error"}; call remains active`);
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
console.log(`[voice-call] Realtime call ended callId=${callId} providerCallId=${callSid} reason=${cause}`);
|
|
702
|
+
});
|
|
703
|
+
callEndPromise = attempt;
|
|
704
|
+
this.terminationAttempts.add(attempt);
|
|
705
|
+
const release = () => this.terminationAttempts.delete(attempt);
|
|
706
|
+
attempt.then(release, release);
|
|
707
|
+
return attempt;
|
|
708
|
+
};
|
|
709
|
+
let registration;
|
|
710
|
+
try {
|
|
711
|
+
registration = this.resolveCallRegistration(callRecord);
|
|
712
|
+
} catch (error) {
|
|
713
|
+
console.error(`[voice-call] Failed to resolve realtime call registration callId=${callId} providerCallId=${callSid}: ${formatErrorMessage(error)}`);
|
|
714
|
+
if (!hadPredecessorOnAdmission) emitCallEnd("error");
|
|
715
|
+
ws.close(1011, "Check realtime configuration for routed agent");
|
|
716
|
+
return null;
|
|
717
|
+
}
|
|
718
|
+
const { baseFields, initialGreeting } = preparedCall;
|
|
719
|
+
if (callRecord.metadata) delete callRecord.metadata.initialMessage;
|
|
720
|
+
this.manager.processEvent({
|
|
721
|
+
id: `realtime-answered-${callSid}`,
|
|
722
|
+
callId,
|
|
723
|
+
type: "call.answered",
|
|
724
|
+
...baseFields
|
|
725
|
+
});
|
|
726
|
+
const { agentId, instructions, provider: realtimeProvider, providerConfig } = registration;
|
|
727
|
+
const initialGreetingInstructions = buildGreetingInstructions(instructions, initialGreeting);
|
|
668
728
|
const harness = createRealtimeVoiceSessionHarness({
|
|
669
729
|
talk: {
|
|
670
730
|
sessionId: `voice-call:${callId}:realtime`,
|
|
671
731
|
mode: "realtime",
|
|
672
732
|
transport: "gateway-relay",
|
|
673
733
|
brain: "agent-consult",
|
|
674
|
-
provider:
|
|
734
|
+
provider: realtimeProvider.id
|
|
675
735
|
},
|
|
676
736
|
talkPayloads: {
|
|
677
737
|
turnStarted: () => ({
|
|
@@ -697,7 +757,7 @@ var RealtimeCallHandler = class {
|
|
|
697
757
|
},
|
|
698
758
|
onTalkEvent: (event) => appendRecentTalkEventMetadata(callRecord, event)
|
|
699
759
|
});
|
|
700
|
-
let providerHandlesInputAudioBargeIn =
|
|
760
|
+
let providerHandlesInputAudioBargeIn = realtimeProvider.capabilities?.handlesInputAudioBargeIn === true;
|
|
701
761
|
const cancelOutputAudioForBargeIn = (source, interruptProvider, clearedAudioBytes = 0) => {
|
|
702
762
|
const outputAudioActive = harness.talk.outputAudioActive;
|
|
703
763
|
const pendingTelephonyAudio = audioPacer.hasPendingAudio();
|
|
@@ -727,20 +787,6 @@ var RealtimeCallHandler = class {
|
|
|
727
787
|
}
|
|
728
788
|
});
|
|
729
789
|
console.log(`[voice-call] Realtime bridge starting for call ${callId} (providerCallId=${callSid}, initialGreeting=${initialGreetingInstructions ? "queued" : "absent"})`);
|
|
730
|
-
let callEndEmitted = false;
|
|
731
|
-
const emitCallEnd = (reason) => {
|
|
732
|
-
if (callEndEmitted) return;
|
|
733
|
-
callEndEmitted = true;
|
|
734
|
-
this.endCallInManager(callSid, callId, reason);
|
|
735
|
-
if (reason !== "error") return;
|
|
736
|
-
Promise.resolve(this.provider.hangupCall({
|
|
737
|
-
callId,
|
|
738
|
-
providerCallId: callSid,
|
|
739
|
-
reason
|
|
740
|
-
})).catch((error) => {
|
|
741
|
-
console.warn(`[voice-call] Failed to hang up realtime call ${callSid}: ${formatErrorMessage(error)}`);
|
|
742
|
-
});
|
|
743
|
-
};
|
|
744
790
|
const sendString = (message) => {
|
|
745
791
|
if (ws.readyState !== WebSocket$1.OPEN) return false;
|
|
746
792
|
if (ws.bufferedAmount > MAX_REALTIME_WS_BUFFERED_BYTES) {
|
|
@@ -773,15 +819,18 @@ var RealtimeCallHandler = class {
|
|
|
773
819
|
speechFrames: BARGE_IN_REQUIRED_LOUD_CHUNKS,
|
|
774
820
|
silenceFrames: 12
|
|
775
821
|
});
|
|
776
|
-
const interruptResponseOnInputAudio = typeof
|
|
777
|
-
const hadPredecessorOnAdmission = this.activeBridgesByCallId.has(callId);
|
|
822
|
+
const interruptResponseOnInputAudio = typeof providerConfig.interruptResponseOnInputAudio === "boolean" ? providerConfig.interruptResponseOnInputAudio : void 0;
|
|
778
823
|
const nativeConsultOwner = {};
|
|
824
|
+
let provisionalCloseReason;
|
|
825
|
+
let sessionClosed = false;
|
|
779
826
|
const userTranscriptAdoption = this.beginUserTranscriptOwnerAdoption(callId);
|
|
780
827
|
const userTranscriptOwner = userTranscriptAdoption.owner;
|
|
781
828
|
const bridgeParams = {
|
|
782
|
-
provider:
|
|
829
|
+
provider: realtimeProvider,
|
|
783
830
|
cfg: this.coreConfig,
|
|
784
|
-
|
|
831
|
+
agentId,
|
|
832
|
+
providerConfig,
|
|
833
|
+
audioFormat: REALTIME_VOICE_AUDIO_FORMAT_G711_ULAW_8KHZ,
|
|
785
834
|
interruptResponseOnInputAudio,
|
|
786
835
|
instructions,
|
|
787
836
|
tools: this.config.tools,
|
|
@@ -810,7 +859,7 @@ var RealtimeCallHandler = class {
|
|
|
810
859
|
},
|
|
811
860
|
onTranscript: (role, text, isFinal) => {
|
|
812
861
|
const owner = nativeConsultOwner.current;
|
|
813
|
-
if (!this.getUserTranscriptState(callId, userTranscriptOwner) || owner && !this.isActiveBridgeOwner(callId, owner)) return;
|
|
862
|
+
if (provisionalCloseReason || !this.getUserTranscriptState(callId, userTranscriptOwner) || owner && !this.isActiveBridgeOwner(callId, owner)) return;
|
|
814
863
|
const turnId = harness.ensureTurn();
|
|
815
864
|
const eventType = role === "assistant" ? isFinal ? "output.text.done" : "output.text.delta" : isFinal ? "transcript.done" : "transcript.delta";
|
|
816
865
|
const payload = role === "assistant" ? { text } : {
|
|
@@ -967,61 +1016,57 @@ var RealtimeCallHandler = class {
|
|
|
967
1016
|
});
|
|
968
1017
|
},
|
|
969
1018
|
onClose: (reason) => {
|
|
970
|
-
const owner = nativeConsultOwner.current;
|
|
971
|
-
const ownsCallState = owner ? this.isActiveBridgeOwner(callId, owner) : false;
|
|
972
|
-
if (owner) {
|
|
973
|
-
this.clearActiveBridgeMappings(callId, callSid, owner);
|
|
974
|
-
this.cancelConsultSession(callId, owner);
|
|
975
|
-
}
|
|
976
|
-
if (ownsCallState) this.clearUserTranscriptState(callId, userTranscriptOwner);
|
|
977
1019
|
harness.finishOutputAudio(reason);
|
|
978
1020
|
harness.emit({
|
|
979
1021
|
type: "session.closed",
|
|
980
1022
|
payload: { reason },
|
|
981
1023
|
final: true
|
|
982
1024
|
});
|
|
983
|
-
|
|
984
|
-
if (
|
|
985
|
-
|
|
986
|
-
|
|
1025
|
+
const owner = nativeConsultOwner.current;
|
|
1026
|
+
if (!owner) {
|
|
1027
|
+
provisionalCloseReason ??= reason;
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
const ownsCallState = this.isActiveBridgeOwner(callId, owner);
|
|
1031
|
+
this.clearActiveBridgeMappings(callId, callSid, owner);
|
|
1032
|
+
this.cancelConsultSession(callId, owner);
|
|
1033
|
+
if (ownsCallState) this.clearUserTranscriptState(callId, userTranscriptOwner);
|
|
1034
|
+
if (reason === "completed" && (!ownsCallState || sessionClosed)) return;
|
|
1035
|
+
this.streamDisconnectLifecycle.retire(callSid, streamSid);
|
|
1036
|
+
if (ws.readyState === WebSocket$1.OPEN) ws.close(reason === "error" ? 1011 : 1e3, "Bridge disconnected");
|
|
1037
|
+
if (ownsCallState) emitCallEnd(reason);
|
|
987
1038
|
}
|
|
988
1039
|
};
|
|
989
|
-
let
|
|
1040
|
+
let candidate;
|
|
990
1041
|
try {
|
|
991
|
-
|
|
1042
|
+
candidate = harness.createBridge(bridgeParams);
|
|
992
1043
|
} catch (error) {
|
|
1044
|
+
console.error("[voice-call] Failed to create realtime bridge:", error);
|
|
1045
|
+
}
|
|
1046
|
+
if (!candidate || provisionalCloseReason) {
|
|
993
1047
|
this.rollbackUserTranscriptOwnerAdoption(callId, userTranscriptAdoption);
|
|
1048
|
+
try {
|
|
1049
|
+
candidate?.close();
|
|
1050
|
+
} catch (error) {
|
|
1051
|
+
console.warn(`[voice-call] Failed to close realtime bridge ${callSid}: ${formatErrorMessage(error)}`);
|
|
1052
|
+
}
|
|
994
1053
|
harness.close();
|
|
995
1054
|
audioPacer.close();
|
|
996
|
-
|
|
997
|
-
if (
|
|
998
|
-
|
|
1055
|
+
const reason = provisionalCloseReason ?? "error";
|
|
1056
|
+
if (!hadPredecessorOnAdmission || !this.activeBridgesByCallId.has(callId)) emitCallEnd(reason);
|
|
1057
|
+
if (ws.readyState === WebSocket$1.OPEN) ws.close(reason === "error" ? 1011 : 1e3, "Failed to create realtime bridge");
|
|
999
1058
|
return null;
|
|
1000
1059
|
}
|
|
1060
|
+
const session = candidate;
|
|
1001
1061
|
this.commitUserTranscriptOwnerAdoption(callId, userTranscriptAdoption);
|
|
1002
1062
|
nativeConsultOwner.current = session;
|
|
1003
1063
|
providerHandlesInputAudioBargeIn = session.bridge.handlesInputAudioBargeIn ?? providerHandlesInputAudioBargeIn;
|
|
1004
|
-
const closeTelephony = (reason) => {
|
|
1005
|
-
try {
|
|
1006
|
-
session.close();
|
|
1007
|
-
} finally {
|
|
1008
|
-
emitCallEnd(reason);
|
|
1009
|
-
}
|
|
1010
|
-
};
|
|
1011
1064
|
const previousConsultSession = this.consultSessionsByCallId.get(callId);
|
|
1012
1065
|
if (previousConsultSession && previousConsultSession.owner !== session) this.cancelConsultSession(callId, previousConsultSession.owner);
|
|
1013
1066
|
this.consultSessionsByCallId.set(callId, {
|
|
1014
1067
|
owner: session,
|
|
1015
1068
|
coordinator: harness.forcedConsults
|
|
1016
1069
|
});
|
|
1017
|
-
this.activeBridgesByCallId.set(callId, session);
|
|
1018
|
-
this.activeBridgesByCallId.set(callSid, session);
|
|
1019
|
-
const telephonyCloser = {
|
|
1020
|
-
owner: session,
|
|
1021
|
-
close: closeTelephony
|
|
1022
|
-
};
|
|
1023
|
-
this.activeTelephonyClosersByCallId.set(callId, telephonyCloser);
|
|
1024
|
-
this.activeTelephonyClosersByCallId.set(callSid, telephonyCloser);
|
|
1025
1070
|
const sendAudioToSession = session.sendAudio.bind(session);
|
|
1026
1071
|
session.sendAudio = (audio) => {
|
|
1027
1072
|
if (speechDetector.accept({
|
|
@@ -1037,7 +1082,6 @@ var RealtimeCallHandler = class {
|
|
|
1037
1082
|
sendAudioToSession(audio);
|
|
1038
1083
|
};
|
|
1039
1084
|
const closeSession = session.close.bind(session);
|
|
1040
|
-
let sessionClosed = false;
|
|
1041
1085
|
session.close = () => {
|
|
1042
1086
|
if (sessionClosed) return;
|
|
1043
1087
|
sessionClosed = true;
|
|
@@ -1051,9 +1095,65 @@ var RealtimeCallHandler = class {
|
|
|
1051
1095
|
audioPacer.close();
|
|
1052
1096
|
}
|
|
1053
1097
|
};
|
|
1098
|
+
let livenessTimer;
|
|
1099
|
+
const clearLivenessTimer = () => {
|
|
1100
|
+
if (livenessTimer) {
|
|
1101
|
+
clearTimeout(livenessTimer);
|
|
1102
|
+
livenessTimer = void 0;
|
|
1103
|
+
}
|
|
1104
|
+
};
|
|
1105
|
+
let bindingClosed = false;
|
|
1106
|
+
const closeBinding = (binding, cause) => {
|
|
1107
|
+
if (bindingClosed) return callEndPromise ?? Promise.resolve();
|
|
1108
|
+
bindingClosed = true;
|
|
1109
|
+
clearLivenessTimer();
|
|
1110
|
+
const ownsCall = this.activeTelephonyBindingsByCallId.get(callId) === binding;
|
|
1111
|
+
let termination = Promise.resolve();
|
|
1112
|
+
try {
|
|
1113
|
+
session.close();
|
|
1114
|
+
} catch (error) {
|
|
1115
|
+
console.warn(`[voice-call] Failed to close realtime bridge ${callSid}: ${formatErrorMessage(error)}`);
|
|
1116
|
+
} finally {
|
|
1117
|
+
this.clearActiveTelephonyBinding(callId, binding);
|
|
1118
|
+
if (cause === "disconnect") this.streamDisconnectLifecycle.disconnect(callSid, streamSid);
|
|
1119
|
+
else this.streamDisconnectLifecycle.retire(callSid, streamSid);
|
|
1120
|
+
if (ownsCall && cause && cause !== "disconnect") termination = emitCallEnd(cause);
|
|
1121
|
+
}
|
|
1122
|
+
return termination;
|
|
1123
|
+
};
|
|
1124
|
+
const telephonyBinding = {
|
|
1125
|
+
bridge: session,
|
|
1126
|
+
close: (cause) => closeBinding(telephonyBinding, cause),
|
|
1127
|
+
endCall: () => {
|
|
1128
|
+
closeBinding(telephonyBinding);
|
|
1129
|
+
if (ws.readyState === WebSocket$1.OPEN) ws.close(1e3, "Call ended");
|
|
1130
|
+
},
|
|
1131
|
+
noteMediaActivity: () => {
|
|
1132
|
+
if (bindingClosed || this.activeTelephonyBindingsByCallId.get(callId) !== telephonyBinding) return;
|
|
1133
|
+
clearLivenessTimer();
|
|
1134
|
+
livenessTimer = setTimeout(() => {
|
|
1135
|
+
console.warn(`[voice-call] Realtime media inactive callId=${callId} providerCallId=${callSid} timeoutMs=${REALTIME_MEDIA_INACTIVITY_TIMEOUT_MS} graceMs=${REALTIME_DISCONNECT_HANGUP_GRACE_MS}`);
|
|
1136
|
+
livenessTimer = setTimeout(() => {
|
|
1137
|
+
telephonyBinding.close("inactivity");
|
|
1138
|
+
if (ws.readyState === WebSocket$1.OPEN) ws.close(1e3, "Media inactivity");
|
|
1139
|
+
}, REALTIME_DISCONNECT_HANGUP_GRACE_MS);
|
|
1140
|
+
livenessTimer.unref?.();
|
|
1141
|
+
}, REALTIME_MEDIA_INACTIVITY_TIMEOUT_MS);
|
|
1142
|
+
livenessTimer.unref?.();
|
|
1143
|
+
},
|
|
1144
|
+
retire: () => {
|
|
1145
|
+
closeBinding(telephonyBinding);
|
|
1146
|
+
}
|
|
1147
|
+
};
|
|
1148
|
+
this.activeBridgesByCallId.set(callId, session);
|
|
1149
|
+
this.activeBridgesByCallId.set(callSid, session);
|
|
1150
|
+
this.activeTelephonyBindingsByCallId.set(callId, telephonyBinding);
|
|
1151
|
+
telephonyBinding.noteMediaActivity();
|
|
1152
|
+
if (previousTelephonyBinding && previousTelephonyBinding !== telephonyBinding) previousTelephonyBinding.retire();
|
|
1054
1153
|
session.connect().catch((error) => {
|
|
1055
1154
|
console.error("[voice-call] Failed to connect realtime bridge:", error);
|
|
1056
1155
|
const ownsCallState = this.isActiveBridgeOwner(callId, session);
|
|
1156
|
+
this.streamDisconnectLifecycle.retire(callSid, streamSid);
|
|
1057
1157
|
try {
|
|
1058
1158
|
session.close();
|
|
1059
1159
|
} catch (closeError) {
|
|
@@ -1063,7 +1163,7 @@ var RealtimeCallHandler = class {
|
|
|
1063
1163
|
ws.close(1011, "Failed to connect");
|
|
1064
1164
|
}
|
|
1065
1165
|
});
|
|
1066
|
-
return
|
|
1166
|
+
return telephonyBinding;
|
|
1067
1167
|
}
|
|
1068
1168
|
beginUserTranscriptOwnerAdoption(callId) {
|
|
1069
1169
|
const adoption = {
|
|
@@ -1187,9 +1287,11 @@ var RealtimeCallHandler = class {
|
|
|
1187
1287
|
for (const key of [callId, callSid]) {
|
|
1188
1288
|
if (this.activeBridgesByCallId.get(key) !== owner) continue;
|
|
1189
1289
|
this.activeBridgesByCallId.delete(key);
|
|
1190
|
-
this.activeTelephonyClosersByCallId.delete(key);
|
|
1191
1290
|
}
|
|
1192
1291
|
}
|
|
1292
|
+
clearActiveTelephonyBinding(callId, binding) {
|
|
1293
|
+
if (this.activeTelephonyBindingsByCallId.get(callId) === binding) this.activeTelephonyBindingsByCallId.delete(callId);
|
|
1294
|
+
}
|
|
1193
1295
|
resolveUserTranscriptContext(callId, owner) {
|
|
1194
1296
|
const state = this.getUserTranscriptState(callId, owner);
|
|
1195
1297
|
return state?.partial ?? state?.recentFinal;
|
|
@@ -1228,14 +1330,6 @@ var RealtimeCallHandler = class {
|
|
|
1228
1330
|
});
|
|
1229
1331
|
}
|
|
1230
1332
|
}
|
|
1231
|
-
closeTelephonyBridge(callIdOrSid, bridge, reason) {
|
|
1232
|
-
const closer = this.activeTelephonyClosersByCallId.get(callIdOrSid);
|
|
1233
|
-
if (closer && closer.owner === bridge) {
|
|
1234
|
-
closer.close(reason);
|
|
1235
|
-
return;
|
|
1236
|
-
}
|
|
1237
|
-
bridge?.close();
|
|
1238
|
-
}
|
|
1239
1333
|
scheduleForcedAgentConsult(params) {
|
|
1240
1334
|
if (this.config.consultPolicy !== "always" || this.activeBridgesByCallId.get(params.callId) !== params.session) return;
|
|
1241
1335
|
const question = params.transcript.trim();
|
|
@@ -1312,7 +1406,7 @@ var RealtimeCallHandler = class {
|
|
|
1312
1406
|
}, FORCED_CONSULT_NATIVE_DEDUPE_MS).unref?.();
|
|
1313
1407
|
}
|
|
1314
1408
|
}
|
|
1315
|
-
|
|
1409
|
+
prepareCallInManager(callSid, callerMeta = {}) {
|
|
1316
1410
|
const baseFields = {
|
|
1317
1411
|
providerCallId: callSid,
|
|
1318
1412
|
timestamp: Date.now(),
|
|
@@ -1324,18 +1418,10 @@ var RealtimeCallHandler = class {
|
|
|
1324
1418
|
if (!callRecord) return null;
|
|
1325
1419
|
const initialGreeting = this.extractInitialGreeting(callRecord);
|
|
1326
1420
|
console.log(`[voice-call] Realtime call ${callRecord.callId} initial greeting ${initialGreeting ? "queued" : "absent"}`);
|
|
1327
|
-
if (callRecord.metadata) delete callRecord.metadata.initialMessage;
|
|
1328
|
-
this.manager.processEvent({
|
|
1329
|
-
id: `realtime-answered-${callSid}`,
|
|
1330
|
-
callId: callRecord.callId,
|
|
1331
|
-
type: "call.answered",
|
|
1332
|
-
...baseFields
|
|
1333
|
-
});
|
|
1334
|
-
const instructions = this.resolveInstructions?.(callRecord) ?? this.config.instructions;
|
|
1335
1421
|
return {
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1422
|
+
callRecord,
|
|
1423
|
+
baseFields,
|
|
1424
|
+
initialGreeting
|
|
1339
1425
|
};
|
|
1340
1426
|
}
|
|
1341
1427
|
resolveRealtimeCall(callSid, callerMeta, baseFields) {
|
|
@@ -1354,17 +1440,57 @@ var RealtimeCallHandler = class {
|
|
|
1354
1440
|
extractInitialGreeting(call) {
|
|
1355
1441
|
return typeof call.metadata?.initialMessage === "string" ? call.metadata.initialMessage : void 0;
|
|
1356
1442
|
}
|
|
1357
|
-
|
|
1358
|
-
this.
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1443
|
+
async executeEndCallTool(params) {
|
|
1444
|
+
const binding = this.activeTelephonyBindingsByCallId.get(params.callId);
|
|
1445
|
+
if (!binding || binding.bridge !== params.bridge || !this.isActiveBridgeOwner(params.callId, params.bridge)) return;
|
|
1446
|
+
let result;
|
|
1447
|
+
try {
|
|
1448
|
+
result = await this.manager.endCall(params.callId);
|
|
1449
|
+
} catch (error) {
|
|
1450
|
+
result = {
|
|
1451
|
+
success: false,
|
|
1452
|
+
error: formatErrorMessage(error)
|
|
1453
|
+
};
|
|
1454
|
+
}
|
|
1455
|
+
if (this.activeTelephonyBindingsByCallId.get(params.callId) !== binding || !this.isActiveBridgeOwner(params.callId, params.bridge)) return;
|
|
1456
|
+
if (!result.success) {
|
|
1457
|
+
const toolResult = { error: `Could not end the current phone call: ${result.error?.trim() || "the telephony provider returned no reason"}. Tell the caller the call could not be ended and they can hang up or ask you to try again.` };
|
|
1458
|
+
await params.bridge.submitToolResult(params.bridgeCallId, toolResult);
|
|
1459
|
+
params.harness.emit({
|
|
1460
|
+
type: "tool.error",
|
|
1461
|
+
turnId: params.turnId,
|
|
1462
|
+
callId: params.bridgeCallId,
|
|
1463
|
+
payload: {
|
|
1464
|
+
name: REALTIME_VOICE_END_CALL_TOOL_NAME,
|
|
1465
|
+
result: toolResult
|
|
1466
|
+
},
|
|
1467
|
+
final: true
|
|
1468
|
+
});
|
|
1469
|
+
return;
|
|
1470
|
+
}
|
|
1471
|
+
params.harness.emit({
|
|
1472
|
+
type: "tool.result",
|
|
1473
|
+
turnId: params.turnId,
|
|
1474
|
+
callId: params.bridgeCallId,
|
|
1475
|
+
payload: {
|
|
1476
|
+
name: REALTIME_VOICE_END_CALL_TOOL_NAME,
|
|
1477
|
+
result: { success: true }
|
|
1478
|
+
},
|
|
1479
|
+
final: true
|
|
1365
1480
|
});
|
|
1481
|
+
binding.endCall();
|
|
1366
1482
|
}
|
|
1367
1483
|
async executeToolCall(bridge, callId, bridgeCallId, name, args, turnId, harness, userTranscriptOwner) {
|
|
1484
|
+
if (name === "openclaw_end_call") {
|
|
1485
|
+
await this.executeEndCallTool({
|
|
1486
|
+
bridge,
|
|
1487
|
+
callId,
|
|
1488
|
+
bridgeCallId,
|
|
1489
|
+
turnId,
|
|
1490
|
+
harness
|
|
1491
|
+
});
|
|
1492
|
+
return;
|
|
1493
|
+
}
|
|
1368
1494
|
const handler = this.toolHandlers.get(name);
|
|
1369
1495
|
const startedAt = Date.now();
|
|
1370
1496
|
const hasResultError = (result) => {
|