@openclaw/voice-call 2026.6.1 → 2026.6.5-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,13 +1,16 @@
1
- import { t as VoiceCallConfigSchema } from "./config-BKyRNKHF.js";
1
+ import { t as VoiceCallConfigSchema } from "./config-jaA6vOZ2.js";
2
2
  import { asOptionalRecord, readStringField } from "openclaw/plugin-sdk/string-coerce-runtime";
3
3
  //#region extensions/voice-call/src/config-compat.ts
4
+ /** Version where legacy voice-call config shape support is removed. */
4
5
  const VOICE_CALL_LEGACY_CONFIG_REMOVAL_VERSION = "2026.6.0";
5
6
  const asObject = asOptionalRecord;
6
7
  const getString = readStringField;
8
+ /** Read finite numeric config values. */
7
9
  function getNumber(obj, key) {
8
10
  const value = obj?.[key];
9
11
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
10
12
  }
13
+ /** Merge legacy provider-specific values into the canonical providers map. */
11
14
  function mergeProviderConfig(providersValue, providerId, compatValues) {
12
15
  if (Object.keys(compatValues).length === 0) return asObject(providersValue);
13
16
  const providers = asObject(providersValue) ?? {};
@@ -20,6 +23,7 @@ function mergeProviderConfig(providersValue, providerId, compatValues) {
20
23
  }
21
24
  };
22
25
  }
26
+ /** Collect legacy voice-call config keys that should be migrated. */
23
27
  function collectVoiceCallLegacyConfigIssues(value) {
24
28
  const raw = asObject(value) ?? {};
25
29
  const realtimeAgentContext = asObject(asObject(raw.realtime)?.agentContext);
@@ -68,11 +72,13 @@ function collectVoiceCallLegacyConfigIssues(value) {
68
72
  });
69
73
  return issues;
70
74
  }
75
+ /** Format runtime warnings for legacy voice-call config keys. */
71
76
  function formatVoiceCallLegacyConfigWarnings(params) {
72
77
  const issues = collectVoiceCallLegacyConfigIssues(params.value);
73
78
  if (issues.length === 0) return [];
74
79
  return [`[voice-call] legacy config keys detected under ${params.configPathPrefix}; runtime loading will not rewrite them, and support for the legacy shape will be removed in ${VOICE_CALL_LEGACY_CONFIG_REMOVAL_VERSION}. Run "${params.doctorFixCommand}".`, ...issues.map((issue) => `[voice-call] ${params.configPathPrefix}.${issue.path}: ${issue.message}`)];
75
80
  }
81
+ /** Migrate legacy voice-call config input to the current canonical shape. */
76
82
  function migrateVoiceCallLegacyConfigInput(params) {
77
83
  const raw = asObject(params.value) ?? {};
78
84
  const realtime = asObject(raw.realtime);
@@ -137,9 +143,11 @@ function migrateVoiceCallLegacyConfigInput(params) {
137
143
  issues
138
144
  };
139
145
  }
146
+ /** Normalize legacy voice-call config input without returning migration metadata. */
140
147
  function normalizeVoiceCallLegacyConfigInput(value) {
141
148
  return migrateVoiceCallLegacyConfigInput({ value }).config;
142
149
  }
150
+ /** Parse voice-call plugin config after applying legacy normalization. */
143
151
  function parseVoiceCallPluginConfig(value) {
144
152
  return VoiceCallConfigSchema.parse(normalizeVoiceCallLegacyConfigInput(value));
145
153
  }
@@ -10,6 +10,7 @@ const BLOCKED_MERGE_KEYS = new Set([
10
10
  "prototype",
11
11
  "constructor"
12
12
  ]);
13
+ /** Deep-merge plain objects, keeping base values when overrides are undefined. */
13
14
  function deepMergeDefined(base, override) {
14
15
  if (!isRecord(base) || !isRecord(override)) return override === void 0 ? base : override;
15
16
  const result = { ...base };
@@ -22,6 +23,7 @@ function deepMergeDefined(base, override) {
22
23
  }
23
24
  //#endregion
24
25
  //#region extensions/voice-call/src/realtime-defaults.ts
26
+ /** Baseline instructions that keep realtime calls brief and route deep work to agent consult. */
25
27
  const DEFAULT_VOICE_CALL_REALTIME_INSTRUCTIONS = `You are OpenClaw's phone-call realtime voice interface. Keep spoken replies brief and natural. When a question needs deeper reasoning, current information, or tools, call ${REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME} before answering.`;
26
28
  //#endregion
27
29
  //#region extensions/voice-call/src/config.ts
@@ -1,17 +1,20 @@
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";
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-8M2m4Isq.js";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
  import fs from "node:fs/promises";
5
5
  //#region extensions/voice-call/doctor-contract-api.ts
6
+ /** Resolve home from doctor env with OS fallback. */
6
7
  function resolveHome(env) {
7
8
  return env.HOME?.trim() || os.homedir();
8
9
  }
10
+ /** Resolve config paths, including "~", against the doctor env home. */
9
11
  function resolveUserPath(input, env) {
10
12
  const trimmed = input.trim();
11
13
  if (!trimmed) return trimmed;
12
14
  if (trimmed.startsWith("~")) return path.resolve(trimmed.replace(/^~(?=$|[\\/])/, resolveHome(env)));
13
15
  return path.resolve(trimmed);
14
16
  }
17
+ /** Read the configured voice-call store path from either package id. */
15
18
  function getVoiceCallConfigStore(config) {
16
19
  for (const pluginId of ["voice-call", "@openclaw/voice-call"]) {
17
20
  const rawConfig = config.plugins?.entries?.[pluginId]?.config;
@@ -21,11 +24,13 @@ function getVoiceCallConfigStore(config) {
21
24
  }
22
25
  return "";
23
26
  }
27
+ /** Resolve the voice-call store path used by legacy and plugin-state call records. */
24
28
  function resolveVoiceCallStorePath(params) {
25
29
  const configuredStore = getVoiceCallConfigStore(params.config);
26
30
  if (configuredStore) return resolveUserPath(configuredStore, params.env);
27
31
  return path.join(resolveHome(params.env), ".openclaw", "voice-calls");
28
32
  }
33
+ /** Return true when a path exists and is a file. */
29
34
  async function fileExists(filePath) {
30
35
  try {
31
36
  return (await fs.stat(filePath)).isFile();
@@ -33,9 +38,11 @@ async function fileExists(filePath) {
33
38
  return false;
34
39
  }
35
40
  }
41
+ /** Build the plugin state key for one migrated event chunk. */
36
42
  function buildChunkKey(eventKey, index) {
37
43
  return `${eventKey}:chunk:${String(index).padStart(4, "0")}`;
38
44
  }
45
+ /** Chunk a prepared call record into bounded plugin state rows. */
39
46
  function prepareChunks(call) {
40
47
  const serialized = JSON.stringify(prepareVoiceCallRecordForStorage(call));
41
48
  const buffer = Buffer.from(serialized, "utf8");
@@ -57,6 +64,7 @@ function prepareChunks(call) {
57
64
  }
58
65
  };
59
66
  }
67
+ /** Read and prepare legacy JSONL call records, collecting line-level warnings. */
60
68
  async function readLegacyCallRecords(filePath) {
61
69
  let content;
62
70
  try {
@@ -99,6 +107,7 @@ async function readLegacyCallRecords(filePath) {
99
107
  warnings
100
108
  };
101
109
  }
110
+ /** Archive the legacy JSONL source after a complete migration. */
102
111
  async function archiveLegacySource(params) {
103
112
  const archivedPath = `${params.filePath}.migrated`;
104
113
  if (await fileExists(archivedPath)) {
@@ -112,6 +121,7 @@ async function archiveLegacySource(params) {
112
121
  params.warnings.push(`Failed archiving Voice Call call-log legacy source: ${String(err)}`);
113
122
  }
114
123
  }
124
+ /** Select newest missing records that fit remaining plugin state capacity. */
115
125
  async function selectEntriesForImport(params) {
116
126
  const existingEventKeys = new Set((await params.eventStore.entries()).map((entry) => entry.key));
117
127
  const missingEntries = params.entries.filter((entry) => !existingEventKeys.has(entry.eventKey));
@@ -135,6 +145,7 @@ async function selectEntriesForImport(params) {
135
145
  entries: selected.toReversed()
136
146
  };
137
147
  }
148
+ /** Import prepared legacy call records into plugin state. */
138
149
  async function importLegacyCallRecords(params) {
139
150
  const selected = await selectEntriesForImport(params);
140
151
  let imported = 0;
@@ -151,6 +162,7 @@ async function importLegacyCallRecords(params) {
151
162
  }
152
163
  return imported;
153
164
  }
165
+ /** Doctor migrations owned by the voice-call plugin. */
154
166
  const stateMigrations = [{
155
167
  id: "voice-call-calls-jsonl-to-plugin-state",
156
168
  label: "Voice Call call log",
@@ -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-13O0Ki99.js";
3
+ import { a as getHeader } from "./runtime-entry-GVY8iEzX.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";
@@ -561,6 +561,7 @@ function verifyPlivoWebhook(ctx, authToken, options) {
561
561
  }
562
562
  //#endregion
563
563
  //#region extensions/voice-call/src/providers/shared/guarded-json-api.ts
564
+ /** Send a provider JSON request through the SSRF guard and parse bounded JSON responses. */
564
565
  async function guardedJsonApiRequest(params) {
565
566
  const { response, release } = await fetchWithSsrFGuard({
566
567
  url: params.url,
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { definePluginEntry, sleep } from "./runtime-api.js";
2
2
  import "./api.js";
3
- import { i as resolveVoiceCallConfig, s as validateProviderConfig } from "./config-BKyRNKHF.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";
6
- import { i as parseVoiceCallPluginConfig, r as normalizeVoiceCallLegacyConfigInput, t as formatVoiceCallLegacyConfigWarnings } from "./config-compat-BLdRz9GR.js";
3
+ import { i as resolveVoiceCallConfig, s as validateProviderConfig } from "./config-jaA6vOZ2.js";
4
+ import { c as getTailscaleSelfInfo, l as setupTailscaleExposureRoute, o as resolveWebhookExposureStatus, p as resolveUserPath, s as cleanupTailscaleExposureRoute, t as createVoiceCallRuntime } from "./runtime-entry-GVY8iEzX.js";
5
+ import { c as getCallHistoryFromStore, m as setVoiceCallStateRuntime } from "./store-8M2m4Isq.js";
6
+ import { i as parseVoiceCallPluginConfig, r as normalizeVoiceCallLegacyConfigInput, t as formatVoiceCallLegacyConfigWarnings } from "./config-compat-BPPFhsJ7.js";
7
7
  import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
8
8
  import { ErrorCodes, callGatewayFromCli, errorShape } from "openclaw/plugin-sdk/gateway-runtime";
9
9
  import { MAX_TCP_PORT, MAX_TIMER_TIMEOUT_MS, addTimerTimeoutGraceMs, clampTimerTimeoutMs, parseStrictNonNegativeInteger, resolveTimerTimeoutMs, timestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime";
@@ -539,6 +539,7 @@ function registerVoiceCallCli(params) {
539
539
  //#region extensions/voice-call/src/gateway-continue-operation.ts
540
540
  const VOICE_CALL_CONTINUE_OPERATION_BUFFER_MS = 3e4;
541
541
  const VOICE_CALL_CONTINUE_OPERATION_CLEANUP_MS = 300 * 1e3;
542
+ /** Create a process-local operation store for gateway continue-call polling. */
542
543
  function createVoiceCallContinueOperationStore(params) {
543
544
  const operations = /* @__PURE__ */ new Map();
544
545
  const resolvePollTimeoutMs = (rt) => {
@@ -1,5 +1,5 @@
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";
1
+ import { a as getHeader, m as escapeXml } from "./runtime-entry-GVY8iEzX.js";
2
+ import { n as reconstructWebhookUrl, r as verifyPlivoWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-C5KNhYC-.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
@@ -16,6 +16,7 @@ const DEFAULT_MAX_QUEUED_AUDIO_BYTES = TELEPHONY_SAMPLE_RATE * 120;
16
16
  const PCM16_MAX_AMPLITUDE = 32768;
17
17
  const MULAW_LINEAR_SAMPLES = new Int16Array(256);
18
18
  for (let i = 0; i < MULAW_LINEAR_SAMPLES.length; i += 1) MULAW_LINEAR_SAMPLES[i] = decodeMulawSample(i);
19
+ /** Paces outgoing mulaw audio frames at telephony cadence. */
19
20
  var RealtimeAudioPacer = class {
20
21
  constructor(params) {
21
22
  this.params = params;
@@ -24,6 +25,7 @@ var RealtimeAudioPacer = class {
24
25
  this.queuedAudioBytes = 0;
25
26
  this.closed = false;
26
27
  }
28
+ /** Queue mulaw audio and split it into 20ms-ish telephony chunks. */
27
29
  sendAudio(muLaw) {
28
30
  if (this.closed || muLaw.length === 0) return;
29
31
  const maxQueuedAudioBytes = this.params.maxQueuedAudioBytes ?? DEFAULT_MAX_QUEUED_AUDIO_BYTES;
@@ -42,6 +44,7 @@ var RealtimeAudioPacer = class {
42
44
  }
43
45
  this.ensurePump();
44
46
  }
47
+ /** Queue a provider mark frame after prior audio frames. */
45
48
  sendMark(name) {
46
49
  if (this.closed || !name) return;
47
50
  this.queue.push({
@@ -50,6 +53,7 @@ var RealtimeAudioPacer = class {
50
53
  });
51
54
  this.ensurePump();
52
55
  }
56
+ /** Clear queued audio and notify the provider stream. */
53
57
  clearAudio() {
54
58
  if (this.closed) return 0;
55
59
  const clearedAudioBytes = this.queuedAudioBytes;
@@ -59,24 +63,29 @@ var RealtimeAudioPacer = class {
59
63
  this.params.send(this.params.serializer.clear());
60
64
  return clearedAudioBytes;
61
65
  }
66
+ /** Stop sending and discard queued frames. */
62
67
  close() {
63
68
  this.closed = true;
64
69
  this.clearTimer();
65
70
  this.queue = [];
66
71
  this.queuedAudioBytes = 0;
67
72
  }
73
+ /** Clear the scheduled pump timer. */
68
74
  clearTimer() {
69
75
  if (!this.timer) return;
70
76
  clearTimeout(this.timer);
71
77
  this.timer = null;
72
78
  }
79
+ /** Start the pump when queued work exists and no timer is active. */
73
80
  ensurePump() {
74
81
  if (!this.timer) this.pump();
75
82
  }
83
+ /** Close the pacer and notify the caller about queued-audio backpressure. */
76
84
  failBackpressure() {
77
85
  this.close();
78
86
  this.params.onBackpressure?.();
79
87
  }
88
+ /** Send one queued item and schedule the next send based on audio duration. */
80
89
  pump() {
81
90
  this.timer = null;
82
91
  if (this.closed) return;
@@ -97,6 +106,7 @@ var RealtimeAudioPacer = class {
97
106
  if (this.queue.length > 0) this.timer = setTimeout(() => this.pump(), delayMs);
98
107
  }
99
108
  };
109
+ /** Calculate normalized RMS from mulaw bytes. */
100
110
  function calculateMulawRms(muLaw) {
101
111
  if (muLaw.length === 0) return 0;
102
112
  let sum = 0;
@@ -106,6 +116,7 @@ function calculateMulawRms(muLaw) {
106
116
  }
107
117
  return Math.sqrt(sum / muLaw.length);
108
118
  }
119
+ /** Detect likely speech start from consecutive loud mulaw chunks. */
109
120
  var RealtimeMulawSpeechStartDetector = class {
110
121
  constructor(params = {}) {
111
122
  this.params = params;
@@ -113,6 +124,7 @@ var RealtimeMulawSpeechStartDetector = class {
113
124
  this.quietChunks = DEFAULT_REQUIRED_QUIET_CHUNKS;
114
125
  this.speaking = false;
115
126
  }
127
+ /** Accept one mulaw chunk and return true only on transition into speaking. */
116
128
  accept(muLaw) {
117
129
  if (calculateMulawRms(muLaw) >= (this.params.rmsThreshold ?? DEFAULT_SPEECH_RMS_THRESHOLD)) {
118
130
  this.quietChunks = 0;
@@ -131,6 +143,7 @@ var RealtimeMulawSpeechStartDetector = class {
131
143
  return false;
132
144
  }
133
145
  };
146
+ /** Decode one G.711 mulaw byte to a linear PCM sample. */
134
147
  function decodeMulawSample(value) {
135
148
  const muLaw = ~value & 255;
136
149
  const sign = muLaw & 128;
@@ -141,6 +154,7 @@ function decodeMulawSample(value) {
141
154
  }
142
155
  //#endregion
143
156
  //#region extensions/voice-call/src/webhook/stream-frame-adapter.ts
157
+ /** Parse numeric timestamps sent as numbers or integer strings. */
144
158
  function parseTimestampMs(value) {
145
159
  if (typeof value === "number" && Number.isFinite(value)) return value;
146
160
  if (typeof value === "string" && /^[+-]?\d+$/.test(value.trim())) {
@@ -148,6 +162,7 @@ function parseTimestampMs(value) {
148
162
  return Number.isSafeInteger(parsed) ? parsed : void 0;
149
163
  }
150
164
  }
165
+ /** Parse a JSON object frame, returning null for invalid or non-object payloads. */
151
166
  function tryParseJson(rawMessage) {
152
167
  try {
153
168
  const parsed = JSON.parse(rawMessage);
@@ -155,16 +170,20 @@ function tryParseJson(rawMessage) {
155
170
  } catch {}
156
171
  return null;
157
172
  }
173
+ /** Read an object-valued field from a parsed frame. */
158
174
  function readRecordField(record, field) {
159
175
  const value = record[field];
160
176
  return typeof value === "object" && value !== null ? value : void 0;
161
177
  }
178
+ /** Normalize base64/base64url padding differences for validation. */
162
179
  function normalizeBase64ForCompare(value) {
163
180
  return value.replace(/=+$/u, "").replace(/-/gu, "+").replace(/_/gu, "/");
164
181
  }
182
+ /** Return true when a payload round-trips as base64. */
165
183
  function isValidBase64Payload(value) {
166
184
  return normalizeBase64ForCompare(Buffer.from(value, "base64").toString("base64")) === normalizeBase64ForCompare(value);
167
185
  }
186
+ /** Parse a common provider media frame. */
168
187
  function parseMediaFrame(msg) {
169
188
  const mediaData = readRecordField(msg, "media");
170
189
  const payload = typeof mediaData?.payload === "string" ? mediaData.payload : void 0;
@@ -176,6 +195,7 @@ function parseMediaFrame(msg) {
176
195
  track: typeof mediaData?.track === "string" ? mediaData.track : void 0
177
196
  };
178
197
  }
198
+ /** Parse a common provider mark frame. */
179
199
  function parseMarkFrame(msg) {
180
200
  const markData = readRecordField(msg, "mark");
181
201
  return {
@@ -183,11 +203,13 @@ function parseMarkFrame(msg) {
183
203
  name: typeof markData?.name === "string" ? markData.name : void 0
184
204
  };
185
205
  }
206
+ /** Parse common media, mark, and stop frames shared by supported providers. */
186
207
  function parseCommonInboundFrame(event, msg) {
187
208
  if (event === "media") return parseMediaFrame(msg);
188
209
  if (event === "mark") return parseMarkFrame(msg);
189
210
  if (event === "stop") return { kind: "stop" };
190
211
  }
212
+ /** Parse one provider frame with provider-specific start/error hooks. */
191
213
  function parseProviderInboundFrame(rawMessage, parseStartFrame, parseExtraFrame) {
192
214
  const msg = tryParseJson(rawMessage);
193
215
  if (!msg) return { kind: "ignored" };
@@ -195,9 +217,11 @@ function parseProviderInboundFrame(rawMessage, parseStartFrame, parseExtraFrame)
195
217
  if (event === "start") return parseStartFrame(msg) ?? { kind: "ignored" };
196
218
  return parseCommonInboundFrame(event, msg) ?? parseExtraFrame?.(event, msg) ?? { kind: "ignored" };
197
219
  }
220
+ /** Include streamSid only when Twilio has already supplied one. */
198
221
  function withOptionalStreamSid(streamSid) {
199
222
  return streamSid === void 0 ? {} : { streamSid };
200
223
  }
224
+ /** Serialize a provider media frame. */
201
225
  function serializeMediaFrame(payloadBase64, streamSid) {
202
226
  return JSON.stringify({
203
227
  event: "media",
@@ -205,12 +229,14 @@ function serializeMediaFrame(payloadBase64, streamSid) {
205
229
  media: { payload: payloadBase64 }
206
230
  });
207
231
  }
232
+ /** Serialize a provider clear frame. */
208
233
  function serializeClearFrame(streamSid) {
209
234
  return JSON.stringify({
210
235
  event: "clear",
211
236
  ...withOptionalStreamSid(streamSid)
212
237
  });
213
238
  }
239
+ /** Serialize a provider mark frame. */
214
240
  function serializeMarkFrame(name, streamSid) {
215
241
  return JSON.stringify({
216
242
  event: "mark",
@@ -218,11 +244,13 @@ function serializeMarkFrame(name, streamSid) {
218
244
  mark: { name }
219
245
  });
220
246
  }
247
+ /** Twilio media stream adapter, retaining streamSid for outbound frames. */
221
248
  var TwilioStreamFrameAdapter = class {
222
249
  constructor() {
223
250
  this.providerName = "twilio";
224
251
  this.streamSid = "";
225
252
  }
253
+ /** Parse one Twilio websocket message into a normalized frame. */
226
254
  parseInbound(rawMessage) {
227
255
  return parseProviderInboundFrame(rawMessage, (msg) => {
228
256
  const startData = readRecordField(msg, "start");
@@ -237,20 +265,25 @@ var TwilioStreamFrameAdapter = class {
237
265
  };
238
266
  });
239
267
  }
268
+ /** Serialize Twilio media with the active streamSid. */
240
269
  serializeMedia(payloadBase64) {
241
270
  return serializeMediaFrame(payloadBase64, this.streamSid);
242
271
  }
272
+ /** Serialize Twilio clear with the active streamSid. */
243
273
  serializeClear() {
244
274
  return serializeClearFrame(this.streamSid);
245
275
  }
276
+ /** Serialize Twilio mark with the active streamSid. */
246
277
  serializeMark(name) {
247
278
  return serializeMarkFrame(name, this.streamSid);
248
279
  }
249
280
  };
281
+ /** Telnyx media stream adapter. */
250
282
  var TelnyxStreamFrameAdapter = class {
251
283
  constructor() {
252
284
  this.providerName = "telnyx";
253
285
  }
286
+ /** Parse one Telnyx websocket message into a normalized frame. */
254
287
  parseInbound(rawMessage) {
255
288
  return parseProviderInboundFrame(rawMessage, (msg) => {
256
289
  const topLevelStreamId = typeof msg.stream_id === "string" && msg.stream_id ? msg.stream_id : void 0;
@@ -273,12 +306,15 @@ var TelnyxStreamFrameAdapter = class {
273
306
  };
274
307
  });
275
308
  }
309
+ /** Serialize Telnyx media. */
276
310
  serializeMedia(payloadBase64) {
277
311
  return serializeMediaFrame(payloadBase64);
278
312
  }
313
+ /** Serialize Telnyx clear. */
279
314
  serializeClear() {
280
315
  return serializeClearFrame();
281
316
  }
317
+ /** Serialize Telnyx mark. */
282
318
  serializeMark(name) {
283
319
  return serializeMarkFrame(name);
284
320
  }
@@ -1,5 +1,5 @@
1
- import { o as resolveVoiceCallSessionKey } from "./config-BKyRNKHF.js";
2
- import { f as resolveVoiceResponseModel } from "./runtime-entry-13O0Ki99.js";
1
+ import { o as resolveVoiceCallSessionKey } from "./config-jaA6vOZ2.js";
2
+ import { f as resolveVoiceResponseModel } from "./runtime-entry-GVY8iEzX.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,7 +1,7 @@
1
1
  import { isBlockedHostnameOrIp, isRequestBodyLimitError, readRequestBodyWithLimit, requestBodyErrorToText } from "./runtime-api.js";
2
2
  import "./api.js";
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";
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-jaA6vOZ2.js";
4
+ import { c as getCallHistoryFromStore, d as persistCallRecord, h as TerminalStates, l as loadActiveCallsFromStore, m as setVoiceCallStateRuntime } from "./store-8M2m4Isq.js";
5
5
  import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
6
6
  import { isLoopbackHost } from "openclaw/plugin-sdk/gateway-runtime";
7
7
  import { MAX_TIMER_TIMEOUT_MS, asDateTimestampMs, resolveExpiresAtMsFromDurationMs, resolveTimerTimeoutMs } from "openclaw/plugin-sdk/number-runtime";
@@ -20,10 +20,12 @@ import { resolveConfiguredCapabilityProvider } from "openclaw/plugin-sdk/provide
20
20
  import { WEBHOOK_BODY_READ_DEFAULTS, createWebhookInFlightLimiter } from "openclaw/plugin-sdk/webhook-ingress";
21
21
  import { WebSocket as WebSocket$1, WebSocketServer } from "ws";
22
22
  //#region extensions/voice-call/src/allowlist.ts
23
+ /** Normalize a phone number to digits only. */
23
24
  function normalizePhoneNumber(input) {
24
25
  if (!input) return "";
25
26
  return input.replace(/\D/g, "");
26
27
  }
28
+ /** Return true when the normalized caller exactly matches an allowlist entry. */
27
29
  function isAllowlistedCaller(normalizedFrom, allowFrom) {
28
30
  if (!normalizedFrom) return false;
29
31
  return (allowFrom ?? []).some((num) => {
@@ -66,16 +68,19 @@ function addTranscriptEntry(call, speaker, text) {
66
68
  }
67
69
  //#endregion
68
70
  //#region extensions/voice-call/src/manager/timer-delays.ts
71
+ /** Convert seconds to a safe timeout delay in milliseconds. */
69
72
  function resolveVoiceCallSecondsTimerDelayMs(seconds, minMs = 1) {
70
73
  if (!Number.isFinite(seconds)) return resolveTimerTimeoutMs(MAX_TIMER_TIMEOUT_MS, MAX_TIMER_TIMEOUT_MS, minMs);
71
74
  const timeoutMs = Math.floor(seconds * 1e3);
72
75
  return resolveTimerTimeoutMs(Number.isFinite(timeoutMs) ? timeoutMs : MAX_TIMER_TIMEOUT_MS, minMs, minMs);
73
76
  }
77
+ /** Normalize a millisecond timeout delay with fallback behavior. */
74
78
  function resolveVoiceCallTimerDelayMs(timeoutMs, fallbackMs = 1) {
75
79
  return resolveTimerTimeoutMs(timeoutMs, fallbackMs);
76
80
  }
77
81
  //#endregion
78
82
  //#region extensions/voice-call/src/manager/timers.ts
83
+ /** Clear and forget the max-duration timer for a call. */
79
84
  function clearMaxDurationTimer(ctx, callId) {
80
85
  const timer = ctx.maxDurationTimers.get(callId);
81
86
  if (timer) {
@@ -83,6 +88,7 @@ function clearMaxDurationTimer(ctx, callId) {
83
88
  ctx.maxDurationTimers.delete(callId);
84
89
  }
85
90
  }
91
+ /** Start or replace the max-duration timer for a call. */
86
92
  function startMaxDurationTimer(params) {
87
93
  clearMaxDurationTimer(params.ctx, params.callId);
88
94
  const maxDurationMs = params.timeoutMs === void 0 ? resolveVoiceCallSecondsTimerDelayMs(params.ctx.config.maxDurationSeconds) : resolveVoiceCallTimerDelayMs(params.timeoutMs);
@@ -101,18 +107,21 @@ function startMaxDurationTimer(params) {
101
107
  }, maxDurationMs);
102
108
  params.ctx.maxDurationTimers.set(params.callId, timer);
103
109
  }
110
+ /** Clear and forget a pending final-transcript waiter. */
104
111
  function clearTranscriptWaiter(ctx, callId) {
105
112
  const waiter = ctx.transcriptWaiters.get(callId);
106
113
  if (!waiter) return;
107
114
  clearTimeout(waiter.timeout);
108
115
  ctx.transcriptWaiters.delete(callId);
109
116
  }
117
+ /** Reject a pending transcript waiter during call finalization or error paths. */
110
118
  function rejectTranscriptWaiter(ctx, callId, reason) {
111
119
  const waiter = ctx.transcriptWaiters.get(callId);
112
120
  if (!waiter) return;
113
121
  clearTranscriptWaiter(ctx, callId);
114
122
  waiter.reject(new Error(reason));
115
123
  }
124
+ /** Resolve a transcript waiter when the matching turn's final transcript arrives. */
116
125
  function resolveTranscriptWaiter(ctx, callId, transcript, turnToken) {
117
126
  const waiter = ctx.transcriptWaiters.get(callId);
118
127
  if (!waiter) return false;
@@ -121,6 +130,7 @@ function resolveTranscriptWaiter(ctx, callId, transcript, turnToken) {
121
130
  waiter.resolve(transcript);
122
131
  return true;
123
132
  }
133
+ /** Wait for the next final transcript for a call, optionally scoped to a turn token. */
124
134
  function waitForFinalTranscript(ctx, callId, turnToken) {
125
135
  if (ctx.transcriptWaiters.has(callId)) return Promise.reject(/* @__PURE__ */ new Error("Already waiting for transcript"));
126
136
  const timeoutMs = resolveVoiceCallTimerDelayMs(ctx.config.transcriptTimeoutMs);
@@ -139,10 +149,12 @@ function waitForFinalTranscript(ctx, callId, turnToken) {
139
149
  }
140
150
  //#endregion
141
151
  //#region extensions/voice-call/src/manager/lifecycle.ts
152
+ /** Remove a provider-call mapping only when it still points at this call. */
142
153
  function removeProviderCallMapping(providerCallIdMap, call) {
143
154
  if (!call.providerCallId) return;
144
155
  if (providerCallIdMap.get(call.providerCallId) === call.callId) providerCallIdMap.delete(call.providerCallId);
145
156
  }
157
+ /** Persist terminal state, clean timers/waiters, and remove active call indexes. */
146
158
  function finalizeCall(params) {
147
159
  const { ctx, call, endReason } = params;
148
160
  call.endedAt = params.endedAt ?? Date.now();
@@ -156,11 +168,13 @@ function finalizeCall(params) {
156
168
  }
157
169
  //#endregion
158
170
  //#region extensions/voice-call/src/manager/lookup.ts
171
+ /** Resolve an active call from provider call id with map lookup plus stale-map fallback scan. */
159
172
  function getCallByProviderCallId(params) {
160
173
  const callId = params.providerCallIdMap.get(params.providerCallId);
161
174
  if (callId) return params.activeCalls.get(callId);
162
175
  for (const call of params.activeCalls.values()) if (call.providerCallId === params.providerCallId) return call;
163
176
  }
177
+ /** Resolve an active call by internal call id or provider call id. */
164
178
  function findCall(params) {
165
179
  const directCall = params.activeCalls.get(params.callIdOrProviderCallId);
166
180
  if (directCall) return directCall;
@@ -172,11 +186,13 @@ function findCall(params) {
172
186
  }
173
187
  //#endregion
174
188
  //#region extensions/voice-call/src/tts-provider-voice.ts
189
+ /** Read voice setting aliases from one provider-specific config block. */
175
190
  function resolveProviderVoiceSetting(providerConfig) {
176
191
  if (!providerConfig || typeof providerConfig !== "object") return;
177
192
  const candidate = providerConfig;
178
193
  return normalizeOptionalString(candidate.speakerVoice) ?? normalizeOptionalString(candidate.speakerVoiceId) ?? normalizeOptionalString(candidate.voice) ?? normalizeOptionalString(candidate.voiceId);
179
194
  }
195
+ /** Resolve the active provider's preferred voice id/name from voice-call TTS config. */
180
196
  function resolvePreferredTtsVoice(config) {
181
197
  const providerId = config.tts?.provider;
182
198
  if (!providerId) return;
@@ -219,6 +235,7 @@ function mapVoiceToPolly(voice) {
219
235
  }
220
236
  //#endregion
221
237
  //#region extensions/voice-call/src/manager/twiml.ts
238
+ /** Generate TwiML that speaks one notification and hangs up. */
222
239
  function generateNotifyTwiml(message, voice) {
223
240
  return `<?xml version="1.0" encoding="UTF-8"?>
224
241
  <Response>
@@ -226,6 +243,7 @@ function generateNotifyTwiml(message, voice) {
226
243
  <Hangup/>
227
244
  </Response>`;
228
245
  }
246
+ /** Generate TwiML that plays DTMF digits before redirecting to a webhook URL. */
229
247
  function generateDtmfRedirectTwiml(digits, webhookUrl) {
230
248
  return `<?xml version="1.0" encoding="UTF-8"?>
231
249
  <Response>
@@ -818,6 +836,7 @@ function processEvent(ctx, event) {
818
836
  }
819
837
  //#endregion
820
838
  //#region extensions/voice-call/src/utils.ts
839
+ /** Resolve user input paths, including "~" against the current OS home. */
821
840
  function resolveUserPath(input) {
822
841
  const trimmed = input.trim();
823
842
  if (!trimmed) return trimmed;
@@ -1089,10 +1108,12 @@ var CallManager = class {
1089
1108
  };
1090
1109
  //#endregion
1091
1110
  //#region extensions/voice-call/src/realtime-agent-context.ts
1111
+ /** Limit injected context while preserving an explicit truncation marker. */
1092
1112
  function limitText(text, maxChars) {
1093
1113
  if (text.length <= maxChars) return text;
1094
1114
  return `${text.slice(0, Math.max(0, maxChars - 32)).trimEnd()}\n[truncated]`;
1095
1115
  }
1116
+ /** Read configured workspace context files through the safe workspace root. */
1096
1117
  async function readWorkspaceVoiceContextFiles(params) {
1097
1118
  const sections = [];
1098
1119
  let remaining = params.maxChars;
@@ -1108,6 +1129,7 @@ async function readWorkspaceVoiceContextFiles(params) {
1108
1129
  }
1109
1130
  return sections;
1110
1131
  }
1132
+ /** Build final realtime instructions from base instructions, consult policy, and fast context. */
1111
1133
  async function buildRealtimeVoiceInstructions(params) {
1112
1134
  const { config } = params;
1113
1135
  const sections = [params.baseInstructions];
@@ -1146,6 +1168,7 @@ async function buildRealtimeVoiceInstructions(params) {
1146
1168
  }
1147
1169
  //#endregion
1148
1170
  //#region extensions/voice-call/src/realtime-fast-context.ts
1171
+ /** Resolve fast-context consult data using caller-oriented labels. */
1149
1172
  async function resolveRealtimeFastContextConsult(params) {
1150
1173
  return await resolveRealtimeVoiceFastContextConsult({
1151
1174
  ...params,
@@ -1157,6 +1180,7 @@ async function resolveRealtimeFastContextConsult(params) {
1157
1180
  }
1158
1181
  //#endregion
1159
1182
  //#region extensions/voice-call/src/response-model.ts
1183
+ /** Resolve provider/model fields from explicit voice config or agent defaults. */
1160
1184
  function resolveVoiceResponseModel(params) {
1161
1185
  const modelRef = params.voiceConfig.responseModel ?? `${params.agentRuntime.defaults.provider}/${params.agentRuntime.defaults.model}`;
1162
1186
  const slashIndex = modelRef.indexOf("/");
@@ -1178,7 +1202,9 @@ function chunkAudio(audio, chunkSize = 160) {
1178
1202
  }
1179
1203
  //#endregion
1180
1204
  //#region extensions/voice-call/src/telephony-tts.ts
1205
+ /** Default timeout for one telephony synthesis request. */
1181
1206
  const TELEPHONY_DEFAULT_TTS_TIMEOUT_MS = 8e3;
1207
+ /** Create a TTS provider that honors voice-call overrides and converts PCM to mulaw. */
1182
1208
  function createTelephonyTtsProvider(params) {
1183
1209
  const { coreConfig, ttsOverride, runtime, logger } = params;
1184
1210
  const mergedConfig = applyTtsOverride(coreConfig, ttsOverride);
@@ -1210,6 +1236,7 @@ function createTelephonyTtsProvider(params) {
1210
1236
  }
1211
1237
  };
1212
1238
  }
1239
+ /** Apply voice-call TTS overrides to core config without mutating the original object. */
1213
1240
  function applyTtsOverride(coreConfig, override) {
1214
1241
  if (!override) return coreConfig;
1215
1242
  const base = coreConfig.messages?.tts;
@@ -1223,12 +1250,14 @@ function applyTtsOverride(coreConfig, override) {
1223
1250
  }
1224
1251
  };
1225
1252
  }
1253
+ /** Merge core and voice-call TTS config, keeping undefined override fields out. */
1226
1254
  function mergeTtsConfig(base, override) {
1227
1255
  if (!base && !override) return;
1228
1256
  if (!override) return base;
1229
1257
  if (!base) return override;
1230
1258
  return deepMergeDefined(base, override);
1231
1259
  }
1260
+ /** Resolve directive override policy for telephony synthesis. */
1232
1261
  function resolveTelephonyModelOverridePolicy(overrides) {
1233
1262
  if (!(overrides?.enabled ?? true)) return {
1234
1263
  enabled: false,
@@ -1252,16 +1281,20 @@ function resolveTelephonyModelOverridePolicy(overrides) {
1252
1281
  allowSeed: allow(overrides?.allowSeed)
1253
1282
  };
1254
1283
  }
1284
+ /** Read model override policy from TTS config when present. */
1255
1285
  function readTelephonyModelOverrides(ttsConfig) {
1256
1286
  const value = ttsConfig?.modelOverrides;
1257
1287
  return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
1258
1288
  }
1289
+ /** Normalize provider ids for config lookup. */
1259
1290
  function normalizeProviderId(value) {
1260
1291
  return typeof value === "string" ? value.trim().toLowerCase() || void 0 : void 0;
1261
1292
  }
1293
+ /** Coerce provider config objects while rejecting arrays and primitives. */
1262
1294
  function asProviderConfig(value) {
1263
1295
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
1264
1296
  }
1297
+ /** Collect named provider configs from canonical and legacy TTS config shapes. */
1265
1298
  function collectTelephonyProviderConfigs(ttsConfig) {
1266
1299
  if (!ttsConfig) return {};
1267
1300
  const entries = {};
@@ -1294,12 +1327,14 @@ function collectTelephonyProviderConfigs(ttsConfig) {
1294
1327
  //#endregion
1295
1328
  //#region extensions/voice-call/src/bounded-child-output.ts
1296
1329
  const DEFAULT_MAX_OUTPUT_CHARS = 16384;
1330
+ /** Create an empty bounded output buffer. */
1297
1331
  function emptyBoundedChildOutput() {
1298
1332
  return {
1299
1333
  text: "",
1300
1334
  truncated: false
1301
1335
  };
1302
1336
  }
1337
+ /** Append output while retaining the newest maxChars and recording truncation. */
1303
1338
  function appendBoundedChildOutput(current, chunk, maxChars = DEFAULT_MAX_OUTPUT_CHARS) {
1304
1339
  const appended = current.text + chunk;
1305
1340
  if (appended.length <= maxChars) return {
@@ -1311,6 +1346,7 @@ function appendBoundedChildOutput(current, chunk, maxChars = DEFAULT_MAX_OUTPUT_
1311
1346
  truncated: true
1312
1347
  };
1313
1348
  }
1349
+ /** Format captured output with a truncation marker when older text was dropped. */
1314
1350
  function formatBoundedChildOutput(output) {
1315
1351
  return output.truncated ? `[output truncated]\n${output.text}` : output.text;
1316
1352
  }
@@ -1680,12 +1716,15 @@ async function startTunnel(config) {
1680
1716
  }
1681
1717
  //#endregion
1682
1718
  //#region extensions/voice-call/src/webhook-exposure.ts
1719
+ /** Return true when a provider requires a public webhook URL or tunnel. */
1683
1720
  function providerRequiresPublicWebhook(providerName) {
1684
1721
  return providerName === "twilio" || providerName === "telnyx" || providerName === "plivo";
1685
1722
  }
1723
+ /** Return true for localhost, private, or otherwise provider-unreachable hosts. */
1686
1724
  function isLocalOnlyWebhookHost(hostname) {
1687
1725
  return isBlockedHostnameOrIp(hostname);
1688
1726
  }
1727
+ /** Return true when a webhook URL parses to a local/private host. */
1689
1728
  function isProviderUnreachableWebhookUrl(webhookUrl) {
1690
1729
  try {
1691
1730
  return isLocalOnlyWebhookHost(new URL(webhookUrl).hostname);
@@ -1693,6 +1732,7 @@ function isProviderUnreachableWebhookUrl(webhookUrl) {
1693
1732
  return false;
1694
1733
  }
1695
1734
  }
1735
+ /** Resolve a human-readable webhook exposure status for doctor/setup surfaces. */
1696
1736
  function resolveWebhookExposureStatus(config) {
1697
1737
  if (config.provider === "mock") return {
1698
1738
  ok: true,
@@ -1729,6 +1769,7 @@ function resolveWebhookExposureStatus(config) {
1729
1769
  }
1730
1770
  //#endregion
1731
1771
  //#region extensions/voice-call/src/http-headers.ts
1772
+ /** Return the first value for a header name regardless of caller casing. */
1732
1773
  function getHeader(headers, name) {
1733
1774
  const target = normalizeLowercaseStringOrEmpty(name);
1734
1775
  const value = headers[target] ?? Object.entries(headers).find(([key]) => normalizeLowercaseStringOrEmpty(key) === target)?.[1];
@@ -2380,19 +2421,23 @@ const TERMINAL_PROVIDER_STATUS_TO_END_REASON = {
2380
2421
  "no-answer": "no-answer",
2381
2422
  canceled: "hangup-bot"
2382
2423
  };
2424
+ /** Normalize provider status text, falling back to "unknown". */
2383
2425
  function normalizeProviderStatus(status) {
2384
2426
  const normalized = normalizeOptionalLowercaseString(status);
2385
2427
  return normalized && normalized.length > 0 ? normalized : "unknown";
2386
2428
  }
2429
+ /** Map terminal provider status strings to OpenClaw end reasons. */
2387
2430
  function mapProviderStatusToEndReason(status) {
2388
2431
  return TERMINAL_PROVIDER_STATUS_TO_END_REASON[normalizeProviderStatus(status)] ?? null;
2389
2432
  }
2433
+ /** Return true when a provider status is terminal. */
2390
2434
  function isProviderStatusTerminal(status) {
2391
2435
  return mapProviderStatusToEndReason(status) !== null;
2392
2436
  }
2393
2437
  //#endregion
2394
2438
  //#region extensions/voice-call/src/webhook/stale-call-reaper.ts
2395
2439
  const CHECK_INTERVAL_MS = 3e4;
2440
+ /** Start a stale-call reaper and return its cleanup callback. */
2396
2441
  function startStaleCallReaper(params) {
2397
2442
  const maxAgeSeconds = params.staleCallReaperSeconds;
2398
2443
  if (!maxAgeSeconds || maxAgeSeconds <= 0) return null;
@@ -2428,7 +2473,7 @@ function loadRealtimeTranscriptionRuntime() {
2428
2473
  return realtimeTranscriptionRuntimePromise;
2429
2474
  }
2430
2475
  function loadResponseGeneratorModule() {
2431
- responseGeneratorModulePromise ??= import("./response-generator-D6UsHyfH.js");
2476
+ responseGeneratorModulePromise ??= import("./response-generator-DjsoOMEg.js");
2432
2477
  return responseGeneratorModulePromise;
2433
2478
  }
2434
2479
  function sanitizeTranscriptForLog(value) {
@@ -3112,15 +3157,15 @@ let mockProviderPromise;
3112
3157
  let realtimeVoiceRuntimePromise;
3113
3158
  let realtimeHandlerPromise;
3114
3159
  function loadTelnyxProvider() {
3115
- telnyxProviderPromise ??= import("./telnyx-68EYJwaz.js");
3160
+ telnyxProviderPromise ??= import("./telnyx-Gm8Z1pUt.js");
3116
3161
  return telnyxProviderPromise;
3117
3162
  }
3118
3163
  function loadTwilioProvider() {
3119
- twilioProviderPromise ??= import("./twilio-D0dUSGam.js");
3164
+ twilioProviderPromise ??= import("./twilio-BuFmgABx.js");
3120
3165
  return twilioProviderPromise;
3121
3166
  }
3122
3167
  function loadPlivoProvider() {
3123
- plivoProviderPromise ??= import("./plivo-DeSZeJ0S.js");
3168
+ plivoProviderPromise ??= import("./plivo-29Dvun7P.js");
3124
3169
  return plivoProviderPromise;
3125
3170
  }
3126
3171
  function loadMockProvider() {
@@ -3132,7 +3177,7 @@ function loadRealtimeVoiceRuntime() {
3132
3177
  return realtimeVoiceRuntimePromise;
3133
3178
  }
3134
3179
  function loadRealtimeHandler() {
3135
- realtimeHandlerPromise ??= import("./realtime-handler-BP6_qBNe.js");
3180
+ realtimeHandlerPromise ??= import("./realtime-handler-ChN5De4P.js");
3136
3181
  return realtimeHandlerPromise;
3137
3182
  }
3138
3183
  function resolveVoiceCallConsultSessionKey(call) {
@@ -1,2 +1,2 @@
1
- import { t as createVoiceCallRuntime } from "./runtime-entry-13O0Ki99.js";
1
+ import { t as createVoiceCallRuntime } from "./runtime-entry-GVY8iEzX.js";
2
2
  export { createVoiceCallRuntime };
package/dist/setup-api.js CHANGED
@@ -1,7 +1,8 @@
1
- import { n as migrateVoiceCallLegacyConfigInput } from "./config-compat-BLdRz9GR.js";
1
+ import { n as migrateVoiceCallLegacyConfigInput } from "./config-compat-BPPFhsJ7.js";
2
2
  import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
3
3
  import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
4
4
  //#region extensions/voice-call/setup-api.ts
5
+ /** Migrate voice-call plugin config inside the full OpenClaw config object. */
5
6
  function migrateVoiceCallPluginConfig(config) {
6
7
  const rawVoiceCallConfig = config.plugins?.entries?.["voice-call"]?.config;
7
8
  if (!isRecord(rawVoiceCallConfig)) return null;
@@ -25,6 +26,7 @@ function migrateVoiceCallPluginConfig(config) {
25
26
  changes: migration.changes
26
27
  };
27
28
  }
29
+ /** Setup plugin entry that registers voice-call config migrations. */
28
30
  var setup_api_default = definePluginEntry({
29
31
  id: "voice-call",
30
32
  name: "Voice Call Setup",
@@ -124,22 +124,30 @@ const { setRuntime: setVoiceCallStateRuntime, clearRuntime: clearVoiceCallStateR
124
124
  });
125
125
  //#endregion
126
126
  //#region extensions/voice-call/src/manager/store.ts
127
+ /** Plugin state namespace for call record event metadata. */
127
128
  const CALL_RECORD_EVENTS_NAMESPACE = "call-record-events";
129
+ /** Plugin state namespace for base64 call record event chunks. */
128
130
  const CALL_RECORD_EVENT_CHUNKS_NAMESPACE = "call-record-event-chunks";
131
+ /** Maximum retained call record events. */
129
132
  const MAX_CALL_RECORD_EVENTS = 1e3;
133
+ /** Extra metadata entries retained so pruning can safely trim oldest rows. */
130
134
  const CALL_RECORD_EVENT_META_MAX_ENTRIES = 1100;
131
135
  const CALL_RECORD_CHUNK_MAX_ENTRIES = 48048;
136
+ /** Raw UTF-8 bytes stored per call record chunk before base64 encoding. */
132
137
  const RAW_CALL_RECORD_CHUNK_BYTES = 47 * 1024;
133
138
  let callRecordEventSequence = 0;
139
+ /** Return the pre-SQLite JSONL call log path for migration/compat checks. */
134
140
  function resolveVoiceCallLegacyCallLogPath(storePath) {
135
141
  return path.join(storePath, "calls.jsonl");
136
142
  }
143
+ /** Build env for plugin state stores rooted at the voice-call store path. */
137
144
  function resolvePluginStateEnv(storePath) {
138
145
  return {
139
146
  ...process.env,
140
147
  OPENCLAW_STATE_DIR: storePath
141
148
  };
142
149
  }
150
+ /** Open the plugin state stores when the runtime is available. */
143
151
  function createCallRecordStateStores(storePath) {
144
152
  const runtime = getOptionalVoiceCallStateRuntime();
145
153
  if (!runtime) return null;
@@ -157,6 +165,7 @@ function createCallRecordStateStores(storePath) {
157
165
  })
158
166
  };
159
167
  }
168
+ /** Open call stores and log failures instead of breaking restore paths. */
160
169
  function tryCreateCallRecordStateStores(storePath) {
161
170
  try {
162
171
  return createCallRecordStateStores(storePath);
@@ -165,12 +174,15 @@ function tryCreateCallRecordStateStores(storePath) {
165
174
  return null;
166
175
  }
167
176
  }
177
+ /** Build the stable storage key for one chunk of an event. */
168
178
  function buildChunkKey(eventKey, index) {
169
179
  return `${eventKey}:chunk:${String(index).padStart(4, "0")}`;
170
180
  }
181
+ /** Build a deterministic key for one legacy JSONL line. */
171
182
  function buildVoiceCallLegacyJsonlEventKey(line, index) {
172
183
  return `jsonl:${String(index).padStart(8, "0")}:${createHash("sha256").update(line).digest("hex")}`;
173
184
  }
185
+ /** Allocate monotonic ordering metadata for newly persisted call records. */
174
186
  function nextCallRecordOrder() {
175
187
  const sequence = callRecordEventSequence;
176
188
  callRecordEventSequence = (callRecordEventSequence + 1) % 1e6;
@@ -179,13 +191,16 @@ function nextCallRecordOrder() {
179
191
  sequence
180
192
  };
181
193
  }
194
+ /** Build a unique event key that preserves timestamp and sequence ordering. */
182
195
  function buildNewEventKey(order) {
183
196
  return `event:${order.persistedAt.toString(36)}:${String(order.sequence).padStart(6, "0")}:${randomUUID()}`;
184
197
  }
198
+ /** Recover the sequence segment from newer event keys. */
185
199
  function parseEventKeySequence(key) {
186
200
  const match = /^event:[^:]+:(\d+):/.exec(key);
187
201
  return match ? Number.parseInt(match[1], 10) : 0;
188
202
  }
203
+ /** Parse a stored call record line from v2 envelope or legacy raw-call JSON. */
189
204
  function parseVoiceCallRecordLine(line, sequence = 0) {
190
205
  if (!line.trim()) return null;
191
206
  try {
@@ -209,9 +224,11 @@ function parseVoiceCallRecordLine(line, sequence = 0) {
209
224
  return null;
210
225
  }
211
226
  }
227
+ /** Count storage chunks needed for a call record. */
212
228
  function countCallRecordChunks(call) {
213
229
  return Math.max(1, Math.ceil(Buffer.byteLength(JSON.stringify(call), "utf8") / RAW_CALL_RECORD_CHUNK_BYTES));
214
230
  }
231
+ /** Truncate oversized call records to fit the bounded plugin state chunk budget. */
215
232
  function prepareVoiceCallRecordForStorage(call) {
216
233
  if (countCallRecordChunks(call) <= 48) return call;
217
234
  const transcriptEntries = call.transcript.length;
@@ -249,6 +266,7 @@ function prepareVoiceCallRecordForStorage(call) {
249
266
  }
250
267
  return call;
251
268
  }
269
+ /** Register a serialized call record event and its chunks, then prune old events. */
252
270
  function registerCallRecordEvent(stores, eventKey, call, order) {
253
271
  const serialized = JSON.stringify(prepareVoiceCallRecordForStorage(call));
254
272
  const buffer = Buffer.from(serialized, "utf8");
@@ -269,18 +287,21 @@ function registerCallRecordEvent(stores, eventKey, call, order) {
269
287
  });
270
288
  pruneCallRecordEvents(stores);
271
289
  }
290
+ /** Delete metadata and all chunk rows for one call record event. */
272
291
  function deleteCallRecordEventRows(stores, eventKey) {
273
292
  const meta = stores.events.lookup(eventKey);
274
293
  stores.events.delete(eventKey);
275
294
  if (!meta) return;
276
295
  for (let index = 0; index < meta.chunkCount; index += 1) stores.chunks.delete(buildChunkKey(eventKey, index));
277
296
  }
297
+ /** Keep only the newest bounded call record events. */
278
298
  function pruneCallRecordEvents(stores) {
279
299
  const rows = stores.events.entries();
280
300
  if (rows.length <= 1e3) return;
281
301
  const sorted = rows.toSorted((a, b) => a.createdAt - b.createdAt || a.key.localeCompare(b.key));
282
302
  for (const row of sorted.slice(0, rows.length - MAX_CALL_RECORD_EVENTS)) deleteCallRecordEventRows(stores, row.key);
283
303
  }
304
+ /** Read and reassemble one chunked call record event. */
284
305
  function readCallRecordEvent(stores, eventKey) {
285
306
  const meta = stores.events.lookup(eventKey);
286
307
  if (!meta) return null;
@@ -292,6 +313,7 @@ function readCallRecordEvent(stores, eventKey) {
292
313
  }
293
314
  return parseVoiceCallRecordLine(Buffer.concat(chunks, meta.byteLength).toString("utf8"))?.call ?? null;
294
315
  }
316
+ /** Read all persisted call records in stable persisted order. */
295
317
  function readCallRecordEvents(stores) {
296
318
  return stores.events.entries().toSorted((a, b) => a.createdAt - b.createdAt || a.key.localeCompare(b.key)).map((entry) => {
297
319
  const call = readCallRecordEvent(stores, entry.key);
@@ -303,6 +325,7 @@ function readCallRecordEvents(stores) {
303
325
  } : null;
304
326
  }).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
327
  }
328
+ /** Persist one call record event to plugin state. */
306
329
  function persistCallRecord(storePath, call) {
307
330
  try {
308
331
  const stores = createCallRecordStateStores(storePath);
@@ -314,6 +337,7 @@ function persistCallRecord(storePath, call) {
314
337
  throw err;
315
338
  }
316
339
  }
340
+ /** Restore nonterminal active calls and provider/event indexes from persisted records. */
317
341
  function loadActiveCallsFromStore(storePath) {
318
342
  const stores = tryCreateCallRecordStateStores(storePath);
319
343
  let calls = [];
@@ -347,6 +371,7 @@ function loadActiveCallsFromStore(storePath) {
347
371
  rejectedProviderCallIds
348
372
  };
349
373
  }
374
+ /** Return the newest persisted call history rows up to the requested limit. */
350
375
  async function getCallHistoryFromStore(storePath, limit = 50) {
351
376
  if (limit <= 0) return [];
352
377
  const stores = tryCreateCallRecordStateStores(storePath);
@@ -1,4 +1,4 @@
1
- import { i as verifyTelnyxWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-tyxHyrev.js";
1
+ import { i as verifyTelnyxWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-C5KNhYC-.js";
2
2
  import crypto from "node:crypto";
3
3
  //#region extensions/voice-call/src/providers/telnyx.ts
4
4
  function normalizeTelnyxDirection(direction) {
@@ -1,13 +1,14 @@
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-13O0Ki99.js";
4
- import { a as verifyTwilioWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-tyxHyrev.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-GVY8iEzX.js";
4
+ import { a as verifyTwilioWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-C5KNhYC-.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";
8
8
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
9
9
  //#region extensions/voice-call/src/providers/twilio/api.ts
10
10
  const TWILIO_API_TIMEOUT_MS = 3e4;
11
+ /** Parse Twilio JSON error responses without trusting response shape. */
11
12
  function parseTwilioApiError(text) {
12
13
  try {
13
14
  const parsed = JSON.parse(text);
@@ -21,6 +22,7 @@ function parseTwilioApiError(text) {
21
22
  return {};
22
23
  }
23
24
  }
25
+ /** Error thrown for non-2xx Twilio REST API responses. */
24
26
  var TwilioApiError = class extends Error {
25
27
  constructor(httpStatus, responseText) {
26
28
  const parsed = parseTwilioApiError(responseText);
@@ -32,6 +34,7 @@ var TwilioApiError = class extends Error {
32
34
  this.twilioCode = parsed.code;
33
35
  }
34
36
  };
37
+ /** POST a form-encoded Twilio REST API request through the SSRF guard. */
35
38
  async function twilioApiRequest(params) {
36
39
  const bodyParams = params.body instanceof URLSearchParams ? params.body : Object.entries(params.body).reduce((acc, [key, value]) => {
37
40
  if (Array.isArray(value)) for (const entry of value) acc.append(key, entry);
@@ -71,9 +74,11 @@ async function twilioApiRequest(params) {
71
74
  }
72
75
  //#endregion
73
76
  //#region extensions/voice-call/src/providers/twilio/twiml-policy.ts
77
+ /** Return true for Twilio outbound call directions. */
74
78
  function isOutboundDirection(direction) {
75
79
  return direction?.startsWith("outbound") ?? false;
76
80
  }
81
+ /** Read the Twilio request fields needed by TwiML decision logic. */
77
82
  function readTwimlRequestView(ctx) {
78
83
  const params = new URLSearchParams(ctx.rawBody);
79
84
  const type = normalizeOptionalString(ctx.query?.type);
@@ -86,6 +91,7 @@ function readTwimlRequestView(ctx) {
86
91
  callIdFromQuery
87
92
  };
88
93
  }
94
+ /** Decide the TwiML response kind for a Twilio webhook request. */
89
95
  function decideTwimlResponse(input) {
90
96
  if (input.callIdFromQuery && !input.isStatusCallback) {
91
97
  if (input.hasStoredTwiml) return {
@@ -109,6 +115,7 @@ function decideTwimlResponse(input) {
109
115
  }
110
116
  //#endregion
111
117
  //#region extensions/voice-call/src/providers/twilio/webhook.ts
118
+ /** Verify a Twilio webhook and map SDK verification details to provider result fields. */
112
119
  function verifyTwilioProviderWebhook(params) {
113
120
  const result = verifyTwilioWebhook(params.ctx, params.authToken, {
114
121
  publicUrl: params.currentPublicUrl || void 0,
@@ -206,6 +213,7 @@ var TwilioProvider = class TwilioProvider {
206
213
  }
207
214
  registerCallStream(callSid, streamSid) {
208
215
  this.callStreamMap.set(callSid, streamSid);
216
+ this.activeStreamCalls.add(callSid);
209
217
  }
210
218
  hasRegisteredStream(callSid) {
211
219
  return this.callStreamMap.has(callSid);
@@ -406,7 +414,6 @@ var TwilioProvider = class TwilioProvider {
406
414
  canStream: Boolean(view.callSid && this.getStreamUrl())
407
415
  });
408
416
  if (decision.consumeStoredTwimlCallId) this.deleteStoredTwiml(decision.consumeStoredTwimlCallId);
409
- if (decision.activateStreamCallSid) this.activeStreamCalls.add(decision.activateStreamCallSid);
410
417
  switch (decision.kind) {
411
418
  case "stored": return storedTwiml ?? TwilioProvider.EMPTY_TWIML;
412
419
  case "queue": return TwilioProvider.QUEUE_TWIML;
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@openclaw/voice-call",
3
- "version": "2026.6.1",
3
+ "version": "2026.6.5-beta.1",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@openclaw/voice-call",
9
- "version": "2026.6.1",
9
+ "version": "2026.6.5-beta.1",
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.6.1"
17
+ "openclaw": ">=2026.6.5-beta.1"
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.6.1",
3
+ "version": "2026.6.5-beta.1",
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.6.1"
17
+ "openclaw": ">=2026.6.5-beta.1"
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.6.1"
34
+ "pluginApi": ">=2026.6.5-beta.1"
35
35
  },
36
36
  "build": {
37
- "openclawVersion": "2026.6.1"
37
+ "openclawVersion": "2026.6.5-beta.1"
38
38
  },
39
39
  "release": {
40
40
  "publishToClawHub": true,