@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.
@@ -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-DXf7kdXt.js";
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 { _ as setVoiceCallStateRuntime, c as getTailscaleSelfInfo, g as getCallHistoryFromStore, l as setupTailscaleExposureRoute, o as resolveWebhookExposureStatus, p as resolveUserPath, s as cleanupTailscaleExposureRoute, t as createVoiceCallRuntime } from "./runtime-entry-DXf7kdXt.js";
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-DXf7kdXt.js";
2
- import { n as reconstructWebhookUrl, r as verifyPlivoWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-mCcsLRBb.js";
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 = true;
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, session) => {
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(session, callId, toolEvent.callId || toolEvent.itemId, toolEvent.name, toolEvent.args, turnId, emitTalkEvent);
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) => setTimeout(resolve, Math.min(CONSULT_TRANSCRIPT_SETTLE_MS - quietFor, deadline - now)));
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-DXf7kdXt.js";
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, { createHash, randomUUID } from "node:crypto";
14
- import { appendRegularFile, privateFileStore, privateFileStoreSync, root } from "openclaw/plugin-sdk/security-runtime";
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(async () => {
493
- params.ctx.maxDurationTimers.delete(params.callId);
494
- const call = params.ctx.activeCalls.get(params.callId);
495
- if (call && !TerminalStates.has(call.state)) {
496
- console.log(`[voice-call] Max duration reached (${Math.ceil(maxDurationMs / 1e3)}s), ending call ${params.callId}`);
497
- call.endReason = "timeout";
498
- persistCallRecord(params.ctx.storePath, call);
499
- await params.onTimeout(params.callId);
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(async () => {
896
- const currentCall = ctx.activeCalls.get(call.callId);
897
- if (currentCall && !TerminalStates.has(currentCall.state)) {
898
- console.log(`[voice-call] Notify mode: hanging up call ${call.callId}`);
899
- await endCall(ctx, call.callId);
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) => this.handleConnection(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", async (data) => {
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-Cu4sOiUO.js");
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-CxEuPBOJ.js");
3115
+ telnyxProviderPromise ??= import("./telnyx-68EYJwaz.js");
3512
3116
  return telnyxProviderPromise;
3513
3117
  }
3514
3118
  function loadTwilioProvider() {
3515
- twilioProviderPromise ??= import("./twilio-DzQmSUQP.js");
3119
+ twilioProviderPromise ??= import("./twilio-D0dUSGam.js");
3516
3120
  return twilioProviderPromise;
3517
3121
  }
3518
3122
  function loadPlivoProvider() {
3519
- plivoProviderPromise ??= import("./plivo-Dpf08ZVJ.js");
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-Crs3kty2.js");
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 { setVoiceCallStateRuntime as _, getHeader as a, getTailscaleSelfInfo as c, chunkAudio as d, resolveVoiceResponseModel as f, getCallHistoryFromStore as g, 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 };
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 };
@@ -1,2 +1,2 @@
1
- import { t as createVoiceCallRuntime } from "./runtime-entry-DXf7kdXt.js";
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-mCcsLRBb.js";
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-DXf7kdXt.js";
4
- import { a as verifyTwilioWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-mCcsLRBb.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-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
- let retryIndex = 0;
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
- const retryDelayMs = TWILIO_CALL_UPDATE_RETRY_DELAYS_MS[retryIndex];
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) => setTimeout(resolve, Math.ceil(waitMs)));
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
  }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@openclaw/voice-call",
3
- "version": "2026.5.31-beta.2",
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.2",
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.2"
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.2",
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.2"
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.2"
34
+ "pluginApi": ">=2026.5.31-beta.4"
35
35
  },
36
36
  "build": {
37
- "openclawVersion": "2026.5.31-beta.2"
37
+ "openclawVersion": "2026.5.31-beta.4"
38
38
  },
39
39
  "release": {
40
40
  "publishToClawHub": true,