@openclaw/voice-call 2026.7.1-beta.6 → 2026.7.2-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 -2
- package/dist/doctor-contract-api.js +27 -10
- package/dist/{guarded-json-api-CLVDU_h1.js → guarded-json-api-DoLAoIRF.js} +32 -41
- package/dist/index.js +9 -15
- package/dist/{plivo-D5wI4-pi.js → plivo-BICQA_yK.js} +2 -2
- package/dist/{realtime-handler-Bz-MNWtD.js → realtime-handler-DJk-eYyf.js} +31 -20
- package/dist/{response-generator-D3YO-WtN.js → response-generator-hmAdGp5h.js} +20 -4
- package/dist/{runtime-entry-DwMgbq2p.js → runtime-entry-DhTLZ2uB.js} +811 -263
- package/dist/runtime-entry.js +1 -1
- package/dist/setup-api.js +86 -2
- package/dist/{store-DtewuuOb.js → store-C1pl3avP.js} +2 -2
- package/dist/{telnyx-DksS6q0n.js → telnyx-B9tF5jeJ.js} +1 -1
- package/dist/{twilio-BYcsdUkj.js → twilio-U_myvowz.js} +7 -10
- package/dist/twilio-region-4fkgz3UG.js +25 -0
- package/npm-shrinkwrap.json +3 -13
- package/openclaw.plugin.json +1 -0
- package/package.json +4 -6
- package/dist/config-CDSeuGp3.js +0 -674
- package/dist/config-compat-D8fcjOHp.js +0 -156
- package/node_modules/commander/LICENSE +0 -22
- package/node_modules/commander/Readme.md +0 -1172
- package/node_modules/commander/index.js +0 -21
- package/node_modules/commander/lib/argument.js +0 -147
- package/node_modules/commander/lib/command.js +0 -2790
- package/node_modules/commander/lib/error.js +0 -36
- package/node_modules/commander/lib/help.js +0 -731
- package/node_modules/commander/lib/option.js +0 -377
- package/node_modules/commander/lib/suggestSimilar.js +0 -99
- package/node_modules/commander/package-support.json +0 -19
- package/node_modules/commander/package.json +0 -64
- package/node_modules/commander/typings/index.d.ts +0 -1113
package/README.md
CHANGED
|
@@ -101,7 +101,7 @@ Notes:
|
|
|
101
101
|
- 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).
|
|
102
102
|
- `mock` is a local dev provider (no network calls).
|
|
103
103
|
- Telnyx requires `telnyx.publicKey` (or `TELNYX_PUBLIC_KEY`) unless `skipSignatureVerification` is true.
|
|
104
|
-
- If older configs still use `provider: "log"`, `twilio.from`, or legacy `streaming.*` OpenAI keys, run `openclaw doctor --fix` to rewrite them.
|
|
104
|
+
- Runtime accepts canonical config only. If older configs still use `provider: "log"`, `twilio.from`, or legacy `streaming.*` OpenAI keys, run `openclaw doctor --fix` to rewrite them.
|
|
105
105
|
- advanced webhook, streaming, and tunnel notes: `https://docs.openclaw.ai/plugins/voice-call`
|
|
106
106
|
- `responseModel` is optional. When unset, voice responses use the runtime default model.
|
|
107
107
|
- `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.
|
|
@@ -163,4 +163,3 @@ Actions:
|
|
|
163
163
|
- Outbound conversation calls suppress barge-in only while the initial greeting is actively speaking, then re-enable normal interruption.
|
|
164
164
|
- Twilio stream disconnect auto-end uses a short grace window so quick reconnects do not end the call.
|
|
165
165
|
- Realtime provider selection is generic. Configure `streaming.provider` / `realtime.provider` and put provider-owned options under `providers.<id>`.
|
|
166
|
-
- Runtime fallback still accepts the old voice-call keys for now, but migration is a doctor step and the compat shim is scheduled to go away in a future release.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { a as MAX_CALL_RECORD_EVENTS, d as parseVoiceCallRecordLine, i as CALL_RECORD_EVENT_META_MAX_ENTRIES, m as resolveVoiceCallLegacyCallLogPath, n as CALL_RECORD_EVENTS_NAMESPACE, o as RAW_CALL_RECORD_CHUNK_BYTES, p as prepareVoiceCallRecordForStorage, r as CALL_RECORD_EVENT_CHUNKS_NAMESPACE, s as buildVoiceCallLegacyJsonlEventKey, t as CALL_RECORD_CHUNK_MAX_ENTRIES } from "./store-
|
|
1
|
+
import { a as MAX_CALL_RECORD_EVENTS, d as parseVoiceCallRecordLine, i as CALL_RECORD_EVENT_META_MAX_ENTRIES, m as resolveVoiceCallLegacyCallLogPath, n as CALL_RECORD_EVENTS_NAMESPACE, o as RAW_CALL_RECORD_CHUNK_BYTES, p as prepareVoiceCallRecordForStorage, r as CALL_RECORD_EVENT_CHUNKS_NAMESPACE, s as buildVoiceCallLegacyJsonlEventKey, t as CALL_RECORD_CHUNK_MAX_ENTRIES } from "./store-C1pl3avP.js";
|
|
2
2
|
import { normalizeAgentId } from "openclaw/plugin-sdk/routing";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import fs from "node:fs/promises";
|
|
6
|
-
import { archiveLegacyStateSource } from "openclaw/plugin-sdk/runtime-doctor";
|
|
6
|
+
import { archiveLegacyStateSource, detectOpenClawStateDatabaseSchemaMigrations, repairOpenClawStateDatabaseSchema } from "openclaw/plugin-sdk/runtime-doctor";
|
|
7
7
|
//#region extensions/voice-call/doctor-contract-api.ts
|
|
8
8
|
/** Resolve home from doctor env with OS fallback. */
|
|
9
9
|
function resolveHome(env) {
|
|
@@ -52,6 +52,15 @@ function resolveVoiceCallStorePath(params) {
|
|
|
52
52
|
if (configuredStore) return resolveUserPath(configuredStore, params.env);
|
|
53
53
|
return path.join(resolveHome(params.env), ".openclaw", "voice-calls");
|
|
54
54
|
}
|
|
55
|
+
function resolveVoiceCallStateDatabaseEnv(params) {
|
|
56
|
+
return {
|
|
57
|
+
...params.env,
|
|
58
|
+
OPENCLAW_STATE_DIR: resolveVoiceCallStorePath(params)
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function describeVoiceCallSchemaMigration(migration) {
|
|
62
|
+
return migration.kind === "agent-databases-composite-primary-key" ? "agent database registry primary key -> agent_id,path" : "audit event ledger -> versioned message lifecycle schema";
|
|
63
|
+
}
|
|
55
64
|
/** Return true when a path exists and is a file. */
|
|
56
65
|
/** Build the plugin state key for one migrated event chunk. */
|
|
57
66
|
function buildChunkKey(eventKey, index) {
|
|
@@ -170,24 +179,32 @@ const stateMigrations = [{
|
|
|
170
179
|
label: "Voice Call call log",
|
|
171
180
|
async detectLegacyState(params) {
|
|
172
181
|
const { entries } = await readLegacyCallRecords(resolveVoiceCallLegacyCallLogPath(resolveVoiceCallStorePath(params)));
|
|
173
|
-
|
|
174
|
-
|
|
182
|
+
const schemaMigrations = detectOpenClawStateDatabaseSchemaMigrations({ env: resolveVoiceCallStateDatabaseEnv(params) });
|
|
183
|
+
if (entries.length === 0 && schemaMigrations.length === 0) return null;
|
|
184
|
+
return { preview: [...schemaMigrations.map((migration) => `- Voice Call SQLite schema: ${describeVoiceCallSchemaMigration(migration)}`), ...entries.length > 0 ? [`- Voice Call call log: ${entries.length} ${entries.length === 1 ? "record" : "records"} -> plugin state (${CALL_RECORD_EVENTS_NAMESPACE})`] : []] };
|
|
175
185
|
},
|
|
176
186
|
async migrateLegacyState(params) {
|
|
177
187
|
const changes = [];
|
|
178
188
|
const warnings = [];
|
|
179
|
-
const
|
|
180
|
-
const filePath = resolveVoiceCallLegacyCallLogPath(storePath);
|
|
189
|
+
const filePath = resolveVoiceCallLegacyCallLogPath(resolveVoiceCallStorePath(params));
|
|
181
190
|
const { entries, warnings: readWarnings } = await readLegacyCallRecords(filePath);
|
|
182
191
|
warnings.push(...readWarnings);
|
|
192
|
+
const stateDatabaseEnv = resolveVoiceCallStateDatabaseEnv(params);
|
|
193
|
+
const schemaMigrations = detectOpenClawStateDatabaseSchemaMigrations({ env: stateDatabaseEnv });
|
|
194
|
+
if (schemaMigrations.length > 0) {
|
|
195
|
+
const repaired = repairOpenClawStateDatabaseSchema({ env: stateDatabaseEnv });
|
|
196
|
+
warnings.push(...repaired.warnings);
|
|
197
|
+
if (repaired.warnings.length > 0) return {
|
|
198
|
+
changes,
|
|
199
|
+
warnings
|
|
200
|
+
};
|
|
201
|
+
changes.push(...schemaMigrations.map((migration) => `Migrated Voice Call SQLite ${describeVoiceCallSchemaMigration(migration)}`));
|
|
202
|
+
}
|
|
183
203
|
if (entries.length === 0) return {
|
|
184
204
|
changes,
|
|
185
205
|
warnings
|
|
186
206
|
};
|
|
187
|
-
const env =
|
|
188
|
-
...params.env,
|
|
189
|
-
OPENCLAW_STATE_DIR: storePath
|
|
190
|
-
};
|
|
207
|
+
const env = stateDatabaseEnv;
|
|
191
208
|
const imported = await importLegacyCallRecords({
|
|
192
209
|
entries,
|
|
193
210
|
eventStore: params.context.openPluginStateKeyedStore({
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { fetchWithSsrFGuard } from "./runtime-api.js";
|
|
2
2
|
import "./api.js";
|
|
3
|
-
import { n as normalizeProxyIp, o as getHeader } from "./runtime-entry-
|
|
3
|
+
import { n as normalizeProxyIp, o as getHeader } from "./runtime-entry-DhTLZ2uB.js";
|
|
4
4
|
import { n as markWebhookReplay, t as createWebhookReplayCache } from "./webhook-replay-aaP79dI8.js";
|
|
5
5
|
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
6
6
|
import { isLoopbackHost } from "openclaw/plugin-sdk/gateway-runtime";
|
|
7
7
|
import { normalizeLowercaseStringOrEmpty, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
8
8
|
import crypto from "node:crypto";
|
|
9
9
|
import { safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
|
|
10
|
+
import { readResponseTextPrefix, readResponseWithLimit } from "openclaw/plugin-sdk/response-limit-runtime";
|
|
10
11
|
//#region extensions/voice-call/src/webhook-security.ts
|
|
11
12
|
const twilioReplayCache = createWebhookReplayCache();
|
|
12
13
|
const plivoReplayCache = createWebhookReplayCache();
|
|
@@ -53,16 +54,14 @@ function isValidHostname(hostname) {
|
|
|
53
54
|
*/
|
|
54
55
|
function extractHostname(hostHeader) {
|
|
55
56
|
if (!hostHeader) return null;
|
|
56
|
-
let hostname;
|
|
57
57
|
if (hostHeader.startsWith("[")) {
|
|
58
58
|
const endBracket = hostHeader.indexOf("]");
|
|
59
59
|
if (endBracket === -1) return null;
|
|
60
|
-
|
|
61
|
-
return normalizeLowercaseStringOrEmpty(hostname);
|
|
60
|
+
return normalizeLowercaseStringOrEmpty(hostHeader.slice(1, endBracket));
|
|
62
61
|
}
|
|
63
62
|
if (hostHeader.includes("@")) return null;
|
|
64
|
-
hostname = hostHeader.split(":")
|
|
65
|
-
if (!isValidHostname(hostname)) return null;
|
|
63
|
+
const hostname = hostHeader.split(":").at(0);
|
|
64
|
+
if (!hostname || !isValidHostname(hostname)) return null;
|
|
66
65
|
return normalizeLowercaseStringOrEmpty(hostname);
|
|
67
66
|
}
|
|
68
67
|
function extractHostnameFromHeader(headerValue) {
|
|
@@ -162,6 +161,18 @@ function buildTwilioVerificationUrl(ctx, publicUrl, urlOptions) {
|
|
|
162
161
|
return publicUrl;
|
|
163
162
|
}
|
|
164
163
|
}
|
|
164
|
+
function redactTwilioVerificationUrlForDiagnostics(url) {
|
|
165
|
+
try {
|
|
166
|
+
const parsed = new URL(url);
|
|
167
|
+
parsed.username = parsed.username ? "***" : "";
|
|
168
|
+
parsed.password = parsed.password ? "***" : "";
|
|
169
|
+
parsed.hash = parsed.hash ? "#***" : "";
|
|
170
|
+
for (const key of Array.from(parsed.searchParams.keys())) parsed.searchParams.set(key, "***");
|
|
171
|
+
return parsed.toString();
|
|
172
|
+
} catch {
|
|
173
|
+
return "<invalid verification URL>";
|
|
174
|
+
}
|
|
175
|
+
}
|
|
165
176
|
function stripPortFromUrl(url) {
|
|
166
177
|
try {
|
|
167
178
|
const parsed = new URL(url);
|
|
@@ -347,8 +358,7 @@ function verifyTwilioWebhook(ctx, authToken, options) {
|
|
|
347
358
|
const isNgrokFreeTier = verificationUrl.includes(".ngrok-free.app") || verificationUrl.includes(".ngrok.io");
|
|
348
359
|
return {
|
|
349
360
|
ok: false,
|
|
350
|
-
reason: `Invalid signature for URL: ${verificationUrl}`,
|
|
351
|
-
verificationUrl,
|
|
361
|
+
reason: `Invalid signature for URL: ${redactTwilioVerificationUrlForDiagnostics(verificationUrl)}`,
|
|
352
362
|
isNgrokFreeTier
|
|
353
363
|
};
|
|
354
364
|
}
|
|
@@ -383,16 +393,18 @@ function toParamMapFromSearchParams(sp) {
|
|
|
383
393
|
}
|
|
384
394
|
function sortedQueryString(params) {
|
|
385
395
|
const parts = [];
|
|
386
|
-
|
|
387
|
-
|
|
396
|
+
const entries = Object.entries(params).toSorted(([left], [right]) => left < right ? -1 : left > right ? 1 : 0);
|
|
397
|
+
for (const [key, entryValues] of entries) {
|
|
398
|
+
const values = [...entryValues].toSorted();
|
|
388
399
|
for (const value of values) parts.push(`${key}=${value}`);
|
|
389
400
|
}
|
|
390
401
|
return parts.join("&");
|
|
391
402
|
}
|
|
392
403
|
function sortedParamsString(params) {
|
|
393
404
|
const parts = [];
|
|
394
|
-
|
|
395
|
-
|
|
405
|
+
const entries = Object.entries(params).toSorted(([left], [right]) => left < right ? -1 : left > right ? 1 : 0);
|
|
406
|
+
for (const [key, entryValues] of entries) {
|
|
407
|
+
const values = [...entryValues].toSorted();
|
|
396
408
|
for (const value of values) parts.push(`${key}${value}`);
|
|
397
409
|
}
|
|
398
410
|
return parts.join("");
|
|
@@ -533,35 +545,12 @@ function appendTruncatedSuffix(text) {
|
|
|
533
545
|
return `${text.trimEnd()}${TRUNCATED_SUFFIX}`;
|
|
534
546
|
}
|
|
535
547
|
async function readProviderResponseTextWithLimit(params) {
|
|
536
|
-
if (
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
let totalBytes = 0;
|
|
540
|
-
let text = "";
|
|
541
|
-
try {
|
|
542
|
-
while (true) {
|
|
543
|
-
const { done, value } = await reader.read();
|
|
544
|
-
if (done) return text + decoder.decode();
|
|
545
|
-
if (!value?.byteLength) continue;
|
|
546
|
-
const remainingBytes = params.maxBytes - totalBytes;
|
|
547
|
-
if (value.byteLength > remainingBytes) {
|
|
548
|
-
if (params.truncateOnLimit) {
|
|
549
|
-
const clipped = remainingBytes > 0 ? value.slice(0, remainingBytes) : void 0;
|
|
550
|
-
if (clipped) text += decoder.decode(clipped, { stream: true });
|
|
551
|
-
await reader.cancel().catch(() => void 0);
|
|
552
|
-
return appendTruncatedSuffix(text + decoder.decode());
|
|
553
|
-
}
|
|
554
|
-
await reader.cancel().catch(() => void 0);
|
|
555
|
-
throw new Error(`provider response body too large: ${totalBytes + value.byteLength} bytes (limit: ${params.maxBytes} bytes)`);
|
|
556
|
-
}
|
|
557
|
-
text += decoder.decode(value, { stream: true });
|
|
558
|
-
totalBytes += value.byteLength;
|
|
559
|
-
}
|
|
560
|
-
} finally {
|
|
561
|
-
try {
|
|
562
|
-
reader.releaseLock();
|
|
563
|
-
} catch {}
|
|
548
|
+
if (params.truncateOnLimit) {
|
|
549
|
+
const prefix = await readResponseTextPrefix(params.response, params.maxBytes);
|
|
550
|
+
return prefix.truncated ? appendTruncatedSuffix(prefix.text) : prefix.text;
|
|
564
551
|
}
|
|
552
|
+
const body = await readResponseWithLimit(params.response, params.maxBytes, { onOverflow: ({ size, maxBytes }) => /* @__PURE__ */ new Error(`provider response body too large: ${size} bytes (limit: ${maxBytes} bytes)`) });
|
|
553
|
+
return new TextDecoder().decode(body);
|
|
565
554
|
}
|
|
566
555
|
async function readProviderJsonResponseText(response) {
|
|
567
556
|
return await readProviderResponseTextWithLimit({
|
|
@@ -578,6 +567,7 @@ async function readProviderErrorResponseSnippet(response) {
|
|
|
578
567
|
}
|
|
579
568
|
//#endregion
|
|
580
569
|
//#region extensions/voice-call/src/providers/shared/guarded-json-api.ts
|
|
570
|
+
const VOICE_CALL_PROVIDER_API_TIMEOUT_MS = 3e4;
|
|
581
571
|
/** Send a provider JSON request through the SSRF guard and parse bounded JSON responses. */
|
|
582
572
|
async function guardedJsonApiRequest(params) {
|
|
583
573
|
const { response, release } = await fetchWithSsrFGuard({
|
|
@@ -588,7 +578,8 @@ async function guardedJsonApiRequest(params) {
|
|
|
588
578
|
body: params.body ? JSON.stringify(params.body) : void 0
|
|
589
579
|
},
|
|
590
580
|
policy: { allowedHostnames: params.allowedHostnames },
|
|
591
|
-
auditContext: params.auditContext
|
|
581
|
+
auditContext: params.auditContext,
|
|
582
|
+
timeoutMs: VOICE_CALL_PROVIDER_API_TIMEOUT_MS
|
|
592
583
|
});
|
|
593
584
|
try {
|
|
594
585
|
if (!response.ok) {
|
package/dist/index.js
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import { definePluginEntry, sleep } from "./runtime-api.js";
|
|
2
2
|
import "./api.js";
|
|
3
|
-
import { c as validateProviderConfig,
|
|
4
|
-
import {
|
|
5
|
-
import { h as setVoiceCallStateRuntime, l as getCallHistoryFromStore } from "./store-DtewuuOb.js";
|
|
6
|
-
import { i as parseVoiceCallPluginConfig, r as normalizeVoiceCallLegacyConfigInput, t as formatVoiceCallLegacyConfigWarnings } from "./config-compat-D8fcjOHp.js";
|
|
3
|
+
import { c as cleanupTailscaleExposureRoute, h as resolveUserPath, l as getTailscaleSelfInfo, s as resolveWebhookExposureStatus, t as createVoiceCallRuntime, u as setupTailscaleExposureRoute, v as VoiceCallConfigSchema, x as validateProviderConfig, y as resolveVoiceCallConfig } from "./runtime-entry-DhTLZ2uB.js";
|
|
4
|
+
import { h as setVoiceCallStateRuntime, l as getCallHistoryFromStore } from "./store-C1pl3avP.js";
|
|
7
5
|
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
8
6
|
import { ErrorCodes, callGatewayFromCli, errorShape } from "openclaw/plugin-sdk/gateway-runtime";
|
|
9
7
|
import { MAX_TCP_PORT, MAX_TIMER_TIMEOUT_MS, addTimerTimeoutGraceMs, clampTimerTimeoutMs, parseStrictNonNegativeInteger, resolveTimerTimeoutMs, timestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime";
|
|
10
8
|
import { normalizeAgentId } from "openclaw/plugin-sdk/routing";
|
|
11
|
-
import { isRecord, normalizeOptionalLowercaseString, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
9
|
+
import { asOptionalRecord, isRecord, normalizeOptionalLowercaseString, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
12
10
|
import { jsonResult } from "openclaw/plugin-sdk/tool-results";
|
|
13
11
|
import { Type } from "typebox";
|
|
14
12
|
import fs from "node:fs";
|
|
@@ -658,12 +656,12 @@ const VOICE_CALL_WRITE_METHOD_SCOPE = { scope: "operator.write" };
|
|
|
658
656
|
const VOICE_CALL_READ_METHOD_SCOPE = { scope: "operator.read" };
|
|
659
657
|
const voiceCallConfigSchema = {
|
|
660
658
|
parse(value) {
|
|
661
|
-
const
|
|
662
|
-
const enabled = typeof
|
|
663
|
-
return
|
|
664
|
-
...
|
|
659
|
+
const config = asOptionalRecord(value) ?? {};
|
|
660
|
+
const enabled = typeof config.enabled === "boolean" ? config.enabled : true;
|
|
661
|
+
return VoiceCallConfigSchema.parse({
|
|
662
|
+
...config,
|
|
665
663
|
enabled,
|
|
666
|
-
provider:
|
|
664
|
+
provider: config.provider ?? (enabled ? "mock" : void 0)
|
|
667
665
|
});
|
|
668
666
|
},
|
|
669
667
|
uiHints: {
|
|
@@ -743,6 +741,7 @@ const voiceCallConfigSchema = {
|
|
|
743
741
|
},
|
|
744
742
|
"streaming.enabled": {
|
|
745
743
|
label: "Enable Streaming",
|
|
744
|
+
help: "Classic streaming transcription currently requires the Twilio call provider.",
|
|
746
745
|
advanced: true
|
|
747
746
|
},
|
|
748
747
|
"streaming.provider": {
|
|
@@ -952,11 +951,6 @@ var voice_call_default = definePluginEntry({
|
|
|
952
951
|
register(api) {
|
|
953
952
|
const config = resolveVoiceCallConfig(voiceCallConfigSchema.parse(api.pluginConfig));
|
|
954
953
|
const validation = validateProviderConfig(config);
|
|
955
|
-
if (api.pluginConfig && typeof api.pluginConfig === "object") for (const warning of formatVoiceCallLegacyConfigWarnings({
|
|
956
|
-
value: api.pluginConfig,
|
|
957
|
-
configPathPrefix: "plugins.entries.voice-call.config",
|
|
958
|
-
doctorFixCommand: "openclaw doctor --fix"
|
|
959
|
-
})) api.logger.warn(warning);
|
|
960
954
|
const runtimeState = getVoiceCallRuntimeGlobalState();
|
|
961
955
|
const continueOperationStore = createVoiceCallContinueOperationStore({
|
|
962
956
|
config,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { g as escapeXml, o as getHeader } from "./runtime-entry-
|
|
2
|
-
import { a as reconstructWebhookUrl, o as verifyPlivoWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-
|
|
1
|
+
import { g as escapeXml, o as getHeader } from "./runtime-entry-DhTLZ2uB.js";
|
|
2
|
+
import { a as reconstructWebhookUrl, o as verifyPlivoWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-DoLAoIRF.js";
|
|
3
3
|
import { normalizeLowercaseStringOrEmpty, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
4
4
|
import crypto from "node:crypto";
|
|
5
5
|
//#region extensions/voice-call/src/providers/plivo.ts
|
|
@@ -884,7 +884,7 @@ var RealtimeCallHandler = class {
|
|
|
884
884
|
}
|
|
885
885
|
});
|
|
886
886
|
console.log(`[voice-call] realtime tool call received callId=${callId} providerCallId=${callSid} tool=${toolEvent.name}`);
|
|
887
|
-
this.executeToolCall(sessionLocal, callId, toolEvent.callId || toolEvent.itemId, toolEvent.name, toolEvent.args, turnId, emitTalkEvent);
|
|
887
|
+
return this.executeToolCall(sessionLocal, callId, toolEvent.callId || toolEvent.itemId, toolEvent.name, toolEvent.args, turnId, emitTalkEvent);
|
|
888
888
|
},
|
|
889
889
|
onEvent: (event) => {
|
|
890
890
|
if (event.type === "input_audio_buffer.speech_started") {
|
|
@@ -1234,12 +1234,13 @@ var RealtimeCallHandler = class {
|
|
|
1234
1234
|
final: true
|
|
1235
1235
|
});
|
|
1236
1236
|
};
|
|
1237
|
-
const submitFinalToolResult = (result) => {
|
|
1238
|
-
bridge.submitToolResult(bridgeCallId, result);
|
|
1237
|
+
const submitFinalToolResult = async (result) => {
|
|
1238
|
+
await bridge.submitToolResult(bridgeCallId, result);
|
|
1239
1239
|
emitFinalToolEvent(result);
|
|
1240
1240
|
};
|
|
1241
|
-
const submitWorkingResponse = () => {
|
|
1241
|
+
const submitWorkingResponse = async () => {
|
|
1242
1242
|
if (handler && name === REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME && bridge.bridge.supportsToolResultContinuation && !this.config.fastContext.enabled) {
|
|
1243
|
+
await bridge.submitToolResult(bridgeCallId, buildRealtimeVoiceAgentConsultWorkingResponse("caller"), { willContinue: true });
|
|
1243
1244
|
emitTalkEvent?.({
|
|
1244
1245
|
type: "tool.progress",
|
|
1245
1246
|
turnId,
|
|
@@ -1249,7 +1250,6 @@ var RealtimeCallHandler = class {
|
|
|
1249
1250
|
status: "working"
|
|
1250
1251
|
}
|
|
1251
1252
|
});
|
|
1252
|
-
bridge.submitToolResult(bridgeCallId, buildRealtimeVoiceAgentConsultWorkingResponse("caller"), { willContinue: true });
|
|
1253
1253
|
}
|
|
1254
1254
|
};
|
|
1255
1255
|
if (name === REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME) {
|
|
@@ -1260,45 +1260,56 @@ var RealtimeCallHandler = class {
|
|
|
1260
1260
|
if (pending) coordinator.remove(pending);
|
|
1261
1261
|
}
|
|
1262
1262
|
const forcedConsult = this.forcedConsultsByCallId.get(callId);
|
|
1263
|
+
if (forcedMatch.kind === "already_delivered" && coordinator.isCancelled(forcedMatch.handle)) {
|
|
1264
|
+
if (forcedConsult) forcedConsult.sendSpeechPrompt = false;
|
|
1265
|
+
await submitFinalToolResult({
|
|
1266
|
+
status: "cancelled",
|
|
1267
|
+
message: "OpenClaw cancelled this consult before completion. Do not restart it."
|
|
1268
|
+
});
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1263
1271
|
if (forcedConsult) {
|
|
1264
1272
|
if (forcedConsult.completedAt || forcedMatch.kind === "already_delivered") {
|
|
1265
|
-
submitFinalToolResult({
|
|
1273
|
+
await submitFinalToolResult({
|
|
1266
1274
|
status: "already_delivered",
|
|
1267
1275
|
message: "OpenClaw already delivered this consult result internally. Do not repeat it."
|
|
1268
1276
|
});
|
|
1269
1277
|
return;
|
|
1270
1278
|
}
|
|
1271
1279
|
forcedConsult.sendSpeechPrompt = false;
|
|
1272
|
-
submitFinalToolResult(await forcedConsult.promise.catch((error) => ({ error: formatErrorMessage(error) })));
|
|
1280
|
+
await submitFinalToolResult(await forcedConsult.promise.catch((error) => ({ error: formatErrorMessage(error) })));
|
|
1273
1281
|
return;
|
|
1274
1282
|
}
|
|
1275
1283
|
const existingNativeConsult = this.nativeConsultsInFlightByCallId.get(callId);
|
|
1276
1284
|
if (existingNativeConsult) {
|
|
1277
1285
|
console.log(`[voice-call] realtime tool call sharing in-flight agent consult callId=${callId} ageMs=${Date.now() - existingNativeConsult.startedAt}`);
|
|
1278
|
-
submitWorkingResponse();
|
|
1279
|
-
submitFinalToolResult(await existingNativeConsult.promise);
|
|
1286
|
+
await submitWorkingResponse();
|
|
1287
|
+
await submitFinalToolResult(await existingNativeConsult.promise);
|
|
1280
1288
|
return;
|
|
1281
1289
|
}
|
|
1282
|
-
submitWorkingResponse();
|
|
1283
1290
|
const state = {
|
|
1284
1291
|
startedAt,
|
|
1285
1292
|
promise: Promise.resolve()
|
|
1286
1293
|
};
|
|
1287
|
-
state.promise = (async () => {
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1294
|
+
state.promise = submitWorkingResponse().then(async () => {
|
|
1295
|
+
try {
|
|
1296
|
+
await this.waitForConsultTranscriptSettle(callId, startedAt);
|
|
1297
|
+
const context = { partialUserTranscript: this.resolveUserTranscriptContext(callId) };
|
|
1298
|
+
state.partialUserTranscript = context.partialUserTranscript;
|
|
1299
|
+
const handlerArgs = withFallbackConsultQuestion(args, context.partialUserTranscript);
|
|
1300
|
+
console.log(`[voice-call] realtime tool call executing callId=${callId} tool=${name} hasHandler=${Boolean(handler)}`);
|
|
1301
|
+
return !handler ? { error: `Tool "${name}" not available` } : await handler(handlerArgs, callId, context);
|
|
1302
|
+
} catch (error) {
|
|
1303
|
+
return { error: formatErrorMessage(error) };
|
|
1304
|
+
}
|
|
1305
|
+
});
|
|
1295
1306
|
this.nativeConsultsInFlightByCallId.set(callId, state);
|
|
1296
1307
|
try {
|
|
1297
1308
|
const result = await state.promise;
|
|
1298
1309
|
const status = result && typeof result === "object" && !Array.isArray(result) && "error" in result ? "error" : "ok";
|
|
1299
1310
|
const error = status === "error" && result && typeof result === "object" && !Array.isArray(result) ? formatErrorMessage(result.error ?? "unknown") : void 0;
|
|
1300
1311
|
console.log(`[voice-call] realtime tool call completed callId=${callId} tool=${name} status=${status} elapsedMs=${Date.now() - startedAt}${error ? ` error=${error}` : ""}`);
|
|
1301
|
-
submitFinalToolResult(result);
|
|
1312
|
+
await submitFinalToolResult(result);
|
|
1302
1313
|
if (status === "ok") this.consumePartialUserTranscript(callId, state.partialUserTranscript);
|
|
1303
1314
|
} finally {
|
|
1304
1315
|
if (this.nativeConsultsInFlightByCallId.get(callId) === state) this.nativeConsultsInFlightByCallId.delete(callId);
|
|
@@ -1312,7 +1323,7 @@ var RealtimeCallHandler = class {
|
|
|
1312
1323
|
const status = result && typeof result === "object" && !Array.isArray(result) && "error" in result ? "error" : "ok";
|
|
1313
1324
|
const error = status === "error" && result && typeof result === "object" && !Array.isArray(result) ? formatErrorMessage(result.error ?? "unknown") : void 0;
|
|
1314
1325
|
console.log(`[voice-call] realtime tool call completed callId=${callId} tool=${name} status=${status} elapsedMs=${Date.now() - startedAt}${error ? ` error=${error}` : ""}`);
|
|
1315
|
-
submitFinalToolResult(result);
|
|
1326
|
+
await submitFinalToolResult(result);
|
|
1316
1327
|
if (name === REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME && status === "ok") this.consumePartialUserTranscript(callId, context.partialUserTranscript);
|
|
1317
1328
|
}
|
|
1318
1329
|
};
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { m as resolveCallAgentId, p as resolveVoiceResponseModel } from "./runtime-entry-DwMgbq2p.js";
|
|
1
|
+
import { b as resolveVoiceCallSessionKey, m as resolveCallAgentId, p as resolveVoiceResponseModel } from "./runtime-entry-DhTLZ2uB.js";
|
|
3
2
|
import { isRecord, normalizeLowercaseStringOrEmpty, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
4
3
|
import crypto from "node:crypto";
|
|
5
|
-
import { applyModelOverrideToSessionEntry } from "openclaw/plugin-sdk/model-session-runtime";
|
|
4
|
+
import { ModelSelectionLockedError, applyModelOverrideToSessionEntry, resolvePersistedSessionRuntimeId } from "openclaw/plugin-sdk/model-session-runtime";
|
|
6
5
|
//#region extensions/voice-call/src/response-generator.ts
|
|
7
6
|
/**
|
|
8
7
|
* Voice call response generator - uses the embedded OpenClaw agent for tool support.
|
|
@@ -67,7 +66,11 @@ function sanitizePlainSpokenText(text) {
|
|
|
67
66
|
const withoutCodeFences = text.replace(/```[\s\S]*?```/g, " ").trim();
|
|
68
67
|
if (!withoutCodeFences) return null;
|
|
69
68
|
const paragraphs = normalizeStringEntries(withoutCodeFences.split(/\n\s*\n+/));
|
|
70
|
-
while (paragraphs.length > 1
|
|
69
|
+
while (paragraphs.length > 1) {
|
|
70
|
+
const firstParagraph = paragraphs.at(0);
|
|
71
|
+
if (!firstParagraph || !isLikelyMetaReasoningParagraph(firstParagraph)) break;
|
|
72
|
+
paragraphs.shift();
|
|
73
|
+
}
|
|
71
74
|
return normalizeSpokenText(paragraphs.join(" "));
|
|
72
75
|
}
|
|
73
76
|
function extractSpokenTextFromPayloads(payloads) {
|
|
@@ -142,6 +145,7 @@ async function generateVoiceResponse(params) {
|
|
|
142
145
|
agentRuntime
|
|
143
146
|
});
|
|
144
147
|
let sessionEntry = existingSessionEntry;
|
|
148
|
+
if (sessionEntry?.modelSelectionLocked === true && voiceConfig.responseModel) throw new ModelSelectionLockedError();
|
|
145
149
|
if (!sessionEntry?.sessionId || voiceConfig.responseModel) sessionEntry = await agentRuntime.session.patchSessionEntry({
|
|
146
150
|
storePath,
|
|
147
151
|
sessionKey: resolvedSessionKey,
|
|
@@ -173,6 +177,8 @@ async function generateVoiceResponse(params) {
|
|
|
173
177
|
error: "Voice response session could not be initialized"
|
|
174
178
|
};
|
|
175
179
|
const sessionId = sessionEntry.sessionId;
|
|
180
|
+
const modelSelectionLocked = sessionEntry.modelSelectionLocked === true;
|
|
181
|
+
const persistedRuntimeId = resolvePersistedSessionRuntimeId(sessionEntry);
|
|
176
182
|
const thinkLevel = agentRuntime.resolveThinkingDefault({
|
|
177
183
|
cfg,
|
|
178
184
|
provider,
|
|
@@ -207,6 +213,11 @@ async function generateVoiceResponse(params) {
|
|
|
207
213
|
prompt: userMessage,
|
|
208
214
|
provider,
|
|
209
215
|
model,
|
|
216
|
+
modelSelectionLocked,
|
|
217
|
+
...persistedRuntimeId ? {
|
|
218
|
+
agentHarnessId: persistedRuntimeId,
|
|
219
|
+
agentHarnessRuntimeOverride: persistedRuntimeId
|
|
220
|
+
} : {},
|
|
210
221
|
thinkLevel,
|
|
211
222
|
verboseLevel: "off",
|
|
212
223
|
timeoutMs,
|
|
@@ -260,6 +271,11 @@ async function generateVoiceResponse(params) {
|
|
|
260
271
|
};
|
|
261
272
|
});
|
|
262
273
|
} catch (err) {
|
|
274
|
+
if (err instanceof ModelSelectionLockedError) return {
|
|
275
|
+
text: null,
|
|
276
|
+
deliveredEarly: false,
|
|
277
|
+
error: err.message
|
|
278
|
+
};
|
|
263
279
|
console.error(`[voice-call] Response generation failed:`, err);
|
|
264
280
|
return {
|
|
265
281
|
text: null,
|