@openclaw/voice-call 2026.5.31-beta.2 → 2026.5.31-beta.4
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/dist/doctor-contract-api.js +211 -0
- package/dist/{guarded-json-api-mCcsLRBb.js → guarded-json-api-tyxHyrev.js} +1 -1
- package/dist/index.js +2 -1
- package/dist/{plivo-Dpf08ZVJ.js → plivo-DeSZeJ0S.js} +2 -2
- package/dist/{realtime-handler-Crs3kty2.js → realtime-handler-BP6_qBNe.js} +6 -4
- package/dist/{response-generator-Cu4sOiUO.js → response-generator-D6UsHyfH.js} +1 -1
- package/dist/{runtime-entry-DXf7kdXt.js → runtime-entry-13O0Ki99.js} +32 -428
- package/dist/runtime-entry.js +1 -1
- package/dist/store-XuAPgOJG.js +361 -0
- package/dist/{telnyx-CxEuPBOJ.js → telnyx-68EYJwaz.js} +1 -1
- package/dist/{twilio-DzQmSUQP.js → twilio-D0dUSGam.js} +8 -8
- package/npm-shrinkwrap.json +3 -3
- package/package.json +4 -4
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { a as MAX_CALL_RECORD_EVENTS, f as prepareVoiceCallRecordForStorage, i as CALL_RECORD_EVENT_META_MAX_ENTRIES, n as CALL_RECORD_EVENTS_NAMESPACE, o as RAW_CALL_RECORD_CHUNK_BYTES, p as resolveVoiceCallLegacyCallLogPath, r as CALL_RECORD_EVENT_CHUNKS_NAMESPACE, s as buildVoiceCallLegacyJsonlEventKey, t as CALL_RECORD_CHUNK_MAX_ENTRIES, u as parseVoiceCallRecordLine } from "./store-XuAPgOJG.js";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import fs from "node:fs/promises";
|
|
5
|
+
//#region extensions/voice-call/doctor-contract-api.ts
|
|
6
|
+
function resolveHome(env) {
|
|
7
|
+
return env.HOME?.trim() || os.homedir();
|
|
8
|
+
}
|
|
9
|
+
function resolveUserPath(input, env) {
|
|
10
|
+
const trimmed = input.trim();
|
|
11
|
+
if (!trimmed) return trimmed;
|
|
12
|
+
if (trimmed.startsWith("~")) return path.resolve(trimmed.replace(/^~(?=$|[\\/])/, resolveHome(env)));
|
|
13
|
+
return path.resolve(trimmed);
|
|
14
|
+
}
|
|
15
|
+
function getVoiceCallConfigStore(config) {
|
|
16
|
+
for (const pluginId of ["voice-call", "@openclaw/voice-call"]) {
|
|
17
|
+
const rawConfig = config.plugins?.entries?.[pluginId]?.config;
|
|
18
|
+
if (!rawConfig || typeof rawConfig !== "object" || Array.isArray(rawConfig)) continue;
|
|
19
|
+
const store = rawConfig.store;
|
|
20
|
+
if (typeof store === "string" && store.trim()) return store.trim();
|
|
21
|
+
}
|
|
22
|
+
return "";
|
|
23
|
+
}
|
|
24
|
+
function resolveVoiceCallStorePath(params) {
|
|
25
|
+
const configuredStore = getVoiceCallConfigStore(params.config);
|
|
26
|
+
if (configuredStore) return resolveUserPath(configuredStore, params.env);
|
|
27
|
+
return path.join(resolveHome(params.env), ".openclaw", "voice-calls");
|
|
28
|
+
}
|
|
29
|
+
async function fileExists(filePath) {
|
|
30
|
+
try {
|
|
31
|
+
return (await fs.stat(filePath)).isFile();
|
|
32
|
+
} catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function buildChunkKey(eventKey, index) {
|
|
37
|
+
return `${eventKey}:chunk:${String(index).padStart(4, "0")}`;
|
|
38
|
+
}
|
|
39
|
+
function prepareChunks(call) {
|
|
40
|
+
const serialized = JSON.stringify(prepareVoiceCallRecordForStorage(call));
|
|
41
|
+
const buffer = Buffer.from(serialized, "utf8");
|
|
42
|
+
const chunkCount = Math.max(1, Math.ceil(buffer.byteLength / RAW_CALL_RECORD_CHUNK_BYTES));
|
|
43
|
+
if (chunkCount > 48) throw new Error(`voice-call record exceeds SQLite chunk limit (${chunkCount}/48)`);
|
|
44
|
+
const chunks = [];
|
|
45
|
+
for (let index = 0; index < chunkCount; index += 1) {
|
|
46
|
+
const chunk = buffer.subarray(index * RAW_CALL_RECORD_CHUNK_BYTES, (index + 1) * RAW_CALL_RECORD_CHUNK_BYTES);
|
|
47
|
+
chunks.push({
|
|
48
|
+
index,
|
|
49
|
+
dataBase64: chunk.toString("base64")
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
chunks,
|
|
54
|
+
meta: {
|
|
55
|
+
chunkCount,
|
|
56
|
+
byteLength: buffer.byteLength
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
async function readLegacyCallRecords(filePath) {
|
|
61
|
+
let content;
|
|
62
|
+
try {
|
|
63
|
+
content = await fs.readFile(filePath, "utf8");
|
|
64
|
+
} catch {
|
|
65
|
+
return {
|
|
66
|
+
entries: [],
|
|
67
|
+
warnings: []
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
const entries = [];
|
|
71
|
+
const warnings = [];
|
|
72
|
+
let index = 0;
|
|
73
|
+
for (const line of content.split("\n")) {
|
|
74
|
+
const parsed = parseVoiceCallRecordLine(line, index);
|
|
75
|
+
if (!parsed) {
|
|
76
|
+
if (line.trim()) warnings.push(`Skipped malformed Voice Call call-log line ${index + 1}`);
|
|
77
|
+
index += 1;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
const prepared = prepareChunks(parsed.call);
|
|
82
|
+
entries.push({
|
|
83
|
+
eventKey: buildVoiceCallLegacyJsonlEventKey(line, index),
|
|
84
|
+
lineNumber: index + 1,
|
|
85
|
+
chunks: prepared.chunks,
|
|
86
|
+
meta: {
|
|
87
|
+
...prepared.meta,
|
|
88
|
+
persistedAt: parsed.persistedAt,
|
|
89
|
+
sequence: parsed.sequence
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
} catch (err) {
|
|
93
|
+
warnings.push(`Skipped Voice Call call-log line ${index + 1}: ${String(err)}`);
|
|
94
|
+
}
|
|
95
|
+
index += 1;
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
entries,
|
|
99
|
+
warnings
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
async function archiveLegacySource(params) {
|
|
103
|
+
const archivedPath = `${params.filePath}.migrated`;
|
|
104
|
+
if (await fileExists(archivedPath)) {
|
|
105
|
+
params.warnings.push(`Left migrated Voice Call call-log source in place because ${archivedPath} already exists`);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
await fs.rename(params.filePath, archivedPath);
|
|
110
|
+
params.changes.push(`Archived Voice Call call-log legacy source -> ${archivedPath}`);
|
|
111
|
+
} catch (err) {
|
|
112
|
+
params.warnings.push(`Failed archiving Voice Call call-log legacy source: ${String(err)}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async function selectEntriesForImport(params) {
|
|
116
|
+
const existingEventKeys = new Set((await params.eventStore.entries()).map((entry) => entry.key));
|
|
117
|
+
const missingEntries = params.entries.filter((entry) => !existingEventKeys.has(entry.eventKey));
|
|
118
|
+
const existingChunks = await params.chunkStore.entries();
|
|
119
|
+
let eventRoom = Math.max(0, MAX_CALL_RECORD_EVENTS - existingEventKeys.size);
|
|
120
|
+
let chunkRoom = Math.max(0, CALL_RECORD_CHUNK_MAX_ENTRIES - existingChunks.length);
|
|
121
|
+
const selected = [];
|
|
122
|
+
let pruned = 0;
|
|
123
|
+
for (const entry of missingEntries.toReversed()) {
|
|
124
|
+
if (eventRoom <= 0 || entry.chunks.length > chunkRoom) {
|
|
125
|
+
pruned++;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
selected.push(entry);
|
|
129
|
+
eventRoom--;
|
|
130
|
+
chunkRoom -= entry.chunks.length;
|
|
131
|
+
}
|
|
132
|
+
if (pruned > 0) params.warnings.push(`Pruned ${pruned} older Voice Call call-log ${pruned === 1 ? "record" : "records"} during migration because plugin state keeps the newest ${MAX_CALL_RECORD_EVENTS} records`);
|
|
133
|
+
return {
|
|
134
|
+
existingEventKeys,
|
|
135
|
+
entries: selected.toReversed()
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
async function importLegacyCallRecords(params) {
|
|
139
|
+
const selected = await selectEntriesForImport(params);
|
|
140
|
+
let imported = 0;
|
|
141
|
+
for (const entry of selected.entries) {
|
|
142
|
+
if (selected.existingEventKeys.has(entry.eventKey)) continue;
|
|
143
|
+
try {
|
|
144
|
+
for (const chunk of entry.chunks) await params.chunkStore.register(buildChunkKey(entry.eventKey, chunk.index), chunk);
|
|
145
|
+
await params.eventStore.register(entry.eventKey, entry.meta);
|
|
146
|
+
selected.existingEventKeys.add(entry.eventKey);
|
|
147
|
+
imported++;
|
|
148
|
+
} catch (err) {
|
|
149
|
+
params.warnings.push(`Failed migrating Voice Call call-log line ${entry.lineNumber}: ${String(err)}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return imported;
|
|
153
|
+
}
|
|
154
|
+
const stateMigrations = [{
|
|
155
|
+
id: "voice-call-calls-jsonl-to-plugin-state",
|
|
156
|
+
label: "Voice Call call log",
|
|
157
|
+
async detectLegacyState(params) {
|
|
158
|
+
const { entries } = await readLegacyCallRecords(resolveVoiceCallLegacyCallLogPath(resolveVoiceCallStorePath(params)));
|
|
159
|
+
if (entries.length === 0) return null;
|
|
160
|
+
return { preview: [`- Voice Call call log: ${entries.length} ${entries.length === 1 ? "record" : "records"} -> plugin state (${CALL_RECORD_EVENTS_NAMESPACE})`] };
|
|
161
|
+
},
|
|
162
|
+
async migrateLegacyState(params) {
|
|
163
|
+
const changes = [];
|
|
164
|
+
const warnings = [];
|
|
165
|
+
const storePath = resolveVoiceCallStorePath(params);
|
|
166
|
+
const filePath = resolveVoiceCallLegacyCallLogPath(storePath);
|
|
167
|
+
const { entries, warnings: readWarnings } = await readLegacyCallRecords(filePath);
|
|
168
|
+
warnings.push(...readWarnings);
|
|
169
|
+
if (entries.length === 0) return {
|
|
170
|
+
changes,
|
|
171
|
+
warnings
|
|
172
|
+
};
|
|
173
|
+
const env = {
|
|
174
|
+
...params.env,
|
|
175
|
+
OPENCLAW_STATE_DIR: storePath
|
|
176
|
+
};
|
|
177
|
+
const imported = await importLegacyCallRecords({
|
|
178
|
+
entries,
|
|
179
|
+
eventStore: params.context.openPluginStateKeyedStore({
|
|
180
|
+
namespace: CALL_RECORD_EVENTS_NAMESPACE,
|
|
181
|
+
maxEntries: CALL_RECORD_EVENT_META_MAX_ENTRIES,
|
|
182
|
+
env
|
|
183
|
+
}),
|
|
184
|
+
chunkStore: params.context.openPluginStateKeyedStore({
|
|
185
|
+
namespace: CALL_RECORD_EVENT_CHUNKS_NAMESPACE,
|
|
186
|
+
maxEntries: CALL_RECORD_CHUNK_MAX_ENTRIES,
|
|
187
|
+
env
|
|
188
|
+
}),
|
|
189
|
+
warnings
|
|
190
|
+
});
|
|
191
|
+
if (imported > 0) changes.push(`Migrated ${imported} Voice Call call-log ${imported === 1 ? "record" : "records"} -> plugin state`);
|
|
192
|
+
if (warnings.some((warning) => warning.startsWith("Failed migrating Voice Call") || warning.startsWith("Skipped malformed Voice Call call-log line") || warning.startsWith("Skipped Voice Call call-log line") || warning.startsWith("Skipped Voice Call call-log migration"))) {
|
|
193
|
+
warnings.push("Left Voice Call call-log source in place because migration was incomplete");
|
|
194
|
+
return {
|
|
195
|
+
changes,
|
|
196
|
+
warnings
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
await archiveLegacySource({
|
|
200
|
+
filePath,
|
|
201
|
+
changes,
|
|
202
|
+
warnings
|
|
203
|
+
});
|
|
204
|
+
return {
|
|
205
|
+
changes,
|
|
206
|
+
warnings
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
}];
|
|
210
|
+
//#endregion
|
|
211
|
+
export { stateMigrations };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { fetchWithSsrFGuard } from "./runtime-api.js";
|
|
2
2
|
import "./api.js";
|
|
3
|
-
import { a as getHeader } from "./runtime-entry-
|
|
3
|
+
import { a as getHeader } from "./runtime-entry-13O0Ki99.js";
|
|
4
4
|
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
5
5
|
import { isLoopbackHost } from "openclaw/plugin-sdk/gateway-runtime";
|
|
6
6
|
import { isFutureDateTimestampMs, resolveExpiresAtMsFromDurationMs } from "openclaw/plugin-sdk/number-runtime";
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { definePluginEntry, sleep } from "./runtime-api.js";
|
|
2
2
|
import "./api.js";
|
|
3
3
|
import { i as resolveVoiceCallConfig, s as validateProviderConfig } from "./config-BKyRNKHF.js";
|
|
4
|
-
import {
|
|
4
|
+
import { c as getTailscaleSelfInfo, l as setupTailscaleExposureRoute, o as resolveWebhookExposureStatus, p as resolveUserPath, s as cleanupTailscaleExposureRoute, t as createVoiceCallRuntime } from "./runtime-entry-13O0Ki99.js";
|
|
5
|
+
import { c as getCallHistoryFromStore, m as setVoiceCallStateRuntime } from "./store-XuAPgOJG.js";
|
|
5
6
|
import { i as parseVoiceCallPluginConfig, r as normalizeVoiceCallLegacyConfigInput, t as formatVoiceCallLegacyConfigWarnings } from "./config-compat-BLdRz9GR.js";
|
|
6
7
|
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
7
8
|
import { ErrorCodes, callGatewayFromCli, errorShape } from "openclaw/plugin-sdk/gateway-runtime";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as getHeader, m as escapeXml } from "./runtime-entry-
|
|
2
|
-
import { n as reconstructWebhookUrl, r as verifyPlivoWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-
|
|
1
|
+
import { a as getHeader, m as escapeXml } from "./runtime-entry-13O0Ki99.js";
|
|
2
|
+
import { n as reconstructWebhookUrl, r as verifyPlivoWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-tyxHyrev.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
|
|
@@ -83,7 +83,7 @@ var RealtimeAudioPacer = class {
|
|
|
83
83
|
const item = this.queue.shift();
|
|
84
84
|
if (!item) return;
|
|
85
85
|
let delayMs = 0;
|
|
86
|
-
let sent
|
|
86
|
+
let sent;
|
|
87
87
|
if (item.type === "audio") {
|
|
88
88
|
this.queuedAudioBytes = Math.max(0, this.queuedAudioBytes - item.chunk.length);
|
|
89
89
|
sent = this.params.send(this.params.serializer.media(item.chunk.toString("base64")));
|
|
@@ -787,7 +787,7 @@ var RealtimeCallHandler = class {
|
|
|
787
787
|
text
|
|
788
788
|
});
|
|
789
789
|
},
|
|
790
|
-
onToolCall: (toolEvent,
|
|
790
|
+
onToolCall: (toolEvent, sessionLocal) => {
|
|
791
791
|
const turnId = ensureTalkTurn();
|
|
792
792
|
emitTalkEvent({
|
|
793
793
|
type: "tool.call",
|
|
@@ -800,7 +800,7 @@ var RealtimeCallHandler = class {
|
|
|
800
800
|
}
|
|
801
801
|
});
|
|
802
802
|
console.log(`[voice-call] realtime tool call received callId=${callId} providerCallId=${callSid} tool=${toolEvent.name}`);
|
|
803
|
-
this.executeToolCall(
|
|
803
|
+
this.executeToolCall(sessionLocal, callId, toolEvent.callId || toolEvent.itemId, toolEvent.name, toolEvent.args, turnId, emitTalkEvent);
|
|
804
804
|
},
|
|
805
805
|
onEvent: (event) => {
|
|
806
806
|
if (event.type === "input_audio_buffer.speech_started") {
|
|
@@ -986,7 +986,9 @@ var RealtimeCallHandler = class {
|
|
|
986
986
|
const now = Date.now();
|
|
987
987
|
const quietFor = now - updatedAt;
|
|
988
988
|
if (quietFor >= CONSULT_TRANSCRIPT_SETTLE_MS || now >= deadline) return;
|
|
989
|
-
await new Promise((resolve) =>
|
|
989
|
+
await new Promise((resolve) => {
|
|
990
|
+
setTimeout(resolve, Math.min(CONSULT_TRANSCRIPT_SETTLE_MS - quietFor, deadline - now));
|
|
991
|
+
});
|
|
990
992
|
}
|
|
991
993
|
}
|
|
992
994
|
clearForcedConsultState(callId) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { o as resolveVoiceCallSessionKey } from "./config-BKyRNKHF.js";
|
|
2
|
-
import { f as resolveVoiceResponseModel } from "./runtime-entry-
|
|
2
|
+
import { f as resolveVoiceResponseModel } from "./runtime-entry-13O0Ki99.js";
|
|
3
3
|
import { isRecord, normalizeLowercaseStringOrEmpty, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
4
4
|
import crypto from "node:crypto";
|
|
5
5
|
import { applyModelOverrideToSessionEntry } from "openclaw/plugin-sdk/model-session-runtime";
|
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
import { isBlockedHostnameOrIp, isRequestBodyLimitError, readRequestBodyWithLimit, requestBodyErrorToText } from "./runtime-api.js";
|
|
2
2
|
import "./api.js";
|
|
3
3
|
import { a as resolveVoiceCallEffectiveConfig, c as deepMergeDefined, i as resolveVoiceCallConfig, n as normalizeVoiceCallConfig, o as resolveVoiceCallSessionKey, r as resolveTwilioAuthToken, s as validateProviderConfig } from "./config-BKyRNKHF.js";
|
|
4
|
+
import { c as getCallHistoryFromStore, d as persistCallRecord, h as TerminalStates, l as loadActiveCallsFromStore, m as setVoiceCallStateRuntime } from "./store-XuAPgOJG.js";
|
|
4
5
|
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
5
6
|
import { isLoopbackHost } from "openclaw/plugin-sdk/gateway-runtime";
|
|
6
7
|
import { MAX_TIMER_TIMEOUT_MS, asDateTimestampMs, resolveExpiresAtMsFromDurationMs, resolveTimerTimeoutMs } from "openclaw/plugin-sdk/number-runtime";
|
|
7
8
|
import { normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, normalizeOptionalString, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
8
9
|
import { REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME, buildRealtimeVoiceAgentConsultPolicyInstructions, consultRealtimeVoiceAgent, convertPcmToMulaw8k, createTalkSessionController, recordTalkObservabilityEvent, resolveRealtimeVoiceAgentConsultTools, resolveRealtimeVoiceAgentConsultToolsAllow, resolveRealtimeVoiceFastContextConsult } from "openclaw/plugin-sdk/realtime-voice";
|
|
9
|
-
import { z } from "zod";
|
|
10
10
|
import fs from "node:fs";
|
|
11
11
|
import os from "node:os";
|
|
12
12
|
import path from "node:path";
|
|
13
|
-
import crypto
|
|
14
|
-
import {
|
|
15
|
-
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
|
|
13
|
+
import crypto from "node:crypto";
|
|
14
|
+
import { root } from "openclaw/plugin-sdk/security-runtime";
|
|
16
15
|
import { parseTtsDirectives } from "openclaw/plugin-sdk/speech";
|
|
17
16
|
import { spawn } from "node:child_process";
|
|
18
17
|
import http from "node:http";
|
|
@@ -33,121 +32,6 @@ function isAllowlistedCaller(normalizedFrom, allowFrom) {
|
|
|
33
32
|
});
|
|
34
33
|
}
|
|
35
34
|
//#endregion
|
|
36
|
-
//#region extensions/voice-call/src/types.ts
|
|
37
|
-
const ProviderNameSchema = z.enum([
|
|
38
|
-
"telnyx",
|
|
39
|
-
"twilio",
|
|
40
|
-
"plivo",
|
|
41
|
-
"mock"
|
|
42
|
-
]);
|
|
43
|
-
const CallStateSchema = z.enum([
|
|
44
|
-
"initiated",
|
|
45
|
-
"ringing",
|
|
46
|
-
"answered",
|
|
47
|
-
"active",
|
|
48
|
-
"speaking",
|
|
49
|
-
"listening",
|
|
50
|
-
"completed",
|
|
51
|
-
"hangup-user",
|
|
52
|
-
"hangup-bot",
|
|
53
|
-
"timeout",
|
|
54
|
-
"error",
|
|
55
|
-
"failed",
|
|
56
|
-
"no-answer",
|
|
57
|
-
"busy",
|
|
58
|
-
"voicemail"
|
|
59
|
-
]);
|
|
60
|
-
const TerminalStates = new Set([
|
|
61
|
-
"completed",
|
|
62
|
-
"hangup-user",
|
|
63
|
-
"hangup-bot",
|
|
64
|
-
"timeout",
|
|
65
|
-
"error",
|
|
66
|
-
"failed",
|
|
67
|
-
"no-answer",
|
|
68
|
-
"busy",
|
|
69
|
-
"voicemail"
|
|
70
|
-
]);
|
|
71
|
-
const EndReasonSchema = z.enum([
|
|
72
|
-
"completed",
|
|
73
|
-
"hangup-user",
|
|
74
|
-
"hangup-bot",
|
|
75
|
-
"timeout",
|
|
76
|
-
"error",
|
|
77
|
-
"failed",
|
|
78
|
-
"no-answer",
|
|
79
|
-
"busy",
|
|
80
|
-
"voicemail"
|
|
81
|
-
]);
|
|
82
|
-
const BaseEventSchema = z.object({
|
|
83
|
-
id: z.string(),
|
|
84
|
-
dedupeKey: z.string().optional(),
|
|
85
|
-
callId: z.string(),
|
|
86
|
-
providerCallId: z.string().optional(),
|
|
87
|
-
timestamp: z.number(),
|
|
88
|
-
turnToken: z.string().optional(),
|
|
89
|
-
direction: z.enum(["inbound", "outbound"]).optional(),
|
|
90
|
-
from: z.string().optional(),
|
|
91
|
-
to: z.string().optional()
|
|
92
|
-
});
|
|
93
|
-
z.discriminatedUnion("type", [
|
|
94
|
-
BaseEventSchema.extend({ type: z.literal("call.initiated") }),
|
|
95
|
-
BaseEventSchema.extend({ type: z.literal("call.ringing") }),
|
|
96
|
-
BaseEventSchema.extend({ type: z.literal("call.answered") }),
|
|
97
|
-
BaseEventSchema.extend({ type: z.literal("call.active") }),
|
|
98
|
-
BaseEventSchema.extend({
|
|
99
|
-
type: z.literal("call.speaking"),
|
|
100
|
-
text: z.string()
|
|
101
|
-
}),
|
|
102
|
-
BaseEventSchema.extend({
|
|
103
|
-
type: z.literal("call.speech"),
|
|
104
|
-
transcript: z.string(),
|
|
105
|
-
isFinal: z.boolean(),
|
|
106
|
-
confidence: z.number().min(0).max(1).optional()
|
|
107
|
-
}),
|
|
108
|
-
BaseEventSchema.extend({
|
|
109
|
-
type: z.literal("call.silence"),
|
|
110
|
-
durationMs: z.number()
|
|
111
|
-
}),
|
|
112
|
-
BaseEventSchema.extend({
|
|
113
|
-
type: z.literal("call.dtmf"),
|
|
114
|
-
digits: z.string()
|
|
115
|
-
}),
|
|
116
|
-
BaseEventSchema.extend({
|
|
117
|
-
type: z.literal("call.ended"),
|
|
118
|
-
reason: EndReasonSchema
|
|
119
|
-
}),
|
|
120
|
-
BaseEventSchema.extend({
|
|
121
|
-
type: z.literal("call.error"),
|
|
122
|
-
error: z.string(),
|
|
123
|
-
retryable: z.boolean().optional()
|
|
124
|
-
})
|
|
125
|
-
]);
|
|
126
|
-
const CallDirectionSchema = z.enum(["outbound", "inbound"]);
|
|
127
|
-
const TranscriptEntrySchema = z.object({
|
|
128
|
-
timestamp: z.number(),
|
|
129
|
-
speaker: z.enum(["bot", "user"]),
|
|
130
|
-
text: z.string(),
|
|
131
|
-
isFinal: z.boolean().default(true)
|
|
132
|
-
});
|
|
133
|
-
const CallRecordSchema = z.object({
|
|
134
|
-
callId: z.string(),
|
|
135
|
-
providerCallId: z.string().optional(),
|
|
136
|
-
provider: ProviderNameSchema,
|
|
137
|
-
direction: CallDirectionSchema,
|
|
138
|
-
state: CallStateSchema,
|
|
139
|
-
from: z.string(),
|
|
140
|
-
to: z.string(),
|
|
141
|
-
sessionKey: z.string().optional(),
|
|
142
|
-
startedAt: z.number(),
|
|
143
|
-
answeredAt: z.number().optional(),
|
|
144
|
-
endedAt: z.number().optional(),
|
|
145
|
-
endReason: EndReasonSchema.optional(),
|
|
146
|
-
transcript: z.array(TranscriptEntrySchema).default([]),
|
|
147
|
-
processedEventIds: z.array(z.string()).default([]),
|
|
148
|
-
metadata: z.record(z.string(), z.unknown()).optional()
|
|
149
|
-
});
|
|
150
|
-
//#endregion
|
|
151
35
|
//#region extensions/voice-call/src/manager/state.ts
|
|
152
36
|
const ConversationStates = new Set(["speaking", "listening"]);
|
|
153
37
|
const StateOrder = [
|
|
@@ -181,292 +65,6 @@ function addTranscriptEntry(call, speaker, text) {
|
|
|
181
65
|
call.transcript.push(entry);
|
|
182
66
|
}
|
|
183
67
|
//#endregion
|
|
184
|
-
//#region extensions/voice-call/src/runtime-state.ts
|
|
185
|
-
const { setRuntime: setVoiceCallStateRuntime, clearRuntime: clearVoiceCallStateRuntime, tryGetRuntime: getOptionalVoiceCallStateRuntime } = createPluginRuntimeStore({
|
|
186
|
-
pluginId: "voice-call-state",
|
|
187
|
-
errorMessage: "Voice Call state runtime not initialized"
|
|
188
|
-
});
|
|
189
|
-
//#endregion
|
|
190
|
-
//#region extensions/voice-call/src/manager/store.ts
|
|
191
|
-
const pendingPersistWrites = /* @__PURE__ */ new Set();
|
|
192
|
-
const CALL_RECORD_EVENTS_NAMESPACE = "call-record-events";
|
|
193
|
-
const CALL_RECORD_EVENT_CHUNKS_NAMESPACE = "call-record-event-chunks";
|
|
194
|
-
const CALL_RECORD_MIGRATIONS_NAMESPACE = "call-record-migrations";
|
|
195
|
-
const CALL_RECORD_JSONL_MIGRATION_KEY = "calls-jsonl-v1";
|
|
196
|
-
const MAX_CALL_RECORD_EVENTS = 1e3;
|
|
197
|
-
const CALL_RECORD_EVENT_META_MAX_ENTRIES = 1100;
|
|
198
|
-
const MAX_CHUNKS_PER_CALL_RECORD_EVENT = 48;
|
|
199
|
-
const CALL_RECORD_CHUNK_MAX_ENTRIES = 48048;
|
|
200
|
-
const RAW_CHUNK_BYTES = 36 * 1024;
|
|
201
|
-
let callRecordEventSequence = 0;
|
|
202
|
-
function resolveCallLogPath(storePath) {
|
|
203
|
-
return path.join(storePath, "calls.jsonl");
|
|
204
|
-
}
|
|
205
|
-
function resolvePluginStateEnv(storePath) {
|
|
206
|
-
return {
|
|
207
|
-
...process.env,
|
|
208
|
-
OPENCLAW_STATE_DIR: storePath
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
function createCallRecordStateStores(storePath) {
|
|
212
|
-
const runtime = getOptionalVoiceCallStateRuntime();
|
|
213
|
-
if (!runtime) return null;
|
|
214
|
-
const env = resolvePluginStateEnv(storePath);
|
|
215
|
-
return {
|
|
216
|
-
events: runtime.state.openSyncKeyedStore({
|
|
217
|
-
namespace: CALL_RECORD_EVENTS_NAMESPACE,
|
|
218
|
-
maxEntries: CALL_RECORD_EVENT_META_MAX_ENTRIES,
|
|
219
|
-
env
|
|
220
|
-
}),
|
|
221
|
-
chunks: runtime.state.openSyncKeyedStore({
|
|
222
|
-
namespace: CALL_RECORD_EVENT_CHUNKS_NAMESPACE,
|
|
223
|
-
maxEntries: CALL_RECORD_CHUNK_MAX_ENTRIES,
|
|
224
|
-
env
|
|
225
|
-
}),
|
|
226
|
-
migrations: runtime.state.openSyncKeyedStore({
|
|
227
|
-
namespace: CALL_RECORD_MIGRATIONS_NAMESPACE,
|
|
228
|
-
maxEntries: 100,
|
|
229
|
-
env
|
|
230
|
-
})
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
function tryCreateCallRecordStateStores(storePath) {
|
|
234
|
-
try {
|
|
235
|
-
return createCallRecordStateStores(storePath);
|
|
236
|
-
} catch (err) {
|
|
237
|
-
console.error("[voice-call] Failed to open SQLite call record store:", err);
|
|
238
|
-
return null;
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
function buildChunkKey(eventKey, index) {
|
|
242
|
-
return `${eventKey}:chunk:${String(index).padStart(4, "0")}`;
|
|
243
|
-
}
|
|
244
|
-
function buildJsonlEventKey(line, index) {
|
|
245
|
-
return `jsonl:${String(index).padStart(8, "0")}:${createHash("sha256").update(line).digest("hex")}`;
|
|
246
|
-
}
|
|
247
|
-
function nextCallRecordOrder() {
|
|
248
|
-
const sequence = callRecordEventSequence;
|
|
249
|
-
callRecordEventSequence = (callRecordEventSequence + 1) % 1e6;
|
|
250
|
-
return {
|
|
251
|
-
persistedAt: Date.now(),
|
|
252
|
-
sequence
|
|
253
|
-
};
|
|
254
|
-
}
|
|
255
|
-
function buildNewEventKey(order) {
|
|
256
|
-
return `event:${order.persistedAt.toString(36)}:${String(order.sequence).padStart(6, "0")}:${randomUUID()}`;
|
|
257
|
-
}
|
|
258
|
-
function parseEventKeySequence(key) {
|
|
259
|
-
const match = /^event:[^:]+:(\d+):/.exec(key);
|
|
260
|
-
return match ? Number.parseInt(match[1], 10) : 0;
|
|
261
|
-
}
|
|
262
|
-
function parseCallRecordLine(line, sequence = 0) {
|
|
263
|
-
if (!line.trim()) return null;
|
|
264
|
-
try {
|
|
265
|
-
const parsed = JSON.parse(line);
|
|
266
|
-
if (parsed && typeof parsed === "object" && parsed.version === 2) {
|
|
267
|
-
const envelope = parsed;
|
|
268
|
-
return {
|
|
269
|
-
call: CallRecordSchema.parse(envelope.call),
|
|
270
|
-
persistedAt: typeof envelope.persistedAt === "number" && Number.isFinite(envelope.persistedAt) ? envelope.persistedAt : 0,
|
|
271
|
-
sequence: typeof envelope.sequence === "number" && Number.isFinite(envelope.sequence) ? envelope.sequence : sequence,
|
|
272
|
-
orderKey: ""
|
|
273
|
-
};
|
|
274
|
-
}
|
|
275
|
-
return {
|
|
276
|
-
call: CallRecordSchema.parse(parsed),
|
|
277
|
-
persistedAt: 0,
|
|
278
|
-
sequence,
|
|
279
|
-
orderKey: ""
|
|
280
|
-
};
|
|
281
|
-
} catch {
|
|
282
|
-
return null;
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
function registerCallRecordEvent(stores, eventKey, call, order) {
|
|
286
|
-
const serialized = JSON.stringify(call);
|
|
287
|
-
const buffer = Buffer.from(serialized, "utf8");
|
|
288
|
-
const chunkCount = Math.max(1, Math.ceil(buffer.byteLength / RAW_CHUNK_BYTES));
|
|
289
|
-
if (chunkCount > MAX_CHUNKS_PER_CALL_RECORD_EVENT) throw new Error(`voice-call record exceeds SQLite chunk limit (${chunkCount}/${MAX_CHUNKS_PER_CALL_RECORD_EVENT})`);
|
|
290
|
-
for (let index = 0; index < chunkCount; index += 1) {
|
|
291
|
-
const chunk = buffer.subarray(index * RAW_CHUNK_BYTES, (index + 1) * RAW_CHUNK_BYTES);
|
|
292
|
-
stores.chunks.register(buildChunkKey(eventKey, index), {
|
|
293
|
-
index,
|
|
294
|
-
dataBase64: chunk.toString("base64")
|
|
295
|
-
});
|
|
296
|
-
}
|
|
297
|
-
stores.events.register(eventKey, {
|
|
298
|
-
chunkCount,
|
|
299
|
-
byteLength: buffer.byteLength,
|
|
300
|
-
persistedAt: order?.persistedAt,
|
|
301
|
-
sequence: order?.sequence
|
|
302
|
-
});
|
|
303
|
-
pruneCallRecordEvents(stores);
|
|
304
|
-
}
|
|
305
|
-
function deleteCallRecordEventRows(stores, eventKey) {
|
|
306
|
-
const meta = stores.events.lookup(eventKey);
|
|
307
|
-
stores.events.delete(eventKey);
|
|
308
|
-
if (!meta) return;
|
|
309
|
-
for (let index = 0; index < meta.chunkCount; index += 1) stores.chunks.delete(buildChunkKey(eventKey, index));
|
|
310
|
-
}
|
|
311
|
-
function pruneCallRecordEvents(stores) {
|
|
312
|
-
const rows = stores.events.entries();
|
|
313
|
-
if (rows.length <= MAX_CALL_RECORD_EVENTS) return;
|
|
314
|
-
const sorted = rows.toSorted((a, b) => a.createdAt - b.createdAt || a.key.localeCompare(b.key));
|
|
315
|
-
for (const row of sorted.slice(0, rows.length - MAX_CALL_RECORD_EVENTS)) deleteCallRecordEventRows(stores, row.key);
|
|
316
|
-
}
|
|
317
|
-
function registerCallRecordEventIfAbsent(stores, eventKey, record) {
|
|
318
|
-
if (!stores.events.lookup(eventKey)) registerCallRecordEvent(stores, eventKey, record.call, {
|
|
319
|
-
persistedAt: record.persistedAt,
|
|
320
|
-
sequence: record.sequence
|
|
321
|
-
});
|
|
322
|
-
}
|
|
323
|
-
function readCallRecordEvent(stores, eventKey) {
|
|
324
|
-
const meta = stores.events.lookup(eventKey);
|
|
325
|
-
if (!meta) return null;
|
|
326
|
-
const chunks = [];
|
|
327
|
-
for (let index = 0; index < meta.chunkCount; index += 1) {
|
|
328
|
-
const chunk = stores.chunks.lookup(buildChunkKey(eventKey, index));
|
|
329
|
-
if (!chunk || chunk.index !== index) return null;
|
|
330
|
-
chunks.push(Buffer.from(chunk.dataBase64, "base64"));
|
|
331
|
-
}
|
|
332
|
-
return parseCallRecordLine(Buffer.concat(chunks, meta.byteLength).toString("utf8"))?.call ?? null;
|
|
333
|
-
}
|
|
334
|
-
function ensureLegacyCallLogImported(storePath, stores) {
|
|
335
|
-
const imported = stores.migrations.lookup(CALL_RECORD_JSONL_MIGRATION_KEY) !== void 0;
|
|
336
|
-
const logPath = resolveCallLogPath(storePath);
|
|
337
|
-
const content = privateFileStoreSync(storePath).readTextIfExists(path.basename(logPath));
|
|
338
|
-
if (content === null) {
|
|
339
|
-
if (!imported) stores.migrations.register(CALL_RECORD_JSONL_MIGRATION_KEY, { importedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
340
|
-
return [];
|
|
341
|
-
}
|
|
342
|
-
const fallbackCalls = [];
|
|
343
|
-
{
|
|
344
|
-
let index = 0;
|
|
345
|
-
let importFailed = false;
|
|
346
|
-
for (const line of content.split("\n")) {
|
|
347
|
-
const parsed = parseCallRecordLine(line, index);
|
|
348
|
-
if (!parsed) {
|
|
349
|
-
index += 1;
|
|
350
|
-
continue;
|
|
351
|
-
}
|
|
352
|
-
try {
|
|
353
|
-
registerCallRecordEventIfAbsent(stores, buildJsonlEventKey(line, index), parsed);
|
|
354
|
-
} catch (err) {
|
|
355
|
-
importFailed = true;
|
|
356
|
-
fallbackCalls.push({
|
|
357
|
-
...parsed,
|
|
358
|
-
orderKey: `jsonl:${String(index).padStart(8, "0")}`
|
|
359
|
-
});
|
|
360
|
-
console.error("[voice-call] Failed to import persisted call record:", err);
|
|
361
|
-
}
|
|
362
|
-
index += 1;
|
|
363
|
-
}
|
|
364
|
-
if (!importFailed) try {
|
|
365
|
-
fs.rmSync(logPath, { force: true });
|
|
366
|
-
} catch {}
|
|
367
|
-
}
|
|
368
|
-
if (!imported) stores.migrations.register(CALL_RECORD_JSONL_MIGRATION_KEY, { importedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
369
|
-
return fallbackCalls;
|
|
370
|
-
}
|
|
371
|
-
function readCallRecordEvents(storePath, stores) {
|
|
372
|
-
const fallbackCalls = ensureLegacyCallLogImported(storePath, stores);
|
|
373
|
-
return [...stores.events.entries().toSorted((a, b) => a.createdAt - b.createdAt || a.key.localeCompare(b.key)).map((entry) => {
|
|
374
|
-
const call = readCallRecordEvent(stores, entry.key);
|
|
375
|
-
return call ? {
|
|
376
|
-
call,
|
|
377
|
-
persistedAt: entry.value.persistedAt ?? entry.createdAt,
|
|
378
|
-
sequence: entry.value.sequence ?? parseEventKeySequence(entry.key),
|
|
379
|
-
orderKey: entry.key
|
|
380
|
-
} : null;
|
|
381
|
-
}).filter((entry) => entry !== null), ...fallbackCalls].toSorted((a, b) => a.persistedAt - b.persistedAt || a.sequence - b.sequence || a.orderKey.localeCompare(b.orderKey)).map((entry) => entry.call);
|
|
382
|
-
}
|
|
383
|
-
function persistCallRecord(storePath, call) {
|
|
384
|
-
const stores = tryCreateCallRecordStateStores(storePath);
|
|
385
|
-
if (stores) try {
|
|
386
|
-
ensureLegacyCallLogImported(storePath, stores);
|
|
387
|
-
const order = nextCallRecordOrder();
|
|
388
|
-
registerCallRecordEvent(stores, buildNewEventKey(order), call, order);
|
|
389
|
-
return;
|
|
390
|
-
} catch (err) {
|
|
391
|
-
console.error("[voice-call] Failed to persist call record:", err);
|
|
392
|
-
}
|
|
393
|
-
const logPath = resolveCallLogPath(storePath);
|
|
394
|
-
const order = nextCallRecordOrder();
|
|
395
|
-
const write = appendRegularFile({
|
|
396
|
-
filePath: logPath,
|
|
397
|
-
content: `${JSON.stringify({
|
|
398
|
-
version: 2,
|
|
399
|
-
...order,
|
|
400
|
-
call
|
|
401
|
-
})}\n`,
|
|
402
|
-
rejectSymlinkParents: true
|
|
403
|
-
}).catch((err) => {
|
|
404
|
-
console.error("[voice-call] Failed to persist call record:", err);
|
|
405
|
-
}).finally(() => {
|
|
406
|
-
pendingPersistWrites.delete(write);
|
|
407
|
-
});
|
|
408
|
-
pendingPersistWrites.add(write);
|
|
409
|
-
}
|
|
410
|
-
function loadActiveCallsFromStore(storePath) {
|
|
411
|
-
const stores = tryCreateCallRecordStateStores(storePath);
|
|
412
|
-
let calls;
|
|
413
|
-
try {
|
|
414
|
-
calls = stores ? readCallRecordEvents(storePath, stores) : readCallRecordsFromLegacyLog(storePath);
|
|
415
|
-
} catch (err) {
|
|
416
|
-
console.error("[voice-call] Failed to read SQLite call records:", err);
|
|
417
|
-
calls = readCallRecordsFromLegacyLog(storePath);
|
|
418
|
-
}
|
|
419
|
-
if (calls.length === 0) return {
|
|
420
|
-
activeCalls: /* @__PURE__ */ new Map(),
|
|
421
|
-
providerCallIdMap: /* @__PURE__ */ new Map(),
|
|
422
|
-
processedEventIds: /* @__PURE__ */ new Set(),
|
|
423
|
-
rejectedProviderCallIds: /* @__PURE__ */ new Set()
|
|
424
|
-
};
|
|
425
|
-
const callMap = /* @__PURE__ */ new Map();
|
|
426
|
-
for (const call of calls) callMap.set(call.callId, call);
|
|
427
|
-
const activeCalls = /* @__PURE__ */ new Map();
|
|
428
|
-
const providerCallIdMap = /* @__PURE__ */ new Map();
|
|
429
|
-
const processedEventIds = /* @__PURE__ */ new Set();
|
|
430
|
-
const rejectedProviderCallIds = /* @__PURE__ */ new Set();
|
|
431
|
-
for (const [callId, call] of callMap) {
|
|
432
|
-
for (const eventId of call.processedEventIds) processedEventIds.add(eventId);
|
|
433
|
-
if (TerminalStates.has(call.state)) continue;
|
|
434
|
-
activeCalls.set(callId, call);
|
|
435
|
-
if (call.providerCallId) providerCallIdMap.set(call.providerCallId, callId);
|
|
436
|
-
}
|
|
437
|
-
return {
|
|
438
|
-
activeCalls,
|
|
439
|
-
providerCallIdMap,
|
|
440
|
-
processedEventIds,
|
|
441
|
-
rejectedProviderCallIds
|
|
442
|
-
};
|
|
443
|
-
}
|
|
444
|
-
async function getCallHistoryFromStore(storePath, limit = 50) {
|
|
445
|
-
if (limit <= 0) return [];
|
|
446
|
-
const stores = tryCreateCallRecordStateStores(storePath);
|
|
447
|
-
if (stores) try {
|
|
448
|
-
return readCallRecordEvents(storePath, stores).slice(-limit);
|
|
449
|
-
} catch (err) {
|
|
450
|
-
console.error("[voice-call] Failed to read SQLite call history:", err);
|
|
451
|
-
}
|
|
452
|
-
const logPath = resolveCallLogPath(storePath);
|
|
453
|
-
const content = await privateFileStore(storePath).readTextIfExists(path.basename(logPath));
|
|
454
|
-
if (content === null) return [];
|
|
455
|
-
const lines = content.trim().split("\n").filter(Boolean);
|
|
456
|
-
const calls = [];
|
|
457
|
-
for (const [index, line] of lines.slice(-limit).entries()) {
|
|
458
|
-
const parsed = parseCallRecordLine(line, index);
|
|
459
|
-
if (parsed) calls.push(parsed.call);
|
|
460
|
-
}
|
|
461
|
-
return calls;
|
|
462
|
-
}
|
|
463
|
-
function readCallRecordsFromLegacyLog(storePath) {
|
|
464
|
-
const logPath = resolveCallLogPath(storePath);
|
|
465
|
-
const content = privateFileStoreSync(storePath).readTextIfExists(path.basename(logPath));
|
|
466
|
-
if (content === null) return [];
|
|
467
|
-
return content.split("\n").map((line, index) => parseCallRecordLine(line, index)?.call ?? null).filter((call) => call !== null);
|
|
468
|
-
}
|
|
469
|
-
//#endregion
|
|
470
68
|
//#region extensions/voice-call/src/manager/timer-delays.ts
|
|
471
69
|
function resolveVoiceCallSecondsTimerDelayMs(seconds, minMs = 1) {
|
|
472
70
|
if (!Number.isFinite(seconds)) return resolveTimerTimeoutMs(MAX_TIMER_TIMEOUT_MS, MAX_TIMER_TIMEOUT_MS, minMs);
|
|
@@ -489,15 +87,17 @@ function startMaxDurationTimer(params) {
|
|
|
489
87
|
clearMaxDurationTimer(params.ctx, params.callId);
|
|
490
88
|
const maxDurationMs = params.timeoutMs === void 0 ? resolveVoiceCallSecondsTimerDelayMs(params.ctx.config.maxDurationSeconds) : resolveVoiceCallTimerDelayMs(params.timeoutMs);
|
|
491
89
|
console.log(`[voice-call] Starting max duration timer (${Math.ceil(maxDurationMs / 1e3)}s) for call ${params.callId}`);
|
|
492
|
-
const timer = setTimeout(
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
90
|
+
const timer = setTimeout(() => {
|
|
91
|
+
(async () => {
|
|
92
|
+
params.ctx.maxDurationTimers.delete(params.callId);
|
|
93
|
+
const call = params.ctx.activeCalls.get(params.callId);
|
|
94
|
+
if (call && !TerminalStates.has(call.state)) {
|
|
95
|
+
console.log(`[voice-call] Max duration reached (${Math.ceil(maxDurationMs / 1e3)}s), ending call ${params.callId}`);
|
|
96
|
+
call.endReason = "timeout";
|
|
97
|
+
persistCallRecord(params.ctx.storePath, call);
|
|
98
|
+
await params.onTimeout(params.callId);
|
|
99
|
+
}
|
|
100
|
+
})();
|
|
501
101
|
}, maxDurationMs);
|
|
502
102
|
params.ctx.maxDurationTimers.set(params.callId, timer);
|
|
503
103
|
}
|
|
@@ -892,12 +492,14 @@ async function speakInitialMessage(ctx, providerCallId) {
|
|
|
892
492
|
const delaySec = ctx.config.outbound.notifyHangupDelaySec;
|
|
893
493
|
const delayMs = resolveVoiceCallSecondsTimerDelayMs(delaySec, 0);
|
|
894
494
|
console.log(`[voice-call] Notify mode: auto-hangup in ${delaySec}s for call ${call.callId}`);
|
|
895
|
-
setTimeout(
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
495
|
+
setTimeout(() => {
|
|
496
|
+
(async () => {
|
|
497
|
+
const currentCall = ctx.activeCalls.get(call.callId);
|
|
498
|
+
if (currentCall && !TerminalStates.has(currentCall.state)) {
|
|
499
|
+
console.log(`[voice-call] Notify mode: hanging up call ${call.callId}`);
|
|
500
|
+
await endCall(ctx, call.callId);
|
|
501
|
+
}
|
|
502
|
+
})();
|
|
901
503
|
}, delayMs);
|
|
902
504
|
} else if (mode === "conversation" && ctx.provider && shouldStartListeningAfterInitialMessage(ctx)) {
|
|
903
505
|
transitionState(call, "listening");
|
|
@@ -2188,7 +1790,9 @@ var MediaStreamHandler = class {
|
|
|
2188
1790
|
noServer: true,
|
|
2189
1791
|
maxPayload: MAX_INBOUND_MESSAGE_BYTES
|
|
2190
1792
|
});
|
|
2191
|
-
this.wss.on("connection", (ws, req) =>
|
|
1793
|
+
this.wss.on("connection", (ws, req) => {
|
|
1794
|
+
this.handleConnection(ws, req);
|
|
1795
|
+
});
|
|
2192
1796
|
}
|
|
2193
1797
|
if (this.getCurrentConnectionCount() >= this.maxConnections) {
|
|
2194
1798
|
this.rejectUpgrade(socket, 503, "Too many media stream connections");
|
|
@@ -2233,7 +1837,7 @@ var MediaStreamHandler = class {
|
|
|
2233
1837
|
ws.close(1013, "Too many pending media stream connections");
|
|
2234
1838
|
return;
|
|
2235
1839
|
}
|
|
2236
|
-
ws.on("message",
|
|
1840
|
+
ws.on("message", (data) => {
|
|
2237
1841
|
try {
|
|
2238
1842
|
const message = parseTwilioMediaMessage(data);
|
|
2239
1843
|
switch (message.event) {
|
|
@@ -2824,7 +2428,7 @@ function loadRealtimeTranscriptionRuntime() {
|
|
|
2824
2428
|
return realtimeTranscriptionRuntimePromise;
|
|
2825
2429
|
}
|
|
2826
2430
|
function loadResponseGeneratorModule() {
|
|
2827
|
-
responseGeneratorModulePromise ??= import("./response-generator-
|
|
2431
|
+
responseGeneratorModulePromise ??= import("./response-generator-D6UsHyfH.js");
|
|
2828
2432
|
return responseGeneratorModulePromise;
|
|
2829
2433
|
}
|
|
2830
2434
|
function sanitizeTranscriptForLog(value) {
|
|
@@ -3508,15 +3112,15 @@ let mockProviderPromise;
|
|
|
3508
3112
|
let realtimeVoiceRuntimePromise;
|
|
3509
3113
|
let realtimeHandlerPromise;
|
|
3510
3114
|
function loadTelnyxProvider() {
|
|
3511
|
-
telnyxProviderPromise ??= import("./telnyx-
|
|
3115
|
+
telnyxProviderPromise ??= import("./telnyx-68EYJwaz.js");
|
|
3512
3116
|
return telnyxProviderPromise;
|
|
3513
3117
|
}
|
|
3514
3118
|
function loadTwilioProvider() {
|
|
3515
|
-
twilioProviderPromise ??= import("./twilio-
|
|
3119
|
+
twilioProviderPromise ??= import("./twilio-D0dUSGam.js");
|
|
3516
3120
|
return twilioProviderPromise;
|
|
3517
3121
|
}
|
|
3518
3122
|
function loadPlivoProvider() {
|
|
3519
|
-
plivoProviderPromise ??= import("./plivo-
|
|
3123
|
+
plivoProviderPromise ??= import("./plivo-DeSZeJ0S.js");
|
|
3520
3124
|
return plivoProviderPromise;
|
|
3521
3125
|
}
|
|
3522
3126
|
function loadMockProvider() {
|
|
@@ -3528,7 +3132,7 @@ function loadRealtimeVoiceRuntime() {
|
|
|
3528
3132
|
return realtimeVoiceRuntimePromise;
|
|
3529
3133
|
}
|
|
3530
3134
|
function loadRealtimeHandler() {
|
|
3531
|
-
realtimeHandlerPromise ??= import("./realtime-handler-
|
|
3135
|
+
realtimeHandlerPromise ??= import("./realtime-handler-BP6_qBNe.js");
|
|
3532
3136
|
return realtimeHandlerPromise;
|
|
3533
3137
|
}
|
|
3534
3138
|
function resolveVoiceCallConsultSessionKey(call) {
|
|
@@ -3793,4 +3397,4 @@ async function createVoiceCallRuntime(params) {
|
|
|
3793
3397
|
}
|
|
3794
3398
|
}
|
|
3795
3399
|
//#endregion
|
|
3796
|
-
export {
|
|
3400
|
+
export { getHeader as a, getTailscaleSelfInfo as c, chunkAudio as d, resolveVoiceResponseModel as f, mapVoiceToPolly as h, normalizeProviderStatus as i, setupTailscaleExposureRoute as l, escapeXml as m, isProviderStatusTerminal as n, resolveWebhookExposureStatus as o, resolveUserPath as p, mapProviderStatusToEndReason as r, cleanupTailscaleExposureRoute as s, createVoiceCallRuntime as t, TELEPHONY_DEFAULT_TTS_TIMEOUT_MS as u };
|
package/dist/runtime-entry.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as createVoiceCallRuntime } from "./runtime-entry-
|
|
1
|
+
import { t as createVoiceCallRuntime } from "./runtime-entry-13O0Ki99.js";
|
|
2
2
|
export { createVoiceCallRuntime };
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
4
|
+
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
|
|
5
|
+
//#region extensions/voice-call/src/types.ts
|
|
6
|
+
const ProviderNameSchema = z.enum([
|
|
7
|
+
"telnyx",
|
|
8
|
+
"twilio",
|
|
9
|
+
"plivo",
|
|
10
|
+
"mock"
|
|
11
|
+
]);
|
|
12
|
+
const CallStateSchema = z.enum([
|
|
13
|
+
"initiated",
|
|
14
|
+
"ringing",
|
|
15
|
+
"answered",
|
|
16
|
+
"active",
|
|
17
|
+
"speaking",
|
|
18
|
+
"listening",
|
|
19
|
+
"completed",
|
|
20
|
+
"hangup-user",
|
|
21
|
+
"hangup-bot",
|
|
22
|
+
"timeout",
|
|
23
|
+
"error",
|
|
24
|
+
"failed",
|
|
25
|
+
"no-answer",
|
|
26
|
+
"busy",
|
|
27
|
+
"voicemail"
|
|
28
|
+
]);
|
|
29
|
+
const TerminalStates = new Set([
|
|
30
|
+
"completed",
|
|
31
|
+
"hangup-user",
|
|
32
|
+
"hangup-bot",
|
|
33
|
+
"timeout",
|
|
34
|
+
"error",
|
|
35
|
+
"failed",
|
|
36
|
+
"no-answer",
|
|
37
|
+
"busy",
|
|
38
|
+
"voicemail"
|
|
39
|
+
]);
|
|
40
|
+
const EndReasonSchema = z.enum([
|
|
41
|
+
"completed",
|
|
42
|
+
"hangup-user",
|
|
43
|
+
"hangup-bot",
|
|
44
|
+
"timeout",
|
|
45
|
+
"error",
|
|
46
|
+
"failed",
|
|
47
|
+
"no-answer",
|
|
48
|
+
"busy",
|
|
49
|
+
"voicemail"
|
|
50
|
+
]);
|
|
51
|
+
const BaseEventSchema = z.object({
|
|
52
|
+
id: z.string(),
|
|
53
|
+
dedupeKey: z.string().optional(),
|
|
54
|
+
callId: z.string(),
|
|
55
|
+
providerCallId: z.string().optional(),
|
|
56
|
+
timestamp: z.number(),
|
|
57
|
+
turnToken: z.string().optional(),
|
|
58
|
+
direction: z.enum(["inbound", "outbound"]).optional(),
|
|
59
|
+
from: z.string().optional(),
|
|
60
|
+
to: z.string().optional()
|
|
61
|
+
});
|
|
62
|
+
z.discriminatedUnion("type", [
|
|
63
|
+
BaseEventSchema.extend({ type: z.literal("call.initiated") }),
|
|
64
|
+
BaseEventSchema.extend({ type: z.literal("call.ringing") }),
|
|
65
|
+
BaseEventSchema.extend({ type: z.literal("call.answered") }),
|
|
66
|
+
BaseEventSchema.extend({ type: z.literal("call.active") }),
|
|
67
|
+
BaseEventSchema.extend({
|
|
68
|
+
type: z.literal("call.speaking"),
|
|
69
|
+
text: z.string()
|
|
70
|
+
}),
|
|
71
|
+
BaseEventSchema.extend({
|
|
72
|
+
type: z.literal("call.speech"),
|
|
73
|
+
transcript: z.string(),
|
|
74
|
+
isFinal: z.boolean(),
|
|
75
|
+
confidence: z.number().min(0).max(1).optional()
|
|
76
|
+
}),
|
|
77
|
+
BaseEventSchema.extend({
|
|
78
|
+
type: z.literal("call.silence"),
|
|
79
|
+
durationMs: z.number()
|
|
80
|
+
}),
|
|
81
|
+
BaseEventSchema.extend({
|
|
82
|
+
type: z.literal("call.dtmf"),
|
|
83
|
+
digits: z.string()
|
|
84
|
+
}),
|
|
85
|
+
BaseEventSchema.extend({
|
|
86
|
+
type: z.literal("call.ended"),
|
|
87
|
+
reason: EndReasonSchema
|
|
88
|
+
}),
|
|
89
|
+
BaseEventSchema.extend({
|
|
90
|
+
type: z.literal("call.error"),
|
|
91
|
+
error: z.string(),
|
|
92
|
+
retryable: z.boolean().optional()
|
|
93
|
+
})
|
|
94
|
+
]);
|
|
95
|
+
const CallDirectionSchema = z.enum(["outbound", "inbound"]);
|
|
96
|
+
const TranscriptEntrySchema = z.object({
|
|
97
|
+
timestamp: z.number(),
|
|
98
|
+
speaker: z.enum(["bot", "user"]),
|
|
99
|
+
text: z.string(),
|
|
100
|
+
isFinal: z.boolean().default(true)
|
|
101
|
+
});
|
|
102
|
+
const CallRecordSchema = z.object({
|
|
103
|
+
callId: z.string(),
|
|
104
|
+
providerCallId: z.string().optional(),
|
|
105
|
+
provider: ProviderNameSchema,
|
|
106
|
+
direction: CallDirectionSchema,
|
|
107
|
+
state: CallStateSchema,
|
|
108
|
+
from: z.string(),
|
|
109
|
+
to: z.string(),
|
|
110
|
+
sessionKey: z.string().optional(),
|
|
111
|
+
startedAt: z.number(),
|
|
112
|
+
answeredAt: z.number().optional(),
|
|
113
|
+
endedAt: z.number().optional(),
|
|
114
|
+
endReason: EndReasonSchema.optional(),
|
|
115
|
+
transcript: z.array(TranscriptEntrySchema).default([]),
|
|
116
|
+
processedEventIds: z.array(z.string()).default([]),
|
|
117
|
+
metadata: z.record(z.string(), z.unknown()).optional()
|
|
118
|
+
});
|
|
119
|
+
//#endregion
|
|
120
|
+
//#region extensions/voice-call/src/runtime-state.ts
|
|
121
|
+
const { setRuntime: setVoiceCallStateRuntime, clearRuntime: clearVoiceCallStateRuntime, tryGetRuntime: getOptionalVoiceCallStateRuntime } = createPluginRuntimeStore({
|
|
122
|
+
pluginId: "voice-call-state",
|
|
123
|
+
errorMessage: "Voice Call state runtime not initialized"
|
|
124
|
+
});
|
|
125
|
+
//#endregion
|
|
126
|
+
//#region extensions/voice-call/src/manager/store.ts
|
|
127
|
+
const CALL_RECORD_EVENTS_NAMESPACE = "call-record-events";
|
|
128
|
+
const CALL_RECORD_EVENT_CHUNKS_NAMESPACE = "call-record-event-chunks";
|
|
129
|
+
const MAX_CALL_RECORD_EVENTS = 1e3;
|
|
130
|
+
const CALL_RECORD_EVENT_META_MAX_ENTRIES = 1100;
|
|
131
|
+
const CALL_RECORD_CHUNK_MAX_ENTRIES = 48048;
|
|
132
|
+
const RAW_CALL_RECORD_CHUNK_BYTES = 47 * 1024;
|
|
133
|
+
let callRecordEventSequence = 0;
|
|
134
|
+
function resolveVoiceCallLegacyCallLogPath(storePath) {
|
|
135
|
+
return path.join(storePath, "calls.jsonl");
|
|
136
|
+
}
|
|
137
|
+
function resolvePluginStateEnv(storePath) {
|
|
138
|
+
return {
|
|
139
|
+
...process.env,
|
|
140
|
+
OPENCLAW_STATE_DIR: storePath
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function createCallRecordStateStores(storePath) {
|
|
144
|
+
const runtime = getOptionalVoiceCallStateRuntime();
|
|
145
|
+
if (!runtime) return null;
|
|
146
|
+
const env = resolvePluginStateEnv(storePath);
|
|
147
|
+
return {
|
|
148
|
+
events: runtime.state.openSyncKeyedStore({
|
|
149
|
+
namespace: CALL_RECORD_EVENTS_NAMESPACE,
|
|
150
|
+
maxEntries: CALL_RECORD_EVENT_META_MAX_ENTRIES,
|
|
151
|
+
env
|
|
152
|
+
}),
|
|
153
|
+
chunks: runtime.state.openSyncKeyedStore({
|
|
154
|
+
namespace: CALL_RECORD_EVENT_CHUNKS_NAMESPACE,
|
|
155
|
+
maxEntries: CALL_RECORD_CHUNK_MAX_ENTRIES,
|
|
156
|
+
env
|
|
157
|
+
})
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function tryCreateCallRecordStateStores(storePath) {
|
|
161
|
+
try {
|
|
162
|
+
return createCallRecordStateStores(storePath);
|
|
163
|
+
} catch (err) {
|
|
164
|
+
console.error("[voice-call] Failed to open SQLite call record store:", err);
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function buildChunkKey(eventKey, index) {
|
|
169
|
+
return `${eventKey}:chunk:${String(index).padStart(4, "0")}`;
|
|
170
|
+
}
|
|
171
|
+
function buildVoiceCallLegacyJsonlEventKey(line, index) {
|
|
172
|
+
return `jsonl:${String(index).padStart(8, "0")}:${createHash("sha256").update(line).digest("hex")}`;
|
|
173
|
+
}
|
|
174
|
+
function nextCallRecordOrder() {
|
|
175
|
+
const sequence = callRecordEventSequence;
|
|
176
|
+
callRecordEventSequence = (callRecordEventSequence + 1) % 1e6;
|
|
177
|
+
return {
|
|
178
|
+
persistedAt: Date.now(),
|
|
179
|
+
sequence
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
function buildNewEventKey(order) {
|
|
183
|
+
return `event:${order.persistedAt.toString(36)}:${String(order.sequence).padStart(6, "0")}:${randomUUID()}`;
|
|
184
|
+
}
|
|
185
|
+
function parseEventKeySequence(key) {
|
|
186
|
+
const match = /^event:[^:]+:(\d+):/.exec(key);
|
|
187
|
+
return match ? Number.parseInt(match[1], 10) : 0;
|
|
188
|
+
}
|
|
189
|
+
function parseVoiceCallRecordLine(line, sequence = 0) {
|
|
190
|
+
if (!line.trim()) return null;
|
|
191
|
+
try {
|
|
192
|
+
const parsed = JSON.parse(line);
|
|
193
|
+
if (parsed && typeof parsed === "object" && parsed.version === 2) {
|
|
194
|
+
const envelope = parsed;
|
|
195
|
+
return {
|
|
196
|
+
call: CallRecordSchema.parse(envelope.call),
|
|
197
|
+
persistedAt: typeof envelope.persistedAt === "number" && Number.isFinite(envelope.persistedAt) ? envelope.persistedAt : 0,
|
|
198
|
+
sequence: typeof envelope.sequence === "number" && Number.isFinite(envelope.sequence) ? envelope.sequence : sequence,
|
|
199
|
+
orderKey: ""
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
return {
|
|
203
|
+
call: CallRecordSchema.parse(parsed),
|
|
204
|
+
persistedAt: 0,
|
|
205
|
+
sequence,
|
|
206
|
+
orderKey: ""
|
|
207
|
+
};
|
|
208
|
+
} catch {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function countCallRecordChunks(call) {
|
|
213
|
+
return Math.max(1, Math.ceil(Buffer.byteLength(JSON.stringify(call), "utf8") / RAW_CALL_RECORD_CHUNK_BYTES));
|
|
214
|
+
}
|
|
215
|
+
function prepareVoiceCallRecordForStorage(call) {
|
|
216
|
+
if (countCallRecordChunks(call) <= 48) return call;
|
|
217
|
+
const transcriptEntries = call.transcript.length;
|
|
218
|
+
const metadata = {
|
|
219
|
+
...call.metadata,
|
|
220
|
+
voiceCallPersistence: {
|
|
221
|
+
transcriptTruncated: true,
|
|
222
|
+
originalTranscriptEntries: transcriptEntries
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
const candidateInputs = [
|
|
226
|
+
{
|
|
227
|
+
transcript: call.transcript.slice(-20),
|
|
228
|
+
metadata
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
transcript: [],
|
|
232
|
+
metadata
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
transcript: [],
|
|
236
|
+
metadata: { voiceCallPersistence: {
|
|
237
|
+
transcriptTruncated: true,
|
|
238
|
+
originalTranscriptEntries: transcriptEntries,
|
|
239
|
+
metadataTruncated: true
|
|
240
|
+
} }
|
|
241
|
+
}
|
|
242
|
+
];
|
|
243
|
+
for (const candidateInput of candidateInputs) {
|
|
244
|
+
const candidate = CallRecordSchema.parse({
|
|
245
|
+
...call,
|
|
246
|
+
...candidateInput
|
|
247
|
+
});
|
|
248
|
+
if (countCallRecordChunks(candidate) <= 48) return candidate;
|
|
249
|
+
}
|
|
250
|
+
return call;
|
|
251
|
+
}
|
|
252
|
+
function registerCallRecordEvent(stores, eventKey, call, order) {
|
|
253
|
+
const serialized = JSON.stringify(prepareVoiceCallRecordForStorage(call));
|
|
254
|
+
const buffer = Buffer.from(serialized, "utf8");
|
|
255
|
+
const chunkCount = Math.max(1, Math.ceil(buffer.byteLength / RAW_CALL_RECORD_CHUNK_BYTES));
|
|
256
|
+
if (chunkCount > 48) throw new Error(`voice-call record exceeds SQLite chunk limit (${chunkCount}/48)`);
|
|
257
|
+
for (let index = 0; index < chunkCount; index += 1) {
|
|
258
|
+
const chunk = buffer.subarray(index * RAW_CALL_RECORD_CHUNK_BYTES, (index + 1) * RAW_CALL_RECORD_CHUNK_BYTES);
|
|
259
|
+
stores.chunks.register(buildChunkKey(eventKey, index), {
|
|
260
|
+
index,
|
|
261
|
+
dataBase64: chunk.toString("base64")
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
stores.events.register(eventKey, {
|
|
265
|
+
chunkCount,
|
|
266
|
+
byteLength: buffer.byteLength,
|
|
267
|
+
persistedAt: order?.persistedAt,
|
|
268
|
+
sequence: order?.sequence
|
|
269
|
+
});
|
|
270
|
+
pruneCallRecordEvents(stores);
|
|
271
|
+
}
|
|
272
|
+
function deleteCallRecordEventRows(stores, eventKey) {
|
|
273
|
+
const meta = stores.events.lookup(eventKey);
|
|
274
|
+
stores.events.delete(eventKey);
|
|
275
|
+
if (!meta) return;
|
|
276
|
+
for (let index = 0; index < meta.chunkCount; index += 1) stores.chunks.delete(buildChunkKey(eventKey, index));
|
|
277
|
+
}
|
|
278
|
+
function pruneCallRecordEvents(stores) {
|
|
279
|
+
const rows = stores.events.entries();
|
|
280
|
+
if (rows.length <= 1e3) return;
|
|
281
|
+
const sorted = rows.toSorted((a, b) => a.createdAt - b.createdAt || a.key.localeCompare(b.key));
|
|
282
|
+
for (const row of sorted.slice(0, rows.length - MAX_CALL_RECORD_EVENTS)) deleteCallRecordEventRows(stores, row.key);
|
|
283
|
+
}
|
|
284
|
+
function readCallRecordEvent(stores, eventKey) {
|
|
285
|
+
const meta = stores.events.lookup(eventKey);
|
|
286
|
+
if (!meta) return null;
|
|
287
|
+
const chunks = [];
|
|
288
|
+
for (let index = 0; index < meta.chunkCount; index += 1) {
|
|
289
|
+
const chunk = stores.chunks.lookup(buildChunkKey(eventKey, index));
|
|
290
|
+
if (!chunk || chunk.index !== index) return null;
|
|
291
|
+
chunks.push(Buffer.from(chunk.dataBase64, "base64"));
|
|
292
|
+
}
|
|
293
|
+
return parseVoiceCallRecordLine(Buffer.concat(chunks, meta.byteLength).toString("utf8"))?.call ?? null;
|
|
294
|
+
}
|
|
295
|
+
function readCallRecordEvents(stores) {
|
|
296
|
+
return stores.events.entries().toSorted((a, b) => a.createdAt - b.createdAt || a.key.localeCompare(b.key)).map((entry) => {
|
|
297
|
+
const call = readCallRecordEvent(stores, entry.key);
|
|
298
|
+
return call ? {
|
|
299
|
+
call,
|
|
300
|
+
persistedAt: entry.value.persistedAt ?? entry.createdAt,
|
|
301
|
+
sequence: entry.value.sequence ?? parseEventKeySequence(entry.key),
|
|
302
|
+
orderKey: entry.key
|
|
303
|
+
} : null;
|
|
304
|
+
}).filter((entry) => entry !== null).toSorted((a, b) => a.persistedAt - b.persistedAt || a.sequence - b.sequence || a.orderKey.localeCompare(b.orderKey)).map((entry) => entry.call);
|
|
305
|
+
}
|
|
306
|
+
function persistCallRecord(storePath, call) {
|
|
307
|
+
try {
|
|
308
|
+
const stores = createCallRecordStateStores(storePath);
|
|
309
|
+
if (!stores) throw new Error("Voice Call state runtime not initialized");
|
|
310
|
+
const order = nextCallRecordOrder();
|
|
311
|
+
registerCallRecordEvent(stores, buildNewEventKey(order), call, order);
|
|
312
|
+
} catch (err) {
|
|
313
|
+
console.error("[voice-call] Failed to persist call record:", err);
|
|
314
|
+
throw err;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
function loadActiveCallsFromStore(storePath) {
|
|
318
|
+
const stores = tryCreateCallRecordStateStores(storePath);
|
|
319
|
+
let calls = [];
|
|
320
|
+
try {
|
|
321
|
+
calls = stores ? readCallRecordEvents(stores) : [];
|
|
322
|
+
} catch (err) {
|
|
323
|
+
console.error("[voice-call] Failed to read SQLite call records:", err);
|
|
324
|
+
}
|
|
325
|
+
if (calls.length === 0) return {
|
|
326
|
+
activeCalls: /* @__PURE__ */ new Map(),
|
|
327
|
+
providerCallIdMap: /* @__PURE__ */ new Map(),
|
|
328
|
+
processedEventIds: /* @__PURE__ */ new Set(),
|
|
329
|
+
rejectedProviderCallIds: /* @__PURE__ */ new Set()
|
|
330
|
+
};
|
|
331
|
+
const callMap = /* @__PURE__ */ new Map();
|
|
332
|
+
for (const call of calls) callMap.set(call.callId, call);
|
|
333
|
+
const activeCalls = /* @__PURE__ */ new Map();
|
|
334
|
+
const providerCallIdMap = /* @__PURE__ */ new Map();
|
|
335
|
+
const processedEventIds = /* @__PURE__ */ new Set();
|
|
336
|
+
const rejectedProviderCallIds = /* @__PURE__ */ new Set();
|
|
337
|
+
for (const [callId, call] of callMap) {
|
|
338
|
+
for (const eventId of call.processedEventIds) processedEventIds.add(eventId);
|
|
339
|
+
if (TerminalStates.has(call.state)) continue;
|
|
340
|
+
activeCalls.set(callId, call);
|
|
341
|
+
if (call.providerCallId) providerCallIdMap.set(call.providerCallId, callId);
|
|
342
|
+
}
|
|
343
|
+
return {
|
|
344
|
+
activeCalls,
|
|
345
|
+
providerCallIdMap,
|
|
346
|
+
processedEventIds,
|
|
347
|
+
rejectedProviderCallIds
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
async function getCallHistoryFromStore(storePath, limit = 50) {
|
|
351
|
+
if (limit <= 0) return [];
|
|
352
|
+
const stores = tryCreateCallRecordStateStores(storePath);
|
|
353
|
+
if (stores) try {
|
|
354
|
+
return readCallRecordEvents(stores).slice(-limit);
|
|
355
|
+
} catch (err) {
|
|
356
|
+
console.error("[voice-call] Failed to read SQLite call history:", err);
|
|
357
|
+
}
|
|
358
|
+
return [];
|
|
359
|
+
}
|
|
360
|
+
//#endregion
|
|
361
|
+
export { MAX_CALL_RECORD_EVENTS as a, getCallHistoryFromStore as c, persistCallRecord as d, prepareVoiceCallRecordForStorage as f, TerminalStates as h, CALL_RECORD_EVENT_META_MAX_ENTRIES as i, loadActiveCallsFromStore as l, setVoiceCallStateRuntime as m, CALL_RECORD_EVENTS_NAMESPACE as n, RAW_CALL_RECORD_CHUNK_BYTES as o, resolveVoiceCallLegacyCallLogPath as p, CALL_RECORD_EVENT_CHUNKS_NAMESPACE as r, buildVoiceCallLegacyJsonlEventKey as s, CALL_RECORD_CHUNK_MAX_ENTRIES as t, parseVoiceCallRecordLine as u };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as verifyTelnyxWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-
|
|
1
|
+
import { i as verifyTelnyxWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-tyxHyrev.js";
|
|
2
2
|
import crypto from "node:crypto";
|
|
3
3
|
//#region extensions/voice-call/src/providers/telnyx.ts
|
|
4
4
|
function normalizeTelnyxDirection(direction) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { fetchWithSsrFGuard } from "./runtime-api.js";
|
|
2
2
|
import "./api.js";
|
|
3
|
-
import { a as getHeader, d as chunkAudio, h as mapVoiceToPolly, i as normalizeProviderStatus, m as escapeXml, n as isProviderStatusTerminal, r as mapProviderStatusToEndReason } from "./runtime-entry-
|
|
4
|
-
import { a as verifyTwilioWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-
|
|
3
|
+
import { a as getHeader, d as chunkAudio, h as mapVoiceToPolly, i as normalizeProviderStatus, m as escapeXml, n as isProviderStatusTerminal, r as mapProviderStatusToEndReason } from "./runtime-entry-13O0Ki99.js";
|
|
4
|
+
import { a as verifyTwilioWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-tyxHyrev.js";
|
|
5
5
|
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
6
6
|
import crypto from "node:crypto";
|
|
7
7
|
import { safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
|
|
@@ -251,17 +251,15 @@ var TwilioProvider = class TwilioProvider {
|
|
|
251
251
|
});
|
|
252
252
|
}
|
|
253
253
|
async updateLiveCallTwiml(providerCallId, twiml, operation) {
|
|
254
|
-
|
|
255
|
-
while (true) try {
|
|
254
|
+
for (const retryDelayMs of TWILIO_CALL_UPDATE_RETRY_DELAYS_MS) try {
|
|
256
255
|
await this.apiRequest(`/Calls/${providerCallId}.json`, { Twiml: twiml });
|
|
257
256
|
return;
|
|
258
257
|
} catch (err) {
|
|
259
|
-
|
|
260
|
-
if (retryDelayMs === void 0 || !isTwilioCallNotInProgressError(err)) throw err;
|
|
261
|
-
retryIndex += 1;
|
|
258
|
+
if (!isTwilioCallNotInProgressError(err)) throw err;
|
|
262
259
|
console.warn(`[voice-call] Twilio ${operation} update hit call state race (21220); retrying in ${retryDelayMs}ms`);
|
|
263
260
|
await setTimeout$1(retryDelayMs);
|
|
264
261
|
}
|
|
262
|
+
await this.apiRequest(`/Calls/${providerCallId}.json`, { Twiml: twiml });
|
|
265
263
|
}
|
|
266
264
|
/**
|
|
267
265
|
* Verify Twilio webhook signature using HMAC-SHA1.
|
|
@@ -618,7 +616,9 @@ var TwilioProvider = class TwilioProvider {
|
|
|
618
616
|
chunkAttempts += 1;
|
|
619
617
|
if (sendAudioChunk(chunk).sent) chunkDelivered += 1;
|
|
620
618
|
const waitMs = nextChunkDueAt - Date.now();
|
|
621
|
-
if (waitMs > 0) await new Promise((resolve) =>
|
|
619
|
+
if (waitMs > 0) await new Promise((resolve) => {
|
|
620
|
+
setTimeout(resolve, Math.ceil(waitMs));
|
|
621
|
+
});
|
|
622
622
|
nextChunkDueAt += CHUNK_DELAY_MS;
|
|
623
623
|
if (signal.aborted) break;
|
|
624
624
|
}
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/voice-call",
|
|
3
|
-
"version": "2026.5.31-beta.
|
|
3
|
+
"version": "2026.5.31-beta.4",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@openclaw/voice-call",
|
|
9
|
-
"version": "2026.5.31-beta.
|
|
9
|
+
"version": "2026.5.31-beta.4",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"commander": "14.0.3",
|
|
12
12
|
"typebox": "1.1.39",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"zod": "4.4.3"
|
|
15
15
|
},
|
|
16
16
|
"peerDependencies": {
|
|
17
|
-
"openclaw": ">=2026.5.31-beta.
|
|
17
|
+
"openclaw": ">=2026.5.31-beta.4"
|
|
18
18
|
},
|
|
19
19
|
"peerDependenciesMeta": {
|
|
20
20
|
"openclaw": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/voice-call",
|
|
3
|
-
"version": "2026.5.31-beta.
|
|
3
|
+
"version": "2026.5.31-beta.4",
|
|
4
4
|
"description": "OpenClaw voice-call plugin for Twilio, Telnyx, and Plivo phone calls.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"zod": "4.4.3"
|
|
15
15
|
},
|
|
16
16
|
"peerDependencies": {
|
|
17
|
-
"openclaw": ">=2026.5.31-beta.
|
|
17
|
+
"openclaw": ">=2026.5.31-beta.4"
|
|
18
18
|
},
|
|
19
19
|
"peerDependenciesMeta": {
|
|
20
20
|
"openclaw": {
|
|
@@ -31,10 +31,10 @@
|
|
|
31
31
|
"minHostVersion": ">=2026.4.10"
|
|
32
32
|
},
|
|
33
33
|
"compat": {
|
|
34
|
-
"pluginApi": ">=2026.5.31-beta.
|
|
34
|
+
"pluginApi": ">=2026.5.31-beta.4"
|
|
35
35
|
},
|
|
36
36
|
"build": {
|
|
37
|
-
"openclawVersion": "2026.5.31-beta.
|
|
37
|
+
"openclawVersion": "2026.5.31-beta.4"
|
|
38
38
|
},
|
|
39
39
|
"release": {
|
|
40
40
|
"publishToClawHub": true,
|