@openclaw/voice-call 2026.8.1-beta.2 → 2026.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/dist/doctor-contract-api.js +10 -5
- package/dist/{guarded-json-api-DEDwgf4J.js → guarded-json-api-CQXCe4aR.js} +1 -1
- package/dist/index.js +390 -338
- package/dist/{mock-BCq0q36l.js → mock-Dv7KxYGt.js} +3 -1
- package/dist/{plivo-DbI17uAc.js → plivo-Ct5BT5LQ.js} +2 -2
- package/dist/{realtime-handler-uXR0QgVa.js → realtime-handler-CSK1A1Gg.js} +270 -144
- package/dist/{response-generator-D-hkzo5Y.js → response-generator-xbT37lYx.js} +59 -12
- package/dist/{runtime-entry-F13eJ4pp.js → runtime-entry-q02qkX8F.js} +435 -231
- package/dist/runtime-entry.js +1 -1
- package/dist/{store-path-51_oC_xn.js → store-path-Cf6k3F2U.js} +3 -10
- package/dist/{telnyx-sIS_WgnY.js → telnyx-C9Yizvox.js} +12 -8
- package/dist/{twilio-CbeVrVAl.js → twilio-BLjVv246.js} +40 -27
- package/node_modules/typebox/build/format/date.d.mts +1 -1
- package/node_modules/typebox/build/format/date.mjs +1 -1
- package/node_modules/typebox/build/format/date_time.d.mts +2 -2
- package/node_modules/typebox/build/format/date_time.mjs +3 -3
- package/node_modules/typebox/build/format/email.d.mts +1 -1
- package/node_modules/typebox/build/format/email.mjs +2 -2
- package/node_modules/typebox/build/format/hostname.mjs +2 -10
- package/node_modules/typebox/build/format/idn_email.d.mts +1 -1
- package/node_modules/typebox/build/format/idn_email.mjs +3 -3
- package/node_modules/typebox/build/format/idn_hostname.d.mts +1 -0
- package/node_modules/typebox/build/format/idn_hostname.mjs +3 -11
- package/node_modules/typebox/build/format/idna/format/bidi.d.mts +3 -0
- package/node_modules/typebox/build/format/idna/format/bidi.mjs +98 -0
- package/node_modules/typebox/build/format/idna/format/puny.d.mts +3 -0
- package/node_modules/typebox/build/format/idna/format/puny.mjs +144 -0
- package/node_modules/typebox/build/format/idna/hostname.d.mts +1 -0
- package/node_modules/typebox/build/format/idna/hostname.mjs +16 -0
- package/node_modules/typebox/build/format/idna/idn-hostname.d.mts +1 -0
- package/node_modules/typebox/build/format/idna/idn-hostname.mjs +23 -0
- package/node_modules/typebox/build/format/idna/index.d.mts +2 -0
- package/node_modules/typebox/build/format/idna/index.mjs +2 -0
- package/node_modules/typebox/build/format/idna/label/ascii.d.mts +1 -0
- package/node_modules/typebox/build/format/idna/label/ascii.mjs +9 -0
- package/node_modules/typebox/build/format/idna/label/puny.d.mts +1 -0
- package/node_modules/typebox/build/format/idna/label/puny.mjs +28 -0
- package/node_modules/typebox/build/format/idna/label/unicode.d.mts +1 -0
- package/node_modules/typebox/build/format/idna/label/unicode.mjs +106 -0
- package/node_modules/typebox/build/format/idna/pattern/pattern.d.mts +30 -0
- package/node_modules/typebox/build/format/idna/pattern/pattern.mjs +70 -0
- package/node_modules/typebox/build/format/ipv4.d.mts +0 -1
- package/node_modules/typebox/build/format/ipv4.mjs +2 -32
- package/node_modules/typebox/build/format/ipv6.mjs +2 -61
- package/node_modules/typebox/build/format/iri.d.mts +2 -2
- package/node_modules/typebox/build/format/iri.mjs +26 -8
- package/node_modules/typebox/build/format/iri_reference.d.mts +2 -2
- package/node_modules/typebox/build/format/iri_reference.mjs +9 -55
- package/node_modules/typebox/build/format/regex.d.mts +1 -2
- package/node_modules/typebox/build/format/regex.mjs +2 -6
- package/node_modules/typebox/build/format/time.d.mts +2 -2
- package/node_modules/typebox/build/format/time.mjs +19 -14
- package/node_modules/typebox/build/format/uri.d.mts +1 -1
- package/node_modules/typebox/build/format/uri.mjs +3 -130
- package/node_modules/typebox/build/format/uri_reference.d.mts +1 -1
- package/node_modules/typebox/build/format/uri_reference.mjs +2 -3
- package/node_modules/typebox/build/format/uri_template.d.mts +2 -3
- package/node_modules/typebox/build/format/uri_template.mjs +4 -5
- package/node_modules/typebox/build/format/url.d.mts +3 -3
- package/node_modules/typebox/build/format/url.mjs +4 -5
- package/node_modules/typebox/build/guard/emit.d.mts +3 -1
- package/node_modules/typebox/build/guard/emit.mjs +12 -5
- package/node_modules/typebox/build/guard/guard.d.mts +8 -2
- package/node_modules/typebox/build/guard/guard.mjs +25 -4
- package/node_modules/typebox/build/guard/string.d.mts +2 -2
- package/node_modules/typebox/build/guard/string.mjs +7 -7
- package/node_modules/typebox/build/schema/engine/_context.mjs +3 -3
- package/node_modules/typebox/build/schema/engine/_functions.mjs +1 -2
- package/node_modules/typebox/build/schema/engine/_regexp.d.mts +2 -0
- package/node_modules/typebox/build/schema/engine/_regexp.mjs +4 -0
- package/node_modules/typebox/build/schema/engine/_stack.mjs +12 -13
- package/node_modules/typebox/build/schema/engine/additionalItems.mjs +17 -7
- package/node_modules/typebox/build/schema/engine/additionalProperties.mjs +4 -3
- package/node_modules/typebox/build/schema/engine/contains.mjs +18 -6
- package/node_modules/typebox/build/schema/engine/enum.mjs +1 -1
- package/node_modules/typebox/build/schema/engine/items.mjs +26 -6
- package/node_modules/typebox/build/schema/engine/maxContains.mjs +3 -3
- package/node_modules/typebox/build/schema/engine/minContains.mjs +14 -4
- package/node_modules/typebox/build/schema/engine/not.mjs +4 -2
- package/node_modules/typebox/build/schema/engine/oneOf.mjs +7 -6
- package/node_modules/typebox/build/schema/engine/pattern.mjs +3 -2
- package/node_modules/typebox/build/schema/engine/patternProperties.mjs +4 -3
- package/node_modules/typebox/build/schema/engine/ref.mjs +1 -7
- package/node_modules/typebox/build/schema/engine/schema.mjs +13 -4
- package/node_modules/typebox/build/schema/engine/type.mjs +1 -1
- package/node_modules/typebox/build/schema/resolve/resolve.d.mts +4 -1
- package/node_modules/typebox/build/schema/resolve/resolve.mjs +202 -1
- package/node_modules/typebox/build/schema/static/propertyNames.d.mts +5 -0
- package/node_modules/typebox/build/schema/static/propertyNames.mjs +3 -0
- package/node_modules/typebox/build/schema/static/schema.d.mts +3 -0
- package/node_modules/typebox/build/schema/types/index.d.mts +0 -2
- package/node_modules/typebox/build/schema/types/index.mjs +0 -2
- package/node_modules/typebox/build/system/memory/clone.mjs +14 -11
- package/node_modules/typebox/build/system/memory/discard.mjs +6 -5
- package/node_modules/typebox/build/system/settings/settings.d.mts +9 -0
- package/node_modules/typebox/build/system/settings/settings.mjs +2 -0
- package/node_modules/typebox/build/type/engine/call/distribute_arguments.d.mts +6 -1
- package/node_modules/typebox/build/type/engine/call/distribute_arguments.mjs +16 -3
- package/node_modules/typebox/build/type/engine/call/instantiate.mjs +34 -4
- package/node_modules/typebox/build/type/engine/evaluate/broaden.d.mts +5 -5
- package/node_modules/typebox/build/type/engine/evaluate/broaden.mjs +20 -26
- package/node_modules/typebox/build/type/engine/evaluate/compare.d.mts +6 -10
- package/node_modules/typebox/build/type/engine/evaluate/compare.mjs +9 -13
- package/node_modules/typebox/build/type/engine/evaluate/composite.d.mts +7 -4
- package/node_modules/typebox/build/type/engine/evaluate/composite.mjs +12 -19
- package/node_modules/typebox/build/type/engine/evaluate/distribute.d.mts +4 -20
- package/node_modules/typebox/build/type/engine/evaluate/distribute.mjs +8 -20
- package/node_modules/typebox/build/type/engine/evaluate/evaluate.d.mts +4 -4
- package/node_modules/typebox/build/type/engine/evaluate/evaluate.mjs +11 -8
- package/node_modules/typebox/build/type/engine/evaluate/flatten.d.mts +1 -1
- package/node_modules/typebox/build/type/engine/evaluate/flatten.mjs +3 -4
- package/node_modules/typebox/build/type/engine/evaluate/narrow.d.mts +17 -2
- package/node_modules/typebox/build/type/engine/evaluate/narrow.mjs +26 -6
- package/node_modules/typebox/build/type/engine/exclude/operation.mjs +3 -4
- package/node_modules/typebox/build/type/engine/extract/operation.d.mts +1 -1
- package/node_modules/typebox/build/type/engine/extract/operation.mjs +3 -4
- package/node_modules/typebox/build/type/engine/priority/priority.d.mts +2 -2
- package/node_modules/typebox/build/type/engine/priority/priority.mjs +3 -4
- package/node_modules/typebox/build/type/script/mapping.d.mts +32 -29
- package/node_modules/typebox/build/type/script/mapping.mjs +18 -25
- package/node_modules/typebox/build/type/script/parser.d.mts +19 -25
- package/node_modules/typebox/build/type/script/parser.mjs +27 -23
- package/node_modules/typebox/build/value/errors/errors.d.mts +2 -2
- package/node_modules/typebox/build/value/errors/errors.mjs +1 -1
- package/node_modules/typebox/package.json +27 -27
- package/node_modules/typebox/readme.md +79 -82
- package/node_modules/ws/lib/permessage-deflate.js +3 -1
- package/node_modules/ws/package.json +1 -1
- package/openclaw.plugin.json +20 -2
- package/package.json +6 -6
- package/node_modules/typebox/build/format/_idna.d.mts +0 -2
- package/node_modules/typebox/build/format/_idna.mjs +0 -246
- package/node_modules/typebox/build/format/_puny.d.mts +0 -1
- package/node_modules/typebox/build/format/_puny.mjs +0 -77
- package/node_modules/typebox/build/schema/resolve/ref.d.mts +0 -3
- package/node_modules/typebox/build/schema/resolve/ref.mjs +0 -146
- package/node_modules/typebox/build/schema/types/contentEncoding.d.mts +0 -9
- package/node_modules/typebox/build/schema/types/contentEncoding.mjs +0 -13
- package/node_modules/typebox/build/schema/types/contentMediaType.d.mts +0 -9
- package/node_modules/typebox/build/schema/types/contentMediaType.mjs +0 -13
package/dist/index.js
CHANGED
|
@@ -1,41 +1,183 @@
|
|
|
1
1
|
import { definePluginEntry, sleep } from "./runtime-api.js";
|
|
2
2
|
import "./api.js";
|
|
3
3
|
import { VOICE_CALL_CLI_DESCRIPTOR } from "./cli-output-mode.js";
|
|
4
|
-
import {
|
|
5
|
-
import { b as setVoiceCallStateRuntime, t as resolveDefaultVoiceCallStoreDir, u as getCallHistoryFromStore } from "./store-path-
|
|
4
|
+
import { C as resolveVoiceCallStreamExposurePaths, b as resolveVoiceCallConfig, c as resolveWebhookExposureStatus, d as setupTailscaleExposureRoutes, g as resolveUserPath, l as cleanupTailscaleExposureRoute, t as createVoiceCallRuntime, u as getTailscaleSelfInfo, w as validateProviderConfig, y as VoiceCallConfigSchema } from "./runtime-entry-q02qkX8F.js";
|
|
5
|
+
import { b as setVoiceCallStateRuntime, d as loadActiveCallsFromStore, l as findCallMatchesInStore, t as resolveDefaultVoiceCallStoreDir, u as getCallHistoryFromStore } from "./store-path-Cf6k3F2U.js";
|
|
6
6
|
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
7
|
-
import { ErrorCodes, callGatewayFromCli, errorShape } from "openclaw/plugin-sdk/gateway-runtime";
|
|
7
|
+
import { ErrorCodes, callGatewayFromCli, errorShape, isGatewayClientRequestError, isGatewayTransportError, redactSensitiveUrlLikeString } from "openclaw/plugin-sdk/gateway-runtime";
|
|
8
8
|
import { resolveGlobalSingleton } from "openclaw/plugin-sdk/global-singleton";
|
|
9
9
|
import { normalizeAgentId, parseAgentSessionKey } from "openclaw/plugin-sdk/routing";
|
|
10
10
|
import { asNonArrayRecord, asOptionalRecord, isRecord, normalizeOptionalLowercaseString, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
11
11
|
import { jsonResult } from "openclaw/plugin-sdk/tool-results";
|
|
12
12
|
import { Type } from "typebox";
|
|
13
|
+
import { MAX_TCP_PORT, MAX_TIMER_TIMEOUT_MS, addTimerTimeoutGraceMs, clampTimerTimeoutMs, parseStrictNonNegativeInteger, resolveTimerTimeoutMs, timestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime";
|
|
13
14
|
import fs from "node:fs";
|
|
14
15
|
import { randomUUID } from "node:crypto";
|
|
15
16
|
import path from "node:path";
|
|
16
|
-
import { MAX_TCP_PORT, MAX_TIMER_TIMEOUT_MS, addTimerTimeoutGraceMs, clampTimerTimeoutMs, parseStrictNonNegativeInteger, resolveTimerTimeoutMs, timestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime";
|
|
17
17
|
import { StringDecoder } from "node:string_decoder";
|
|
18
18
|
import { format } from "node:util";
|
|
19
|
-
//#region extensions/voice-call/src/cli.ts
|
|
20
|
-
|
|
21
|
-
const VOICE_CALL_GATEWAY_OPERATION_TIMEOUT_MS = 3e4;
|
|
22
|
-
const VOICE_CALL_GATEWAY_TRANSCRIPT_BUFFER_MS = 1e4;
|
|
23
|
-
const VOICE_CALL_GATEWAY_POLL_INTERVAL_MS = 1e3;
|
|
24
|
-
function writeStdoutLine(...values) {
|
|
19
|
+
//#region extensions/voice-call/src/cli-command-io.ts
|
|
20
|
+
function writeCliLine(...values) {
|
|
25
21
|
process.stdout.write(`${format(...values)}\n`);
|
|
26
22
|
}
|
|
27
|
-
function
|
|
23
|
+
function writeCliJson(value) {
|
|
28
24
|
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
29
25
|
}
|
|
30
|
-
function
|
|
26
|
+
function parseCliInteger(raw, optionName, opts) {
|
|
31
27
|
const min = opts?.min ?? 0;
|
|
32
28
|
const parsed = parseStrictNonNegativeInteger(raw?.trim() ?? "");
|
|
33
29
|
if (parsed === void 0 || parsed < min || opts?.max !== void 0 && parsed > opts.max) throw new Error(`Invalid numeric value for ${optionName}: ${raw ?? ""}`);
|
|
34
30
|
return parsed;
|
|
35
31
|
}
|
|
32
|
+
//#endregion
|
|
33
|
+
//#region extensions/voice-call/src/cli-call-log.ts
|
|
34
|
+
function percentile(values, p) {
|
|
35
|
+
if (values.length === 0) return 0;
|
|
36
|
+
const sorted = [...values].toSorted((a, b) => a - b);
|
|
37
|
+
return sorted[Math.min(sorted.length - 1, Math.max(0, Math.ceil(p / 100 * sorted.length) - 1))] ?? 0;
|
|
38
|
+
}
|
|
39
|
+
function summarizeSeries(values) {
|
|
40
|
+
if (values.length === 0) return {
|
|
41
|
+
count: 0,
|
|
42
|
+
minMs: 0,
|
|
43
|
+
maxMs: 0,
|
|
44
|
+
avgMs: 0,
|
|
45
|
+
p50Ms: 0,
|
|
46
|
+
p95Ms: 0
|
|
47
|
+
};
|
|
48
|
+
const minMs = values.reduce((min, value) => value < min ? value : min);
|
|
49
|
+
const maxMs = values.reduce((max, value) => value > max ? value : max);
|
|
50
|
+
const avgMs = values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
51
|
+
return {
|
|
52
|
+
count: values.length,
|
|
53
|
+
minMs,
|
|
54
|
+
maxMs,
|
|
55
|
+
avgMs,
|
|
56
|
+
p50Ms: percentile(values, 50),
|
|
57
|
+
p95Ms: percentile(values, 95)
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function writeVoiceCallLatencySummary(calls) {
|
|
61
|
+
const turnLatencyMs = [];
|
|
62
|
+
const listenWaitMs = [];
|
|
63
|
+
for (const call of calls) {
|
|
64
|
+
const metadata = isRecord(call) && isRecord(call.metadata) ? call.metadata : void 0;
|
|
65
|
+
const latency = metadata?.lastTurnLatencyMs;
|
|
66
|
+
const listenWait = metadata?.lastTurnListenWaitMs;
|
|
67
|
+
if (typeof latency === "number" && Number.isFinite(latency)) turnLatencyMs.push(latency);
|
|
68
|
+
if (typeof listenWait === "number" && Number.isFinite(listenWait)) listenWaitMs.push(listenWait);
|
|
69
|
+
}
|
|
70
|
+
writeCliJson({
|
|
71
|
+
recordsScanned: calls.length,
|
|
72
|
+
turnLatency: summarizeSeries(turnLatencyMs),
|
|
73
|
+
listenWait: summarizeSeries(listenWaitMs)
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
function registerVoiceCallLogs(params) {
|
|
77
|
+
params.root.command("tail").description("Tail voice-call JSONL logs (prints new lines; useful during provider tests)").option("--file <path>", "Path to calls.jsonl", params.defaultFile).option("--since <n>", "Print last N lines first", "25").option("--poll <ms>", "Poll interval in ms", "250").action(async (options) => {
|
|
78
|
+
const file = options.file;
|
|
79
|
+
const since = parseCliInteger(options.since, "--since", { min: 0 });
|
|
80
|
+
const pollMs = parseCliInteger(options.poll, "--poll", { min: 50 });
|
|
81
|
+
const tailSqliteHistory = async (initialLimit) => {
|
|
82
|
+
params.ensureHistoryStateRuntime();
|
|
83
|
+
const seen = /* @__PURE__ */ new Set();
|
|
84
|
+
const printCall = (call) => {
|
|
85
|
+
const line = JSON.stringify(call);
|
|
86
|
+
if (!seen.has(line)) {
|
|
87
|
+
seen.add(line);
|
|
88
|
+
writeCliLine(line);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
if (initialLimit > 0) for (const call of await getCallHistoryFromStore(path.dirname(file), initialLimit)) printCall(call);
|
|
92
|
+
for (;;) {
|
|
93
|
+
try {
|
|
94
|
+
for (const call of await getCallHistoryFromStore(path.dirname(file), 1e3)) printCall(call);
|
|
95
|
+
} catch {}
|
|
96
|
+
await sleep(pollMs);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
if (fs.existsSync(file) && path.basename(file) !== "calls.jsonl") {
|
|
100
|
+
const initial = fs.readFileSync(file);
|
|
101
|
+
let decoder = new StringDecoder("utf8");
|
|
102
|
+
const initialLines = decoder.write(initial).split("\n");
|
|
103
|
+
let pendingLine = initialLines.pop() ?? "";
|
|
104
|
+
const lines = initialLines.filter(Boolean);
|
|
105
|
+
for (const line of lines.slice(Math.max(0, lines.length - since))) writeCliLine(line);
|
|
106
|
+
let offset = initial.length;
|
|
107
|
+
let lastObservedSize = initial.length;
|
|
108
|
+
for (;;) {
|
|
109
|
+
try {
|
|
110
|
+
const stat = fs.statSync(file);
|
|
111
|
+
if (stat.size < lastObservedSize) {
|
|
112
|
+
offset = 0;
|
|
113
|
+
decoder = new StringDecoder("utf8");
|
|
114
|
+
pendingLine = "";
|
|
115
|
+
}
|
|
116
|
+
lastObservedSize = stat.size;
|
|
117
|
+
if (stat.size > offset) {
|
|
118
|
+
const fd = fs.openSync(file, "r");
|
|
119
|
+
try {
|
|
120
|
+
const buf = Buffer.alloc(stat.size - offset);
|
|
121
|
+
const bytesRead = fs.readSync(fd, buf, 0, buf.length, offset);
|
|
122
|
+
offset += bytesRead;
|
|
123
|
+
const text = decoder.write(buf.subarray(0, bytesRead));
|
|
124
|
+
const completeLines = `${pendingLine}${text}`.split("\n");
|
|
125
|
+
pendingLine = completeLines.pop() ?? "";
|
|
126
|
+
for (const line of completeLines.filter(Boolean)) writeCliLine(line);
|
|
127
|
+
} finally {
|
|
128
|
+
fs.closeSync(fd);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
} catch {}
|
|
132
|
+
await sleep(pollMs);
|
|
133
|
+
}
|
|
134
|
+
} else await tailSqliteHistory(since);
|
|
135
|
+
});
|
|
136
|
+
params.root.command("latency").description("Summarize turn latency metrics from voice-call JSONL logs").option("--file <path>", "Path to calls.jsonl", params.defaultFile).option("--last <n>", "Analyze last N records", "200").action(async (options) => {
|
|
137
|
+
const file = options.file;
|
|
138
|
+
const last = parseCliInteger(options.last, "--last", { min: 1 });
|
|
139
|
+
if (fs.existsSync(file) && path.basename(file) !== "calls.jsonl") writeVoiceCallLatencySummary(fs.readFileSync(file, "utf8").split("\n").filter(Boolean).slice(-last).map((line) => {
|
|
140
|
+
try {
|
|
141
|
+
const parsed = JSON.parse(line);
|
|
142
|
+
return (isRecord(parsed) ? parsed.call : void 0) ?? parsed;
|
|
143
|
+
} catch {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
}).filter((call) => call !== null));
|
|
147
|
+
else {
|
|
148
|
+
params.ensureHistoryStateRuntime();
|
|
149
|
+
writeVoiceCallLatencySummary(await getCallHistoryFromStore(path.dirname(file), last));
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
//#endregion
|
|
154
|
+
//#region extensions/voice-call/src/cli-gateway-call.ts
|
|
155
|
+
const VOICE_CALL_GATEWAY_DEFAULT_TIMEOUT_MS = 5e3;
|
|
156
|
+
const VOICE_CALL_GATEWAY_OPERATION_TIMEOUT_MS = 3e4;
|
|
157
|
+
const VOICE_CALL_GATEWAY_TRANSCRIPT_BUFFER_MS = 1e4;
|
|
158
|
+
const VOICE_CALL_GATEWAY_POLL_INTERVAL_MS = 1e3;
|
|
36
159
|
function isGatewayUnavailableForLocalFallback(err) {
|
|
160
|
+
return isGatewayTransportError(err) && err.kind === "closed" && (err.code === void 0 || err.code === 1006);
|
|
161
|
+
}
|
|
162
|
+
function isGatewayCredentialFailure(err) {
|
|
163
|
+
return err instanceof Error && (err.name === "GatewayCredentialsRequiredError" || err.name === "GatewayExplicitAuthRequiredError" || err.name === "GatewaySecretRefUnavailableError");
|
|
164
|
+
}
|
|
165
|
+
function gatewayOperationalError(err) {
|
|
37
166
|
const message = formatErrorMessage(err);
|
|
38
|
-
|
|
167
|
+
const detail = (() => {
|
|
168
|
+
if (isGatewayClientRequestError(err)) return `Gateway responded but voicecall failed: ${message}\nThe running Gateway owns the voice-call runtime; check \`openclaw gateway status\` or restart it.`;
|
|
169
|
+
if (isGatewayCredentialFailure(err)) return `Gateway requires credentials: ${message}\nConfigure gateway.auth or pair this device with \`openclaw devices approve --latest\`.`;
|
|
170
|
+
if (isGatewayTransportError(err)) {
|
|
171
|
+
const url = err.connectionDetails.url;
|
|
172
|
+
if (err.kind === "timeout") return `Gateway at ${url} did not answer within ${err.timeoutMs === void 0 ? "the configured timeout" : `${err.timeoutMs}ms`}: ${message}\nIt may be starting or wedged; check \`openclaw gateway status\`.`;
|
|
173
|
+
return `Gateway connection at ${url} failed: ${message}\nCheck gateway.auth and \`openclaw gateway status\`, then retry.`;
|
|
174
|
+
}
|
|
175
|
+
return `Gateway voicecall request failed: ${message}\nCheck \`openclaw gateway status\`, then retry.`;
|
|
176
|
+
})();
|
|
177
|
+
return new Error(redactSensitiveUrlLikeString(detail));
|
|
178
|
+
}
|
|
179
|
+
function isUnknownMethod(err, method) {
|
|
180
|
+
return formatErrorMessage(err).includes(`unknown method: ${method}`);
|
|
39
181
|
}
|
|
40
182
|
async function callVoiceCallGateway(method, params, opts) {
|
|
41
183
|
try {
|
|
@@ -52,21 +194,18 @@ async function callVoiceCallGateway(method, params, opts) {
|
|
|
52
194
|
ok: false,
|
|
53
195
|
error: err
|
|
54
196
|
};
|
|
55
|
-
throw err;
|
|
197
|
+
throw gatewayOperationalError(err);
|
|
56
198
|
}
|
|
57
199
|
}
|
|
58
|
-
function
|
|
200
|
+
function resolveOperationTimeout(config) {
|
|
59
201
|
return Math.max(VOICE_CALL_GATEWAY_OPERATION_TIMEOUT_MS, addTimerTimeoutGraceMs(config.ringTimeoutMs) ?? 1);
|
|
60
202
|
}
|
|
61
|
-
function
|
|
203
|
+
function resolveContinueTimeout(config) {
|
|
62
204
|
return clampTimerTimeoutMs(config.transcriptTimeoutMs + VOICE_CALL_GATEWAY_OPERATION_TIMEOUT_MS + VOICE_CALL_GATEWAY_TRANSCRIPT_BUFFER_MS) ?? 1;
|
|
63
205
|
}
|
|
64
206
|
function resolveVoiceCallDeadlineMs(timeoutMs, nowMs = Date.now()) {
|
|
65
207
|
return nowMs + (clampTimerTimeoutMs(timeoutMs) ?? MAX_TIMER_TIMEOUT_MS);
|
|
66
208
|
}
|
|
67
|
-
function isUnknownGatewayMethod(err, method) {
|
|
68
|
-
return formatErrorMessage(err).includes(`unknown method: ${method}`);
|
|
69
|
-
}
|
|
70
209
|
function readGatewayOperationId(payload) {
|
|
71
210
|
if (isRecord(payload) && typeof payload.operationId === "string" && payload.operationId) return payload.operationId;
|
|
72
211
|
throw new Error("voicecall gateway response missing operationId");
|
|
@@ -88,7 +227,12 @@ function readCompletedContinueResult(payload) {
|
|
|
88
227
|
};
|
|
89
228
|
throw new Error("voicecall gateway response has unknown operation status");
|
|
90
229
|
}
|
|
91
|
-
async function
|
|
230
|
+
async function pollContinueGateway(payload, fallbackTimeoutMs) {
|
|
231
|
+
if (!isRecord(payload) || typeof payload.operationId !== "string") return payload;
|
|
232
|
+
const params = {
|
|
233
|
+
operationId: readGatewayOperationId(payload),
|
|
234
|
+
timeoutMs: readGatewayPollTimeoutMs(payload, fallbackTimeoutMs)
|
|
235
|
+
};
|
|
92
236
|
const deadlineMs = resolveVoiceCallDeadlineMs(params.timeoutMs);
|
|
93
237
|
for (;;) {
|
|
94
238
|
const remainingMs = deadlineMs - Date.now();
|
|
@@ -104,6 +248,54 @@ async function pollVoiceCallContinueGateway(params) {
|
|
|
104
248
|
}
|
|
105
249
|
throw new Error("voicecall continue timed out waiting for gateway operation");
|
|
106
250
|
}
|
|
251
|
+
async function ensureStandaloneRuntime(params) {
|
|
252
|
+
try {
|
|
253
|
+
return await params.ensureRuntime();
|
|
254
|
+
} catch (err) {
|
|
255
|
+
if (err instanceof Error && "code" in err && err.code === "EADDRINUSE") throw new Error(`Voice-call webhook port ${params.config.serve.port} is already in use. A running Gateway probably already serves it; operational commands route through that Gateway. Check \`openclaw gateway status\` and retry.`, { cause: err });
|
|
256
|
+
throw err;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
async function runGatewayManagerCommand(params) {
|
|
260
|
+
const gateway = await params.gatewayCall();
|
|
261
|
+
if (gateway.ok) {
|
|
262
|
+
writeCliJson(params.resolveGatewayPayload ? await params.resolveGatewayPayload(gateway.payload) : gateway.payload);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
const runtime = await ensureStandaloneRuntime(params);
|
|
266
|
+
const result = await params.managerFallback(runtime.manager);
|
|
267
|
+
if (!result.success) throw new Error(result.error || `${params.failureLabel} failed`);
|
|
268
|
+
writeCliJson(result);
|
|
269
|
+
}
|
|
270
|
+
function readGatewayCallId(payload, invalidCallIdMessage) {
|
|
271
|
+
if (isRecord(payload) && typeof payload.callId === "string") {
|
|
272
|
+
if (!invalidCallIdMessage || payload.callId) return payload.callId;
|
|
273
|
+
}
|
|
274
|
+
if (invalidCallIdMessage) throw new Error(invalidCallIdMessage);
|
|
275
|
+
if (isRecord(payload) && typeof payload.error === "string") throw new Error(payload.error);
|
|
276
|
+
throw new Error("voicecall gateway response missing callId");
|
|
277
|
+
}
|
|
278
|
+
async function initiateVoiceCall(params) {
|
|
279
|
+
const mode = params.mode === "notify" || params.mode === "conversation" ? params.mode : params.defaultMode;
|
|
280
|
+
const gateway = await callVoiceCallGateway(params.method, {
|
|
281
|
+
...params.to ? { to: params.to } : {},
|
|
282
|
+
...params.message ? { message: params.message } : {},
|
|
283
|
+
...mode ? { mode } : {}
|
|
284
|
+
}, { timeoutMs: resolveOperationTimeout(params.config) });
|
|
285
|
+
if (gateway.ok) return readGatewayCallId(gateway.payload, params.failureMessage);
|
|
286
|
+
const runtime = await ensureStandaloneRuntime(params);
|
|
287
|
+
const to = params.to ?? runtime.config.toNumber;
|
|
288
|
+
if (!to) throw new Error("Missing --to and no toNumber configured");
|
|
289
|
+
const result = await runtime.manager.initiateCall(to, void 0, {
|
|
290
|
+
message: params.message,
|
|
291
|
+
mode
|
|
292
|
+
});
|
|
293
|
+
if (!result.success) throw new Error(result.error || params.failureMessage || "initiate failed");
|
|
294
|
+
if (params.failureMessage && !result.callId) throw new Error(params.failureMessage);
|
|
295
|
+
return result.callId;
|
|
296
|
+
}
|
|
297
|
+
//#endregion
|
|
298
|
+
//#region extensions/voice-call/src/cli.ts
|
|
107
299
|
function resolveMode(input) {
|
|
108
300
|
const raw = normalizeOptionalLowercaseString(input) ?? "";
|
|
109
301
|
if (raw === "serve" || raw === "off") return raw;
|
|
@@ -113,35 +305,6 @@ function resolveDefaultStorePath(config) {
|
|
|
113
305
|
const base = config.store?.trim() ? resolveUserPath(config.store) : resolveDefaultVoiceCallStoreDir();
|
|
114
306
|
return path.join(base, "calls.jsonl");
|
|
115
307
|
}
|
|
116
|
-
function percentile(values, p) {
|
|
117
|
-
if (values.length === 0) return 0;
|
|
118
|
-
const sorted = [...values].toSorted((a, b) => a - b);
|
|
119
|
-
return sorted[Math.min(sorted.length - 1, Math.max(0, Math.ceil(p / 100 * sorted.length) - 1))] ?? 0;
|
|
120
|
-
}
|
|
121
|
-
function summarizeSeries(values) {
|
|
122
|
-
if (values.length === 0) return {
|
|
123
|
-
count: 0,
|
|
124
|
-
minMs: 0,
|
|
125
|
-
maxMs: 0,
|
|
126
|
-
avgMs: 0,
|
|
127
|
-
p50Ms: 0,
|
|
128
|
-
p95Ms: 0
|
|
129
|
-
};
|
|
130
|
-
const minMs = values.reduce((min, value) => value < min ? value : min, Number.POSITIVE_INFINITY);
|
|
131
|
-
const maxMs = values.reduce((max, value) => value > max ? value : max, Number.NEGATIVE_INFINITY);
|
|
132
|
-
const avgMs = values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
133
|
-
return {
|
|
134
|
-
count: values.length,
|
|
135
|
-
minMs,
|
|
136
|
-
maxMs,
|
|
137
|
-
avgMs,
|
|
138
|
-
p50Ms: percentile(values, 50),
|
|
139
|
-
p95Ms: percentile(values, 95)
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
function resolveCallMode(mode) {
|
|
143
|
-
return mode === "notify" || mode === "conversation" ? mode : void 0;
|
|
144
|
-
}
|
|
145
308
|
function buildSetupStatus(config) {
|
|
146
309
|
const validation = validateProviderConfig(config);
|
|
147
310
|
const webhookExposure = resolveWebhookExposureStatus(config);
|
|
@@ -178,45 +341,8 @@ function buildSetupStatus(config) {
|
|
|
178
341
|
};
|
|
179
342
|
}
|
|
180
343
|
function writeSetupStatus(status) {
|
|
181
|
-
|
|
182
|
-
for (const check of status.checks)
|
|
183
|
-
}
|
|
184
|
-
async function initiateCallAndPrintId(params) {
|
|
185
|
-
const result = await params.runtime.manager.initiateCall(params.to, void 0, {
|
|
186
|
-
message: params.message,
|
|
187
|
-
mode: resolveCallMode(params.mode)
|
|
188
|
-
});
|
|
189
|
-
if (!result.success) throw new Error(result.error || "initiate failed");
|
|
190
|
-
writeStdoutJson({ callId: result.callId });
|
|
191
|
-
}
|
|
192
|
-
function writeGatewayCallId(payload) {
|
|
193
|
-
if (isRecord(payload) && typeof payload.callId === "string") {
|
|
194
|
-
writeStdoutJson({ callId: payload.callId });
|
|
195
|
-
return;
|
|
196
|
-
}
|
|
197
|
-
if (isRecord(payload) && typeof payload.error === "string") throw new Error(payload.error);
|
|
198
|
-
throw new Error("voicecall gateway response missing callId");
|
|
199
|
-
}
|
|
200
|
-
async function initiateCallViaGatewayOrRuntime(params) {
|
|
201
|
-
const mode = resolveCallMode(params.mode);
|
|
202
|
-
const gateway = await callVoiceCallGateway(params.method, {
|
|
203
|
-
...params.to ? { to: params.to } : {},
|
|
204
|
-
...params.message ? { message: params.message } : {},
|
|
205
|
-
...mode ? { mode } : {}
|
|
206
|
-
}, { timeoutMs: resolveGatewayOperationTimeoutMs(params.config) });
|
|
207
|
-
if (gateway.ok) {
|
|
208
|
-
writeGatewayCallId(gateway.payload);
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
211
|
-
const rt = await params.ensureRuntime();
|
|
212
|
-
const to = params.to ?? rt.config.toNumber;
|
|
213
|
-
if (!to) throw new Error("Missing --to and no toNumber configured");
|
|
214
|
-
await initiateCallAndPrintId({
|
|
215
|
-
runtime: rt,
|
|
216
|
-
to,
|
|
217
|
-
message: params.message,
|
|
218
|
-
mode: params.mode
|
|
219
|
-
});
|
|
344
|
+
writeCliLine("Voice Call setup: %s", status.ok ? "OK" : "needs attention");
|
|
345
|
+
for (const check of status.checks) writeCliLine("%s %s: %s", check.ok ? "OK" : "FAIL", check.id, check.message);
|
|
220
346
|
}
|
|
221
347
|
function registerVoiceCallCli(params) {
|
|
222
348
|
const { program, config, ensureRuntime, stateRuntime } = params;
|
|
@@ -227,7 +353,7 @@ function registerVoiceCallCli(params) {
|
|
|
227
353
|
root.command("setup").description("Show Voice Call provider and webhook setup status").option("--json", "Print machine-readable JSON").action((options) => {
|
|
228
354
|
const status = buildSetupStatus(config);
|
|
229
355
|
if (options.json) {
|
|
230
|
-
|
|
356
|
+
writeCliJson(status);
|
|
231
357
|
return;
|
|
232
358
|
}
|
|
233
359
|
writeSetupStatus(status);
|
|
@@ -235,7 +361,7 @@ function registerVoiceCallCli(params) {
|
|
|
235
361
|
root.command("smoke").description("Check Voice Call readiness and optionally place a short outbound test call").option("-t, --to <phone>", "Phone number to call for a live smoke").option("--message <text>", "Message to speak during the smoke call", "OpenClaw voice call smoke test.").option("--mode <mode>", "Call mode: notify or conversation", "notify").option("--yes", "Actually place the live outbound call").option("--json", "Print machine-readable JSON").action(async (options) => {
|
|
236
362
|
const setup = buildSetupStatus(config);
|
|
237
363
|
if (!setup.ok) {
|
|
238
|
-
if (options.json)
|
|
364
|
+
if (options.json) writeCliJson({
|
|
239
365
|
ok: false,
|
|
240
366
|
setup
|
|
241
367
|
});
|
|
@@ -244,19 +370,19 @@ function registerVoiceCallCli(params) {
|
|
|
244
370
|
return;
|
|
245
371
|
}
|
|
246
372
|
if (!options.to) {
|
|
247
|
-
if (options.json)
|
|
373
|
+
if (options.json) writeCliJson({
|
|
248
374
|
ok: true,
|
|
249
375
|
setup,
|
|
250
376
|
liveCall: false
|
|
251
377
|
});
|
|
252
378
|
else {
|
|
253
379
|
writeSetupStatus(setup);
|
|
254
|
-
|
|
380
|
+
writeCliLine("live-call: skipped (pass --to and --yes to place one)");
|
|
255
381
|
}
|
|
256
382
|
return;
|
|
257
383
|
}
|
|
258
384
|
if (!options.yes) {
|
|
259
|
-
if (options.json)
|
|
385
|
+
if (options.json) writeCliJson({
|
|
260
386
|
ok: true,
|
|
261
387
|
setup,
|
|
262
388
|
liveCall: false,
|
|
@@ -264,29 +390,22 @@ function registerVoiceCallCli(params) {
|
|
|
264
390
|
});
|
|
265
391
|
else {
|
|
266
392
|
writeSetupStatus(setup);
|
|
267
|
-
|
|
393
|
+
writeCliLine("live-call: dry run for %s (add --yes to place it)", options.to);
|
|
268
394
|
}
|
|
269
395
|
return;
|
|
270
396
|
}
|
|
271
|
-
const
|
|
272
|
-
|
|
397
|
+
const callId = await initiateVoiceCall({
|
|
398
|
+
ensureRuntime,
|
|
399
|
+
config,
|
|
400
|
+
method: "voicecall.start",
|
|
273
401
|
to: options.to,
|
|
274
|
-
|
|
275
|
-
mode
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
else {
|
|
280
|
-
const result = await (await ensureRuntime()).manager.initiateCall(options.to, void 0, {
|
|
281
|
-
message: options.message,
|
|
282
|
-
mode
|
|
283
|
-
});
|
|
284
|
-
if (!result.success) throw new Error(result.error || "smoke call failed");
|
|
285
|
-
callId = result.callId;
|
|
286
|
-
}
|
|
287
|
-
if (typeof callId !== "string" || !callId) throw new Error("smoke call failed");
|
|
402
|
+
message: options.message,
|
|
403
|
+
mode: options.mode,
|
|
404
|
+
defaultMode: "notify",
|
|
405
|
+
failureMessage: "smoke call failed"
|
|
406
|
+
});
|
|
288
407
|
if (options.json) {
|
|
289
|
-
|
|
408
|
+
writeCliJson({
|
|
290
409
|
ok: true,
|
|
291
410
|
setup,
|
|
292
411
|
liveCall: true,
|
|
@@ -295,245 +414,162 @@ function registerVoiceCallCli(params) {
|
|
|
295
414
|
return;
|
|
296
415
|
}
|
|
297
416
|
writeSetupStatus(setup);
|
|
298
|
-
|
|
417
|
+
writeCliLine("live-call: started %s", callId);
|
|
299
418
|
});
|
|
300
419
|
root.command("call").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)").option("--mode <mode>", "Call mode: notify (hangup after message) or conversation (stay open)", "conversation").action(async (options) => {
|
|
301
|
-
await
|
|
420
|
+
writeCliJson({ callId: await initiateVoiceCall({
|
|
302
421
|
ensureRuntime,
|
|
303
422
|
config,
|
|
304
423
|
method: "voicecall.initiate",
|
|
305
424
|
to: options.to,
|
|
306
425
|
message: options.message,
|
|
307
426
|
mode: options.mode
|
|
308
|
-
});
|
|
427
|
+
}) });
|
|
309
428
|
});
|
|
310
429
|
root.command("start").description("Alias for voicecall call").requiredOption("--to <phone>", "Phone number to call").option("--message <text>", "Message to speak when call connects").option("--mode <mode>", "Call mode: notify (hangup after message) or conversation (stay open)", "conversation").action(async (options) => {
|
|
311
|
-
await
|
|
430
|
+
writeCliJson({ callId: await initiateVoiceCall({
|
|
312
431
|
ensureRuntime,
|
|
313
432
|
config,
|
|
314
433
|
method: "voicecall.start",
|
|
315
434
|
to: options.to,
|
|
316
435
|
message: options.message,
|
|
317
436
|
mode: options.mode
|
|
318
|
-
});
|
|
437
|
+
}) });
|
|
319
438
|
});
|
|
320
439
|
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(async (options) => {
|
|
321
|
-
|
|
322
|
-
try {
|
|
323
|
-
gateway = await callVoiceCallGateway("voicecall.continue.start", {
|
|
324
|
-
callId: options.callId,
|
|
325
|
-
message: options.message
|
|
326
|
-
}, { timeoutMs: resolveGatewayOperationTimeoutMs(config) });
|
|
327
|
-
} catch (err) {
|
|
328
|
-
if (!isUnknownGatewayMethod(err, "voicecall.continue.start")) throw err;
|
|
329
|
-
gateway = await callVoiceCallGateway("voicecall.continue", {
|
|
330
|
-
callId: options.callId,
|
|
331
|
-
message: options.message
|
|
332
|
-
}, { timeoutMs: resolveGatewayContinueTimeoutMs(config) });
|
|
333
|
-
}
|
|
334
|
-
if (gateway.ok) {
|
|
335
|
-
if (isRecord(gateway.payload) && typeof gateway.payload.operationId === "string") {
|
|
336
|
-
writeStdoutJson(await pollVoiceCallContinueGateway({
|
|
337
|
-
operationId: readGatewayOperationId(gateway.payload),
|
|
338
|
-
timeoutMs: readGatewayPollTimeoutMs(gateway.payload, resolveGatewayContinueTimeoutMs(config))
|
|
339
|
-
}));
|
|
340
|
-
return;
|
|
341
|
-
}
|
|
342
|
-
writeStdoutJson(gateway.payload);
|
|
343
|
-
return;
|
|
344
|
-
}
|
|
345
|
-
const result = await (await ensureRuntime()).manager.continueCall(options.callId, options.message);
|
|
346
|
-
if (!result.success) throw new Error(result.error || "continue failed");
|
|
347
|
-
writeStdoutJson(result);
|
|
348
|
-
});
|
|
349
|
-
root.command("speak").description("Speak a message without waiting for response").requiredOption("--call-id <id>", "Call ID").requiredOption("--message <text>", "Message to speak").action(async (options) => {
|
|
350
|
-
const gateway = await callVoiceCallGateway("voicecall.speak", {
|
|
440
|
+
const gatewayParams = {
|
|
351
441
|
callId: options.callId,
|
|
352
442
|
message: options.message
|
|
443
|
+
};
|
|
444
|
+
const continueTimeoutMs = resolveContinueTimeout(config);
|
|
445
|
+
await runGatewayManagerCommand({
|
|
446
|
+
config,
|
|
447
|
+
ensureRuntime,
|
|
448
|
+
gatewayCall: async () => {
|
|
449
|
+
try {
|
|
450
|
+
return await callVoiceCallGateway("voicecall.continue.start", gatewayParams, { timeoutMs: resolveOperationTimeout(config) });
|
|
451
|
+
} catch (err) {
|
|
452
|
+
if (!isUnknownMethod(err, "voicecall.continue.start")) throw err;
|
|
453
|
+
return callVoiceCallGateway("voicecall.continue", gatewayParams, { timeoutMs: continueTimeoutMs });
|
|
454
|
+
}
|
|
455
|
+
},
|
|
456
|
+
resolveGatewayPayload: (payload) => pollContinueGateway(payload, continueTimeoutMs),
|
|
457
|
+
managerFallback: (manager) => manager.continueCall(options.callId, options.message),
|
|
458
|
+
failureLabel: "continue"
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
root.command("speak").description("Speak a message without waiting for response").requiredOption("--call-id <id>", "Call ID").requiredOption("--message <text>", "Message to speak").action(async (options) => {
|
|
462
|
+
await runGatewayManagerCommand({
|
|
463
|
+
config,
|
|
464
|
+
ensureRuntime,
|
|
465
|
+
gatewayCall: () => callVoiceCallGateway("voicecall.speak", {
|
|
466
|
+
callId: options.callId,
|
|
467
|
+
message: options.message
|
|
468
|
+
}),
|
|
469
|
+
managerFallback: (manager) => manager.speak(options.callId, options.message),
|
|
470
|
+
failureLabel: "speak"
|
|
353
471
|
});
|
|
354
|
-
if (gateway.ok) {
|
|
355
|
-
writeStdoutJson(gateway.payload);
|
|
356
|
-
return;
|
|
357
|
-
}
|
|
358
|
-
const result = await (await ensureRuntime()).manager.speak(options.callId, options.message);
|
|
359
|
-
if (!result.success) throw new Error(result.error || "speak failed");
|
|
360
|
-
writeStdoutJson(result);
|
|
361
472
|
});
|
|
362
473
|
root.command("dtmf").description("Send DTMF digits to an active call").requiredOption("--call-id <id>", "Call ID").requiredOption("--digits <digits>", "DTMF digits").action(async (options) => {
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
474
|
+
await runGatewayManagerCommand({
|
|
475
|
+
config,
|
|
476
|
+
ensureRuntime,
|
|
477
|
+
gatewayCall: () => callVoiceCallGateway("voicecall.dtmf", {
|
|
478
|
+
callId: options.callId,
|
|
479
|
+
digits: options.digits
|
|
480
|
+
}),
|
|
481
|
+
managerFallback: (manager) => manager.sendDtmf(options.callId, options.digits),
|
|
482
|
+
failureLabel: "dtmf"
|
|
366
483
|
});
|
|
367
|
-
if (gateway.ok) {
|
|
368
|
-
writeStdoutJson(gateway.payload);
|
|
369
|
-
return;
|
|
370
|
-
}
|
|
371
|
-
const result = await (await ensureRuntime()).manager.sendDtmf(options.callId, options.digits);
|
|
372
|
-
if (!result.success) throw new Error(result.error || "dtmf failed");
|
|
373
|
-
writeStdoutJson(result);
|
|
374
484
|
});
|
|
375
485
|
root.command("end").description("Hang up an active call").requiredOption("--call-id <id>", "Call ID").action(async (options) => {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
writeStdoutJson(result);
|
|
486
|
+
await runGatewayManagerCommand({
|
|
487
|
+
config,
|
|
488
|
+
ensureRuntime,
|
|
489
|
+
gatewayCall: () => callVoiceCallGateway("voicecall.end", { callId: options.callId }),
|
|
490
|
+
managerFallback: (manager) => manager.endCall(options.callId),
|
|
491
|
+
failureLabel: "end"
|
|
492
|
+
});
|
|
384
493
|
});
|
|
385
494
|
root.command("status").description("Show call status").option("--call-id <id>", "Call ID").option("--json", "Print machine-readable JSON").action(async (options) => {
|
|
386
495
|
const gateway = await callVoiceCallGateway("voicecall.status", options.callId ? { callId: options.callId } : void 0);
|
|
387
496
|
if (gateway.ok) {
|
|
388
497
|
if (options.callId && isRecord(gateway.payload)) {
|
|
389
498
|
if (gateway.payload.found === true && "call" in gateway.payload) {
|
|
390
|
-
|
|
499
|
+
writeCliJson(gateway.payload.call);
|
|
391
500
|
return;
|
|
392
501
|
}
|
|
393
502
|
if (gateway.payload.found === false) {
|
|
394
|
-
|
|
503
|
+
writeCliJson({ found: false });
|
|
395
504
|
return;
|
|
396
505
|
}
|
|
397
506
|
}
|
|
398
|
-
|
|
507
|
+
writeCliJson(gateway.payload);
|
|
399
508
|
return;
|
|
400
509
|
}
|
|
401
|
-
|
|
510
|
+
ensureHistoryStateRuntime();
|
|
511
|
+
const storePath = path.dirname(resolveDefaultStorePath(config));
|
|
402
512
|
if (options.callId) {
|
|
403
|
-
|
|
513
|
+
const persisted = await findCallMatchesInStore(storePath, options.callId);
|
|
514
|
+
writeCliJson(persisted.byCallId ?? persisted.byProviderCallId ?? { found: false });
|
|
404
515
|
return;
|
|
405
516
|
}
|
|
406
|
-
|
|
517
|
+
writeCliJson({
|
|
407
518
|
found: true,
|
|
408
|
-
calls:
|
|
519
|
+
calls: Array.from(loadActiveCallsFromStore(storePath).activeCalls.values())
|
|
409
520
|
});
|
|
410
521
|
});
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
const tailSqliteHistory = async (initialLimit) => {
|
|
416
|
-
ensureHistoryStateRuntime();
|
|
417
|
-
const seen = /* @__PURE__ */ new Set();
|
|
418
|
-
const printCall = (call) => {
|
|
419
|
-
const line = JSON.stringify(call);
|
|
420
|
-
if (!seen.has(line)) {
|
|
421
|
-
seen.add(line);
|
|
422
|
-
writeStdoutLine(line);
|
|
423
|
-
}
|
|
424
|
-
};
|
|
425
|
-
if (initialLimit > 0) for (const call of await getCallHistoryFromStore(path.dirname(file), initialLimit)) printCall(call);
|
|
426
|
-
for (;;) {
|
|
427
|
-
try {
|
|
428
|
-
for (const call of await getCallHistoryFromStore(path.dirname(file), 1e3)) printCall(call);
|
|
429
|
-
} catch {}
|
|
430
|
-
await sleep(pollMs);
|
|
431
|
-
}
|
|
432
|
-
};
|
|
433
|
-
if (fs.existsSync(file) && path.basename(file) !== "calls.jsonl") {
|
|
434
|
-
const initial = fs.readFileSync(file);
|
|
435
|
-
let decoder = new StringDecoder("utf8");
|
|
436
|
-
const initialLines = decoder.write(initial).split("\n");
|
|
437
|
-
let pendingLine = initialLines.pop() ?? "";
|
|
438
|
-
const lines = initialLines.filter(Boolean);
|
|
439
|
-
for (const line of lines.slice(Math.max(0, lines.length - since))) writeStdoutLine(line);
|
|
440
|
-
let offset = initial.length;
|
|
441
|
-
let lastObservedSize = initial.length;
|
|
442
|
-
for (;;) {
|
|
443
|
-
try {
|
|
444
|
-
const stat = fs.statSync(file);
|
|
445
|
-
if (stat.size < lastObservedSize) {
|
|
446
|
-
offset = 0;
|
|
447
|
-
decoder = new StringDecoder("utf8");
|
|
448
|
-
pendingLine = "";
|
|
449
|
-
}
|
|
450
|
-
lastObservedSize = stat.size;
|
|
451
|
-
if (stat.size > offset) {
|
|
452
|
-
const fd = fs.openSync(file, "r");
|
|
453
|
-
try {
|
|
454
|
-
const buf = Buffer.alloc(stat.size - offset);
|
|
455
|
-
const bytesRead = fs.readSync(fd, buf, 0, buf.length, offset);
|
|
456
|
-
offset += bytesRead;
|
|
457
|
-
const text = decoder.write(buf.subarray(0, bytesRead));
|
|
458
|
-
const completeLines = `${pendingLine}${text}`.split("\n");
|
|
459
|
-
pendingLine = completeLines.pop() ?? "";
|
|
460
|
-
for (const line of completeLines.filter(Boolean)) writeStdoutLine(line);
|
|
461
|
-
} finally {
|
|
462
|
-
fs.closeSync(fd);
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
} catch {}
|
|
466
|
-
await sleep(pollMs);
|
|
467
|
-
}
|
|
468
|
-
} else await tailSqliteHistory(since);
|
|
469
|
-
});
|
|
470
|
-
root.command("latency").description("Summarize turn latency metrics from voice-call JSONL logs").option("--file <path>", "Path to calls.jsonl", resolveDefaultStorePath(config)).option("--last <n>", "Analyze last N records", "200").action(async (options) => {
|
|
471
|
-
const file = options.file;
|
|
472
|
-
const last = parseVoiceCallIntOption(options.last, "--last", { min: 1 });
|
|
473
|
-
if (fs.existsSync(file) && path.basename(file) !== "calls.jsonl") writeVoiceCallLatencySummary(fs.readFileSync(file, "utf8").split("\n").filter(Boolean).slice(-last).map((line) => {
|
|
474
|
-
try {
|
|
475
|
-
const parsed = JSON.parse(line);
|
|
476
|
-
return parsed.call ?? parsed;
|
|
477
|
-
} catch {
|
|
478
|
-
return null;
|
|
479
|
-
}
|
|
480
|
-
}).filter((call) => call !== null));
|
|
481
|
-
else {
|
|
482
|
-
ensureHistoryStateRuntime();
|
|
483
|
-
writeVoiceCallLatencySummary(await getCallHistoryFromStore(path.dirname(file), last));
|
|
484
|
-
}
|
|
522
|
+
registerVoiceCallLogs({
|
|
523
|
+
root,
|
|
524
|
+
defaultFile: resolveDefaultStorePath(config),
|
|
525
|
+
ensureHistoryStateRuntime
|
|
485
526
|
});
|
|
486
|
-
function writeVoiceCallLatencySummary(calls) {
|
|
487
|
-
const turnLatencyMs = [];
|
|
488
|
-
const listenWaitMs = [];
|
|
489
|
-
for (const call of calls) {
|
|
490
|
-
const latency = call.metadata?.lastTurnLatencyMs;
|
|
491
|
-
const listenWait = call.metadata?.lastTurnListenWaitMs;
|
|
492
|
-
if (typeof latency === "number" && Number.isFinite(latency)) turnLatencyMs.push(latency);
|
|
493
|
-
if (typeof listenWait === "number" && Number.isFinite(listenWait)) listenWaitMs.push(listenWait);
|
|
494
|
-
}
|
|
495
|
-
writeStdoutJson({
|
|
496
|
-
recordsScanned: calls.length,
|
|
497
|
-
turnLatency: summarizeSeries(turnLatencyMs),
|
|
498
|
-
listenWait: summarizeSeries(listenWaitMs)
|
|
499
|
-
});
|
|
500
|
-
}
|
|
501
527
|
root.command("expose").description("Enable/disable Tailscale serve/funnel for the webhook").option("--mode <mode>", "off | serve (tailnet) | funnel (public)", "funnel").option("--path <path>", "Tailscale path to expose (recommend matching serve.path)").option("--port <port>", "Local webhook port").option("--serve-path <path>", "Local webhook path").action(async (options) => {
|
|
502
528
|
const mode = resolveMode(options.mode ?? "funnel");
|
|
503
|
-
const servePort =
|
|
529
|
+
const servePort = parseCliInteger(options.port ?? String(config.serve.port ?? 3334), "--port", {
|
|
504
530
|
min: 1,
|
|
505
531
|
max: MAX_TCP_PORT
|
|
506
532
|
});
|
|
507
533
|
const servePath = options.servePath ?? config.serve.path ?? "/voice/webhook";
|
|
508
534
|
const tsPath = options.path ?? config.tailscale?.path ?? servePath;
|
|
509
|
-
const
|
|
535
|
+
const streamExposurePaths = resolveVoiceCallStreamExposurePaths(config, {
|
|
536
|
+
publicWebhookPath: tsPath,
|
|
537
|
+
localWebhookPath: servePath
|
|
538
|
+
});
|
|
539
|
+
const streamPaths = streamExposurePaths.map(({ publicPath }) => publicPath);
|
|
540
|
+
const localUrl = `http://127.0.0.1:${servePort}${servePath}`;
|
|
510
541
|
if (mode === "off") {
|
|
511
|
-
await cleanupTailscaleExposureRoute({
|
|
512
|
-
mode:
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
await cleanupTailscaleExposureRoute({
|
|
516
|
-
mode: "funnel",
|
|
517
|
-
path: tsPath
|
|
542
|
+
for (const exposurePath of [tsPath, ...streamPaths]) for (const tailscaleMode of ["serve", "funnel"]) await cleanupTailscaleExposureRoute({
|
|
543
|
+
mode: tailscaleMode,
|
|
544
|
+
port: config.tailscale.port,
|
|
545
|
+
path: exposurePath
|
|
518
546
|
});
|
|
519
|
-
|
|
547
|
+
writeCliJson({
|
|
520
548
|
ok: true,
|
|
521
549
|
mode: "off",
|
|
522
|
-
path: tsPath
|
|
550
|
+
path: tsPath,
|
|
551
|
+
streamPaths
|
|
523
552
|
});
|
|
524
553
|
return;
|
|
525
554
|
}
|
|
526
|
-
const publicUrl = await
|
|
555
|
+
const publicUrl = await setupTailscaleExposureRoutes({
|
|
527
556
|
mode,
|
|
528
|
-
|
|
529
|
-
|
|
557
|
+
port: config.tailscale.port,
|
|
558
|
+
routes: [{
|
|
559
|
+
path: tsPath,
|
|
560
|
+
localUrl
|
|
561
|
+
}, ...streamExposurePaths.map(({ publicPath, localPath }) => ({
|
|
562
|
+
path: publicPath,
|
|
563
|
+
localUrl: `http://127.0.0.1:${servePort}${localPath}`
|
|
564
|
+
}))]
|
|
530
565
|
});
|
|
531
566
|
const tsInfo = publicUrl ? null : await getTailscaleSelfInfo();
|
|
532
567
|
const enableUrl = tsInfo?.nodeId ? `https://login.tailscale.com/f/${mode}?node=${tsInfo.nodeId}` : null;
|
|
533
|
-
|
|
568
|
+
writeCliJson({
|
|
534
569
|
ok: Boolean(publicUrl),
|
|
535
570
|
mode,
|
|
536
571
|
path: tsPath,
|
|
572
|
+
streamPaths,
|
|
537
573
|
localUrl,
|
|
538
574
|
publicUrl,
|
|
539
575
|
hint: publicUrl ? void 0 : {
|
|
@@ -826,20 +862,32 @@ function isCliOnlyProcess() {
|
|
|
826
862
|
const VOICE_CALL_RUNTIME_COORDINATOR_KEY = Symbol.for("openclaw.voice-call.runtimeCoordinator");
|
|
827
863
|
var VoiceCallRuntimeLifecycleError = class extends Error {};
|
|
828
864
|
function getVoiceCallRuntimeCoordinator() {
|
|
829
|
-
return resolveGlobalSingleton(VOICE_CALL_RUNTIME_COORDINATOR_KEY, () => ({
|
|
830
|
-
epochCounter: 0,
|
|
831
|
-
registeredEpoch: 0
|
|
832
|
-
}));
|
|
865
|
+
return resolveGlobalSingleton(VOICE_CALL_RUNTIME_COORDINATOR_KEY, () => ({ epochCounter: 0 }));
|
|
833
866
|
}
|
|
834
|
-
function activateVoiceCallRuntimeGeneration(coordinator, generation) {
|
|
835
|
-
if (
|
|
867
|
+
function activateVoiceCallRuntimeGeneration(coordinator, registration, generation) {
|
|
868
|
+
if (registration.epoch < (coordinator.current?.epoch ?? 0) || registration.generation !== generation) throw new VoiceCallRuntimeLifecycleError("Voice call runtime generation was superseded; use the current plugin registration");
|
|
836
869
|
if (generation.retired) throw new VoiceCallRuntimeLifecycleError("Voice call runtime generation is retired; use the current plugin registration");
|
|
837
|
-
if (coordinator.current !==
|
|
838
|
-
if (coordinator.current) coordinator.current.retired = true;
|
|
839
|
-
coordinator.current =
|
|
840
|
-
coordinator.registeredEpoch = generation.epoch;
|
|
870
|
+
if (coordinator.current !== registration) {
|
|
871
|
+
if (coordinator.current) coordinator.current.generation.retired = true;
|
|
872
|
+
coordinator.current = registration;
|
|
841
873
|
}
|
|
842
874
|
}
|
|
875
|
+
function stopVoiceCallRuntimeGeneration(coordinator, generation) {
|
|
876
|
+
const ownedSlot = coordinator.slot?.owner === generation ? coordinator.slot : void 0;
|
|
877
|
+
if (!ownedSlot || ownedSlot.state === "stopping") return ownedSlot?.promise ?? Promise.resolve();
|
|
878
|
+
const stopPromise = Promise.resolve().then(async () => {
|
|
879
|
+
await (ownedSlot.state === "running" ? ownedSlot.runtime : await ownedSlot.promise).stop();
|
|
880
|
+
});
|
|
881
|
+
const stoppingSlot = {
|
|
882
|
+
state: "stopping",
|
|
883
|
+
owner: generation,
|
|
884
|
+
promise: stopPromise
|
|
885
|
+
};
|
|
886
|
+
if (coordinator.slot === ownedSlot) coordinator.slot = stoppingSlot;
|
|
887
|
+
return stopPromise.finally(() => {
|
|
888
|
+
if (coordinator.slot === stoppingSlot) coordinator.slot = void 0;
|
|
889
|
+
});
|
|
890
|
+
}
|
|
843
891
|
var voice_call_default = definePluginEntry({
|
|
844
892
|
id: "voice-call",
|
|
845
893
|
name: "Voice Call",
|
|
@@ -849,25 +897,26 @@ var voice_call_default = definePluginEntry({
|
|
|
849
897
|
const config = resolveVoiceCallConfig(voiceCallConfigSchema.parse(api.pluginConfig));
|
|
850
898
|
const validation = validateProviderConfig(config);
|
|
851
899
|
const runtimeCoordinator = getVoiceCallRuntimeCoordinator();
|
|
852
|
-
const
|
|
900
|
+
const runtimeRegistration = api.registrationMode !== "full" && runtimeCoordinator.current ? runtimeCoordinator.current : {
|
|
853
901
|
epoch: ++runtimeCoordinator.epochCounter,
|
|
854
|
-
retired: false
|
|
902
|
+
generation: { retired: false }
|
|
855
903
|
};
|
|
856
904
|
const continueOperationStore = createVoiceCallContinueOperationStore({
|
|
857
905
|
config,
|
|
858
906
|
coreConfig: api.config
|
|
859
907
|
});
|
|
860
|
-
const
|
|
861
|
-
|
|
908
|
+
const activateRuntimeGeneration = (generation) => activateVoiceCallRuntimeGeneration(runtimeCoordinator, runtimeRegistration, generation);
|
|
909
|
+
const ensureRuntimeForGeneration = async (runtimeGeneration) => {
|
|
910
|
+
activateRuntimeGeneration(runtimeGeneration);
|
|
862
911
|
if (!config.enabled) throw new Error("Voice call disabled in plugin config");
|
|
863
912
|
if (!validation.valid) throw new Error(validation.errors.join("; "));
|
|
864
913
|
while (true) {
|
|
865
|
-
|
|
914
|
+
activateRuntimeGeneration(runtimeGeneration);
|
|
866
915
|
const slot = runtimeCoordinator.slot;
|
|
867
916
|
if (slot) {
|
|
868
917
|
if (slot.owner !== runtimeGeneration) {
|
|
869
|
-
if (slot.
|
|
870
|
-
await slot.
|
|
918
|
+
if (slot.owner.retired) {
|
|
919
|
+
await stopVoiceCallRuntimeGeneration(runtimeCoordinator, slot.owner);
|
|
871
920
|
continue;
|
|
872
921
|
}
|
|
873
922
|
throw new VoiceCallRuntimeLifecycleError("A previous voice call runtime generation is still active; retry after it stops");
|
|
@@ -884,7 +933,7 @@ var voice_call_default = definePluginEntry({
|
|
|
884
933
|
if (runtimeCoordinator.slot === slot) runtimeCoordinator.slot = void 0;
|
|
885
934
|
throw err;
|
|
886
935
|
}
|
|
887
|
-
|
|
936
|
+
activateRuntimeGeneration(runtimeGeneration);
|
|
888
937
|
if (runtimeCoordinator.slot !== slot) continue;
|
|
889
938
|
runtimeCoordinator.slot = {
|
|
890
939
|
state: "running",
|
|
@@ -893,20 +942,30 @@ var voice_call_default = definePluginEntry({
|
|
|
893
942
|
};
|
|
894
943
|
return createdRuntime;
|
|
895
944
|
}
|
|
896
|
-
const
|
|
897
|
-
config,
|
|
898
|
-
coreConfig: api.config,
|
|
899
|
-
fullConfig: api.config,
|
|
900
|
-
agentRuntime: api.runtime.agent,
|
|
901
|
-
stateRuntime: api.runtime.state,
|
|
902
|
-
ttsRuntime: api.runtime.tts,
|
|
903
|
-
logger: api.logger
|
|
904
|
-
});
|
|
905
|
-
runtimeCoordinator.slot = {
|
|
945
|
+
const startingSlot = {
|
|
906
946
|
state: "starting",
|
|
907
947
|
owner: runtimeGeneration,
|
|
908
|
-
promise:
|
|
948
|
+
promise: createVoiceCallRuntime({
|
|
949
|
+
config,
|
|
950
|
+
coreConfig: api.config,
|
|
951
|
+
fullConfig: api.config,
|
|
952
|
+
agentRuntime: api.runtime.agent,
|
|
953
|
+
stateRuntime: api.runtime.state,
|
|
954
|
+
ttsRuntime: api.runtime.tts,
|
|
955
|
+
logger: api.logger
|
|
956
|
+
})
|
|
909
957
|
};
|
|
958
|
+
runtimeCoordinator.slot = startingSlot;
|
|
959
|
+
}
|
|
960
|
+
};
|
|
961
|
+
const ensureRuntime = async (runtimeGeneration = runtimeRegistration.generation) => {
|
|
962
|
+
try {
|
|
963
|
+
const runtime = await ensureRuntimeForGeneration(runtimeGeneration);
|
|
964
|
+
runtimeGeneration.serviceHealth?.clearFailure();
|
|
965
|
+
return runtime;
|
|
966
|
+
} catch (err) {
|
|
967
|
+
if (!(err instanceof VoiceCallRuntimeLifecycleError)) runtimeGeneration.serviceHealth?.reportFailure(err);
|
|
968
|
+
throw err;
|
|
910
969
|
}
|
|
911
970
|
};
|
|
912
971
|
const commands = createVoiceCallCommandService(ensureRuntime);
|
|
@@ -1034,48 +1093,41 @@ var voice_call_default = definePluginEntry({
|
|
|
1034
1093
|
});
|
|
1035
1094
|
api.registerService({
|
|
1036
1095
|
id: "voicecall",
|
|
1037
|
-
start: () => {
|
|
1096
|
+
start: (ctx) => {
|
|
1038
1097
|
if (isCliOnlyProcess()) return;
|
|
1039
1098
|
try {
|
|
1040
|
-
|
|
1099
|
+
if (runtimeRegistration.generation.retired) {
|
|
1100
|
+
if (runtimeCoordinator.current !== runtimeRegistration) throw new VoiceCallRuntimeLifecycleError("Voice call runtime generation was superseded; use the current plugin registration");
|
|
1101
|
+
runtimeRegistration.generation = { retired: false };
|
|
1102
|
+
}
|
|
1103
|
+
runtimeRegistration.generation.serviceHealth = ctx.serviceHealth;
|
|
1104
|
+
activateRuntimeGeneration(runtimeRegistration.generation);
|
|
1041
1105
|
} catch (err) {
|
|
1042
|
-
|
|
1043
|
-
|
|
1106
|
+
ctx.serviceHealth?.reportFailure(err);
|
|
1107
|
+
api.logger.error(`[voice-call] Failed to start runtime: ${formatErrorMessage(err)}`);
|
|
1108
|
+
return;
|
|
1044
1109
|
}
|
|
1045
1110
|
if (!config.enabled) return;
|
|
1046
1111
|
if (!validation.valid) {
|
|
1047
|
-
|
|
1112
|
+
const error = /* @__PURE__ */ new Error(`setup incomplete: ${validation.errors.join("; ")}`);
|
|
1113
|
+
ctx.serviceHealth?.reportFailure(error);
|
|
1114
|
+
api.logger.error(`[voice-call] Runtime not started: ${error.message}`);
|
|
1048
1115
|
return;
|
|
1049
1116
|
}
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
if (err instanceof VoiceCallRuntimeLifecycleError
|
|
1117
|
+
const startingGeneration = runtimeRegistration.generation;
|
|
1118
|
+
ensureRuntime(startingGeneration).catch((err) => {
|
|
1119
|
+
if (err instanceof VoiceCallRuntimeLifecycleError) return;
|
|
1120
|
+
ctx.serviceHealth?.reportFailure(err);
|
|
1053
1121
|
api.logger.error(`[voice-call] Failed to start runtime: ${formatErrorMessage(err)}`);
|
|
1054
1122
|
});
|
|
1055
1123
|
},
|
|
1056
1124
|
stop: async () => {
|
|
1057
|
-
|
|
1125
|
+
const runtimeGeneration = runtimeRegistration.generation;
|
|
1058
1126
|
runtimeGeneration.retired = true;
|
|
1059
|
-
const ownedSlot = runtimeCoordinator.slot?.owner === runtimeGeneration ? runtimeCoordinator.slot : void 0;
|
|
1060
|
-
let stoppingSlot;
|
|
1061
|
-
const stopPromise = ownedSlot?.state === "stopping" ? ownedSlot.promise : Promise.resolve().then(async () => {
|
|
1062
|
-
if (!ownedSlot) return;
|
|
1063
|
-
await (ownedSlot.state === "running" ? ownedSlot.runtime : await ownedSlot.promise).stop();
|
|
1064
|
-
});
|
|
1065
|
-
runtimeGeneration.stopPromise = stopPromise;
|
|
1066
|
-
if (ownedSlot && ownedSlot.state !== "stopping") {
|
|
1067
|
-
stoppingSlot = {
|
|
1068
|
-
state: "stopping",
|
|
1069
|
-
owner: runtimeGeneration,
|
|
1070
|
-
promise: stopPromise
|
|
1071
|
-
};
|
|
1072
|
-
if (runtimeCoordinator.slot === ownedSlot) runtimeCoordinator.slot = stoppingSlot;
|
|
1073
|
-
} else if (ownedSlot) stoppingSlot = ownedSlot;
|
|
1074
1127
|
try {
|
|
1075
|
-
await
|
|
1128
|
+
await stopVoiceCallRuntimeGeneration(runtimeCoordinator, runtimeGeneration);
|
|
1076
1129
|
} finally {
|
|
1077
|
-
|
|
1078
|
-
if (runtimeCoordinator.current === runtimeGeneration) runtimeCoordinator.current = void 0;
|
|
1130
|
+
runtimeGeneration.serviceHealth = void 0;
|
|
1079
1131
|
}
|
|
1080
1132
|
}
|
|
1081
1133
|
});
|