@openclaw/voice-call 2026.7.1 → 2026.7.2-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -2
- package/dist/doctor-contract-api.js +34 -12
- package/dist/{guarded-json-api-CLVDU_h1.js → guarded-json-api-BKOgUU1q.js} +32 -41
- package/dist/index.js +12 -28
- package/dist/{plivo-D5wI4-pi.js → plivo-ByAIoPFp.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-Cdj8EuLg.js} +20 -4
- package/dist/{runtime-entry-DwMgbq2p.js → runtime-entry-DSgyqIwE.js} +820 -274
- package/dist/runtime-entry.js +1 -1
- package/dist/setup-api.js +86 -2
- package/dist/{store-DtewuuOb.js → store-path-nYL-yM0S.js} +12 -5
- package/dist/{telnyx-DksS6q0n.js → telnyx-DMbLYwj4.js} +1 -1
- package/dist/{twilio-BYcsdUkj.js → twilio-BTKKlkLT.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
|
|
1
|
+
import { a as CALL_RECORD_EVENT_META_MAX_ENTRIES, c as buildVoiceCallLegacyJsonlEventKey, f as parseVoiceCallRecordLine, h as resolveVoiceCallLegacyCallLogPath, i as CALL_RECORD_EVENT_CHUNKS_NAMESPACE, m as prepareVoiceCallRecordForStorage, n as CALL_RECORD_CHUNK_MAX_ENTRIES, o as MAX_CALL_RECORD_EVENTS, r as CALL_RECORD_EVENTS_NAMESPACE, s as RAW_CALL_RECORD_CHUNK_BYTES, t as resolveDefaultVoiceCallStoreDir } from "./store-path-nYL-yM0S.js";
|
|
2
2
|
import { normalizeAgentId } from "openclaw/plugin-sdk/routing";
|
|
3
|
-
import os from "node:os";
|
|
4
3
|
import path from "node:path";
|
|
4
|
+
import os from "node:os";
|
|
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) {
|
|
@@ -50,7 +50,22 @@ function resolveSessionStoreAgentIds(params) {
|
|
|
50
50
|
function resolveVoiceCallStorePath(params) {
|
|
51
51
|
const configuredStore = getVoiceCallConfigStore(params.config);
|
|
52
52
|
if (configuredStore) return resolveUserPath(configuredStore, params.env);
|
|
53
|
-
return
|
|
53
|
+
return resolveDefaultVoiceCallStoreDir(params.env);
|
|
54
|
+
}
|
|
55
|
+
function resolveVoiceCallStateDatabaseEnv(params) {
|
|
56
|
+
return {
|
|
57
|
+
...params.env,
|
|
58
|
+
OPENCLAW_STATE_DIR: resolveVoiceCallStorePath(params)
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function describeVoiceCallSchemaMigration(migration) {
|
|
62
|
+
switch (migration.kind) {
|
|
63
|
+
case "agent-databases-composite-primary-key": return "agent database registry primary key -> agent_id,path";
|
|
64
|
+
case "audit-events-v2": return "audit event ledger -> versioned message lifecycle schema";
|
|
65
|
+
case "operator-approvals-system-agent": return "operator approvals -> OpenClaw system changes";
|
|
66
|
+
case "strict-tables-v3": return "tables -> SQLite STRICT typing";
|
|
67
|
+
}
|
|
68
|
+
return migration.kind;
|
|
54
69
|
}
|
|
55
70
|
/** Return true when a path exists and is a file. */
|
|
56
71
|
/** Build the plugin state key for one migrated event chunk. */
|
|
@@ -170,24 +185,31 @@ const stateMigrations = [{
|
|
|
170
185
|
label: "Voice Call call log",
|
|
171
186
|
async detectLegacyState(params) {
|
|
172
187
|
const { entries } = await readLegacyCallRecords(resolveVoiceCallLegacyCallLogPath(resolveVoiceCallStorePath(params)));
|
|
173
|
-
|
|
174
|
-
|
|
188
|
+
const schemaMigrations = detectOpenClawStateDatabaseSchemaMigrations({ env: resolveVoiceCallStateDatabaseEnv(params) });
|
|
189
|
+
if (entries.length === 0 && schemaMigrations.length === 0) return null;
|
|
190
|
+
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
191
|
},
|
|
176
192
|
async migrateLegacyState(params) {
|
|
177
193
|
const changes = [];
|
|
178
194
|
const warnings = [];
|
|
179
|
-
const
|
|
180
|
-
const filePath = resolveVoiceCallLegacyCallLogPath(storePath);
|
|
195
|
+
const filePath = resolveVoiceCallLegacyCallLogPath(resolveVoiceCallStorePath(params));
|
|
181
196
|
const { entries, warnings: readWarnings } = await readLegacyCallRecords(filePath);
|
|
182
197
|
warnings.push(...readWarnings);
|
|
198
|
+
const stateDatabaseEnv = resolveVoiceCallStateDatabaseEnv(params);
|
|
199
|
+
if (detectOpenClawStateDatabaseSchemaMigrations({ env: stateDatabaseEnv }).length > 0) {
|
|
200
|
+
const repaired = repairOpenClawStateDatabaseSchema({ env: stateDatabaseEnv });
|
|
201
|
+
warnings.push(...repaired.warnings);
|
|
202
|
+
if (repaired.warnings.length > 0) return {
|
|
203
|
+
changes,
|
|
204
|
+
warnings
|
|
205
|
+
};
|
|
206
|
+
changes.push(...repaired.changes.map((change) => change.replace(/^Migrated shared state /, "Migrated Voice Call SQLite ").replaceAll("→", "->")));
|
|
207
|
+
}
|
|
183
208
|
if (entries.length === 0) return {
|
|
184
209
|
changes,
|
|
185
210
|
warnings
|
|
186
211
|
};
|
|
187
|
-
const env =
|
|
188
|
-
...params.env,
|
|
189
|
-
OPENCLAW_STATE_DIR: storePath
|
|
190
|
-
};
|
|
212
|
+
const env = stateDatabaseEnv;
|
|
191
213
|
const imported = await importLegacyCallRecords({
|
|
192
214
|
entries,
|
|
193
215
|
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-DSgyqIwE.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,27 +1,23 @@
|
|
|
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-DSgyqIwE.js";
|
|
4
|
+
import { g as setVoiceCallStateRuntime, t as resolveDefaultVoiceCallStoreDir, u as getCallHistoryFromStore } from "./store-path-nYL-yM0S.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";
|
|
15
|
-
import os from "node:os";
|
|
16
|
-
import path from "node:path";
|
|
17
13
|
import { randomUUID } from "node:crypto";
|
|
14
|
+
import path from "node:path";
|
|
18
15
|
import { format } from "node:util";
|
|
19
16
|
//#region extensions/voice-call/src/cli.ts
|
|
20
17
|
const VOICE_CALL_GATEWAY_DEFAULT_TIMEOUT_MS = 5e3;
|
|
21
18
|
const VOICE_CALL_GATEWAY_OPERATION_TIMEOUT_MS = 3e4;
|
|
22
19
|
const VOICE_CALL_GATEWAY_TRANSCRIPT_BUFFER_MS = 1e4;
|
|
23
20
|
const VOICE_CALL_GATEWAY_POLL_INTERVAL_MS = 1e3;
|
|
24
|
-
const voiceCallCliDeps = { callGatewayFromCli };
|
|
25
21
|
function writeStdoutLine(...values) {
|
|
26
22
|
process.stdout.write(`${format(...values)}\n`);
|
|
27
23
|
}
|
|
@@ -43,7 +39,7 @@ async function callVoiceCallGateway(method, params, opts) {
|
|
|
43
39
|
const timeoutMs = typeof opts?.timeoutMs === "number" && Number.isFinite(opts.timeoutMs) ? Math.max(1, Math.ceil(opts.timeoutMs)) : VOICE_CALL_GATEWAY_DEFAULT_TIMEOUT_MS;
|
|
44
40
|
return {
|
|
45
41
|
ok: true,
|
|
46
|
-
payload: await
|
|
42
|
+
payload: await callGatewayFromCli(method, {
|
|
47
43
|
json: true,
|
|
48
44
|
timeout: String(timeoutMs)
|
|
49
45
|
}, params, { progress: false })
|
|
@@ -107,15 +103,7 @@ function resolveMode(input) {
|
|
|
107
103
|
return "funnel";
|
|
108
104
|
}
|
|
109
105
|
function resolveDefaultStorePath(config) {
|
|
110
|
-
const
|
|
111
|
-
const existing = [resolvedPreferred].find((dir) => {
|
|
112
|
-
try {
|
|
113
|
-
return fs.existsSync(path.join(dir, "calls.jsonl")) || fs.existsSync(dir);
|
|
114
|
-
} catch {
|
|
115
|
-
return false;
|
|
116
|
-
}
|
|
117
|
-
}) ?? resolvedPreferred;
|
|
118
|
-
const base = config.store?.trim() ? resolveUserPath(config.store) : existing;
|
|
106
|
+
const base = config.store?.trim() ? resolveUserPath(config.store) : resolveDefaultVoiceCallStoreDir();
|
|
119
107
|
return path.join(base, "calls.jsonl");
|
|
120
108
|
}
|
|
121
109
|
function percentile(values, p) {
|
|
@@ -658,12 +646,12 @@ const VOICE_CALL_WRITE_METHOD_SCOPE = { scope: "operator.write" };
|
|
|
658
646
|
const VOICE_CALL_READ_METHOD_SCOPE = { scope: "operator.read" };
|
|
659
647
|
const voiceCallConfigSchema = {
|
|
660
648
|
parse(value) {
|
|
661
|
-
const
|
|
662
|
-
const enabled = typeof
|
|
663
|
-
return
|
|
664
|
-
...
|
|
649
|
+
const config = asOptionalRecord(value) ?? {};
|
|
650
|
+
const enabled = typeof config.enabled === "boolean" ? config.enabled : true;
|
|
651
|
+
return VoiceCallConfigSchema.parse({
|
|
652
|
+
...config,
|
|
665
653
|
enabled,
|
|
666
|
-
provider:
|
|
654
|
+
provider: config.provider ?? (enabled ? "mock" : void 0)
|
|
667
655
|
});
|
|
668
656
|
},
|
|
669
657
|
uiHints: {
|
|
@@ -743,6 +731,7 @@ const voiceCallConfigSchema = {
|
|
|
743
731
|
},
|
|
744
732
|
"streaming.enabled": {
|
|
745
733
|
label: "Enable Streaming",
|
|
734
|
+
help: "Classic streaming transcription currently requires the Twilio call provider.",
|
|
746
735
|
advanced: true
|
|
747
736
|
},
|
|
748
737
|
"streaming.provider": {
|
|
@@ -952,11 +941,6 @@ var voice_call_default = definePluginEntry({
|
|
|
952
941
|
register(api) {
|
|
953
942
|
const config = resolveVoiceCallConfig(voiceCallConfigSchema.parse(api.pluginConfig));
|
|
954
943
|
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
944
|
const runtimeState = getVoiceCallRuntimeGlobalState();
|
|
961
945
|
const continueOperationStore = createVoiceCallContinueOperationStore({
|
|
962
946
|
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-DSgyqIwE.js";
|
|
2
|
+
import { a as reconstructWebhookUrl, o as verifyPlivoWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-BKOgUU1q.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-DSgyqIwE.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,
|