@openclaw/voice-call 2026.5.31-beta.2 → 2026.5.31-beta.3

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-jfYyJL2N.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-jfYyJL2N.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-jfYyJL2N.js";
2
+ import { n as reconstructWebhookUrl, r as verifyPlivoWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-Xqw21Tku.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
@@ -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") {
@@ -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-jfYyJL2N.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);
@@ -2824,7 +2422,7 @@ function loadRealtimeTranscriptionRuntime() {
2824
2422
  return realtimeTranscriptionRuntimePromise;
2825
2423
  }
2826
2424
  function loadResponseGeneratorModule() {
2827
- responseGeneratorModulePromise ??= import("./response-generator-Cu4sOiUO.js");
2425
+ responseGeneratorModulePromise ??= import("./response-generator-Drtih8-c.js");
2828
2426
  return responseGeneratorModulePromise;
2829
2427
  }
2830
2428
  function sanitizeTranscriptForLog(value) {
@@ -3508,15 +3106,15 @@ let mockProviderPromise;
3508
3106
  let realtimeVoiceRuntimePromise;
3509
3107
  let realtimeHandlerPromise;
3510
3108
  function loadTelnyxProvider() {
3511
- telnyxProviderPromise ??= import("./telnyx-CxEuPBOJ.js");
3109
+ telnyxProviderPromise ??= import("./telnyx-nVwnn20d.js");
3512
3110
  return telnyxProviderPromise;
3513
3111
  }
3514
3112
  function loadTwilioProvider() {
3515
- twilioProviderPromise ??= import("./twilio-DzQmSUQP.js");
3113
+ twilioProviderPromise ??= import("./twilio-PJ1mjc8C.js");
3516
3114
  return twilioProviderPromise;
3517
3115
  }
3518
3116
  function loadPlivoProvider() {
3519
- plivoProviderPromise ??= import("./plivo-Dpf08ZVJ.js");
3117
+ plivoProviderPromise ??= import("./plivo-9ceKrjuk.js");
3520
3118
  return plivoProviderPromise;
3521
3119
  }
3522
3120
  function loadMockProvider() {
@@ -3528,7 +3126,7 @@ function loadRealtimeVoiceRuntime() {
3528
3126
  return realtimeVoiceRuntimePromise;
3529
3127
  }
3530
3128
  function loadRealtimeHandler() {
3531
- realtimeHandlerPromise ??= import("./realtime-handler-Crs3kty2.js");
3129
+ realtimeHandlerPromise ??= import("./realtime-handler-RU9ZmesO.js");
3532
3130
  return realtimeHandlerPromise;
3533
3131
  }
3534
3132
  function resolveVoiceCallConsultSessionKey(call) {
@@ -3793,4 +3391,4 @@ async function createVoiceCallRuntime(params) {
3793
3391
  }
3794
3392
  }
3795
3393
  //#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 };
3394
+ 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-jfYyJL2N.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-Xqw21Tku.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-jfYyJL2N.js";
4
+ import { a as verifyTwilioWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-Xqw21Tku.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";
@@ -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.3",
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.3",
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.3"
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.3",
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.3"
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.3"
35
35
  },
36
36
  "build": {
37
- "openclawVersion": "2026.5.31-beta.2"
37
+ "openclawVersion": "2026.5.31-beta.3"
38
38
  },
39
39
  "release": {
40
40
  "publishToClawHub": true,