@openclaw/voice-call 2026.10.5-beta.3 → 2026.10.10-beta.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 +1 -1
- package/dist/.setup/{carrier-api-BYKw_fsQ.mjs → carrier-api-Bt9KxU9b.mjs} +1 -1
- package/dist/.setup/{cli-dyyK8Upw.mjs → cli-RHaXpQxE.mjs} +8 -4
- package/dist/.setup/{config-CsrGuamE.mjs → config-BdbTQCSh.mjs} +2 -11
- package/dist/.setup/{plivo-COfD-Kuc.mjs → plivo-C3NbDLe-.mjs} +2 -2
- package/dist/.setup/{realtime-handler-BdAuKAC2.mjs → realtime-handler-dG-YrnQ5.mjs} +55 -144
- package/dist/.setup/{realtime-transcription.runtime-CbJAs5t_.mjs → realtime-transcription.runtime-DIBopS1D.mjs} +1 -2
- package/dist/.setup/realtime-voice.runtime-S9H9Y0Cp.mjs +1 -0
- package/dist/.setup/{response-generator-COu0_8-d.mjs → response-generator-DEU_bfHI.mjs} +25 -17
- package/dist/.setup/{runtime-entry-STjFmi_B.mjs → runtime-entry-BqhtjzE-.mjs} +32 -77
- package/dist/.setup/{telnyx-B4P0peEe.mjs → telnyx-BuC-Ttbj.mjs} +1 -1
- package/dist/.setup/{twilio-BzAYgH7M.mjs → twilio-BVBIIqx-.mjs} +3 -5
- package/dist/doctor-contract-api.js +3 -1
- package/dist/index.js +34 -63
- package/dist/runtime-entry.js +1 -2
- package/dist/state-retention-api.js +1 -2
- package/node_modules/typebox/build/value/clean/from_object.mjs +1 -1
- package/node_modules/typebox/package.json +1 -1
- package/package.json +5 -5
- package/dist/.setup/realtime-agent-context.legacy-DAEWY27o.mjs +0 -53
- package/dist/.setup/realtime-voice.runtime-vtdCOWg-.mjs +0 -2
package/README.md
CHANGED
|
@@ -102,7 +102,7 @@ Notes:
|
|
|
102
102
|
- Twilio defaults to US1. For a non-US Region, set `twilio.region` to `ie1` or `au1` and use credentials created in that Region; see [Twilio's regional REST API guide](https://www.twilio.com/docs/global-infrastructure/using-the-twilio-rest-api-in-a-non-us-region).
|
|
103
103
|
- `mock` is a local dev provider (no network calls).
|
|
104
104
|
- Telnyx requires `telnyx.publicKey` (or `TELNYX_PUBLIC_KEY`) unless `skipSignatureVerification` is true.
|
|
105
|
-
- Runtime accepts
|
|
105
|
+
- Runtime accepts only the current config format. Doctor retains migrations for `provider: "log"`, `twilio.from`, legacy `streaming.*` OpenAI keys, and `realtime.agentContext.includeSystemPrompt` because supported releases can still preserve these settings. Run `openclaw doctor --fix` to normalize them; existing values in the current format are preserved.
|
|
106
106
|
- advanced webhook, streaming, and tunnel notes: `https://docs.openclaw.ai/plugins/voice-call`
|
|
107
107
|
- `responseModel` is optional. When unset, voice responses use the runtime default model.
|
|
108
108
|
- `sessionScope` defaults to `per-phone`, preserving caller memory across calls. Use `per-call` for reception, booking, IVR, and bridge flows where each carrier call should start fresh. Use `main` to share the configured agent's main session (`agent:<agentId>:main`, or `global` when core `session.scope` is `"global"`). Custom core `session.mainKey` values are ignored.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { fetchWithSsrFGuard } from "../api.js";
|
|
2
|
-
import { c as getHeader, r as normalizeProxyIp } from "./runtime-entry-
|
|
2
|
+
import { c as getHeader, r as normalizeProxyIp } from "./runtime-entry-BqhtjzE-.mjs";
|
|
3
3
|
import { n as reserveWebhookReplay, t as createWebhookReplayCache } from "./webhook-replay-B7RJNWNG.mjs";
|
|
4
4
|
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
5
5
|
import { isLoopbackHost } from "openclaw/plugin-sdk/gateway-runtime";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { sleep } from "../api.js";
|
|
2
|
-
import { g as CallBriefSchema, l as resolveVoiceCallStreamExposurePaths, u as validateProviderConfig } from "./config-
|
|
3
|
-
import { S as resolveVoiceCallAgentId, _ as findCallInStore, b as setVoiceCallStateRuntime, d as getTailscaleSelfInfo, f as setupTailscaleExposureRoutes, g as MAX_CALL_RECORD_EVENTS, l as resolveWebhookExposureStatus, u as cleanupTailscaleExposureRoute, v as getCallHistoryFromStore, y as loadActiveCallsFromStore } from "./runtime-entry-
|
|
2
|
+
import { g as CallBriefSchema, l as resolveVoiceCallStreamExposurePaths, u as validateProviderConfig } from "./config-BdbTQCSh.mjs";
|
|
3
|
+
import { S as resolveVoiceCallAgentId, _ as findCallInStore, b as setVoiceCallStateRuntime, d as getTailscaleSelfInfo, f as setupTailscaleExposureRoutes, g as MAX_CALL_RECORD_EVENTS, l as resolveWebhookExposureStatus, u as cleanupTailscaleExposureRoute, v as getCallHistoryFromStore, y as loadActiveCallsFromStore } from "./runtime-entry-BqhtjzE-.mjs";
|
|
4
4
|
import { r as resolveUserPath, t as resolveDefaultVoiceCallStoreDir } from "./store-path-Cfwff6j7.mjs";
|
|
5
5
|
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
6
6
|
import { callGatewayFromCli, isGatewayClientRequestError, isGatewayTransportError, redactSensitiveUrlLikeString } from "openclaw/plugin-sdk/gateway-runtime";
|
|
@@ -484,8 +484,12 @@ function registerVoiceCallCli(params) {
|
|
|
484
484
|
mode: options.mode
|
|
485
485
|
}) });
|
|
486
486
|
});
|
|
487
|
-
|
|
488
|
-
|
|
487
|
+
for (const name of ["call", "start"]) {
|
|
488
|
+
const command = root.command(name);
|
|
489
|
+
if (name === "call") command.description("Initiate an outbound voice call").requiredOption("-m, --message <text>", "Message to speak when call connects").option("-t, --to <phone>", "Phone number to call (E.164 format, uses config toNumber if not set)");
|
|
490
|
+
else command.description("Alias for voicecall call").requiredOption("--to <phone>", "Phone number to call").option("--message <text>", "Message to speak when call connects");
|
|
491
|
+
command.option("--mode <mode>", "Call mode: notify (hangup after message) or conversation (stay open)", "conversation").option("--brief <text-or-json>", "Per-call task or structured JSON brief").option("--brief-file <path>", "Read a structured JSON brief from a file").action(callAction(name === "call" ? "voicecall.initiate" : "voicecall.start"));
|
|
492
|
+
}
|
|
489
493
|
root.command("continue").description("Speak a message and wait for a response").requiredOption("--call-id <id>", "Call ID").requiredOption("--message <text>", "Message to speak").action((options) => {
|
|
490
494
|
const gatewayParams = {
|
|
491
495
|
callId: options.callId,
|
|
@@ -286,16 +286,7 @@ const VoiceCallRealtimeConfigSchema = z.object({
|
|
|
286
286
|
agentContext: VoiceCallRealtimeAgentContextConfigSchema.default(VoiceCallRealtimeAgentContextConfigSchema.parse({})),
|
|
287
287
|
/** Provider-owned raw config blobs keyed by provider id. */
|
|
288
288
|
providers: VoiceCallProvidersConfigSchema
|
|
289
|
-
}).strict()
|
|
290
|
-
enabled: false,
|
|
291
|
-
instructions: DEFAULT_VOICE_CALL_REALTIME_INSTRUCTIONS,
|
|
292
|
-
toolPolicy: "safe-read-only",
|
|
293
|
-
consultPolicy: "auto",
|
|
294
|
-
tools: [],
|
|
295
|
-
fastContext: VoiceCallRealtimeFastContextConfigSchema.parse({}),
|
|
296
|
-
agentContext: VoiceCallRealtimeAgentContextConfigSchema.parse({}),
|
|
297
|
-
providers: {}
|
|
298
|
-
});
|
|
289
|
+
}).strict();
|
|
299
290
|
const VoiceCallStreamingConfigSchema = z.object({
|
|
300
291
|
/** Enable Twilio Media Streams for real-time transcription. */
|
|
301
292
|
enabled: z.boolean().default(false),
|
|
@@ -359,7 +350,7 @@ const VoiceCallConfigSchema = z.object({
|
|
|
359
350
|
tunnel: VoiceCallTunnelConfigSchema.default(VoiceCallTunnelConfigSchema.parse({})),
|
|
360
351
|
webhookSecurity: VoiceCallWebhookSecurityConfigSchema.default(VoiceCallWebhookSecurityConfigSchema.parse({})),
|
|
361
352
|
streaming: VoiceCallStreamingConfigSchema.default(VoiceCallStreamingConfigSchema.parse({})),
|
|
362
|
-
realtime: VoiceCallRealtimeConfigSchema,
|
|
353
|
+
realtime: VoiceCallRealtimeConfigSchema.default(VoiceCallRealtimeConfigSchema.parse({})),
|
|
363
354
|
/** Session memory scope for voice conversations. */
|
|
364
355
|
sessionScope: VoiceCallSessionScopeSchema.default("per-phone"),
|
|
365
356
|
/** Public webhook URL override (if set, bypasses tunnel auto-detection) */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { c as getHeader } from "./runtime-entry-
|
|
1
|
+
import { c as getHeader } from "./runtime-entry-BqhtjzE-.mjs";
|
|
2
2
|
import { t as escapeXml } from "./voice-mapping-1lzFmzOF.mjs";
|
|
3
|
-
import { n as reconstructWebhookUrl, r as verifyPlivoWebhook, t as createCarrierApi } from "./carrier-api-
|
|
3
|
+
import { n as reconstructWebhookUrl, r as verifyPlivoWebhook, t as createCarrierApi } from "./carrier-api-Bt9KxU9b.mjs";
|
|
4
4
|
import { normalizeLowercaseStringOrEmpty, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
5
5
|
import crypto from "node:crypto";
|
|
6
6
|
//#region extensions/voice-call/src/providers/plivo.ts
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { d as REALTIME_VOICE_END_CALL_TOOL_NAME, g as CallBriefSchema, s as resolveVoiceCallPublicPathPrefix } from "./config-
|
|
2
|
-
import { n as appendRecentTalkEventMetadata, s as canonicalizeVoiceCallMediaBase64 } from "./runtime-entry-
|
|
1
|
+
import { d as REALTIME_VOICE_END_CALL_TOOL_NAME, g as CallBriefSchema, s as resolveVoiceCallPublicPathPrefix } from "./config-BdbTQCSh.mjs";
|
|
2
|
+
import { n as appendRecentTalkEventMetadata, s as canonicalizeVoiceCallMediaBase64 } from "./runtime-entry-BqhtjzE-.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, sleep } from "openclaw/plugin-sdk/runtime-env";
|
|
@@ -10,7 +10,6 @@ import { randomUUID } from "node:crypto";
|
|
|
10
10
|
import { safeParseJson, sliceUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
|
11
11
|
import "node:http";
|
|
12
12
|
import { WebSocket, WebSocketServer, rejectWebSocketUpgrade } from "openclaw/plugin-sdk/websocket-runtime";
|
|
13
|
-
import { createDeferred } from "openclaw/plugin-sdk/extension-shared";
|
|
14
13
|
//#region extensions/voice-call/src/webhook/realtime-audio-pacer.ts
|
|
15
14
|
const TELEPHONY_SAMPLE_RATE = 8e3;
|
|
16
15
|
const TELEPHONY_CHUNK_BYTES = 160;
|
|
@@ -789,31 +788,12 @@ function withFallbackConsultQuestion(args, fallback) {
|
|
|
789
788
|
question
|
|
790
789
|
};
|
|
791
790
|
}
|
|
792
|
-
function captureNativeConsultTranscript(state
|
|
793
|
-
const
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
return {
|
|
799
|
-
buffer,
|
|
800
|
-
text: buffer?.text
|
|
801
|
-
};
|
|
802
|
-
};
|
|
803
|
-
const initial = read();
|
|
804
|
-
let frozen;
|
|
805
|
-
const invocation = {
|
|
806
|
-
id: invocationId,
|
|
807
|
-
read: () => frozen ?? read(),
|
|
808
|
-
freeze: () => {
|
|
809
|
-
frozen = initial.buffer?.final ? {
|
|
810
|
-
buffer: initial.buffer,
|
|
811
|
-
text: initial.buffer.text
|
|
812
|
-
} : initial;
|
|
813
|
-
}
|
|
814
|
-
};
|
|
815
|
-
state.nativeConsultInvocation = invocation;
|
|
816
|
-
return invocation.read;
|
|
791
|
+
function captureNativeConsultTranscript(state) {
|
|
792
|
+
const buffer = state.partial ?? state.recentFinal;
|
|
793
|
+
return () => ({
|
|
794
|
+
buffer,
|
|
795
|
+
text: buffer?.text
|
|
796
|
+
});
|
|
817
797
|
}
|
|
818
798
|
function clearRecentFinalUserTranscript(state) {
|
|
819
799
|
clearTimeout(state.recentFinalTimer);
|
|
@@ -823,7 +803,7 @@ function clearRecentFinalUserTranscript(state) {
|
|
|
823
803
|
function consumeUserTranscript(state, consumed) {
|
|
824
804
|
const buffer = consumed?.buffer;
|
|
825
805
|
const text = consumed?.text?.trim();
|
|
826
|
-
if (!
|
|
806
|
+
if (!buffer || !text || buffer !== state.partial && buffer !== state.recentFinal || !buffer.text.toLowerCase().startsWith(text.toLowerCase())) return;
|
|
827
807
|
const remaining = buffer.text.slice(text.length).trimStart();
|
|
828
808
|
if (remaining) {
|
|
829
809
|
buffer.text = remaining;
|
|
@@ -831,12 +811,6 @@ function consumeUserTranscript(state, consumed) {
|
|
|
831
811
|
} else if (buffer === state.partial) state.partial = void 0;
|
|
832
812
|
else clearRecentFinalUserTranscript(state);
|
|
833
813
|
}
|
|
834
|
-
async function waitForNativeConsult(state) {
|
|
835
|
-
return await Promise.race([state.promise.then((result) => ({
|
|
836
|
-
kind: "completed",
|
|
837
|
-
result
|
|
838
|
-
})), state.cancellation.then(() => ({ kind: "cancelled" }))]);
|
|
839
|
-
}
|
|
840
814
|
var RealtimeCallHandler = class {
|
|
841
815
|
constructor(config, manager, resolveCallRegistration, servePath, streamDisconnectLifecycle, coreConfig, holdOpeningMaxMs) {
|
|
842
816
|
this.config = config;
|
|
@@ -852,9 +826,7 @@ var RealtimeCallHandler = class {
|
|
|
852
826
|
this.serverClosingSockets = /* @__PURE__ */ new WeakSet();
|
|
853
827
|
this.activeBridgesByCallId = /* @__PURE__ */ new Map();
|
|
854
828
|
this.activeTelephonyBindingsByCallId = /* @__PURE__ */ new Map();
|
|
855
|
-
this.userTranscriptStatesByCallId = /* @__PURE__ */ new Map();
|
|
856
829
|
this.forcedConsultsByCallId = /* @__PURE__ */ new Map();
|
|
857
|
-
this.consultSessionsByCallId = /* @__PURE__ */ new Map();
|
|
858
830
|
this.nativeConsultsInFlightByCallId = /* @__PURE__ */ new Map();
|
|
859
831
|
this.terminationAttempts = /* @__PURE__ */ new Set();
|
|
860
832
|
this.admissions = /* @__PURE__ */ new Set();
|
|
@@ -1155,7 +1127,7 @@ var RealtimeCallHandler = class {
|
|
|
1155
1127
|
let registration;
|
|
1156
1128
|
let sessionPolicy;
|
|
1157
1129
|
try {
|
|
1158
|
-
registration = this.resolveCallRegistration(callRecord);
|
|
1130
|
+
registration = await this.resolveCallRegistration(callRecord);
|
|
1159
1131
|
sessionPolicy = resolveRealtimeVoiceSessionPolicy({
|
|
1160
1132
|
isAgentProxy: false,
|
|
1161
1133
|
capabilities: registration.capabilities,
|
|
@@ -1273,11 +1245,8 @@ var RealtimeCallHandler = class {
|
|
|
1273
1245
|
telephonyBinding.close("inactivity");
|
|
1274
1246
|
}
|
|
1275
1247
|
});
|
|
1276
|
-
let provisionalCloseReason;
|
|
1277
1248
|
let sessionClosed = false;
|
|
1278
|
-
const previousTranscriptOwner = this.userTranscriptStatesByCallId.get(callId);
|
|
1279
1249
|
const userTranscriptOwner = {};
|
|
1280
|
-
this.userTranscriptStatesByCallId.set(callId, userTranscriptOwner);
|
|
1281
1250
|
let transcriptPersistence = Promise.resolve();
|
|
1282
1251
|
let transcriptFailure;
|
|
1283
1252
|
const reportTranscriptFailure = (error) => {
|
|
@@ -1298,7 +1267,6 @@ var RealtimeCallHandler = class {
|
|
|
1298
1267
|
if (failures.length === 1) throw failures[0];
|
|
1299
1268
|
if (failures.length > 1) throw new AggregateError(failures, "Realtime provider and transcript cleanup failed");
|
|
1300
1269
|
};
|
|
1301
|
-
let continuityGeneration = 0;
|
|
1302
1270
|
const bridgeParams = {
|
|
1303
1271
|
provider: realtimeProvider,
|
|
1304
1272
|
cfg: this.coreConfig,
|
|
@@ -1311,18 +1279,17 @@ var RealtimeCallHandler = class {
|
|
|
1311
1279
|
tools: handlesAgentConsult ? [] : this.config.tools,
|
|
1312
1280
|
...handlesAgentConsult ? { runAgentConsult: async (request) => {
|
|
1313
1281
|
const owner = nativeConsultOwner;
|
|
1314
|
-
const generation = continuityGeneration;
|
|
1315
1282
|
request.signal?.throwIfAborted();
|
|
1316
1283
|
const invocationId = randomUUID();
|
|
1317
|
-
const transcript = captureNativeConsultTranscript(userTranscriptOwner
|
|
1284
|
+
const transcript = captureNativeConsultTranscript(userTranscriptOwner);
|
|
1318
1285
|
activity.beginConsult();
|
|
1319
1286
|
try {
|
|
1320
1287
|
await transcriptPersistence;
|
|
1321
|
-
if (!owner || sessionClosed ||
|
|
1288
|
+
if (!owner || sessionClosed || !this.isActiveBridgeOwner(callId, owner)) throw new Error("Realtime call delegation owner is no longer active");
|
|
1322
1289
|
if (toolPolicy === "none") throw new Error("Agent consultation is disabled for this call");
|
|
1323
1290
|
const result = await this.executeToolCall(owner, callId, invocationId, REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME, { question: request.prompt }, harness.ensureTurn(), harness, userTranscriptOwner, transcript, { signal: request.signal });
|
|
1324
1291
|
request.signal?.throwIfAborted();
|
|
1325
|
-
if (sessionClosed ||
|
|
1292
|
+
if (sessionClosed || !this.isActiveBridgeOwner(callId, owner)) throw new Error("Realtime call delegation owner is no longer active");
|
|
1326
1293
|
const text = readSpeakableRealtimeVoiceToolResult(result, {
|
|
1327
1294
|
keys: ["text", "output"],
|
|
1328
1295
|
maxChars: FORCED_CONSULT_RESULT_MAX_CHARS
|
|
@@ -1338,7 +1305,7 @@ var RealtimeCallHandler = class {
|
|
|
1338
1305
|
audioSink: audioController.audioSink,
|
|
1339
1306
|
onTranscript: (role, text, isFinal) => {
|
|
1340
1307
|
const owner = nativeConsultOwner;
|
|
1341
|
-
if (
|
|
1308
|
+
if (outboundGreeting.isBlocked() || sessionClosed && !isFinal || !owner || !this.isActiveBridgeOwner(callId, owner)) return;
|
|
1342
1309
|
if (text.trim()) {
|
|
1343
1310
|
outboundGreeting.claim();
|
|
1344
1311
|
activity.noteSpeech();
|
|
@@ -1366,8 +1333,7 @@ var RealtimeCallHandler = class {
|
|
|
1366
1333
|
});
|
|
1367
1334
|
if (!isFinal) {
|
|
1368
1335
|
if (role === "user" && text.trim()) {
|
|
1369
|
-
const state =
|
|
1370
|
-
if (!state) return;
|
|
1336
|
+
const state = userTranscriptOwner;
|
|
1371
1337
|
const partial = state.partial ??= { text: "" };
|
|
1372
1338
|
partial.text = limitPartialUserTranscript(appendTranscriptText(partial.text, text));
|
|
1373
1339
|
partial.raw = limitPartialUserTranscript(`${partial.raw ?? ""}${text}`);
|
|
@@ -1379,8 +1345,7 @@ var RealtimeCallHandler = class {
|
|
|
1379
1345
|
let transcript = text;
|
|
1380
1346
|
let consultTranscript;
|
|
1381
1347
|
if (role === "user") {
|
|
1382
|
-
const state =
|
|
1383
|
-
if (!state) return;
|
|
1348
|
+
const state = userTranscriptOwner;
|
|
1384
1349
|
transcript = resolveFinalTranscriptText({
|
|
1385
1350
|
partial: state.partial?.text,
|
|
1386
1351
|
rawPartial: state.partial?.raw,
|
|
@@ -1388,7 +1353,6 @@ var RealtimeCallHandler = class {
|
|
|
1388
1353
|
});
|
|
1389
1354
|
const buffer = state.partial ?? { text: transcript };
|
|
1390
1355
|
buffer.text = transcript;
|
|
1391
|
-
buffer.final = true;
|
|
1392
1356
|
state.partial = void 0;
|
|
1393
1357
|
clearRecentFinalUserTranscript(state);
|
|
1394
1358
|
state.recentFinal = buffer;
|
|
@@ -1397,7 +1361,6 @@ var RealtimeCallHandler = class {
|
|
|
1397
1361
|
text: transcript
|
|
1398
1362
|
};
|
|
1399
1363
|
const timer = setTimeout(() => {
|
|
1400
|
-
if (!this.getUserTranscriptState(callId, state) || state.recentFinalTimer !== timer) return;
|
|
1401
1364
|
state.recentFinal = void 0;
|
|
1402
1365
|
state.recentFinalTimer = void 0;
|
|
1403
1366
|
}, RECENT_FINAL_USER_TRANSCRIPT_TTL_MS);
|
|
@@ -1416,9 +1379,8 @@ var RealtimeCallHandler = class {
|
|
|
1416
1379
|
isFinal: true
|
|
1417
1380
|
} : { type: "call.assistant-speech" }
|
|
1418
1381
|
};
|
|
1419
|
-
const generation = continuityGeneration;
|
|
1420
1382
|
transcriptPersistence = this.manager.processEvent(event).then(() => {
|
|
1421
|
-
if (role !== "user" || handlesAgentConsult || sessionClosed ||
|
|
1383
|
+
if (role !== "user" || handlesAgentConsult || sessionClosed || !this.isActiveBridgeOwner(callId, session)) return;
|
|
1422
1384
|
this.scheduleForcedAgentConsult({
|
|
1423
1385
|
harness,
|
|
1424
1386
|
session,
|
|
@@ -1438,14 +1400,13 @@ var RealtimeCallHandler = class {
|
|
|
1438
1400
|
transcriptPersistence.catch(reportTranscriptFailure);
|
|
1439
1401
|
},
|
|
1440
1402
|
onToolCall: async (toolEvent, sessionLocal) => {
|
|
1441
|
-
const generation = continuityGeneration;
|
|
1442
1403
|
const isConsult = toolEvent.name === REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME;
|
|
1443
1404
|
const invocationId = toolEvent.callId || toolEvent.itemId;
|
|
1444
|
-
const transcript = isConsult ? captureNativeConsultTranscript(userTranscriptOwner
|
|
1405
|
+
const transcript = isConsult ? captureNativeConsultTranscript(userTranscriptOwner) : void 0;
|
|
1445
1406
|
if (isConsult) activity.beginConsult();
|
|
1446
1407
|
try {
|
|
1447
1408
|
await transcriptPersistence;
|
|
1448
|
-
if (sessionClosed ||
|
|
1409
|
+
if (sessionClosed || !this.isActiveBridgeOwner(callId, sessionLocal)) return;
|
|
1449
1410
|
const turnId = harness.ensureTurn();
|
|
1450
1411
|
harness.emit({
|
|
1451
1412
|
type: "tool.call",
|
|
@@ -1465,17 +1426,12 @@ var RealtimeCallHandler = class {
|
|
|
1465
1426
|
},
|
|
1466
1427
|
onEvent: (event) => {
|
|
1467
1428
|
if (event.direction === "client" && event.type === "session.continuity.reset") {
|
|
1468
|
-
continuityGeneration += 1;
|
|
1469
1429
|
const turnId = harness.talk.activeTurnId;
|
|
1470
1430
|
const owner = nativeConsultOwner;
|
|
1471
1431
|
if (owner && this.isActiveBridgeOwner(callId, owner)) {
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
clearRecentFinalUserTranscript(state);
|
|
1476
|
-
state.nativeConsultInvocation = void 0;
|
|
1477
|
-
}
|
|
1478
|
-
this.cancelConsultSession(callId, owner, "continuity-reset");
|
|
1432
|
+
userTranscriptOwner.partial = void 0;
|
|
1433
|
+
clearRecentFinalUserTranscript(userTranscriptOwner);
|
|
1434
|
+
this.cancelConsultSession(callId, owner, harness.forcedConsults, "continuity-reset");
|
|
1479
1435
|
}
|
|
1480
1436
|
harness.flushOutput(() => {
|
|
1481
1437
|
audioPacer.clearAudio();
|
|
@@ -1547,10 +1503,7 @@ var RealtimeCallHandler = class {
|
|
|
1547
1503
|
final: true
|
|
1548
1504
|
});
|
|
1549
1505
|
const owner = nativeConsultOwner;
|
|
1550
|
-
if (!owner)
|
|
1551
|
-
provisionalCloseReason ??= reason;
|
|
1552
|
-
return;
|
|
1553
|
-
}
|
|
1506
|
+
if (!owner) return;
|
|
1554
1507
|
if (sessionClosed) {
|
|
1555
1508
|
if (reason === "error") {
|
|
1556
1509
|
this.streamDisconnectLifecycle.retire(callSid, streamSid);
|
|
@@ -1571,32 +1524,17 @@ var RealtimeCallHandler = class {
|
|
|
1571
1524
|
} catch (error) {
|
|
1572
1525
|
console.error("[voice-call] Failed to create realtime bridge:", error);
|
|
1573
1526
|
}
|
|
1574
|
-
if (!candidate
|
|
1575
|
-
if (this.getUserTranscriptState(callId, userTranscriptOwner)) {
|
|
1576
|
-
clearRecentFinalUserTranscript(userTranscriptOwner);
|
|
1577
|
-
if (previousTranscriptOwner) this.userTranscriptStatesByCallId.set(callId, previousTranscriptOwner);
|
|
1578
|
-
else this.userTranscriptStatesByCallId.delete(callId);
|
|
1579
|
-
}
|
|
1580
|
-
try {
|
|
1581
|
-
await drainProviderClose(() => candidate?.close());
|
|
1582
|
-
} catch (error) {
|
|
1583
|
-
console.warn(`[voice-call] Failed to close realtime bridge ${callSid}: ${formatErrorMessage(error)}`);
|
|
1584
|
-
}
|
|
1527
|
+
if (!candidate) {
|
|
1585
1528
|
harness.close();
|
|
1586
1529
|
audioPacer.close();
|
|
1587
1530
|
outboundGreeting.close();
|
|
1588
1531
|
hostSpeech.close();
|
|
1589
1532
|
activity.close();
|
|
1590
|
-
|
|
1591
|
-
if (
|
|
1592
|
-
if (ws.readyState === WebSocket.OPEN) ws.close(reason === "error" ? 1011 : 1e3, "Failed to create realtime bridge");
|
|
1533
|
+
if (!this.activeBridgesByCallId.has(callId)) emitCallEnd("error");
|
|
1534
|
+
if (ws.readyState === WebSocket.OPEN) ws.close(1011, "Failed to create realtime bridge");
|
|
1593
1535
|
return null;
|
|
1594
1536
|
}
|
|
1595
1537
|
const session = candidate;
|
|
1596
|
-
if (this.getUserTranscriptState(callId, userTranscriptOwner) && previousTranscriptOwner) {
|
|
1597
|
-
clearTimeout(previousTranscriptOwner.recentFinalTimer);
|
|
1598
|
-
previousTranscriptOwner.recentFinalTimer = void 0;
|
|
1599
|
-
}
|
|
1600
1538
|
nativeConsultOwner = session;
|
|
1601
1539
|
const localBargeIn = !(session.bridge.handlesInputAudioBargeIn ?? providerHandlesInputAudioBargeIn) && resolveRealtimeVoiceBargeIn({
|
|
1602
1540
|
configuredBargeIn: void 0,
|
|
@@ -1604,12 +1542,6 @@ var RealtimeCallHandler = class {
|
|
|
1604
1542
|
capabilities,
|
|
1605
1543
|
outputAudioMode: session.bridge.outputAudioMode
|
|
1606
1544
|
});
|
|
1607
|
-
const previousConsultSession = this.consultSessionsByCallId.get(callId);
|
|
1608
|
-
if (previousConsultSession && previousConsultSession.owner !== session) this.cancelConsultSession(callId, previousConsultSession.owner);
|
|
1609
|
-
this.consultSessionsByCallId.set(callId, {
|
|
1610
|
-
owner: session,
|
|
1611
|
-
coordinator: harness.forcedConsults
|
|
1612
|
-
});
|
|
1613
1545
|
const sendAudioToSession = session.sendAudio.bind(session);
|
|
1614
1546
|
session.sendAudio = (audio) => {
|
|
1615
1547
|
if (sessionClosed) return;
|
|
@@ -1638,14 +1570,11 @@ var RealtimeCallHandler = class {
|
|
|
1638
1570
|
outboundGreeting.close();
|
|
1639
1571
|
hostSpeech.close();
|
|
1640
1572
|
activity.close();
|
|
1641
|
-
this.cancelConsultSession(callId, session);
|
|
1573
|
+
this.cancelConsultSession(callId, session, harness.forcedConsults);
|
|
1642
1574
|
audioPacer.close();
|
|
1643
1575
|
sessionClosePromise = drainProviderClose(closeSession).finally(() => {
|
|
1644
1576
|
for (const key of [callId, callSid]) if (this.activeBridgesByCallId.get(key) === session) this.activeBridgesByCallId.delete(key);
|
|
1645
|
-
|
|
1646
|
-
clearRecentFinalUserTranscript(userTranscriptOwner);
|
|
1647
|
-
this.userTranscriptStatesByCallId.delete(callId);
|
|
1648
|
-
}
|
|
1577
|
+
clearRecentFinalUserTranscript(userTranscriptOwner);
|
|
1649
1578
|
harness.close();
|
|
1650
1579
|
});
|
|
1651
1580
|
return sessionClosePromise;
|
|
@@ -1722,14 +1651,8 @@ var RealtimeCallHandler = class {
|
|
|
1722
1651
|
});
|
|
1723
1652
|
return telephonyBinding;
|
|
1724
1653
|
}
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
return state === owner ? state : void 0;
|
|
1728
|
-
}
|
|
1729
|
-
cancelConsultSession(callId, owner, mode = "retire") {
|
|
1730
|
-
const session = this.consultSessionsByCallId.get(callId);
|
|
1731
|
-
if (!session || session.owner !== owner) return;
|
|
1732
|
-
if (mode === "retire") session.coordinator.clearPending();
|
|
1654
|
+
cancelConsultSession(callId, owner, coordinator, mode = "retire") {
|
|
1655
|
+
coordinator.clearPending();
|
|
1733
1656
|
const forced = this.forcedConsultsByCallId.get(callId);
|
|
1734
1657
|
if (forced?.owner === owner) {
|
|
1735
1658
|
forced.cancelled = true;
|
|
@@ -1742,16 +1665,15 @@ var RealtimeCallHandler = class {
|
|
|
1742
1665
|
this.nativeConsultsInFlightByCallId.delete(callId);
|
|
1743
1666
|
native.cancel();
|
|
1744
1667
|
}
|
|
1745
|
-
if (mode === "continuity-reset")
|
|
1746
|
-
else this.consultSessionsByCallId.delete(callId);
|
|
1668
|
+
if (mode === "continuity-reset") coordinator.clear();
|
|
1747
1669
|
}
|
|
1748
1670
|
isActiveBridgeOwner(callId, owner) {
|
|
1749
1671
|
return this.activeBridgesByCallId.get(callId) === owner;
|
|
1750
1672
|
}
|
|
1751
|
-
async waitForConsultTranscriptSettle(
|
|
1673
|
+
async waitForConsultTranscriptSettle(owner, startedAt) {
|
|
1752
1674
|
const deadline = startedAt + CONSULT_TRANSCRIPT_SETTLE_MAX_MS;
|
|
1753
1675
|
while (true) {
|
|
1754
|
-
const updatedAt =
|
|
1676
|
+
const updatedAt = owner.partial?.updatedAt;
|
|
1755
1677
|
if (!updatedAt) return;
|
|
1756
1678
|
const now = Date.now();
|
|
1757
1679
|
const quietFor = now - updatedAt;
|
|
@@ -1824,7 +1746,7 @@ var RealtimeCallHandler = class {
|
|
|
1824
1746
|
params.session.sendUserMessage(buildForcedConsultSpeechPrompt(text));
|
|
1825
1747
|
}
|
|
1826
1748
|
console.log(`[voice-call] realtime forced agent consult completed callId=${params.callId} providerCallId=${params.callSid} elapsedMs=${Date.now() - startedAt}`);
|
|
1827
|
-
consumeUserTranscript(
|
|
1749
|
+
consumeUserTranscript(params.userTranscriptOwner, params.consultTranscript);
|
|
1828
1750
|
} catch (error) {
|
|
1829
1751
|
if (!state.cancelled) {
|
|
1830
1752
|
const result = buildRealtimeVoiceAgentErrorProviderResult(error);
|
|
@@ -2007,63 +1929,52 @@ var RealtimeCallHandler = class {
|
|
|
2007
1929
|
if (!bridgeCallId.trim() || existingNativeConsult.invocationId !== bridgeCallId) return await submitBusyResult();
|
|
2008
1930
|
console.log(`[voice-call] realtime tool call sharing in-flight agent consult callId=${callId} ageMs=${Date.now() - existingNativeConsult.startedAt}`);
|
|
2009
1931
|
await submitWorkingResponse();
|
|
2010
|
-
const
|
|
2011
|
-
if (
|
|
2012
|
-
return await submitFinalToolResult(
|
|
1932
|
+
const result = await existingNativeConsult.promise;
|
|
1933
|
+
if (existingNativeConsult.cancelled || !this.isActiveBridgeOwner(callId, bridge)) return;
|
|
1934
|
+
return await submitFinalToolResult(result);
|
|
2013
1935
|
}
|
|
1936
|
+
let transcript;
|
|
2014
1937
|
const abortController = new AbortController();
|
|
2015
|
-
const { promise: cancellation, resolve: releaseCancellation } = createDeferred();
|
|
2016
|
-
const { promise: consult, resolve: completeConsult } = createDeferred();
|
|
2017
1938
|
const state = {
|
|
2018
1939
|
owner: bridge,
|
|
2019
1940
|
invocationId: bridgeCallId,
|
|
2020
1941
|
startedAt,
|
|
2021
|
-
promise:
|
|
2022
|
-
|
|
1942
|
+
promise: (async () => {
|
|
1943
|
+
await submitWorkingResponse();
|
|
1944
|
+
await this.waitForConsultTranscriptSettle(userTranscriptOwner, startedAt);
|
|
1945
|
+
if (abortController.signal.aborted || !this.isActiveBridgeOwner(callId, bridge)) return;
|
|
1946
|
+
transcript = consultTranscript?.();
|
|
1947
|
+
const context = {
|
|
1948
|
+
partialUserTranscript: transcript?.text,
|
|
1949
|
+
abortSignal: abortController.signal
|
|
1950
|
+
};
|
|
1951
|
+
const handlerArgs = withFallbackConsultQuestion(args, context.partialUserTranscript);
|
|
1952
|
+
return await invokeHandler(handlerArgs, context);
|
|
1953
|
+
})().catch(buildRealtimeVoiceAgentErrorProviderResult),
|
|
2023
1954
|
get cancelled() {
|
|
2024
1955
|
return abortController.signal.aborted;
|
|
2025
1956
|
},
|
|
2026
1957
|
cancel: () => {
|
|
2027
1958
|
abortController.abort(/* @__PURE__ */ new Error("Realtime native consult owner was cancelled."));
|
|
2028
|
-
releaseCancellation();
|
|
2029
1959
|
}
|
|
2030
1960
|
};
|
|
2031
1961
|
if (delegation?.signal?.aborted) state.cancel();
|
|
2032
1962
|
else delegation?.signal?.addEventListener("abort", state.cancel, { once: true });
|
|
2033
1963
|
this.nativeConsultsInFlightByCallId.set(callId, state);
|
|
2034
|
-
(async () => {
|
|
2035
|
-
try {
|
|
2036
|
-
await submitWorkingResponse();
|
|
2037
|
-
if (state.cancelled || !this.isActiveBridgeOwner(callId, bridge)) return;
|
|
2038
|
-
await Promise.race([this.waitForConsultTranscriptSettle(callId, userTranscriptOwner, startedAt), state.cancellation]);
|
|
2039
|
-
if (state.cancelled || !this.isActiveBridgeOwner(callId, bridge)) return;
|
|
2040
|
-
const transcript = consultTranscript?.();
|
|
2041
|
-
const context = {
|
|
2042
|
-
partialUserTranscript: transcript?.text,
|
|
2043
|
-
abortSignal: abortController.signal
|
|
2044
|
-
};
|
|
2045
|
-
state.transcript = transcript;
|
|
2046
|
-
const handlerArgs = withFallbackConsultQuestion(args, context.partialUserTranscript);
|
|
2047
|
-
return await invokeHandler(handlerArgs, context);
|
|
2048
|
-
} catch (error) {
|
|
2049
|
-
return buildRealtimeVoiceAgentErrorProviderResult(error);
|
|
2050
|
-
}
|
|
2051
|
-
})().then(completeConsult);
|
|
2052
1964
|
try {
|
|
2053
|
-
const
|
|
2054
|
-
if (
|
|
2055
|
-
const result = outcome.result;
|
|
1965
|
+
const result = await state.promise;
|
|
1966
|
+
if (state.cancelled || !this.isActiveBridgeOwner(callId, bridge)) return;
|
|
2056
1967
|
const failed = logResult(result);
|
|
2057
1968
|
await submitFinalToolResult(result);
|
|
2058
|
-
if (!failed) consumeUserTranscript(
|
|
1969
|
+
if (!failed) consumeUserTranscript(userTranscriptOwner, transcript);
|
|
2059
1970
|
return result;
|
|
2060
1971
|
} finally {
|
|
2061
1972
|
delegation?.signal?.removeEventListener("abort", state.cancel);
|
|
2062
1973
|
if (this.nativeConsultsInFlightByCallId.get(callId) === state) this.nativeConsultsInFlightByCallId.delete(callId);
|
|
2063
1974
|
}
|
|
2064
1975
|
}
|
|
2065
|
-
const transcript =
|
|
2066
|
-
const result = await invokeHandler(args, { partialUserTranscript: (transcript
|
|
1976
|
+
const transcript = userTranscriptOwner;
|
|
1977
|
+
const result = await invokeHandler(args, { partialUserTranscript: (transcript.partial ?? transcript.recentFinal)?.text });
|
|
2067
1978
|
logResult(result);
|
|
2068
1979
|
return await submitFinalToolResult(result);
|
|
2069
1980
|
}
|
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export { getRealtimeTranscriptionProvider, listRealtimeTranscriptionProviders };
|
|
1
|
+
export { getRealtimeTranscriptionProvider, listRealtimeTranscriptionProviders } from "openclaw/plugin-sdk/realtime-transcription";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { getRealtimeVoiceProvider, listRealtimeVoiceProviders, resolveConfiguredRealtimeVoiceProviderAsync } from "openclaw/plugin-sdk/realtime-voice";
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { c as resolveVoiceCallSessionKey } from "./config-
|
|
2
|
-
import { m as resolveVoiceResponseModel, x as resolveCallAgentId } from "./runtime-entry-
|
|
1
|
+
import { c as resolveVoiceCallSessionKey } from "./config-BdbTQCSh.mjs";
|
|
2
|
+
import { m as resolveVoiceResponseModel, x as resolveCallAgentId } from "./runtime-entry-BqhtjzE-.mjs";
|
|
3
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";
|
|
7
7
|
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
|
8
8
|
import { resolveDefaultModelForAgent } from "openclaw/plugin-sdk/agent-runtime";
|
|
9
|
-
import { ModelSelectionLockedError, applyModelOverrideWithAuthProfileCompatibility, resolvePersistedSessionRuntimeId } from "openclaw/plugin-sdk/model-session-runtime";
|
|
9
|
+
import { ModelSelectionLockedError, applyModelOverrideWithAuthProfileCompatibility, prepareAuthProfileProvider, resolvePersistedSessionRuntimeId } from "openclaw/plugin-sdk/model-session-runtime";
|
|
10
10
|
//#region extensions/voice-call/src/response-generator.ts
|
|
11
11
|
const VOICE_SPOKEN_OUTPUT_CONTRACT = [
|
|
12
12
|
"Output format requirements:",
|
|
@@ -133,7 +133,7 @@ async function generateVoiceResponse(params) {
|
|
|
133
133
|
const workspaceDir = agentRuntime.resolveAgentWorkspaceDir(cfg, agentId);
|
|
134
134
|
await agentRuntime.ensureAgentWorkspace({ dir: workspaceDir });
|
|
135
135
|
const now = Date.now();
|
|
136
|
-
let sessionEntry = agentRuntime.session.
|
|
136
|
+
let sessionEntry = await agentRuntime.session.getSessionEntryAsync({
|
|
137
137
|
storePath,
|
|
138
138
|
sessionKey: resolvedSessionKey
|
|
139
139
|
});
|
|
@@ -146,7 +146,7 @@ async function generateVoiceResponse(params) {
|
|
|
146
146
|
agentId
|
|
147
147
|
});
|
|
148
148
|
if (sessionEntry?.modelSelectionLocked === true && voiceConfig.responseModel) throw new ModelSelectionLockedError();
|
|
149
|
-
if (!sessionEntry?.sessionId || voiceConfig.responseModel) sessionEntry = await agentRuntime.session.
|
|
149
|
+
if (!sessionEntry?.sessionId || voiceConfig.responseModel) sessionEntry = await agentRuntime.session.prepareSessionEntryPatch({
|
|
150
150
|
storePath,
|
|
151
151
|
sessionKey: resolvedSessionKey,
|
|
152
152
|
replaceEntry: true,
|
|
@@ -154,23 +154,31 @@ async function generateVoiceResponse(params) {
|
|
|
154
154
|
sessionId: crypto.randomUUID(),
|
|
155
155
|
updatedAt: now
|
|
156
156
|
},
|
|
157
|
-
|
|
157
|
+
prepare: async (entry) => {
|
|
158
158
|
const next = entry.sessionId ? { ...entry } : {
|
|
159
159
|
...entry,
|
|
160
160
|
sessionId: crypto.randomUUID(),
|
|
161
161
|
updatedAt: now
|
|
162
162
|
};
|
|
163
|
-
if (voiceConfig.responseModel)
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
163
|
+
if (voiceConfig.responseModel) {
|
|
164
|
+
const profileId = entry.authProfileOverride?.trim();
|
|
165
|
+
const recordedProvider = profileId ? await prepareAuthProfileProvider({
|
|
166
|
+
agentDir,
|
|
167
|
+
profileId
|
|
168
|
+
}) : void 0;
|
|
169
|
+
applyModelOverrideWithAuthProfileCompatibility({
|
|
170
|
+
cfg,
|
|
171
|
+
agentDir,
|
|
172
|
+
entry: next,
|
|
173
|
+
recordedProvider,
|
|
174
|
+
currentProvider: entry.providerOverride?.trim() || entry.modelProvider?.trim() || configuredModel.provider,
|
|
175
|
+
selection: {
|
|
176
|
+
provider,
|
|
177
|
+
model
|
|
178
|
+
},
|
|
179
|
+
selectionSource: "auto"
|
|
180
|
+
});
|
|
181
|
+
}
|
|
174
182
|
return next;
|
|
175
183
|
}
|
|
176
184
|
}) ?? void 0;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isBlockedHostnameOrIp, isRequestBodyLimitError, readRequestBodyWithLimit, requestBodyErrorToText, sendHttpRequestRejection } from "../api.js";
|
|
2
|
-
import { a as resolveVoiceCallEffectiveConfig, c as resolveVoiceCallSessionKey, f as resolveVoiceCallRealtimeTools, g as CallBriefSchema, h as normalizePhoneNumber, i as resolveVoiceCallConfig, l as resolveVoiceCallStreamExposurePaths, m as isAllowlistedCaller, n as normalizeVoiceCallConfig, o as resolveVoiceCallNumberRouteKeyForCall, r as resolveTwilioAuthToken, u as validateProviderConfig } from "./config-
|
|
2
|
+
import { a as resolveVoiceCallEffectiveConfig, c as resolveVoiceCallSessionKey, f as resolveVoiceCallRealtimeTools, g as CallBriefSchema, h as normalizePhoneNumber, i as resolveVoiceCallConfig, l as resolveVoiceCallStreamExposurePaths, m as isAllowlistedCaller, n as normalizeVoiceCallConfig, o as resolveVoiceCallNumberRouteKeyForCall, r as resolveTwilioAuthToken, u as validateProviderConfig } from "./config-BdbTQCSh.mjs";
|
|
3
3
|
import { n as mapVoiceToPolly, t as escapeXml } from "./voice-mapping-1lzFmzOF.mjs";
|
|
4
4
|
import { r as resolveUserPath, t as resolveDefaultVoiceCallStoreDir } from "./store-path-Cfwff6j7.mjs";
|
|
5
5
|
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
@@ -10,7 +10,7 @@ import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env";
|
|
|
10
10
|
import { listAgentIds, resolveDefaultAgentId } from "openclaw/plugin-sdk/agent-scope-runtime";
|
|
11
11
|
import { REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME, assertRealtimeVoiceAgentConsultModelSelectionUnlocked, buildRealtimeVoiceAgentConsultPolicyInstructions, consultRealtimeVoiceAgent, convertPcmToMulaw8k, createTalkSessionController, recordTalkObservabilityEvent, resolveRealtimeVoiceAgentConsultToolsAllow, resolveRealtimeVoiceFastContextConsult } from "openclaw/plugin-sdk/realtime-voice";
|
|
12
12
|
import { z } from "zod";
|
|
13
|
-
import {
|
|
13
|
+
import { createNativeSessionBindingScope } from "openclaw/plugin-sdk/agent-harness-session-runtime";
|
|
14
14
|
import { buildAssistantMessage, createEmptyTransportUsage } from "openclaw/plugin-sdk/provider-transport-runtime";
|
|
15
15
|
import { appendSessionTranscriptMessageByIdentityStrict } from "openclaw/plugin-sdk/session-transcript-runtime";
|
|
16
16
|
import { WEBHOOK_BODY_READ_DEFAULTS, createWebhookInFlightLimiter, normalizeWebhookPath, rawDataToString } from "openclaw/plugin-sdk/webhook-ingress";
|
|
@@ -20,15 +20,14 @@ import { KeyedAsyncQueue } from "openclaw/plugin-sdk/keyed-async-queue";
|
|
|
20
20
|
import crypto, { randomUUID } from "node:crypto";
|
|
21
21
|
import { redactIdentifier } from "openclaw/plugin-sdk/logging-core";
|
|
22
22
|
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
|
|
23
|
-
import
|
|
24
|
-
import {
|
|
23
|
+
import { resolveRealtimeVoiceAgentContextInstructions } from "openclaw/plugin-sdk/realtime-bootstrap-context";
|
|
24
|
+
import { runCommandWithTimeout, spawnOomScoreAdjusted } from "openclaw/plugin-sdk/process-runtime";
|
|
25
25
|
import { sliceUtf16Safe, truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
|
26
|
-
import { runCommandWithTimeout } from "openclaw/plugin-sdk/process-runtime";
|
|
27
26
|
import http from "node:http";
|
|
28
27
|
import { URL as URL$1 } from "node:url";
|
|
29
28
|
import { pruneMapToMaxSize } from "openclaw/plugin-sdk/collection-runtime";
|
|
30
29
|
import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
|
|
31
|
-
import {
|
|
30
|
+
import { resolveConfiguredCapabilityProviderAsync } from "openclaw/plugin-sdk/provider-selection-runtime";
|
|
32
31
|
import { WebSocket, WebSocketServer, rejectWebSocketUpgrade } from "openclaw/plugin-sdk/websocket-runtime";
|
|
33
32
|
import { createDeferred } from "openclaw/plugin-sdk/extension-shared";
|
|
34
33
|
import { canonicalizeBase64 } from "openclaw/plugin-sdk/media-runtime";
|
|
@@ -491,7 +490,7 @@ function createCallDeliveryRuntime(params) {
|
|
|
491
490
|
}),
|
|
492
491
|
idempotencyKey: message.idempotencyKey
|
|
493
492
|
},
|
|
494
|
-
preparation: { source:
|
|
493
|
+
preparation: { source: createNativeSessionBindingScope(assertActive).assertLegacyCurrent }
|
|
495
494
|
});
|
|
496
495
|
if (result.kind !== "result") throw new Error(`Requester transcript delivery failed: ${result.kind}`);
|
|
497
496
|
return;
|
|
@@ -597,21 +596,6 @@ function trimCallReplayKeys(keys) {
|
|
|
597
596
|
const overflow = keys.length - 500;
|
|
598
597
|
if (overflow > 0) keys.splice(0, overflow);
|
|
599
598
|
}
|
|
600
|
-
/**
|
|
601
|
-
* Reserve one rejected provider call. The token prevents a stale failed
|
|
602
|
-
* hangup from deleting a newer reservation after bounded eviction and reuse.
|
|
603
|
-
*/
|
|
604
|
-
function reserveRejectedProviderCall(calls, providerCallId) {
|
|
605
|
-
if (calls.has(providerCallId)) return;
|
|
606
|
-
const reservation = Symbol(providerCallId);
|
|
607
|
-
calls.set(providerCallId, reservation);
|
|
608
|
-
pruneOldestEntries(calls);
|
|
609
|
-
return reservation;
|
|
610
|
-
}
|
|
611
|
-
/** Release a rejected-call reservation only when the failing attempt still owns it. */
|
|
612
|
-
function releaseRejectedProviderCall(calls, providerCallId, reservation) {
|
|
613
|
-
if (calls.get(providerCallId) === reservation) calls.delete(providerCallId);
|
|
614
|
-
}
|
|
615
599
|
//#endregion
|
|
616
600
|
//#region extensions/voice-call/src/manager/store.ts
|
|
617
601
|
const CALL_RECORD_EVENTS_NAMESPACE = "call-record-events";
|
|
@@ -1013,11 +997,10 @@ async function finalizeCall(params) {
|
|
|
1013
997
|
}
|
|
1014
998
|
//#endregion
|
|
1015
999
|
//#region extensions/voice-call/src/manager/lookup.ts
|
|
1016
|
-
/** Resolve an active call
|
|
1000
|
+
/** Resolve an active call through the index maintained by call mutations. */
|
|
1017
1001
|
function getCallByProviderCallId(params) {
|
|
1018
1002
|
const callId = params.providerCallIdMap.get(params.providerCallId);
|
|
1019
|
-
|
|
1020
|
-
for (const call of params.activeCalls.values()) if (call.providerCallId === params.providerCallId) return call;
|
|
1003
|
+
return callId ? params.activeCalls.get(callId) : void 0;
|
|
1021
1004
|
}
|
|
1022
1005
|
/** Resolve an active call by internal call id or provider call id. */
|
|
1023
1006
|
function findCall(params) {
|
|
@@ -1671,8 +1654,7 @@ async function processEventInQueue(ctx, event) {
|
|
|
1671
1654
|
metadata: { rejectionReason: "inbound-policy" }
|
|
1672
1655
|
}, ctx.stateRuntime);
|
|
1673
1656
|
if (ctx.isStopping()) return { kind: "processed" };
|
|
1674
|
-
|
|
1675
|
-
if (rejectionReservation === void 0) return { kind: "ignored" };
|
|
1657
|
+
rememberManagerReplayKey(ctx.rejectedProviderCallIds, pid);
|
|
1676
1658
|
rememberManagerReplayKey(ctx.processedEventIds, dedupeKey);
|
|
1677
1659
|
log.info(`Rejecting inbound call by policy: ${pid}`);
|
|
1678
1660
|
ctx.trackCallWork(ctx.provider.hangupCall({
|
|
@@ -1680,7 +1662,7 @@ async function processEventInQueue(ctx, event) {
|
|
|
1680
1662
|
providerCallId: pid,
|
|
1681
1663
|
reason: "hangup-bot"
|
|
1682
1664
|
}).catch((err) => {
|
|
1683
|
-
|
|
1665
|
+
ctx.rejectedProviderCallIds.delete(pid);
|
|
1684
1666
|
const message = formatErrorMessage(err);
|
|
1685
1667
|
log.warn(`Failed to reject inbound call ${pid}: ${message}`);
|
|
1686
1668
|
}));
|
|
@@ -1985,7 +1967,7 @@ var CallManager = class {
|
|
|
1985
1967
|
this.activeCalls = /* @__PURE__ */ new Map();
|
|
1986
1968
|
this.providerCallIdMap = /* @__PURE__ */ new Map();
|
|
1987
1969
|
this.processedEventIds = /* @__PURE__ */ new Set();
|
|
1988
|
-
this.rejectedProviderCallIds = /* @__PURE__ */ new
|
|
1970
|
+
this.rejectedProviderCallIds = /* @__PURE__ */ new Set();
|
|
1989
1971
|
this.provider = null;
|
|
1990
1972
|
this.webhookUrl = null;
|
|
1991
1973
|
this.activeTurnCalls = /* @__PURE__ */ new Set();
|
|
@@ -2030,7 +2012,7 @@ var CallManager = class {
|
|
|
2030
2012
|
const persisted = await loadActiveCallsFromStore(this.storePath, this.stateRuntime);
|
|
2031
2013
|
if (this.closing) return;
|
|
2032
2014
|
this.processedEventIds = persisted.processedEventIds;
|
|
2033
|
-
this.rejectedProviderCallIds = /* @__PURE__ */ new
|
|
2015
|
+
this.rejectedProviderCallIds = /* @__PURE__ */ new Set();
|
|
2034
2016
|
for (const call of persisted.interruptedDeliveries) {
|
|
2035
2017
|
const metadata = { ...call.metadata };
|
|
2036
2018
|
for (const key of ["callReport", "liveTranscriptDelivery"]) {
|
|
@@ -2291,13 +2273,12 @@ var CallManager = class {
|
|
|
2291
2273
|
};
|
|
2292
2274
|
//#endregion
|
|
2293
2275
|
//#region extensions/voice-call/src/realtime-agent-context.ts
|
|
2294
|
-
const contextSdk = realtimeBootstrapSdk;
|
|
2295
2276
|
/** Build final realtime instructions from base instructions, consult policy, and agent context. */
|
|
2296
2277
|
async function buildRealtimeVoiceInstructions(params) {
|
|
2297
2278
|
const { config } = params;
|
|
2298
2279
|
const contextConfig = config.realtime.agentContext;
|
|
2299
2280
|
const sections = [params.baseInstructions, buildRealtimeVoiceAgentConsultPolicyInstructions(config.realtime)];
|
|
2300
|
-
|
|
2281
|
+
sections.push(await resolveRealtimeVoiceAgentContextInstructions({
|
|
2301
2282
|
config: params.coreConfig,
|
|
2302
2283
|
agentId: params.agentId,
|
|
2303
2284
|
files: contextConfig.enabled && contextConfig.includeWorkspaceFiles ? contextConfig.files : [],
|
|
@@ -2305,14 +2286,6 @@ async function buildRealtimeVoiceInstructions(params) {
|
|
|
2305
2286
|
maxChars: contextConfig.maxChars,
|
|
2306
2287
|
warn: params.warn
|
|
2307
2288
|
}));
|
|
2308
|
-
else {
|
|
2309
|
-
const { buildLegacyRealtimeVoiceAgentContext } = await import("./realtime-agent-context.legacy-DAEWY27o.mjs");
|
|
2310
|
-
sections.push(await buildLegacyRealtimeVoiceAgentContext({
|
|
2311
|
-
config: contextConfig,
|
|
2312
|
-
coreConfig: params.coreConfig,
|
|
2313
|
-
agentId: params.agentId
|
|
2314
|
-
}));
|
|
2315
|
-
}
|
|
2316
2289
|
return sections.filter(Boolean).join("\n\n");
|
|
2317
2290
|
}
|
|
2318
2291
|
//#endregion
|
|
@@ -2554,7 +2527,7 @@ async function startNgrokTunnel(config) {
|
|
|
2554
2527
|
];
|
|
2555
2528
|
if (config.ngrokDomain) args.push("--domain", config.ngrokDomain);
|
|
2556
2529
|
return new Promise((resolve, reject) => {
|
|
2557
|
-
const proc =
|
|
2530
|
+
const proc = spawnOomScoreAdjusted("ngrok", args, {
|
|
2558
2531
|
stdio: [
|
|
2559
2532
|
"ignore",
|
|
2560
2533
|
"pipe",
|
|
@@ -2734,7 +2707,6 @@ const DEFAULT_MAX_CONNECTIONS = 128;
|
|
|
2734
2707
|
const MAX_INBOUND_MESSAGE_BYTES = 65536;
|
|
2735
2708
|
const MAX_WS_BUFFERED_BYTES = 1048576;
|
|
2736
2709
|
const MAX_PENDING_TTS_OPERATIONS_PER_STREAM = 8;
|
|
2737
|
-
const MAX_IGNORED_PLAYBACK_MARKS_PER_STREAM = 64;
|
|
2738
2710
|
const PLAYBACK_MARK_TIMEOUT_GRACE_MS = 2e3;
|
|
2739
2711
|
const CLOSE_REASON_LOG_MAX_CHARS = 120;
|
|
2740
2712
|
function sanitizeLogText(value, maxChars) {
|
|
@@ -2763,7 +2735,6 @@ var MediaStreamHandler = class {
|
|
|
2763
2735
|
this.ttsQueues = /* @__PURE__ */ new Map();
|
|
2764
2736
|
this.ttsActiveControllers = /* @__PURE__ */ new Map();
|
|
2765
2737
|
this.pendingPlaybackMarks = /* @__PURE__ */ new Map();
|
|
2766
|
-
this.ignoredPlaybackMarks = /* @__PURE__ */ new Map();
|
|
2767
2738
|
this.preStartTimeoutMs = resolveTimerTimeoutMs(config.preStartTimeoutMs, DEFAULT_PRE_START_TIMEOUT_MS);
|
|
2768
2739
|
this.maxPendingConnections = config.maxPendingConnections ?? DEFAULT_MAX_PENDING_CONNECTIONS;
|
|
2769
2740
|
this.maxPendingConnectionsPerIp = config.maxPendingConnectionsPerIp ?? DEFAULT_MAX_PENDING_CONNECTIONS_PER_IP;
|
|
@@ -3119,7 +3090,6 @@ Content-Length: ${Buffer.byteLength(body)}\r\n\r
|
|
|
3119
3090
|
const marks = this.pendingPlaybackMarks.get(streamSid) ?? /* @__PURE__ */ new Map();
|
|
3120
3091
|
this.pendingPlaybackMarks.set(streamSid, marks);
|
|
3121
3092
|
if (marks.has(name)) throw new Error(`Telephony playback mark is already pending: ${name}`);
|
|
3122
|
-
this.ignoredPlaybackMarks.get(streamSid)?.delete(name);
|
|
3123
3093
|
let pending;
|
|
3124
3094
|
const acknowledgement = new Promise((resolve, reject) => {
|
|
3125
3095
|
const timeout = setTimeout(() => {
|
|
@@ -3129,15 +3099,14 @@ Content-Length: ${Buffer.byteLength(body)}\r\n\r
|
|
|
3129
3099
|
timeout.unref?.();
|
|
3130
3100
|
const onAbort = () => {
|
|
3131
3101
|
const reason = signal.reason instanceof Error ? signal.reason : /* @__PURE__ */ new Error("Telephony playback mark wait aborted");
|
|
3132
|
-
pending(reason
|
|
3102
|
+
pending(reason);
|
|
3133
3103
|
};
|
|
3134
|
-
pending = (error
|
|
3104
|
+
pending = (error) => {
|
|
3135
3105
|
if (marks.get(name) !== pending) return;
|
|
3136
3106
|
clearTimeout(timeout);
|
|
3137
3107
|
signal.removeEventListener("abort", onAbort);
|
|
3138
3108
|
marks.delete(name);
|
|
3139
3109
|
if (marks.size === 0) this.pendingPlaybackMarks.delete(streamSid);
|
|
3140
|
-
if (ignoreLateAck) this.ignorePlaybackMark(streamSid, name);
|
|
3141
3110
|
if (error) reject(error);
|
|
3142
3111
|
else resolve();
|
|
3143
3112
|
};
|
|
@@ -3186,27 +3155,12 @@ Content-Length: ${Buffer.byteLength(body)}\r\n\r
|
|
|
3186
3155
|
this.clearAudio(streamSid);
|
|
3187
3156
|
}
|
|
3188
3157
|
acknowledgePlaybackMark(streamSid, name) {
|
|
3189
|
-
const ignored = this.ignoredPlaybackMarks.get(streamSid);
|
|
3190
|
-
if (ignored?.delete(name)) {
|
|
3191
|
-
if (ignored.size === 0) this.ignoredPlaybackMarks.delete(streamSid);
|
|
3192
|
-
return;
|
|
3193
|
-
}
|
|
3194
3158
|
this.pendingPlaybackMarks.get(streamSid)?.get(name)?.();
|
|
3195
3159
|
}
|
|
3196
3160
|
invalidatePlaybackMarks(streamSid) {
|
|
3197
3161
|
const marks = this.pendingPlaybackMarks.get(streamSid);
|
|
3198
3162
|
if (!marks) return;
|
|
3199
|
-
for (const pending of marks.values()) pending(/* @__PURE__ */ new Error("Telephony playback cleared before completion")
|
|
3200
|
-
}
|
|
3201
|
-
ignorePlaybackMark(streamSid, name) {
|
|
3202
|
-
const ignored = this.ignoredPlaybackMarks.get(streamSid) ?? /* @__PURE__ */ new Set();
|
|
3203
|
-
ignored.add(name);
|
|
3204
|
-
while (ignored.size > MAX_IGNORED_PLAYBACK_MARKS_PER_STREAM) {
|
|
3205
|
-
const oldest = ignored.values().next().value;
|
|
3206
|
-
if (oldest === void 0) break;
|
|
3207
|
-
ignored.delete(oldest);
|
|
3208
|
-
}
|
|
3209
|
-
this.ignoredPlaybackMarks.set(streamSid, ignored);
|
|
3163
|
+
for (const pending of marks.values()) pending(/* @__PURE__ */ new Error("Telephony playback cleared before completion"));
|
|
3210
3164
|
}
|
|
3211
3165
|
async processQueue(streamSid) {
|
|
3212
3166
|
while (true) {
|
|
@@ -3284,7 +3238,6 @@ Content-Length: ${Buffer.byteLength(body)}\r\n\r
|
|
|
3284
3238
|
this.ttsActiveControllers.delete(streamSid);
|
|
3285
3239
|
this.ttsQueues.delete(streamSid);
|
|
3286
3240
|
this.invalidatePlaybackMarks(streamSid);
|
|
3287
|
-
this.ignoredPlaybackMarks.delete(streamSid);
|
|
3288
3241
|
}
|
|
3289
3242
|
abortTtsPlayback(streamSid) {
|
|
3290
3243
|
for (const entry of this.ttsQueues.get(streamSid)?.splice(0) ?? []) {
|
|
@@ -3391,7 +3344,6 @@ var StreamDisconnectGrace = class {
|
|
|
3391
3344
|
const current = this.streams.get(providerCallId);
|
|
3392
3345
|
if (!current || current.streamId !== streamId || current.disconnectTimer) return;
|
|
3393
3346
|
const disconnectTimer = setTimeout(() => {
|
|
3394
|
-
if (this.streams.get(providerCallId)?.disconnectTimer !== disconnectTimer) return;
|
|
3395
3347
|
this.streams.delete(providerCallId);
|
|
3396
3348
|
this.onGraceExpired({
|
|
3397
3349
|
providerCallId,
|
|
@@ -3449,8 +3401,8 @@ function appendRecentTalkEventMetadata(metadata, event, mode) {
|
|
|
3449
3401
|
const MAX_WEBHOOK_BODY_BYTES = WEBHOOK_BODY_READ_DEFAULTS.preAuth.maxBytes;
|
|
3450
3402
|
const WEBHOOK_BODY_TIMEOUT_MS = WEBHOOK_BODY_READ_DEFAULTS.preAuth.timeoutMs;
|
|
3451
3403
|
const MISSING_REMOTE_ADDRESS_IN_FLIGHT_KEY = "__voice_call_no_remote__";
|
|
3452
|
-
const loadRealtimeTranscriptionRuntime = createLazyRuntimeModule(() => import("./realtime-transcription.runtime-
|
|
3453
|
-
const loadResponseGeneratorModule = createLazyRuntimeModule(() => import("./response-generator-
|
|
3404
|
+
const loadRealtimeTranscriptionRuntime = createLazyRuntimeModule(() => import("./realtime-transcription.runtime-DIBopS1D.mjs"));
|
|
3405
|
+
const loadResponseGeneratorModule = createLazyRuntimeModule(() => import("./response-generator-DEU_bfHI.mjs"));
|
|
3454
3406
|
function buildRequestUrl(requestUrl) {
|
|
3455
3407
|
return new URL$1(requestUrl ?? "/", "http://localhost");
|
|
3456
3408
|
}
|
|
@@ -3585,7 +3537,7 @@ var VoiceCallWebhookServer = class {
|
|
|
3585
3537
|
const streaming = this.config.streaming;
|
|
3586
3538
|
const pluginConfig = this.fullConfig ?? this.coreConfig ?? void 0;
|
|
3587
3539
|
const { getRealtimeTranscriptionProvider, listRealtimeTranscriptionProviders } = await loadRealtimeTranscriptionRuntime();
|
|
3588
|
-
const resolution =
|
|
3540
|
+
const resolution = await resolveConfiguredCapabilityProviderAsync({
|
|
3589
3541
|
configuredProviderId: streaming.provider,
|
|
3590
3542
|
providerConfigs: streaming.providers,
|
|
3591
3543
|
cfg: pluginConfig,
|
|
@@ -3596,10 +3548,13 @@ var VoiceCallWebhookServer = class {
|
|
|
3596
3548
|
cfg,
|
|
3597
3549
|
rawConfig
|
|
3598
3550
|
}) ?? rawConfig,
|
|
3599
|
-
isProviderConfigured: ({ provider, cfg, providerConfig }) => provider.
|
|
3551
|
+
isProviderConfigured: ({ provider, cfg, providerConfig }) => provider.isConfiguredAsync ? provider.isConfiguredAsync({
|
|
3552
|
+
cfg,
|
|
3553
|
+
providerConfig
|
|
3554
|
+
}) : provider.isConfigured?.({
|
|
3600
3555
|
cfg,
|
|
3601
3556
|
providerConfig
|
|
3602
|
-
})
|
|
3557
|
+
}) ?? false
|
|
3603
3558
|
});
|
|
3604
3559
|
if (!resolution.ok) {
|
|
3605
3560
|
this.logger.warn(resolution.code === "missing-configured-provider" ? `Streaming enabled but realtime transcription provider "${resolution.configuredProviderId}" is not registered` : resolution.code === "no-registered-provider" ? "Streaming enabled but no realtime transcription provider is registered" : `Streaming enabled but provider "${resolution.provider?.id}" is not configured`);
|
|
@@ -4123,7 +4078,7 @@ async function resolveProvider(config) {
|
|
|
4123
4078
|
const allowNgrokFreeTierLoopbackBypass = config.tunnel?.provider === "ngrok" && isLoopbackHost(config.serve?.bind ?? "") && (config.tunnel?.allowNgrokFreeTierLoopbackBypass ?? false);
|
|
4124
4079
|
switch (config.provider) {
|
|
4125
4080
|
case "telnyx": {
|
|
4126
|
-
const { TelnyxProvider } = await import("./telnyx-
|
|
4081
|
+
const { TelnyxProvider } = await import("./telnyx-BuC-Ttbj.mjs");
|
|
4127
4082
|
return new TelnyxProvider({
|
|
4128
4083
|
apiKey: config.telnyx?.apiKey,
|
|
4129
4084
|
connectionId: config.telnyx?.connectionId,
|
|
@@ -4131,7 +4086,7 @@ async function resolveProvider(config) {
|
|
|
4131
4086
|
}, { skipVerification: config.skipSignatureVerification });
|
|
4132
4087
|
}
|
|
4133
4088
|
case "twilio": {
|
|
4134
|
-
const { TwilioProvider } = await import("./twilio-
|
|
4089
|
+
const { TwilioProvider } = await import("./twilio-BVBIIqx-.mjs");
|
|
4135
4090
|
return new TwilioProvider({
|
|
4136
4091
|
accountSid: config.twilio?.accountSid,
|
|
4137
4092
|
authToken: resolveTwilioAuthToken(config),
|
|
@@ -4145,7 +4100,7 @@ async function resolveProvider(config) {
|
|
|
4145
4100
|
});
|
|
4146
4101
|
}
|
|
4147
4102
|
case "plivo": {
|
|
4148
|
-
const { PlivoProvider } = await import("./plivo-
|
|
4103
|
+
const { PlivoProvider } = await import("./plivo-C3NbDLe-.mjs");
|
|
4149
4104
|
return new PlivoProvider({
|
|
4150
4105
|
authId: config.plivo?.authId,
|
|
4151
4106
|
authToken: config.plivo?.authToken
|
|
@@ -4228,10 +4183,10 @@ async function createVoiceCallRuntime(params) {
|
|
|
4228
4183
|
const provider = await resolveProvider(config);
|
|
4229
4184
|
if (stateRuntime) setVoiceCallStateRuntime({ state: stateRuntime });
|
|
4230
4185
|
const manager = new CallManager(config, void 0, cfg.session, stateRuntime);
|
|
4231
|
-
const realtimeVoiceRuntime = config.realtime.enabled ? await import("./realtime-voice.runtime-
|
|
4186
|
+
const realtimeVoiceRuntime = config.realtime.enabled ? await import("./realtime-voice.runtime-S9H9Y0Cp.mjs") : null;
|
|
4232
4187
|
const webhookServer = new VoiceCallWebhookServer(params.scheduler, config, manager, provider, coreConfig, cfg, agentRuntime, log);
|
|
4233
4188
|
if (realtimeVoiceRuntime) {
|
|
4234
|
-
const { RealtimeCallHandler } = await import("./realtime-handler-
|
|
4189
|
+
const { RealtimeCallHandler } = await import("./realtime-handler-dG-YrnQ5.mjs");
|
|
4235
4190
|
const resolveRealtimeInstructions = await createRealtimeInstructionsResolver({
|
|
4236
4191
|
config,
|
|
4237
4192
|
coreConfig: cfg,
|
|
@@ -4241,11 +4196,11 @@ async function createVoiceCallRuntime(params) {
|
|
|
4241
4196
|
...config.realtime,
|
|
4242
4197
|
tools: resolveVoiceCallRealtimeTools(config.realtime.toolPolicy, config.realtime.tools)
|
|
4243
4198
|
};
|
|
4244
|
-
const resolveCallRegistration = (call) => {
|
|
4199
|
+
const resolveCallRegistration = async (call) => {
|
|
4245
4200
|
const numberRouteKey = resolveVoiceCallNumberRouteKeyForCall(call);
|
|
4246
4201
|
const effectiveConfig = resolveVoiceCallEffectiveConfig(config, numberRouteKey).config;
|
|
4247
4202
|
const agentId = resolveCallAgentId(call);
|
|
4248
|
-
const resolved = realtimeVoiceRuntime.
|
|
4203
|
+
const resolved = await realtimeVoiceRuntime.resolveConfiguredRealtimeVoiceProviderAsync({
|
|
4249
4204
|
configuredProviderId: effectiveConfig.realtime.provider,
|
|
4250
4205
|
providerConfigs: effectiveConfig.realtime.providers,
|
|
4251
4206
|
cfg,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as verifyTelnyxWebhook, t as createCarrierApi } from "./carrier-api-
|
|
1
|
+
import { i as verifyTelnyxWebhook, t as createCarrierApi } from "./carrier-api-Bt9KxU9b.mjs";
|
|
2
2
|
import crypto from "node:crypto";
|
|
3
3
|
//#region extensions/voice-call/src/providers/telnyx.ts
|
|
4
4
|
const HANGUP_REASONS = /* @__PURE__ */ new Map([
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { a as mapProviderStatusToEndReason, c as getHeader, h as generateDtmfRedirectTwiml, i as isProviderStatusTerminal, o as normalizeProviderStatus } from "./runtime-entry-
|
|
1
|
+
import { a as mapProviderStatusToEndReason, c as getHeader, h as generateDtmfRedirectTwiml, i as isProviderStatusTerminal, o as normalizeProviderStatus } from "./runtime-entry-BqhtjzE-.mjs";
|
|
2
2
|
import { n as requireSupportedTwilioApiHostname, r as resolveTwilioApiBaseUrl } from "./twilio-region-wBOx-8sz.mjs";
|
|
3
3
|
import { n as mapVoiceToPolly, t as escapeXml } from "./voice-mapping-1lzFmzOF.mjs";
|
|
4
|
-
import { a as verifyTwilioWebhook, t as createCarrierApi } from "./carrier-api-
|
|
4
|
+
import { a as verifyTwilioWebhook, t as createCarrierApi } from "./carrier-api-Bt9KxU9b.mjs";
|
|
5
5
|
import { asNullableObjectRecord, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
6
6
|
import { sleepWithAbort } from "openclaw/plugin-sdk/runtime-env";
|
|
7
7
|
import crypto from "node:crypto";
|
|
@@ -67,9 +67,7 @@ var TwilioProvider = class TwilioProvider {
|
|
|
67
67
|
releaseCallState(providerCallId, callId) {
|
|
68
68
|
const webhookUrl = this.callWebhookUrls.get(providerCallId);
|
|
69
69
|
let resolvedCallId = callId;
|
|
70
|
-
if (!resolvedCallId && webhookUrl)
|
|
71
|
-
resolvedCallId = new URL(webhookUrl).searchParams.get("callId") || void 0;
|
|
72
|
-
} catch {}
|
|
70
|
+
if (!resolvedCallId && webhookUrl) resolvedCallId = new URL(webhookUrl).searchParams.get("callId") || void 0;
|
|
73
71
|
if (resolvedCallId) this.twimlStorage.delete(resolvedCallId);
|
|
74
72
|
this.callWebhookUrls.delete(providerCallId);
|
|
75
73
|
this.callStreamMap.delete(providerCallId);
|
|
@@ -35,11 +35,13 @@ const schemaMigrationDescriptions = {
|
|
|
35
35
|
"state-table-retirement-v10": "retired shared-state tables -> removed tables and indexes",
|
|
36
36
|
"state-table-retirement-v11": "retired skill curator tables -> removed tables and indexes",
|
|
37
37
|
"singleton-state-foldin-v12": "singleton state tables -> shared configuration state",
|
|
38
|
-
"state-consolidation-v13": "cron jobs and subagent runs ->
|
|
38
|
+
"state-consolidation-v13": "cron jobs and subagent runs -> current JSON storage",
|
|
39
39
|
"creator-namespace-v14": "cron creators -> explicit principal namespaces",
|
|
40
40
|
"conversation-binding-targets-v15": "conversation bindings -> exact target keys without agent/session projections",
|
|
41
41
|
"prepared-worker-ownership-v17": "prepared workers -> one-use capacity and fixed workspace ownership",
|
|
42
42
|
"github-publication-requester-authority-v18": "GitHub publication receipts -> original requesting authority",
|
|
43
|
+
"portable-identity-columns-v22": "ACP resume keys and receipt cursors -> portable columns",
|
|
44
|
+
"predicate-columns-v21": "meeting transcripts and delivery retention -> derived predicate columns",
|
|
43
45
|
"worker-placement-execution-mode-v8": "cloud worker placements -> execution-mode claims",
|
|
44
46
|
"operator-approvals-system-agent": "operator approvals -> OpenClaw system changes",
|
|
45
47
|
"session-watch-cursor-provenance-v4": "session watch cursors -> provenance column",
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { definePluginEntry } from "./api.js";
|
|
2
2
|
import { VOICE_CALL_CLI_DESCRIPTOR } from "./cli-output-mode.js";
|
|
3
|
-
import { g as CallBriefSchema, i as resolveVoiceCallConfig, t as VoiceCallConfigSchema, u as validateProviderConfig } from "./.setup/config-
|
|
4
|
-
import { C as TerminalStates, t as createVoiceCallRuntime } from "./.setup/runtime-entry-
|
|
3
|
+
import { g as CallBriefSchema, i as resolveVoiceCallConfig, t as VoiceCallConfigSchema, u as validateProviderConfig } from "./.setup/config-BdbTQCSh.mjs";
|
|
4
|
+
import { C as TerminalStates, t as createVoiceCallRuntime } from "./.setup/runtime-entry-BqhtjzE-.mjs";
|
|
5
5
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
6
6
|
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
7
7
|
import { ErrorCodes, errorShape } from "openclaw/plugin-sdk/gateway-runtime";
|
|
@@ -330,10 +330,9 @@ function isCliOnlyProcess() {
|
|
|
330
330
|
const VOICE_CALL_RUNTIME_COORDINATOR_KEY = Symbol.for("openclaw.voice-call.runtimeCoordinator");
|
|
331
331
|
var VoiceCallRuntimeLifecycleError = class extends Error {};
|
|
332
332
|
function getVoiceCallRuntimeCoordinator() {
|
|
333
|
-
return resolveGlobalSingleton(VOICE_CALL_RUNTIME_COORDINATOR_KEY, () => ({
|
|
333
|
+
return resolveGlobalSingleton(VOICE_CALL_RUNTIME_COORDINATOR_KEY, () => ({}));
|
|
334
334
|
}
|
|
335
335
|
function activateVoiceCallRuntimeGeneration(coordinator, registration, generation) {
|
|
336
|
-
if (registration.epoch < (coordinator.current?.epoch ?? 0) || registration.generation !== generation) throw new VoiceCallRuntimeLifecycleError("Voice call runtime generation was superseded; use the current plugin registration");
|
|
337
336
|
if (generation.retired) throw new VoiceCallRuntimeLifecycleError("Voice call runtime generation is retired; use the current plugin registration");
|
|
338
337
|
if (coordinator.current !== registration) {
|
|
339
338
|
if (coordinator.current) coordinator.current.generation.retired = true;
|
|
@@ -342,18 +341,9 @@ function activateVoiceCallRuntimeGeneration(coordinator, registration, generatio
|
|
|
342
341
|
}
|
|
343
342
|
function stopVoiceCallRuntimeGeneration(coordinator, generation) {
|
|
344
343
|
const ownedSlot = coordinator.slot?.owner === generation ? coordinator.slot : void 0;
|
|
345
|
-
if (!ownedSlot
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
});
|
|
349
|
-
const stoppingSlot = {
|
|
350
|
-
state: "stopping",
|
|
351
|
-
owner: generation,
|
|
352
|
-
promise: stopPromise
|
|
353
|
-
};
|
|
354
|
-
if (coordinator.slot === ownedSlot) coordinator.slot = stoppingSlot;
|
|
355
|
-
return stopPromise.finally(() => {
|
|
356
|
-
if (coordinator.slot === stoppingSlot) coordinator.slot = void 0;
|
|
344
|
+
if (!ownedSlot) return Promise.resolve();
|
|
345
|
+
return ownedSlot.stopPromise ??= ownedSlot.promise.then((runtime) => runtime.stop()).finally(() => {
|
|
346
|
+
if (coordinator.slot === ownedSlot) coordinator.slot = void 0;
|
|
357
347
|
});
|
|
358
348
|
}
|
|
359
349
|
var voice_call_default = definePluginEntry({
|
|
@@ -366,7 +356,6 @@ var voice_call_default = definePluginEntry({
|
|
|
366
356
|
const validation = validateProviderConfig(config);
|
|
367
357
|
const runtimeCoordinator = getVoiceCallRuntimeCoordinator();
|
|
368
358
|
const runtimeRegistration = {
|
|
369
|
-
epoch: ++runtimeCoordinator.epochCounter,
|
|
370
359
|
generation: { retired: false },
|
|
371
360
|
ensureRuntime: () => ensureRegisteredRuntime()
|
|
372
361
|
};
|
|
@@ -380,58 +369,40 @@ var voice_call_default = definePluginEntry({
|
|
|
380
369
|
if (!config.enabled) throw new Error("Voice call disabled in plugin config");
|
|
381
370
|
if (!validation.valid) throw new Error(validation.errors.join("; "));
|
|
382
371
|
if ((config.reports.enabled || config.live.transcript) && !runtimeGeneration.runInServiceContext) throw new VoiceCallRuntimeLifecycleError("Voice call requester reporting requires service startup");
|
|
383
|
-
|
|
372
|
+
const previous = runtimeCoordinator.slot;
|
|
373
|
+
if (previous && (previous.owner !== runtimeGeneration || previous.stopPromise)) {
|
|
374
|
+
await stopVoiceCallRuntimeGeneration(runtimeCoordinator, previous.owner);
|
|
384
375
|
activateRuntimeGeneration(runtimeGeneration);
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
if (slot.owner !== runtimeGeneration) {
|
|
388
|
-
if (slot.owner.retired) {
|
|
389
|
-
await stopVoiceCallRuntimeGeneration(runtimeCoordinator, slot.owner);
|
|
390
|
-
continue;
|
|
391
|
-
}
|
|
392
|
-
throw new VoiceCallRuntimeLifecycleError("A previous voice call runtime generation is still active; retry after it stops");
|
|
393
|
-
}
|
|
394
|
-
if (slot.state === "running") return slot.runtime;
|
|
395
|
-
if (slot.state === "stopping") {
|
|
396
|
-
await slot.promise;
|
|
397
|
-
continue;
|
|
398
|
-
}
|
|
399
|
-
let createdRuntime;
|
|
400
|
-
try {
|
|
401
|
-
createdRuntime = await slot.promise;
|
|
402
|
-
} catch (err) {
|
|
403
|
-
if (runtimeCoordinator.slot === slot) runtimeCoordinator.slot = void 0;
|
|
404
|
-
throw err;
|
|
405
|
-
}
|
|
406
|
-
activateRuntimeGeneration(runtimeGeneration);
|
|
407
|
-
if (runtimeCoordinator.slot !== slot) continue;
|
|
408
|
-
runtimeCoordinator.slot = {
|
|
409
|
-
state: "running",
|
|
410
|
-
owner: runtimeGeneration,
|
|
411
|
-
runtime: createdRuntime
|
|
412
|
-
};
|
|
413
|
-
return createdRuntime;
|
|
414
|
-
}
|
|
376
|
+
}
|
|
377
|
+
if (!runtimeCoordinator.slot) {
|
|
415
378
|
const scheduler = runtimeGeneration.scheduler;
|
|
416
379
|
if (!scheduler || scheduler.signal.aborted) throw new VoiceCallRuntimeLifecycleError("Voice call service is not running; start the Gateway and retry");
|
|
417
|
-
const runtimePromise = createVoiceCallRuntime({
|
|
418
|
-
scheduler,
|
|
419
|
-
config,
|
|
420
|
-
coreConfig: api.config,
|
|
421
|
-
fullConfig: api.config,
|
|
422
|
-
agentRuntime: api.runtime.agent,
|
|
423
|
-
stateRuntime: api.runtime.state,
|
|
424
|
-
ttsRuntime: api.runtime.tts,
|
|
425
|
-
deliveryRuntime: api.runtime,
|
|
426
|
-
runInServiceContext: runtimeGeneration.runInServiceContext,
|
|
427
|
-
logger: api.logger
|
|
428
|
-
});
|
|
429
380
|
runtimeCoordinator.slot = {
|
|
430
|
-
state: "starting",
|
|
431
381
|
owner: runtimeGeneration,
|
|
432
|
-
promise:
|
|
382
|
+
promise: createVoiceCallRuntime({
|
|
383
|
+
scheduler,
|
|
384
|
+
config,
|
|
385
|
+
coreConfig: api.config,
|
|
386
|
+
fullConfig: api.config,
|
|
387
|
+
agentRuntime: api.runtime.agent,
|
|
388
|
+
stateRuntime: api.runtime.state,
|
|
389
|
+
ttsRuntime: api.runtime.tts,
|
|
390
|
+
deliveryRuntime: api.runtime,
|
|
391
|
+
runInServiceContext: runtimeGeneration.runInServiceContext,
|
|
392
|
+
logger: api.logger
|
|
393
|
+
})
|
|
433
394
|
};
|
|
434
395
|
}
|
|
396
|
+
const slot = runtimeCoordinator.slot;
|
|
397
|
+
let runtime;
|
|
398
|
+
try {
|
|
399
|
+
runtime = await slot.promise;
|
|
400
|
+
} catch (err) {
|
|
401
|
+
if (runtimeCoordinator.slot === slot && !slot.stopPromise) runtimeCoordinator.slot = void 0;
|
|
402
|
+
throw err;
|
|
403
|
+
}
|
|
404
|
+
activateRuntimeGeneration(runtimeGeneration);
|
|
405
|
+
return runtime;
|
|
435
406
|
};
|
|
436
407
|
const ensureRegisteredRuntime = async (runtimeGeneration = runtimeRegistration.generation) => {
|
|
437
408
|
try {
|
|
@@ -597,7 +568,7 @@ var voice_call_default = definePluginEntry({
|
|
|
597
568
|
};
|
|
598
569
|
});
|
|
599
570
|
api.registerCli(async ({ program }) => {
|
|
600
|
-
const { registerVoiceCallCli } = await import("./.setup/cli-
|
|
571
|
+
const { registerVoiceCallCli } = await import("./.setup/cli-RHaXpQxE.mjs");
|
|
601
572
|
registerVoiceCallCli({
|
|
602
573
|
program,
|
|
603
574
|
config,
|
package/dist/runtime-entry.js
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export { createVoiceCallRuntime };
|
|
1
|
+
export { t as createVoiceCallRuntime } from "./.setup/runtime-entry-BqhtjzE-.mjs";
|
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export { packageName, stateMigrations };
|
|
1
|
+
export { t as packageName, n as stateMigrations } from "./.setup/state-retention-api-ONgqE-_V.mjs";
|
|
@@ -12,7 +12,7 @@ export function FromObject(context, type, value) {
|
|
|
12
12
|
return value;
|
|
13
13
|
const additionalProperties = GetAdditionalProperties(type);
|
|
14
14
|
for (const key of Guard.Keys(value)) {
|
|
15
|
-
if (Guard.HasPropertyKey(type.properties, key)) {
|
|
15
|
+
if (Guard.HasPropertyKey(type.properties, key) && IsSchema(type.properties[key])) {
|
|
16
16
|
value[key] = FromType(context, type.properties[key], value[key]);
|
|
17
17
|
continue;
|
|
18
18
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/voice-call",
|
|
3
|
-
"version": "2026.10.
|
|
3
|
+
"version": "2026.10.10-beta.1",
|
|
4
4
|
"description": "OpenClaw voice-call plugin for Twilio, Telnyx, and Plivo phone calls.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
},
|
|
9
9
|
"type": "module",
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"typebox": "1.3.
|
|
11
|
+
"typebox": "1.3.35",
|
|
12
12
|
"zod": "4.6.5"
|
|
13
13
|
},
|
|
14
14
|
"peerDependencies": {
|
|
15
|
-
"openclaw": ">=2026.10.
|
|
15
|
+
"openclaw": ">=2026.10.10-beta.1"
|
|
16
16
|
},
|
|
17
17
|
"peerDependenciesMeta": {
|
|
18
18
|
"openclaw": {
|
|
@@ -31,10 +31,10 @@
|
|
|
31
31
|
"minHostVersion": ">=2026.4.10"
|
|
32
32
|
},
|
|
33
33
|
"compat": {
|
|
34
|
-
"pluginApi": ">=2026.10.
|
|
34
|
+
"pluginApi": ">=2026.10.10-beta.1"
|
|
35
35
|
},
|
|
36
36
|
"build": {
|
|
37
|
-
"openclawVersion": "2026.10.
|
|
37
|
+
"openclawVersion": "2026.10.10-beta.1"
|
|
38
38
|
},
|
|
39
39
|
"release": {
|
|
40
40
|
"publishToClawHub": true,
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { asOptionalRecord, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
2
|
-
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
|
3
|
-
import { root } from "openclaw/plugin-sdk/security-runtime";
|
|
4
|
-
import { resolveAgentIdentity, resolveAgentWorkspaceDir } from "openclaw/plugin-sdk/agent-runtime";
|
|
5
|
-
//#region extensions/voice-call/src/realtime-agent-context.legacy.ts
|
|
6
|
-
function limitLegacyContextText(text, maxChars) {
|
|
7
|
-
if (text.length <= maxChars) return text;
|
|
8
|
-
const truncated = `${truncateUtf16Safe(text, Math.max(0, maxChars - 32)).trimEnd()}\n[truncated]`;
|
|
9
|
-
return truncateUtf16Safe(truncated, maxChars);
|
|
10
|
-
}
|
|
11
|
-
async function buildLegacyRealtimeVoiceAgentContext(params) {
|
|
12
|
-
const { config, coreConfig, agentId } = params;
|
|
13
|
-
if (!config.enabled) return;
|
|
14
|
-
const capsule = [
|
|
15
|
-
"OpenClaw agent voice context:",
|
|
16
|
-
`- Agent id: ${agentId}`,
|
|
17
|
-
"- Use this context to match the OpenClaw agent's personality and standing preferences on fast voice turns.",
|
|
18
|
-
"- Treat this as compact context only; call openclaw_agent_consult when the caller needs the full agent brain, tools, memory, or workspace state."
|
|
19
|
-
];
|
|
20
|
-
if (config.includeIdentity) {
|
|
21
|
-
const identity = asOptionalRecord(resolveAgentIdentity(coreConfig, agentId));
|
|
22
|
-
const lines = [
|
|
23
|
-
["Name", identity?.name],
|
|
24
|
-
["Emoji", identity?.emoji],
|
|
25
|
-
["Vibe", identity?.vibe],
|
|
26
|
-
["Theme", identity?.theme],
|
|
27
|
-
["Creature/persona", identity?.creature]
|
|
28
|
-
].flatMap(([label, value]) => {
|
|
29
|
-
const text = normalizeOptionalString(value);
|
|
30
|
-
return text ? [`- ${label}: ${text}`] : [];
|
|
31
|
-
});
|
|
32
|
-
if (lines.length > 0) capsule.push(`Configured identity:\n${lines.join("\n")}`);
|
|
33
|
-
}
|
|
34
|
-
if (config.includeWorkspaceFiles) {
|
|
35
|
-
const workspace = await root(resolveAgentWorkspaceDir(coreConfig, agentId)).catch(() => null);
|
|
36
|
-
if (workspace) {
|
|
37
|
-
const sections = [];
|
|
38
|
-
let remaining = config.maxChars;
|
|
39
|
-
for (const file of config.files) {
|
|
40
|
-
if (remaining <= 0) continue;
|
|
41
|
-
const trimmed = (await workspace.readText(file).catch(() => void 0))?.trim();
|
|
42
|
-
if (!trimmed) continue;
|
|
43
|
-
const section = `### ${file}\n${limitLegacyContextText(trimmed, Math.max(0, remaining - file.length - 16))}`;
|
|
44
|
-
sections.push(section);
|
|
45
|
-
remaining -= section.length;
|
|
46
|
-
}
|
|
47
|
-
if (sections.length > 0) capsule.push(`Workspace voice context:\n${sections.join("\n\n")}`);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
return limitLegacyContextText(capsule.join("\n\n"), config.maxChars);
|
|
51
|
-
}
|
|
52
|
-
//#endregion
|
|
53
|
-
export { buildLegacyRealtimeVoiceAgentContext };
|