@openclaw/voice-call 2026.10.5-beta.4 → 2026.10.10-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.
package/README.md CHANGED
@@ -102,7 +102,7 @@ Notes:
102
102
  - Twilio defaults to US1. For a non-US Region, set `twilio.region` to `ie1` or `au1` and use credentials created in that Region; see [Twilio's regional REST API guide](https://www.twilio.com/docs/global-infrastructure/using-the-twilio-rest-api-in-a-non-us-region).
103
103
  - `mock` is a local dev provider (no network calls).
104
104
  - Telnyx requires `telnyx.publicKey` (or `TELNYX_PUBLIC_KEY`) unless `skipSignatureVerification` is true.
105
- - Runtime accepts canonical config only. Doctor retains migrations for `provider: "log"`, `twilio.from`, legacy `streaming.*` OpenAI keys, and `realtime.agentContext.includeSystemPrompt` because supported releases can still preserve these settings. Run `openclaw doctor --fix` to normalize them; existing canonical values are preserved.
105
+ - Runtime accepts only the current config format. Doctor retains migrations for `provider: "log"`, `twilio.from`, legacy `streaming.*` OpenAI keys, and `realtime.agentContext.includeSystemPrompt` because supported releases can still preserve these settings. Run `openclaw doctor --fix` to normalize them; existing values in the current format are preserved.
106
106
  - advanced webhook, streaming, and tunnel notes: `https://docs.openclaw.ai/plugins/voice-call`
107
107
  - `responseModel` is optional. When unset, voice responses use the runtime default model.
108
108
  - `sessionScope` defaults to `per-phone`, preserving caller memory across calls. Use `per-call` for reception, booking, IVR, and bridge flows where each carrier call should start fresh. Use `main` to share the configured agent's main session (`agent:<agentId>:main`, or `global` when core `session.scope` is `"global"`). Custom core `session.mainKey` values are ignored.
@@ -1,5 +1,5 @@
1
1
  import { fetchWithSsrFGuard } from "../api.js";
2
- import { c as getHeader, r as normalizeProxyIp } from "./runtime-entry-STjFmi_B.mjs";
2
+ import { c as getHeader, r as normalizeProxyIp } from "./runtime-entry-BqhtjzE-.mjs";
3
3
  import { n as reserveWebhookReplay, t as createWebhookReplayCache } from "./webhook-replay-B7RJNWNG.mjs";
4
4
  import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
5
5
  import { isLoopbackHost } from "openclaw/plugin-sdk/gateway-runtime";
@@ -1,6 +1,6 @@
1
1
  import { sleep } from "../api.js";
2
- import { g as CallBriefSchema, l as resolveVoiceCallStreamExposurePaths, u as validateProviderConfig } from "./config-CsrGuamE.mjs";
3
- import { S as resolveVoiceCallAgentId, _ as findCallInStore, b as setVoiceCallStateRuntime, d as getTailscaleSelfInfo, f as setupTailscaleExposureRoutes, g as MAX_CALL_RECORD_EVENTS, l as resolveWebhookExposureStatus, u as cleanupTailscaleExposureRoute, v as getCallHistoryFromStore, y as loadActiveCallsFromStore } from "./runtime-entry-STjFmi_B.mjs";
2
+ import { g as CallBriefSchema, l as resolveVoiceCallStreamExposurePaths, u as validateProviderConfig } from "./config-BdbTQCSh.mjs";
3
+ import { S as resolveVoiceCallAgentId, _ as findCallInStore, b as setVoiceCallStateRuntime, d as getTailscaleSelfInfo, f as setupTailscaleExposureRoutes, g as MAX_CALL_RECORD_EVENTS, l as resolveWebhookExposureStatus, u as cleanupTailscaleExposureRoute, v as getCallHistoryFromStore, y as loadActiveCallsFromStore } from "./runtime-entry-BqhtjzE-.mjs";
4
4
  import { r as resolveUserPath, t as resolveDefaultVoiceCallStoreDir } from "./store-path-Cfwff6j7.mjs";
5
5
  import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
6
6
  import { callGatewayFromCli, isGatewayClientRequestError, isGatewayTransportError, redactSensitiveUrlLikeString } from "openclaw/plugin-sdk/gateway-runtime";
@@ -484,8 +484,12 @@ function registerVoiceCallCli(params) {
484
484
  mode: options.mode
485
485
  }) });
486
486
  });
487
- root.command("call").description("Initiate an outbound voice call").requiredOption("-m, --message <text>", "Message to speak when call connects").option("-t, --to <phone>", "Phone number to call (E.164 format, uses config toNumber if not set)").option("--mode <mode>", "Call mode: notify (hangup after message) or conversation (stay open)", "conversation").option("--brief <text-or-json>", "Per-call task or structured JSON brief").option("--brief-file <path>", "Read a structured JSON brief from a file").action(callAction("voicecall.initiate"));
488
- root.command("start").description("Alias for voicecall call").requiredOption("--to <phone>", "Phone number to call").option("--message <text>", "Message to speak when call connects").option("--mode <mode>", "Call mode: notify (hangup after message) or conversation (stay open)", "conversation").option("--brief <text-or-json>", "Per-call task or structured JSON brief").option("--brief-file <path>", "Read a structured JSON brief from a file").action(callAction("voicecall.start"));
487
+ for (const name of ["call", "start"]) {
488
+ const command = root.command(name);
489
+ if (name === "call") command.description("Initiate an outbound voice call").requiredOption("-m, --message <text>", "Message to speak when call connects").option("-t, --to <phone>", "Phone number to call (E.164 format, uses config toNumber if not set)");
490
+ else command.description("Alias for voicecall call").requiredOption("--to <phone>", "Phone number to call").option("--message <text>", "Message to speak when call connects");
491
+ command.option("--mode <mode>", "Call mode: notify (hangup after message) or conversation (stay open)", "conversation").option("--brief <text-or-json>", "Per-call task or structured JSON brief").option("--brief-file <path>", "Read a structured JSON brief from a file").action(callAction(name === "call" ? "voicecall.initiate" : "voicecall.start"));
492
+ }
489
493
  root.command("continue").description("Speak a message and wait for a response").requiredOption("--call-id <id>", "Call ID").requiredOption("--message <text>", "Message to speak").action((options) => {
490
494
  const gatewayParams = {
491
495
  callId: options.callId,
@@ -286,16 +286,7 @@ const VoiceCallRealtimeConfigSchema = z.object({
286
286
  agentContext: VoiceCallRealtimeAgentContextConfigSchema.default(VoiceCallRealtimeAgentContextConfigSchema.parse({})),
287
287
  /** Provider-owned raw config blobs keyed by provider id. */
288
288
  providers: VoiceCallProvidersConfigSchema
289
- }).strict().default({
290
- enabled: false,
291
- instructions: DEFAULT_VOICE_CALL_REALTIME_INSTRUCTIONS,
292
- toolPolicy: "safe-read-only",
293
- consultPolicy: "auto",
294
- tools: [],
295
- fastContext: VoiceCallRealtimeFastContextConfigSchema.parse({}),
296
- agentContext: VoiceCallRealtimeAgentContextConfigSchema.parse({}),
297
- providers: {}
298
- });
289
+ }).strict();
299
290
  const VoiceCallStreamingConfigSchema = z.object({
300
291
  /** Enable Twilio Media Streams for real-time transcription. */
301
292
  enabled: z.boolean().default(false),
@@ -359,7 +350,7 @@ const VoiceCallConfigSchema = z.object({
359
350
  tunnel: VoiceCallTunnelConfigSchema.default(VoiceCallTunnelConfigSchema.parse({})),
360
351
  webhookSecurity: VoiceCallWebhookSecurityConfigSchema.default(VoiceCallWebhookSecurityConfigSchema.parse({})),
361
352
  streaming: VoiceCallStreamingConfigSchema.default(VoiceCallStreamingConfigSchema.parse({})),
362
- realtime: VoiceCallRealtimeConfigSchema,
353
+ realtime: VoiceCallRealtimeConfigSchema.default(VoiceCallRealtimeConfigSchema.parse({})),
363
354
  /** Session memory scope for voice conversations. */
364
355
  sessionScope: VoiceCallSessionScopeSchema.default("per-phone"),
365
356
  /** Public webhook URL override (if set, bypasses tunnel auto-detection) */
@@ -1,6 +1,6 @@
1
- import { c as getHeader } from "./runtime-entry-STjFmi_B.mjs";
1
+ import { c as getHeader } from "./runtime-entry-BqhtjzE-.mjs";
2
2
  import { t as escapeXml } from "./voice-mapping-1lzFmzOF.mjs";
3
- import { n as reconstructWebhookUrl, r as verifyPlivoWebhook, t as createCarrierApi } from "./carrier-api-BYKw_fsQ.mjs";
3
+ import { n as reconstructWebhookUrl, r as verifyPlivoWebhook, t as createCarrierApi } from "./carrier-api-Bt9KxU9b.mjs";
4
4
  import { normalizeLowercaseStringOrEmpty, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
5
5
  import crypto from "node:crypto";
6
6
  //#region extensions/voice-call/src/providers/plivo.ts
@@ -1,5 +1,5 @@
1
- import { d as REALTIME_VOICE_END_CALL_TOOL_NAME, g as CallBriefSchema, s as resolveVoiceCallPublicPathPrefix } from "./config-CsrGuamE.mjs";
2
- import { n as appendRecentTalkEventMetadata, s as canonicalizeVoiceCallMediaBase64 } from "./runtime-entry-STjFmi_B.mjs";
1
+ import { d as REALTIME_VOICE_END_CALL_TOOL_NAME, g as CallBriefSchema, s as resolveVoiceCallPublicPathPrefix } from "./config-BdbTQCSh.mjs";
2
+ import { n as appendRecentTalkEventMetadata, s as canonicalizeVoiceCallMediaBase64 } from "./runtime-entry-BqhtjzE-.mjs";
3
3
  import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
4
4
  import { asNullableRecord, asOptionalObjectRecord, asOptionalRecord, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
5
5
  import { createSubsystemLogger, sleep } from "openclaw/plugin-sdk/runtime-env";
@@ -10,7 +10,6 @@ import { randomUUID } from "node:crypto";
10
10
  import { safeParseJson, sliceUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
11
11
  import "node:http";
12
12
  import { WebSocket, WebSocketServer, rejectWebSocketUpgrade } from "openclaw/plugin-sdk/websocket-runtime";
13
- import { createDeferred } from "openclaw/plugin-sdk/extension-shared";
14
13
  //#region extensions/voice-call/src/webhook/realtime-audio-pacer.ts
15
14
  const TELEPHONY_SAMPLE_RATE = 8e3;
16
15
  const TELEPHONY_CHUNK_BYTES = 160;
@@ -789,31 +788,12 @@ function withFallbackConsultQuestion(args, fallback) {
789
788
  question
790
789
  };
791
790
  }
792
- function captureNativeConsultTranscript(state, invocationId) {
793
- const previous = state.nativeConsultInvocation;
794
- if (invocationId.trim() && previous?.id === invocationId) return previous.read;
795
- previous?.freeze();
796
- const read = () => {
797
- const buffer = state.partial ?? state.recentFinal;
798
- return {
799
- buffer,
800
- text: buffer?.text
801
- };
802
- };
803
- const initial = read();
804
- let frozen;
805
- const invocation = {
806
- id: invocationId,
807
- read: () => frozen ?? read(),
808
- freeze: () => {
809
- frozen = initial.buffer?.final ? {
810
- buffer: initial.buffer,
811
- text: initial.buffer.text
812
- } : initial;
813
- }
814
- };
815
- state.nativeConsultInvocation = invocation;
816
- return invocation.read;
791
+ function captureNativeConsultTranscript(state) {
792
+ const buffer = state.partial ?? state.recentFinal;
793
+ return () => ({
794
+ buffer,
795
+ text: buffer?.text
796
+ });
817
797
  }
818
798
  function clearRecentFinalUserTranscript(state) {
819
799
  clearTimeout(state.recentFinalTimer);
@@ -823,7 +803,7 @@ function clearRecentFinalUserTranscript(state) {
823
803
  function consumeUserTranscript(state, consumed) {
824
804
  const buffer = consumed?.buffer;
825
805
  const text = consumed?.text?.trim();
826
- if (!state || !buffer || !text || buffer !== state.partial && buffer !== state.recentFinal || !buffer.text.toLowerCase().startsWith(text.toLowerCase())) return;
806
+ if (!buffer || !text || buffer !== state.partial && buffer !== state.recentFinal || !buffer.text.toLowerCase().startsWith(text.toLowerCase())) return;
827
807
  const remaining = buffer.text.slice(text.length).trimStart();
828
808
  if (remaining) {
829
809
  buffer.text = remaining;
@@ -831,12 +811,6 @@ function consumeUserTranscript(state, consumed) {
831
811
  } else if (buffer === state.partial) state.partial = void 0;
832
812
  else clearRecentFinalUserTranscript(state);
833
813
  }
834
- async function waitForNativeConsult(state) {
835
- return await Promise.race([state.promise.then((result) => ({
836
- kind: "completed",
837
- result
838
- })), state.cancellation.then(() => ({ kind: "cancelled" }))]);
839
- }
840
814
  var RealtimeCallHandler = class {
841
815
  constructor(config, manager, resolveCallRegistration, servePath, streamDisconnectLifecycle, coreConfig, holdOpeningMaxMs) {
842
816
  this.config = config;
@@ -852,9 +826,7 @@ var RealtimeCallHandler = class {
852
826
  this.serverClosingSockets = /* @__PURE__ */ new WeakSet();
853
827
  this.activeBridgesByCallId = /* @__PURE__ */ new Map();
854
828
  this.activeTelephonyBindingsByCallId = /* @__PURE__ */ new Map();
855
- this.userTranscriptStatesByCallId = /* @__PURE__ */ new Map();
856
829
  this.forcedConsultsByCallId = /* @__PURE__ */ new Map();
857
- this.consultSessionsByCallId = /* @__PURE__ */ new Map();
858
830
  this.nativeConsultsInFlightByCallId = /* @__PURE__ */ new Map();
859
831
  this.terminationAttempts = /* @__PURE__ */ new Set();
860
832
  this.admissions = /* @__PURE__ */ new Set();
@@ -1155,7 +1127,7 @@ var RealtimeCallHandler = class {
1155
1127
  let registration;
1156
1128
  let sessionPolicy;
1157
1129
  try {
1158
- registration = this.resolveCallRegistration(callRecord);
1130
+ registration = await this.resolveCallRegistration(callRecord);
1159
1131
  sessionPolicy = resolveRealtimeVoiceSessionPolicy({
1160
1132
  isAgentProxy: false,
1161
1133
  capabilities: registration.capabilities,
@@ -1273,11 +1245,8 @@ var RealtimeCallHandler = class {
1273
1245
  telephonyBinding.close("inactivity");
1274
1246
  }
1275
1247
  });
1276
- let provisionalCloseReason;
1277
1248
  let sessionClosed = false;
1278
- const previousTranscriptOwner = this.userTranscriptStatesByCallId.get(callId);
1279
1249
  const userTranscriptOwner = {};
1280
- this.userTranscriptStatesByCallId.set(callId, userTranscriptOwner);
1281
1250
  let transcriptPersistence = Promise.resolve();
1282
1251
  let transcriptFailure;
1283
1252
  const reportTranscriptFailure = (error) => {
@@ -1298,7 +1267,6 @@ var RealtimeCallHandler = class {
1298
1267
  if (failures.length === 1) throw failures[0];
1299
1268
  if (failures.length > 1) throw new AggregateError(failures, "Realtime provider and transcript cleanup failed");
1300
1269
  };
1301
- let continuityGeneration = 0;
1302
1270
  const bridgeParams = {
1303
1271
  provider: realtimeProvider,
1304
1272
  cfg: this.coreConfig,
@@ -1311,18 +1279,17 @@ var RealtimeCallHandler = class {
1311
1279
  tools: handlesAgentConsult ? [] : this.config.tools,
1312
1280
  ...handlesAgentConsult ? { runAgentConsult: async (request) => {
1313
1281
  const owner = nativeConsultOwner;
1314
- const generation = continuityGeneration;
1315
1282
  request.signal?.throwIfAborted();
1316
1283
  const invocationId = randomUUID();
1317
- const transcript = captureNativeConsultTranscript(userTranscriptOwner, invocationId);
1284
+ const transcript = captureNativeConsultTranscript(userTranscriptOwner);
1318
1285
  activity.beginConsult();
1319
1286
  try {
1320
1287
  await transcriptPersistence;
1321
- if (!owner || sessionClosed || generation !== continuityGeneration || !this.isActiveBridgeOwner(callId, owner)) throw new Error("Realtime call delegation owner is no longer active");
1288
+ if (!owner || sessionClosed || !this.isActiveBridgeOwner(callId, owner)) throw new Error("Realtime call delegation owner is no longer active");
1322
1289
  if (toolPolicy === "none") throw new Error("Agent consultation is disabled for this call");
1323
1290
  const result = await this.executeToolCall(owner, callId, invocationId, REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME, { question: request.prompt }, harness.ensureTurn(), harness, userTranscriptOwner, transcript, { signal: request.signal });
1324
1291
  request.signal?.throwIfAborted();
1325
- if (sessionClosed || generation !== continuityGeneration || !this.isActiveBridgeOwner(callId, owner)) throw new Error("Realtime call delegation owner is no longer active");
1292
+ if (sessionClosed || !this.isActiveBridgeOwner(callId, owner)) throw new Error("Realtime call delegation owner is no longer active");
1326
1293
  const text = readSpeakableRealtimeVoiceToolResult(result, {
1327
1294
  keys: ["text", "output"],
1328
1295
  maxChars: FORCED_CONSULT_RESULT_MAX_CHARS
@@ -1338,7 +1305,7 @@ var RealtimeCallHandler = class {
1338
1305
  audioSink: audioController.audioSink,
1339
1306
  onTranscript: (role, text, isFinal) => {
1340
1307
  const owner = nativeConsultOwner;
1341
- if (provisionalCloseReason || outboundGreeting.isBlocked() || sessionClosed && !isFinal || !this.getUserTranscriptState(callId, userTranscriptOwner) || owner && !this.isActiveBridgeOwner(callId, owner)) return;
1308
+ if (outboundGreeting.isBlocked() || sessionClosed && !isFinal || !owner || !this.isActiveBridgeOwner(callId, owner)) return;
1342
1309
  if (text.trim()) {
1343
1310
  outboundGreeting.claim();
1344
1311
  activity.noteSpeech();
@@ -1366,8 +1333,7 @@ var RealtimeCallHandler = class {
1366
1333
  });
1367
1334
  if (!isFinal) {
1368
1335
  if (role === "user" && text.trim()) {
1369
- const state = this.getUserTranscriptState(callId, userTranscriptOwner);
1370
- if (!state) return;
1336
+ const state = userTranscriptOwner;
1371
1337
  const partial = state.partial ??= { text: "" };
1372
1338
  partial.text = limitPartialUserTranscript(appendTranscriptText(partial.text, text));
1373
1339
  partial.raw = limitPartialUserTranscript(`${partial.raw ?? ""}${text}`);
@@ -1379,8 +1345,7 @@ var RealtimeCallHandler = class {
1379
1345
  let transcript = text;
1380
1346
  let consultTranscript;
1381
1347
  if (role === "user") {
1382
- const state = this.getUserTranscriptState(callId, userTranscriptOwner);
1383
- if (!state) return;
1348
+ const state = userTranscriptOwner;
1384
1349
  transcript = resolveFinalTranscriptText({
1385
1350
  partial: state.partial?.text,
1386
1351
  rawPartial: state.partial?.raw,
@@ -1388,7 +1353,6 @@ var RealtimeCallHandler = class {
1388
1353
  });
1389
1354
  const buffer = state.partial ?? { text: transcript };
1390
1355
  buffer.text = transcript;
1391
- buffer.final = true;
1392
1356
  state.partial = void 0;
1393
1357
  clearRecentFinalUserTranscript(state);
1394
1358
  state.recentFinal = buffer;
@@ -1397,7 +1361,6 @@ var RealtimeCallHandler = class {
1397
1361
  text: transcript
1398
1362
  };
1399
1363
  const timer = setTimeout(() => {
1400
- if (!this.getUserTranscriptState(callId, state) || state.recentFinalTimer !== timer) return;
1401
1364
  state.recentFinal = void 0;
1402
1365
  state.recentFinalTimer = void 0;
1403
1366
  }, RECENT_FINAL_USER_TRANSCRIPT_TTL_MS);
@@ -1416,9 +1379,8 @@ var RealtimeCallHandler = class {
1416
1379
  isFinal: true
1417
1380
  } : { type: "call.assistant-speech" }
1418
1381
  };
1419
- const generation = continuityGeneration;
1420
1382
  transcriptPersistence = this.manager.processEvent(event).then(() => {
1421
- if (role !== "user" || handlesAgentConsult || sessionClosed || generation !== continuityGeneration || !this.getUserTranscriptState(callId, userTranscriptOwner) || !this.isActiveBridgeOwner(callId, session)) return;
1383
+ if (role !== "user" || handlesAgentConsult || sessionClosed || !this.isActiveBridgeOwner(callId, session)) return;
1422
1384
  this.scheduleForcedAgentConsult({
1423
1385
  harness,
1424
1386
  session,
@@ -1438,14 +1400,13 @@ var RealtimeCallHandler = class {
1438
1400
  transcriptPersistence.catch(reportTranscriptFailure);
1439
1401
  },
1440
1402
  onToolCall: async (toolEvent, sessionLocal) => {
1441
- const generation = continuityGeneration;
1442
1403
  const isConsult = toolEvent.name === REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME;
1443
1404
  const invocationId = toolEvent.callId || toolEvent.itemId;
1444
- const transcript = isConsult ? captureNativeConsultTranscript(userTranscriptOwner, invocationId) : void 0;
1405
+ const transcript = isConsult ? captureNativeConsultTranscript(userTranscriptOwner) : void 0;
1445
1406
  if (isConsult) activity.beginConsult();
1446
1407
  try {
1447
1408
  await transcriptPersistence;
1448
- if (sessionClosed || generation !== continuityGeneration || !this.isActiveBridgeOwner(callId, sessionLocal)) return;
1409
+ if (sessionClosed || !this.isActiveBridgeOwner(callId, sessionLocal)) return;
1449
1410
  const turnId = harness.ensureTurn();
1450
1411
  harness.emit({
1451
1412
  type: "tool.call",
@@ -1465,17 +1426,12 @@ var RealtimeCallHandler = class {
1465
1426
  },
1466
1427
  onEvent: (event) => {
1467
1428
  if (event.direction === "client" && event.type === "session.continuity.reset") {
1468
- continuityGeneration += 1;
1469
1429
  const turnId = harness.talk.activeTurnId;
1470
1430
  const owner = nativeConsultOwner;
1471
1431
  if (owner && this.isActiveBridgeOwner(callId, owner)) {
1472
- const state = this.getUserTranscriptState(callId, userTranscriptOwner);
1473
- if (state) {
1474
- state.partial = void 0;
1475
- clearRecentFinalUserTranscript(state);
1476
- state.nativeConsultInvocation = void 0;
1477
- }
1478
- this.cancelConsultSession(callId, owner, "continuity-reset");
1432
+ userTranscriptOwner.partial = void 0;
1433
+ clearRecentFinalUserTranscript(userTranscriptOwner);
1434
+ this.cancelConsultSession(callId, owner, harness.forcedConsults, "continuity-reset");
1479
1435
  }
1480
1436
  harness.flushOutput(() => {
1481
1437
  audioPacer.clearAudio();
@@ -1547,10 +1503,7 @@ var RealtimeCallHandler = class {
1547
1503
  final: true
1548
1504
  });
1549
1505
  const owner = nativeConsultOwner;
1550
- if (!owner) {
1551
- provisionalCloseReason ??= reason;
1552
- return;
1553
- }
1506
+ if (!owner) return;
1554
1507
  if (sessionClosed) {
1555
1508
  if (reason === "error") {
1556
1509
  this.streamDisconnectLifecycle.retire(callSid, streamSid);
@@ -1571,32 +1524,17 @@ var RealtimeCallHandler = class {
1571
1524
  } catch (error) {
1572
1525
  console.error("[voice-call] Failed to create realtime bridge:", error);
1573
1526
  }
1574
- if (!candidate || provisionalCloseReason) {
1575
- if (this.getUserTranscriptState(callId, userTranscriptOwner)) {
1576
- clearRecentFinalUserTranscript(userTranscriptOwner);
1577
- if (previousTranscriptOwner) this.userTranscriptStatesByCallId.set(callId, previousTranscriptOwner);
1578
- else this.userTranscriptStatesByCallId.delete(callId);
1579
- }
1580
- try {
1581
- await drainProviderClose(() => candidate?.close());
1582
- } catch (error) {
1583
- console.warn(`[voice-call] Failed to close realtime bridge ${callSid}: ${formatErrorMessage(error)}`);
1584
- }
1527
+ if (!candidate) {
1585
1528
  harness.close();
1586
1529
  audioPacer.close();
1587
1530
  outboundGreeting.close();
1588
1531
  hostSpeech.close();
1589
1532
  activity.close();
1590
- const reason = provisionalCloseReason ?? "error";
1591
- if (!this.activeBridgesByCallId.has(callId)) emitCallEnd(reason);
1592
- if (ws.readyState === WebSocket.OPEN) ws.close(reason === "error" ? 1011 : 1e3, "Failed to create realtime bridge");
1533
+ if (!this.activeBridgesByCallId.has(callId)) emitCallEnd("error");
1534
+ if (ws.readyState === WebSocket.OPEN) ws.close(1011, "Failed to create realtime bridge");
1593
1535
  return null;
1594
1536
  }
1595
1537
  const session = candidate;
1596
- if (this.getUserTranscriptState(callId, userTranscriptOwner) && previousTranscriptOwner) {
1597
- clearTimeout(previousTranscriptOwner.recentFinalTimer);
1598
- previousTranscriptOwner.recentFinalTimer = void 0;
1599
- }
1600
1538
  nativeConsultOwner = session;
1601
1539
  const localBargeIn = !(session.bridge.handlesInputAudioBargeIn ?? providerHandlesInputAudioBargeIn) && resolveRealtimeVoiceBargeIn({
1602
1540
  configuredBargeIn: void 0,
@@ -1604,12 +1542,6 @@ var RealtimeCallHandler = class {
1604
1542
  capabilities,
1605
1543
  outputAudioMode: session.bridge.outputAudioMode
1606
1544
  });
1607
- const previousConsultSession = this.consultSessionsByCallId.get(callId);
1608
- if (previousConsultSession && previousConsultSession.owner !== session) this.cancelConsultSession(callId, previousConsultSession.owner);
1609
- this.consultSessionsByCallId.set(callId, {
1610
- owner: session,
1611
- coordinator: harness.forcedConsults
1612
- });
1613
1545
  const sendAudioToSession = session.sendAudio.bind(session);
1614
1546
  session.sendAudio = (audio) => {
1615
1547
  if (sessionClosed) return;
@@ -1638,14 +1570,11 @@ var RealtimeCallHandler = class {
1638
1570
  outboundGreeting.close();
1639
1571
  hostSpeech.close();
1640
1572
  activity.close();
1641
- this.cancelConsultSession(callId, session);
1573
+ this.cancelConsultSession(callId, session, harness.forcedConsults);
1642
1574
  audioPacer.close();
1643
1575
  sessionClosePromise = drainProviderClose(closeSession).finally(() => {
1644
1576
  for (const key of [callId, callSid]) if (this.activeBridgesByCallId.get(key) === session) this.activeBridgesByCallId.delete(key);
1645
- if (this.getUserTranscriptState(callId, userTranscriptOwner)) {
1646
- clearRecentFinalUserTranscript(userTranscriptOwner);
1647
- this.userTranscriptStatesByCallId.delete(callId);
1648
- }
1577
+ clearRecentFinalUserTranscript(userTranscriptOwner);
1649
1578
  harness.close();
1650
1579
  });
1651
1580
  return sessionClosePromise;
@@ -1722,14 +1651,8 @@ var RealtimeCallHandler = class {
1722
1651
  });
1723
1652
  return telephonyBinding;
1724
1653
  }
1725
- getUserTranscriptState(callId, owner) {
1726
- const state = this.userTranscriptStatesByCallId.get(callId);
1727
- return state === owner ? state : void 0;
1728
- }
1729
- cancelConsultSession(callId, owner, mode = "retire") {
1730
- const session = this.consultSessionsByCallId.get(callId);
1731
- if (!session || session.owner !== owner) return;
1732
- if (mode === "retire") session.coordinator.clearPending();
1654
+ cancelConsultSession(callId, owner, coordinator, mode = "retire") {
1655
+ coordinator.clearPending();
1733
1656
  const forced = this.forcedConsultsByCallId.get(callId);
1734
1657
  if (forced?.owner === owner) {
1735
1658
  forced.cancelled = true;
@@ -1742,16 +1665,15 @@ var RealtimeCallHandler = class {
1742
1665
  this.nativeConsultsInFlightByCallId.delete(callId);
1743
1666
  native.cancel();
1744
1667
  }
1745
- if (mode === "continuity-reset") session.coordinator.clear();
1746
- else this.consultSessionsByCallId.delete(callId);
1668
+ if (mode === "continuity-reset") coordinator.clear();
1747
1669
  }
1748
1670
  isActiveBridgeOwner(callId, owner) {
1749
1671
  return this.activeBridgesByCallId.get(callId) === owner;
1750
1672
  }
1751
- async waitForConsultTranscriptSettle(callId, owner, startedAt) {
1673
+ async waitForConsultTranscriptSettle(owner, startedAt) {
1752
1674
  const deadline = startedAt + CONSULT_TRANSCRIPT_SETTLE_MAX_MS;
1753
1675
  while (true) {
1754
- const updatedAt = this.getUserTranscriptState(callId, owner)?.partial?.updatedAt;
1676
+ const updatedAt = owner.partial?.updatedAt;
1755
1677
  if (!updatedAt) return;
1756
1678
  const now = Date.now();
1757
1679
  const quietFor = now - updatedAt;
@@ -1824,7 +1746,7 @@ var RealtimeCallHandler = class {
1824
1746
  params.session.sendUserMessage(buildForcedConsultSpeechPrompt(text));
1825
1747
  }
1826
1748
  console.log(`[voice-call] realtime forced agent consult completed callId=${params.callId} providerCallId=${params.callSid} elapsedMs=${Date.now() - startedAt}`);
1827
- consumeUserTranscript(this.getUserTranscriptState(params.callId, params.userTranscriptOwner), params.consultTranscript);
1749
+ consumeUserTranscript(params.userTranscriptOwner, params.consultTranscript);
1828
1750
  } catch (error) {
1829
1751
  if (!state.cancelled) {
1830
1752
  const result = buildRealtimeVoiceAgentErrorProviderResult(error);
@@ -2007,63 +1929,52 @@ var RealtimeCallHandler = class {
2007
1929
  if (!bridgeCallId.trim() || existingNativeConsult.invocationId !== bridgeCallId) return await submitBusyResult();
2008
1930
  console.log(`[voice-call] realtime tool call sharing in-flight agent consult callId=${callId} ageMs=${Date.now() - existingNativeConsult.startedAt}`);
2009
1931
  await submitWorkingResponse();
2010
- const outcome = await waitForNativeConsult(existingNativeConsult);
2011
- if (outcome.kind === "cancelled") return;
2012
- return await submitFinalToolResult(outcome.result);
1932
+ const result = await existingNativeConsult.promise;
1933
+ if (existingNativeConsult.cancelled || !this.isActiveBridgeOwner(callId, bridge)) return;
1934
+ return await submitFinalToolResult(result);
2013
1935
  }
1936
+ let transcript;
2014
1937
  const abortController = new AbortController();
2015
- const { promise: cancellation, resolve: releaseCancellation } = createDeferred();
2016
- const { promise: consult, resolve: completeConsult } = createDeferred();
2017
1938
  const state = {
2018
1939
  owner: bridge,
2019
1940
  invocationId: bridgeCallId,
2020
1941
  startedAt,
2021
- promise: consult,
2022
- cancellation,
1942
+ promise: (async () => {
1943
+ await submitWorkingResponse();
1944
+ await this.waitForConsultTranscriptSettle(userTranscriptOwner, startedAt);
1945
+ if (abortController.signal.aborted || !this.isActiveBridgeOwner(callId, bridge)) return;
1946
+ transcript = consultTranscript?.();
1947
+ const context = {
1948
+ partialUserTranscript: transcript?.text,
1949
+ abortSignal: abortController.signal
1950
+ };
1951
+ const handlerArgs = withFallbackConsultQuestion(args, context.partialUserTranscript);
1952
+ return await invokeHandler(handlerArgs, context);
1953
+ })().catch(buildRealtimeVoiceAgentErrorProviderResult),
2023
1954
  get cancelled() {
2024
1955
  return abortController.signal.aborted;
2025
1956
  },
2026
1957
  cancel: () => {
2027
1958
  abortController.abort(/* @__PURE__ */ new Error("Realtime native consult owner was cancelled."));
2028
- releaseCancellation();
2029
1959
  }
2030
1960
  };
2031
1961
  if (delegation?.signal?.aborted) state.cancel();
2032
1962
  else delegation?.signal?.addEventListener("abort", state.cancel, { once: true });
2033
1963
  this.nativeConsultsInFlightByCallId.set(callId, state);
2034
- (async () => {
2035
- try {
2036
- await submitWorkingResponse();
2037
- if (state.cancelled || !this.isActiveBridgeOwner(callId, bridge)) return;
2038
- await Promise.race([this.waitForConsultTranscriptSettle(callId, userTranscriptOwner, startedAt), state.cancellation]);
2039
- if (state.cancelled || !this.isActiveBridgeOwner(callId, bridge)) return;
2040
- const transcript = consultTranscript?.();
2041
- const context = {
2042
- partialUserTranscript: transcript?.text,
2043
- abortSignal: abortController.signal
2044
- };
2045
- state.transcript = transcript;
2046
- const handlerArgs = withFallbackConsultQuestion(args, context.partialUserTranscript);
2047
- return await invokeHandler(handlerArgs, context);
2048
- } catch (error) {
2049
- return buildRealtimeVoiceAgentErrorProviderResult(error);
2050
- }
2051
- })().then(completeConsult);
2052
1964
  try {
2053
- const outcome = await waitForNativeConsult(state);
2054
- if (outcome.kind === "cancelled") return;
2055
- const result = outcome.result;
1965
+ const result = await state.promise;
1966
+ if (state.cancelled || !this.isActiveBridgeOwner(callId, bridge)) return;
2056
1967
  const failed = logResult(result);
2057
1968
  await submitFinalToolResult(result);
2058
- if (!failed) consumeUserTranscript(this.getUserTranscriptState(callId, userTranscriptOwner), state.transcript);
1969
+ if (!failed) consumeUserTranscript(userTranscriptOwner, transcript);
2059
1970
  return result;
2060
1971
  } finally {
2061
1972
  delegation?.signal?.removeEventListener("abort", state.cancel);
2062
1973
  if (this.nativeConsultsInFlightByCallId.get(callId) === state) this.nativeConsultsInFlightByCallId.delete(callId);
2063
1974
  }
2064
1975
  }
2065
- const transcript = this.getUserTranscriptState(callId, userTranscriptOwner);
2066
- const result = await invokeHandler(args, { partialUserTranscript: (transcript?.partial ?? transcript?.recentFinal)?.text });
1976
+ const transcript = userTranscriptOwner;
1977
+ const result = await invokeHandler(args, { partialUserTranscript: (transcript.partial ?? transcript.recentFinal)?.text });
2067
1978
  logResult(result);
2068
1979
  return await submitFinalToolResult(result);
2069
1980
  }
@@ -1,2 +1 @@
1
- import { getRealtimeTranscriptionProvider, listRealtimeTranscriptionProviders } from "openclaw/plugin-sdk/realtime-transcription";
2
- export { getRealtimeTranscriptionProvider, listRealtimeTranscriptionProviders };
1
+ export { getRealtimeTranscriptionProvider, listRealtimeTranscriptionProviders } from "openclaw/plugin-sdk/realtime-transcription";
@@ -0,0 +1 @@
1
+ export { getRealtimeVoiceProvider, listRealtimeVoiceProviders, resolveConfiguredRealtimeVoiceProviderAsync } from "openclaw/plugin-sdk/realtime-voice";
@@ -1,12 +1,12 @@
1
- import { c as resolveVoiceCallSessionKey } from "./config-CsrGuamE.mjs";
2
- import { m as resolveVoiceResponseModel, x as resolveCallAgentId } from "./runtime-entry-STjFmi_B.mjs";
1
+ import { c as resolveVoiceCallSessionKey } from "./config-BdbTQCSh.mjs";
2
+ import { m as resolveVoiceResponseModel, x as resolveCallAgentId } from "./runtime-entry-BqhtjzE-.mjs";
3
3
  import { normalizeLowercaseStringOrEmpty, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
4
4
  import { resolveAgentConfig } from "openclaw/plugin-sdk/agent-scope-runtime";
5
5
  import { isValidAgentHarnessSessionStoreEntry } from "openclaw/plugin-sdk/session-store-runtime";
6
6
  import crypto from "node:crypto";
7
7
  import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
8
8
  import { resolveDefaultModelForAgent } from "openclaw/plugin-sdk/agent-runtime";
9
- import { ModelSelectionLockedError, applyModelOverrideWithAuthProfileCompatibility, resolvePersistedSessionRuntimeId } from "openclaw/plugin-sdk/model-session-runtime";
9
+ import { ModelSelectionLockedError, applyModelOverrideWithAuthProfileCompatibility, prepareAuthProfileProvider, resolvePersistedSessionRuntimeId } from "openclaw/plugin-sdk/model-session-runtime";
10
10
  //#region extensions/voice-call/src/response-generator.ts
11
11
  const VOICE_SPOKEN_OUTPUT_CONTRACT = [
12
12
  "Output format requirements:",
@@ -133,7 +133,7 @@ async function generateVoiceResponse(params) {
133
133
  const workspaceDir = agentRuntime.resolveAgentWorkspaceDir(cfg, agentId);
134
134
  await agentRuntime.ensureAgentWorkspace({ dir: workspaceDir });
135
135
  const now = Date.now();
136
- let sessionEntry = agentRuntime.session.getSessionEntry({
136
+ let sessionEntry = await agentRuntime.session.getSessionEntryAsync({
137
137
  storePath,
138
138
  sessionKey: resolvedSessionKey
139
139
  });
@@ -146,7 +146,7 @@ async function generateVoiceResponse(params) {
146
146
  agentId
147
147
  });
148
148
  if (sessionEntry?.modelSelectionLocked === true && voiceConfig.responseModel) throw new ModelSelectionLockedError();
149
- if (!sessionEntry?.sessionId || voiceConfig.responseModel) sessionEntry = await agentRuntime.session.patchSessionEntry({
149
+ if (!sessionEntry?.sessionId || voiceConfig.responseModel) sessionEntry = await agentRuntime.session.prepareSessionEntryPatch({
150
150
  storePath,
151
151
  sessionKey: resolvedSessionKey,
152
152
  replaceEntry: true,
@@ -154,23 +154,31 @@ async function generateVoiceResponse(params) {
154
154
  sessionId: crypto.randomUUID(),
155
155
  updatedAt: now
156
156
  },
157
- update: (entry) => {
157
+ prepare: async (entry) => {
158
158
  const next = entry.sessionId ? { ...entry } : {
159
159
  ...entry,
160
160
  sessionId: crypto.randomUUID(),
161
161
  updatedAt: now
162
162
  };
163
- if (voiceConfig.responseModel) applyModelOverrideWithAuthProfileCompatibility({
164
- cfg,
165
- agentDir,
166
- entry: next,
167
- currentProvider: entry.providerOverride?.trim() || entry.modelProvider?.trim() || configuredModel.provider,
168
- selection: {
169
- provider,
170
- model
171
- },
172
- selectionSource: "auto"
173
- });
163
+ if (voiceConfig.responseModel) {
164
+ const profileId = entry.authProfileOverride?.trim();
165
+ const recordedProvider = profileId ? await prepareAuthProfileProvider({
166
+ agentDir,
167
+ profileId
168
+ }) : void 0;
169
+ applyModelOverrideWithAuthProfileCompatibility({
170
+ cfg,
171
+ agentDir,
172
+ entry: next,
173
+ recordedProvider,
174
+ currentProvider: entry.providerOverride?.trim() || entry.modelProvider?.trim() || configuredModel.provider,
175
+ selection: {
176
+ provider,
177
+ model
178
+ },
179
+ selectionSource: "auto"
180
+ });
181
+ }
174
182
  return next;
175
183
  }
176
184
  }) ?? void 0;
@@ -1,5 +1,5 @@
1
1
  import { isBlockedHostnameOrIp, isRequestBodyLimitError, readRequestBodyWithLimit, requestBodyErrorToText, sendHttpRequestRejection } from "../api.js";
2
- import { a as resolveVoiceCallEffectiveConfig, c as resolveVoiceCallSessionKey, f as resolveVoiceCallRealtimeTools, g as CallBriefSchema, h as normalizePhoneNumber, i as resolveVoiceCallConfig, l as resolveVoiceCallStreamExposurePaths, m as isAllowlistedCaller, n as normalizeVoiceCallConfig, o as resolveVoiceCallNumberRouteKeyForCall, r as resolveTwilioAuthToken, u as validateProviderConfig } from "./config-CsrGuamE.mjs";
2
+ import { a as resolveVoiceCallEffectiveConfig, c as resolveVoiceCallSessionKey, f as resolveVoiceCallRealtimeTools, g as CallBriefSchema, h as normalizePhoneNumber, i as resolveVoiceCallConfig, l as resolveVoiceCallStreamExposurePaths, m as isAllowlistedCaller, n as normalizeVoiceCallConfig, o as resolveVoiceCallNumberRouteKeyForCall, r as resolveTwilioAuthToken, u as validateProviderConfig } from "./config-BdbTQCSh.mjs";
3
3
  import { n as mapVoiceToPolly, t as escapeXml } from "./voice-mapping-1lzFmzOF.mjs";
4
4
  import { r as resolveUserPath, t as resolveDefaultVoiceCallStoreDir } from "./store-path-Cfwff6j7.mjs";
5
5
  import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
@@ -10,7 +10,7 @@ import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env";
10
10
  import { listAgentIds, resolveDefaultAgentId } from "openclaw/plugin-sdk/agent-scope-runtime";
11
11
  import { REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME, assertRealtimeVoiceAgentConsultModelSelectionUnlocked, buildRealtimeVoiceAgentConsultPolicyInstructions, consultRealtimeVoiceAgent, convertPcmToMulaw8k, createTalkSessionController, recordTalkObservabilityEvent, resolveRealtimeVoiceAgentConsultToolsAllow, resolveRealtimeVoiceFastContextConsult } from "openclaw/plugin-sdk/realtime-voice";
12
12
  import { z } from "zod";
13
- import { createNativeSessionBindingAuthority } from "openclaw/plugin-sdk/agent-harness-session-runtime";
13
+ import { createNativeSessionBindingScope } from "openclaw/plugin-sdk/agent-harness-session-runtime";
14
14
  import { buildAssistantMessage, createEmptyTransportUsage } from "openclaw/plugin-sdk/provider-transport-runtime";
15
15
  import { appendSessionTranscriptMessageByIdentityStrict } from "openclaw/plugin-sdk/session-transcript-runtime";
16
16
  import { WEBHOOK_BODY_READ_DEFAULTS, createWebhookInFlightLimiter, normalizeWebhookPath, rawDataToString } from "openclaw/plugin-sdk/webhook-ingress";
@@ -20,15 +20,14 @@ import { KeyedAsyncQueue } from "openclaw/plugin-sdk/keyed-async-queue";
20
20
  import crypto, { randomUUID } from "node:crypto";
21
21
  import { redactIdentifier } from "openclaw/plugin-sdk/logging-core";
22
22
  import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
23
- import * as realtimeBootstrapSdk from "openclaw/plugin-sdk/realtime-bootstrap-context";
24
- import { spawn } from "node:child_process";
23
+ import { resolveRealtimeVoiceAgentContextInstructions } from "openclaw/plugin-sdk/realtime-bootstrap-context";
24
+ import { runCommandWithTimeout, spawnOomScoreAdjusted } from "openclaw/plugin-sdk/process-runtime";
25
25
  import { sliceUtf16Safe, truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
26
- import { runCommandWithTimeout } from "openclaw/plugin-sdk/process-runtime";
27
26
  import http from "node:http";
28
27
  import { URL as URL$1 } from "node:url";
29
28
  import { pruneMapToMaxSize } from "openclaw/plugin-sdk/collection-runtime";
30
29
  import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
31
- import { resolveConfiguredCapabilityProvider } from "openclaw/plugin-sdk/provider-selection-runtime";
30
+ import { resolveConfiguredCapabilityProviderAsync } from "openclaw/plugin-sdk/provider-selection-runtime";
32
31
  import { WebSocket, WebSocketServer, rejectWebSocketUpgrade } from "openclaw/plugin-sdk/websocket-runtime";
33
32
  import { createDeferred } from "openclaw/plugin-sdk/extension-shared";
34
33
  import { canonicalizeBase64 } from "openclaw/plugin-sdk/media-runtime";
@@ -491,7 +490,7 @@ function createCallDeliveryRuntime(params) {
491
490
  }),
492
491
  idempotencyKey: message.idempotencyKey
493
492
  },
494
- preparation: { source: createNativeSessionBindingAuthority([], assertActive).assertLegacyCurrent }
493
+ preparation: { source: createNativeSessionBindingScope(assertActive).assertLegacyCurrent }
495
494
  });
496
495
  if (result.kind !== "result") throw new Error(`Requester transcript delivery failed: ${result.kind}`);
497
496
  return;
@@ -597,21 +596,6 @@ function trimCallReplayKeys(keys) {
597
596
  const overflow = keys.length - 500;
598
597
  if (overflow > 0) keys.splice(0, overflow);
599
598
  }
600
- /**
601
- * Reserve one rejected provider call. The token prevents a stale failed
602
- * hangup from deleting a newer reservation after bounded eviction and reuse.
603
- */
604
- function reserveRejectedProviderCall(calls, providerCallId) {
605
- if (calls.has(providerCallId)) return;
606
- const reservation = Symbol(providerCallId);
607
- calls.set(providerCallId, reservation);
608
- pruneOldestEntries(calls);
609
- return reservation;
610
- }
611
- /** Release a rejected-call reservation only when the failing attempt still owns it. */
612
- function releaseRejectedProviderCall(calls, providerCallId, reservation) {
613
- if (calls.get(providerCallId) === reservation) calls.delete(providerCallId);
614
- }
615
599
  //#endregion
616
600
  //#region extensions/voice-call/src/manager/store.ts
617
601
  const CALL_RECORD_EVENTS_NAMESPACE = "call-record-events";
@@ -1013,11 +997,10 @@ async function finalizeCall(params) {
1013
997
  }
1014
998
  //#endregion
1015
999
  //#region extensions/voice-call/src/manager/lookup.ts
1016
- /** Resolve an active call from provider call id with map lookup plus stale-map fallback scan. */
1000
+ /** Resolve an active call through the index maintained by call mutations. */
1017
1001
  function getCallByProviderCallId(params) {
1018
1002
  const callId = params.providerCallIdMap.get(params.providerCallId);
1019
- if (callId) return params.activeCalls.get(callId);
1020
- for (const call of params.activeCalls.values()) if (call.providerCallId === params.providerCallId) return call;
1003
+ return callId ? params.activeCalls.get(callId) : void 0;
1021
1004
  }
1022
1005
  /** Resolve an active call by internal call id or provider call id. */
1023
1006
  function findCall(params) {
@@ -1671,8 +1654,7 @@ async function processEventInQueue(ctx, event) {
1671
1654
  metadata: { rejectionReason: "inbound-policy" }
1672
1655
  }, ctx.stateRuntime);
1673
1656
  if (ctx.isStopping()) return { kind: "processed" };
1674
- const rejectionReservation = reserveRejectedProviderCall(ctx.rejectedProviderCallIds, pid);
1675
- if (rejectionReservation === void 0) return { kind: "ignored" };
1657
+ rememberManagerReplayKey(ctx.rejectedProviderCallIds, pid);
1676
1658
  rememberManagerReplayKey(ctx.processedEventIds, dedupeKey);
1677
1659
  log.info(`Rejecting inbound call by policy: ${pid}`);
1678
1660
  ctx.trackCallWork(ctx.provider.hangupCall({
@@ -1680,7 +1662,7 @@ async function processEventInQueue(ctx, event) {
1680
1662
  providerCallId: pid,
1681
1663
  reason: "hangup-bot"
1682
1664
  }).catch((err) => {
1683
- releaseRejectedProviderCall(ctx.rejectedProviderCallIds, pid, rejectionReservation);
1665
+ ctx.rejectedProviderCallIds.delete(pid);
1684
1666
  const message = formatErrorMessage(err);
1685
1667
  log.warn(`Failed to reject inbound call ${pid}: ${message}`);
1686
1668
  }));
@@ -1985,7 +1967,7 @@ var CallManager = class {
1985
1967
  this.activeCalls = /* @__PURE__ */ new Map();
1986
1968
  this.providerCallIdMap = /* @__PURE__ */ new Map();
1987
1969
  this.processedEventIds = /* @__PURE__ */ new Set();
1988
- this.rejectedProviderCallIds = /* @__PURE__ */ new Map();
1970
+ this.rejectedProviderCallIds = /* @__PURE__ */ new Set();
1989
1971
  this.provider = null;
1990
1972
  this.webhookUrl = null;
1991
1973
  this.activeTurnCalls = /* @__PURE__ */ new Set();
@@ -2030,7 +2012,7 @@ var CallManager = class {
2030
2012
  const persisted = await loadActiveCallsFromStore(this.storePath, this.stateRuntime);
2031
2013
  if (this.closing) return;
2032
2014
  this.processedEventIds = persisted.processedEventIds;
2033
- this.rejectedProviderCallIds = /* @__PURE__ */ new Map();
2015
+ this.rejectedProviderCallIds = /* @__PURE__ */ new Set();
2034
2016
  for (const call of persisted.interruptedDeliveries) {
2035
2017
  const metadata = { ...call.metadata };
2036
2018
  for (const key of ["callReport", "liveTranscriptDelivery"]) {
@@ -2291,13 +2273,12 @@ var CallManager = class {
2291
2273
  };
2292
2274
  //#endregion
2293
2275
  //#region extensions/voice-call/src/realtime-agent-context.ts
2294
- const contextSdk = realtimeBootstrapSdk;
2295
2276
  /** Build final realtime instructions from base instructions, consult policy, and agent context. */
2296
2277
  async function buildRealtimeVoiceInstructions(params) {
2297
2278
  const { config } = params;
2298
2279
  const contextConfig = config.realtime.agentContext;
2299
2280
  const sections = [params.baseInstructions, buildRealtimeVoiceAgentConsultPolicyInstructions(config.realtime)];
2300
- if (contextSdk.resolveRealtimeVoiceAgentContextInstructions) sections.push(await contextSdk.resolveRealtimeVoiceAgentContextInstructions({
2281
+ sections.push(await resolveRealtimeVoiceAgentContextInstructions({
2301
2282
  config: params.coreConfig,
2302
2283
  agentId: params.agentId,
2303
2284
  files: contextConfig.enabled && contextConfig.includeWorkspaceFiles ? contextConfig.files : [],
@@ -2305,14 +2286,6 @@ async function buildRealtimeVoiceInstructions(params) {
2305
2286
  maxChars: contextConfig.maxChars,
2306
2287
  warn: params.warn
2307
2288
  }));
2308
- else {
2309
- const { buildLegacyRealtimeVoiceAgentContext } = await import("./realtime-agent-context.legacy-DAEWY27o.mjs");
2310
- sections.push(await buildLegacyRealtimeVoiceAgentContext({
2311
- config: contextConfig,
2312
- coreConfig: params.coreConfig,
2313
- agentId: params.agentId
2314
- }));
2315
- }
2316
2289
  return sections.filter(Boolean).join("\n\n");
2317
2290
  }
2318
2291
  //#endregion
@@ -2554,7 +2527,7 @@ async function startNgrokTunnel(config) {
2554
2527
  ];
2555
2528
  if (config.ngrokDomain) args.push("--domain", config.ngrokDomain);
2556
2529
  return new Promise((resolve, reject) => {
2557
- const proc = spawn("ngrok", args, {
2530
+ const proc = spawnOomScoreAdjusted("ngrok", args, {
2558
2531
  stdio: [
2559
2532
  "ignore",
2560
2533
  "pipe",
@@ -2734,7 +2707,6 @@ const DEFAULT_MAX_CONNECTIONS = 128;
2734
2707
  const MAX_INBOUND_MESSAGE_BYTES = 65536;
2735
2708
  const MAX_WS_BUFFERED_BYTES = 1048576;
2736
2709
  const MAX_PENDING_TTS_OPERATIONS_PER_STREAM = 8;
2737
- const MAX_IGNORED_PLAYBACK_MARKS_PER_STREAM = 64;
2738
2710
  const PLAYBACK_MARK_TIMEOUT_GRACE_MS = 2e3;
2739
2711
  const CLOSE_REASON_LOG_MAX_CHARS = 120;
2740
2712
  function sanitizeLogText(value, maxChars) {
@@ -2763,7 +2735,6 @@ var MediaStreamHandler = class {
2763
2735
  this.ttsQueues = /* @__PURE__ */ new Map();
2764
2736
  this.ttsActiveControllers = /* @__PURE__ */ new Map();
2765
2737
  this.pendingPlaybackMarks = /* @__PURE__ */ new Map();
2766
- this.ignoredPlaybackMarks = /* @__PURE__ */ new Map();
2767
2738
  this.preStartTimeoutMs = resolveTimerTimeoutMs(config.preStartTimeoutMs, DEFAULT_PRE_START_TIMEOUT_MS);
2768
2739
  this.maxPendingConnections = config.maxPendingConnections ?? DEFAULT_MAX_PENDING_CONNECTIONS;
2769
2740
  this.maxPendingConnectionsPerIp = config.maxPendingConnectionsPerIp ?? DEFAULT_MAX_PENDING_CONNECTIONS_PER_IP;
@@ -3119,7 +3090,6 @@ Content-Length: ${Buffer.byteLength(body)}\r\n\r
3119
3090
  const marks = this.pendingPlaybackMarks.get(streamSid) ?? /* @__PURE__ */ new Map();
3120
3091
  this.pendingPlaybackMarks.set(streamSid, marks);
3121
3092
  if (marks.has(name)) throw new Error(`Telephony playback mark is already pending: ${name}`);
3122
- this.ignoredPlaybackMarks.get(streamSid)?.delete(name);
3123
3093
  let pending;
3124
3094
  const acknowledgement = new Promise((resolve, reject) => {
3125
3095
  const timeout = setTimeout(() => {
@@ -3129,15 +3099,14 @@ Content-Length: ${Buffer.byteLength(body)}\r\n\r
3129
3099
  timeout.unref?.();
3130
3100
  const onAbort = () => {
3131
3101
  const reason = signal.reason instanceof Error ? signal.reason : /* @__PURE__ */ new Error("Telephony playback mark wait aborted");
3132
- pending(reason, true);
3102
+ pending(reason);
3133
3103
  };
3134
- pending = (error, ignoreLateAck = false) => {
3104
+ pending = (error) => {
3135
3105
  if (marks.get(name) !== pending) return;
3136
3106
  clearTimeout(timeout);
3137
3107
  signal.removeEventListener("abort", onAbort);
3138
3108
  marks.delete(name);
3139
3109
  if (marks.size === 0) this.pendingPlaybackMarks.delete(streamSid);
3140
- if (ignoreLateAck) this.ignorePlaybackMark(streamSid, name);
3141
3110
  if (error) reject(error);
3142
3111
  else resolve();
3143
3112
  };
@@ -3186,27 +3155,12 @@ Content-Length: ${Buffer.byteLength(body)}\r\n\r
3186
3155
  this.clearAudio(streamSid);
3187
3156
  }
3188
3157
  acknowledgePlaybackMark(streamSid, name) {
3189
- const ignored = this.ignoredPlaybackMarks.get(streamSid);
3190
- if (ignored?.delete(name)) {
3191
- if (ignored.size === 0) this.ignoredPlaybackMarks.delete(streamSid);
3192
- return;
3193
- }
3194
3158
  this.pendingPlaybackMarks.get(streamSid)?.get(name)?.();
3195
3159
  }
3196
3160
  invalidatePlaybackMarks(streamSid) {
3197
3161
  const marks = this.pendingPlaybackMarks.get(streamSid);
3198
3162
  if (!marks) return;
3199
- for (const pending of marks.values()) pending(/* @__PURE__ */ new Error("Telephony playback cleared before completion"), true);
3200
- }
3201
- ignorePlaybackMark(streamSid, name) {
3202
- const ignored = this.ignoredPlaybackMarks.get(streamSid) ?? /* @__PURE__ */ new Set();
3203
- ignored.add(name);
3204
- while (ignored.size > MAX_IGNORED_PLAYBACK_MARKS_PER_STREAM) {
3205
- const oldest = ignored.values().next().value;
3206
- if (oldest === void 0) break;
3207
- ignored.delete(oldest);
3208
- }
3209
- this.ignoredPlaybackMarks.set(streamSid, ignored);
3163
+ for (const pending of marks.values()) pending(/* @__PURE__ */ new Error("Telephony playback cleared before completion"));
3210
3164
  }
3211
3165
  async processQueue(streamSid) {
3212
3166
  while (true) {
@@ -3284,7 +3238,6 @@ Content-Length: ${Buffer.byteLength(body)}\r\n\r
3284
3238
  this.ttsActiveControllers.delete(streamSid);
3285
3239
  this.ttsQueues.delete(streamSid);
3286
3240
  this.invalidatePlaybackMarks(streamSid);
3287
- this.ignoredPlaybackMarks.delete(streamSid);
3288
3241
  }
3289
3242
  abortTtsPlayback(streamSid) {
3290
3243
  for (const entry of this.ttsQueues.get(streamSid)?.splice(0) ?? []) {
@@ -3391,7 +3344,6 @@ var StreamDisconnectGrace = class {
3391
3344
  const current = this.streams.get(providerCallId);
3392
3345
  if (!current || current.streamId !== streamId || current.disconnectTimer) return;
3393
3346
  const disconnectTimer = setTimeout(() => {
3394
- if (this.streams.get(providerCallId)?.disconnectTimer !== disconnectTimer) return;
3395
3347
  this.streams.delete(providerCallId);
3396
3348
  this.onGraceExpired({
3397
3349
  providerCallId,
@@ -3449,8 +3401,8 @@ function appendRecentTalkEventMetadata(metadata, event, mode) {
3449
3401
  const MAX_WEBHOOK_BODY_BYTES = WEBHOOK_BODY_READ_DEFAULTS.preAuth.maxBytes;
3450
3402
  const WEBHOOK_BODY_TIMEOUT_MS = WEBHOOK_BODY_READ_DEFAULTS.preAuth.timeoutMs;
3451
3403
  const MISSING_REMOTE_ADDRESS_IN_FLIGHT_KEY = "__voice_call_no_remote__";
3452
- const loadRealtimeTranscriptionRuntime = createLazyRuntimeModule(() => import("./realtime-transcription.runtime-CbJAs5t_.mjs"));
3453
- const loadResponseGeneratorModule = createLazyRuntimeModule(() => import("./response-generator-COu0_8-d.mjs"));
3404
+ const loadRealtimeTranscriptionRuntime = createLazyRuntimeModule(() => import("./realtime-transcription.runtime-DIBopS1D.mjs"));
3405
+ const loadResponseGeneratorModule = createLazyRuntimeModule(() => import("./response-generator-DEU_bfHI.mjs"));
3454
3406
  function buildRequestUrl(requestUrl) {
3455
3407
  return new URL$1(requestUrl ?? "/", "http://localhost");
3456
3408
  }
@@ -3585,7 +3537,7 @@ var VoiceCallWebhookServer = class {
3585
3537
  const streaming = this.config.streaming;
3586
3538
  const pluginConfig = this.fullConfig ?? this.coreConfig ?? void 0;
3587
3539
  const { getRealtimeTranscriptionProvider, listRealtimeTranscriptionProviders } = await loadRealtimeTranscriptionRuntime();
3588
- const resolution = resolveConfiguredCapabilityProvider({
3540
+ const resolution = await resolveConfiguredCapabilityProviderAsync({
3589
3541
  configuredProviderId: streaming.provider,
3590
3542
  providerConfigs: streaming.providers,
3591
3543
  cfg: pluginConfig,
@@ -3596,10 +3548,13 @@ var VoiceCallWebhookServer = class {
3596
3548
  cfg,
3597
3549
  rawConfig
3598
3550
  }) ?? rawConfig,
3599
- isProviderConfigured: ({ provider, cfg, providerConfig }) => provider.isConfigured({
3551
+ isProviderConfigured: ({ provider, cfg, providerConfig }) => provider.isConfiguredAsync ? provider.isConfiguredAsync({
3552
+ cfg,
3553
+ providerConfig
3554
+ }) : provider.isConfigured?.({
3600
3555
  cfg,
3601
3556
  providerConfig
3602
- })
3557
+ }) ?? false
3603
3558
  });
3604
3559
  if (!resolution.ok) {
3605
3560
  this.logger.warn(resolution.code === "missing-configured-provider" ? `Streaming enabled but realtime transcription provider "${resolution.configuredProviderId}" is not registered` : resolution.code === "no-registered-provider" ? "Streaming enabled but no realtime transcription provider is registered" : `Streaming enabled but provider "${resolution.provider?.id}" is not configured`);
@@ -4123,7 +4078,7 @@ async function resolveProvider(config) {
4123
4078
  const allowNgrokFreeTierLoopbackBypass = config.tunnel?.provider === "ngrok" && isLoopbackHost(config.serve?.bind ?? "") && (config.tunnel?.allowNgrokFreeTierLoopbackBypass ?? false);
4124
4079
  switch (config.provider) {
4125
4080
  case "telnyx": {
4126
- const { TelnyxProvider } = await import("./telnyx-B4P0peEe.mjs");
4081
+ const { TelnyxProvider } = await import("./telnyx-BuC-Ttbj.mjs");
4127
4082
  return new TelnyxProvider({
4128
4083
  apiKey: config.telnyx?.apiKey,
4129
4084
  connectionId: config.telnyx?.connectionId,
@@ -4131,7 +4086,7 @@ async function resolveProvider(config) {
4131
4086
  }, { skipVerification: config.skipSignatureVerification });
4132
4087
  }
4133
4088
  case "twilio": {
4134
- const { TwilioProvider } = await import("./twilio-BzAYgH7M.mjs");
4089
+ const { TwilioProvider } = await import("./twilio-BVBIIqx-.mjs");
4135
4090
  return new TwilioProvider({
4136
4091
  accountSid: config.twilio?.accountSid,
4137
4092
  authToken: resolveTwilioAuthToken(config),
@@ -4145,7 +4100,7 @@ async function resolveProvider(config) {
4145
4100
  });
4146
4101
  }
4147
4102
  case "plivo": {
4148
- const { PlivoProvider } = await import("./plivo-COfD-Kuc.mjs");
4103
+ const { PlivoProvider } = await import("./plivo-C3NbDLe-.mjs");
4149
4104
  return new PlivoProvider({
4150
4105
  authId: config.plivo?.authId,
4151
4106
  authToken: config.plivo?.authToken
@@ -4228,10 +4183,10 @@ async function createVoiceCallRuntime(params) {
4228
4183
  const provider = await resolveProvider(config);
4229
4184
  if (stateRuntime) setVoiceCallStateRuntime({ state: stateRuntime });
4230
4185
  const manager = new CallManager(config, void 0, cfg.session, stateRuntime);
4231
- const realtimeVoiceRuntime = config.realtime.enabled ? await import("./realtime-voice.runtime-vtdCOWg-.mjs") : null;
4186
+ const realtimeVoiceRuntime = config.realtime.enabled ? await import("./realtime-voice.runtime-S9H9Y0Cp.mjs") : null;
4232
4187
  const webhookServer = new VoiceCallWebhookServer(params.scheduler, config, manager, provider, coreConfig, cfg, agentRuntime, log);
4233
4188
  if (realtimeVoiceRuntime) {
4234
- const { RealtimeCallHandler } = await import("./realtime-handler-BdAuKAC2.mjs");
4189
+ const { RealtimeCallHandler } = await import("./realtime-handler-dG-YrnQ5.mjs");
4235
4190
  const resolveRealtimeInstructions = await createRealtimeInstructionsResolver({
4236
4191
  config,
4237
4192
  coreConfig: cfg,
@@ -4241,11 +4196,11 @@ async function createVoiceCallRuntime(params) {
4241
4196
  ...config.realtime,
4242
4197
  tools: resolveVoiceCallRealtimeTools(config.realtime.toolPolicy, config.realtime.tools)
4243
4198
  };
4244
- const resolveCallRegistration = (call) => {
4199
+ const resolveCallRegistration = async (call) => {
4245
4200
  const numberRouteKey = resolveVoiceCallNumberRouteKeyForCall(call);
4246
4201
  const effectiveConfig = resolveVoiceCallEffectiveConfig(config, numberRouteKey).config;
4247
4202
  const agentId = resolveCallAgentId(call);
4248
- const resolved = realtimeVoiceRuntime.resolveConfiguredRealtimeVoiceProvider({
4203
+ const resolved = await realtimeVoiceRuntime.resolveConfiguredRealtimeVoiceProviderAsync({
4249
4204
  configuredProviderId: effectiveConfig.realtime.provider,
4250
4205
  providerConfigs: effectiveConfig.realtime.providers,
4251
4206
  cfg,
@@ -1,4 +1,4 @@
1
- import { i as verifyTelnyxWebhook, t as createCarrierApi } from "./carrier-api-BYKw_fsQ.mjs";
1
+ import { i as verifyTelnyxWebhook, t as createCarrierApi } from "./carrier-api-Bt9KxU9b.mjs";
2
2
  import crypto from "node:crypto";
3
3
  //#region extensions/voice-call/src/providers/telnyx.ts
4
4
  const HANGUP_REASONS = /* @__PURE__ */ new Map([
@@ -1,7 +1,7 @@
1
- import { a as mapProviderStatusToEndReason, c as getHeader, h as generateDtmfRedirectTwiml, i as isProviderStatusTerminal, o as normalizeProviderStatus } from "./runtime-entry-STjFmi_B.mjs";
1
+ import { a as mapProviderStatusToEndReason, c as getHeader, h as generateDtmfRedirectTwiml, i as isProviderStatusTerminal, o as normalizeProviderStatus } from "./runtime-entry-BqhtjzE-.mjs";
2
2
  import { n as requireSupportedTwilioApiHostname, r as resolveTwilioApiBaseUrl } from "./twilio-region-wBOx-8sz.mjs";
3
3
  import { n as mapVoiceToPolly, t as escapeXml } from "./voice-mapping-1lzFmzOF.mjs";
4
- import { a as verifyTwilioWebhook, t as createCarrierApi } from "./carrier-api-BYKw_fsQ.mjs";
4
+ import { a as verifyTwilioWebhook, t as createCarrierApi } from "./carrier-api-Bt9KxU9b.mjs";
5
5
  import { asNullableObjectRecord, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
6
6
  import { sleepWithAbort } from "openclaw/plugin-sdk/runtime-env";
7
7
  import crypto from "node:crypto";
@@ -67,9 +67,7 @@ var TwilioProvider = class TwilioProvider {
67
67
  releaseCallState(providerCallId, callId) {
68
68
  const webhookUrl = this.callWebhookUrls.get(providerCallId);
69
69
  let resolvedCallId = callId;
70
- if (!resolvedCallId && webhookUrl) try {
71
- resolvedCallId = new URL(webhookUrl).searchParams.get("callId") || void 0;
72
- } catch {}
70
+ if (!resolvedCallId && webhookUrl) resolvedCallId = new URL(webhookUrl).searchParams.get("callId") || void 0;
73
71
  if (resolvedCallId) this.twimlStorage.delete(resolvedCallId);
74
72
  this.callWebhookUrls.delete(providerCallId);
75
73
  this.callStreamMap.delete(providerCallId);
@@ -35,11 +35,13 @@ const schemaMigrationDescriptions = {
35
35
  "state-table-retirement-v10": "retired shared-state tables -> removed tables and indexes",
36
36
  "state-table-retirement-v11": "retired skill curator tables -> removed tables and indexes",
37
37
  "singleton-state-foldin-v12": "singleton state tables -> shared configuration state",
38
- "state-consolidation-v13": "cron jobs and subagent runs -> canonical JSON storage",
38
+ "state-consolidation-v13": "cron jobs and subagent runs -> current JSON storage",
39
39
  "creator-namespace-v14": "cron creators -> explicit principal namespaces",
40
40
  "conversation-binding-targets-v15": "conversation bindings -> exact target keys without agent/session projections",
41
41
  "prepared-worker-ownership-v17": "prepared workers -> one-use capacity and fixed workspace ownership",
42
42
  "github-publication-requester-authority-v18": "GitHub publication receipts -> original requesting authority",
43
+ "portable-identity-columns-v22": "ACP resume keys and receipt cursors -> portable columns",
44
+ "predicate-columns-v21": "meeting transcripts and delivery retention -> derived predicate columns",
43
45
  "worker-placement-execution-mode-v8": "cloud worker placements -> execution-mode claims",
44
46
  "operator-approvals-system-agent": "operator approvals -> OpenClaw system changes",
45
47
  "session-watch-cursor-provenance-v4": "session watch cursors -> provenance column",
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { definePluginEntry } from "./api.js";
2
2
  import { VOICE_CALL_CLI_DESCRIPTOR } from "./cli-output-mode.js";
3
- import { g as CallBriefSchema, i as resolveVoiceCallConfig, t as VoiceCallConfigSchema, u as validateProviderConfig } from "./.setup/config-CsrGuamE.mjs";
4
- import { C as TerminalStates, t as createVoiceCallRuntime } from "./.setup/runtime-entry-STjFmi_B.mjs";
3
+ import { g as CallBriefSchema, i as resolveVoiceCallConfig, t as VoiceCallConfigSchema, u as validateProviderConfig } from "./.setup/config-BdbTQCSh.mjs";
4
+ import { C as TerminalStates, t as createVoiceCallRuntime } from "./.setup/runtime-entry-BqhtjzE-.mjs";
5
5
  import { AsyncLocalStorage } from "node:async_hooks";
6
6
  import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
7
7
  import { ErrorCodes, errorShape } from "openclaw/plugin-sdk/gateway-runtime";
@@ -330,10 +330,9 @@ function isCliOnlyProcess() {
330
330
  const VOICE_CALL_RUNTIME_COORDINATOR_KEY = Symbol.for("openclaw.voice-call.runtimeCoordinator");
331
331
  var VoiceCallRuntimeLifecycleError = class extends Error {};
332
332
  function getVoiceCallRuntimeCoordinator() {
333
- return resolveGlobalSingleton(VOICE_CALL_RUNTIME_COORDINATOR_KEY, () => ({ epochCounter: 0 }));
333
+ return resolveGlobalSingleton(VOICE_CALL_RUNTIME_COORDINATOR_KEY, () => ({}));
334
334
  }
335
335
  function activateVoiceCallRuntimeGeneration(coordinator, registration, generation) {
336
- if (registration.epoch < (coordinator.current?.epoch ?? 0) || registration.generation !== generation) throw new VoiceCallRuntimeLifecycleError("Voice call runtime generation was superseded; use the current plugin registration");
337
336
  if (generation.retired) throw new VoiceCallRuntimeLifecycleError("Voice call runtime generation is retired; use the current plugin registration");
338
337
  if (coordinator.current !== registration) {
339
338
  if (coordinator.current) coordinator.current.generation.retired = true;
@@ -342,18 +341,9 @@ function activateVoiceCallRuntimeGeneration(coordinator, registration, generatio
342
341
  }
343
342
  function stopVoiceCallRuntimeGeneration(coordinator, generation) {
344
343
  const ownedSlot = coordinator.slot?.owner === generation ? coordinator.slot : void 0;
345
- if (!ownedSlot || ownedSlot.state === "stopping") return ownedSlot?.promise ?? Promise.resolve();
346
- const stopPromise = Promise.resolve().then(async () => {
347
- await (ownedSlot.state === "running" ? ownedSlot.runtime : await ownedSlot.promise).stop();
348
- });
349
- const stoppingSlot = {
350
- state: "stopping",
351
- owner: generation,
352
- promise: stopPromise
353
- };
354
- if (coordinator.slot === ownedSlot) coordinator.slot = stoppingSlot;
355
- return stopPromise.finally(() => {
356
- if (coordinator.slot === stoppingSlot) coordinator.slot = void 0;
344
+ if (!ownedSlot) return Promise.resolve();
345
+ return ownedSlot.stopPromise ??= ownedSlot.promise.then((runtime) => runtime.stop()).finally(() => {
346
+ if (coordinator.slot === ownedSlot) coordinator.slot = void 0;
357
347
  });
358
348
  }
359
349
  var voice_call_default = definePluginEntry({
@@ -366,7 +356,6 @@ var voice_call_default = definePluginEntry({
366
356
  const validation = validateProviderConfig(config);
367
357
  const runtimeCoordinator = getVoiceCallRuntimeCoordinator();
368
358
  const runtimeRegistration = {
369
- epoch: ++runtimeCoordinator.epochCounter,
370
359
  generation: { retired: false },
371
360
  ensureRuntime: () => ensureRegisteredRuntime()
372
361
  };
@@ -380,58 +369,40 @@ var voice_call_default = definePluginEntry({
380
369
  if (!config.enabled) throw new Error("Voice call disabled in plugin config");
381
370
  if (!validation.valid) throw new Error(validation.errors.join("; "));
382
371
  if ((config.reports.enabled || config.live.transcript) && !runtimeGeneration.runInServiceContext) throw new VoiceCallRuntimeLifecycleError("Voice call requester reporting requires service startup");
383
- while (true) {
372
+ const previous = runtimeCoordinator.slot;
373
+ if (previous && (previous.owner !== runtimeGeneration || previous.stopPromise)) {
374
+ await stopVoiceCallRuntimeGeneration(runtimeCoordinator, previous.owner);
384
375
  activateRuntimeGeneration(runtimeGeneration);
385
- const slot = runtimeCoordinator.slot;
386
- if (slot) {
387
- if (slot.owner !== runtimeGeneration) {
388
- if (slot.owner.retired) {
389
- await stopVoiceCallRuntimeGeneration(runtimeCoordinator, slot.owner);
390
- continue;
391
- }
392
- throw new VoiceCallRuntimeLifecycleError("A previous voice call runtime generation is still active; retry after it stops");
393
- }
394
- if (slot.state === "running") return slot.runtime;
395
- if (slot.state === "stopping") {
396
- await slot.promise;
397
- continue;
398
- }
399
- let createdRuntime;
400
- try {
401
- createdRuntime = await slot.promise;
402
- } catch (err) {
403
- if (runtimeCoordinator.slot === slot) runtimeCoordinator.slot = void 0;
404
- throw err;
405
- }
406
- activateRuntimeGeneration(runtimeGeneration);
407
- if (runtimeCoordinator.slot !== slot) continue;
408
- runtimeCoordinator.slot = {
409
- state: "running",
410
- owner: runtimeGeneration,
411
- runtime: createdRuntime
412
- };
413
- return createdRuntime;
414
- }
376
+ }
377
+ if (!runtimeCoordinator.slot) {
415
378
  const scheduler = runtimeGeneration.scheduler;
416
379
  if (!scheduler || scheduler.signal.aborted) throw new VoiceCallRuntimeLifecycleError("Voice call service is not running; start the Gateway and retry");
417
- const runtimePromise = createVoiceCallRuntime({
418
- scheduler,
419
- config,
420
- coreConfig: api.config,
421
- fullConfig: api.config,
422
- agentRuntime: api.runtime.agent,
423
- stateRuntime: api.runtime.state,
424
- ttsRuntime: api.runtime.tts,
425
- deliveryRuntime: api.runtime,
426
- runInServiceContext: runtimeGeneration.runInServiceContext,
427
- logger: api.logger
428
- });
429
380
  runtimeCoordinator.slot = {
430
- state: "starting",
431
381
  owner: runtimeGeneration,
432
- promise: runtimePromise
382
+ promise: createVoiceCallRuntime({
383
+ scheduler,
384
+ config,
385
+ coreConfig: api.config,
386
+ fullConfig: api.config,
387
+ agentRuntime: api.runtime.agent,
388
+ stateRuntime: api.runtime.state,
389
+ ttsRuntime: api.runtime.tts,
390
+ deliveryRuntime: api.runtime,
391
+ runInServiceContext: runtimeGeneration.runInServiceContext,
392
+ logger: api.logger
393
+ })
433
394
  };
434
395
  }
396
+ const slot = runtimeCoordinator.slot;
397
+ let runtime;
398
+ try {
399
+ runtime = await slot.promise;
400
+ } catch (err) {
401
+ if (runtimeCoordinator.slot === slot && !slot.stopPromise) runtimeCoordinator.slot = void 0;
402
+ throw err;
403
+ }
404
+ activateRuntimeGeneration(runtimeGeneration);
405
+ return runtime;
435
406
  };
436
407
  const ensureRegisteredRuntime = async (runtimeGeneration = runtimeRegistration.generation) => {
437
408
  try {
@@ -597,7 +568,7 @@ var voice_call_default = definePluginEntry({
597
568
  };
598
569
  });
599
570
  api.registerCli(async ({ program }) => {
600
- const { registerVoiceCallCli } = await import("./.setup/cli-dyyK8Upw.mjs");
571
+ const { registerVoiceCallCli } = await import("./.setup/cli-RHaXpQxE.mjs");
601
572
  registerVoiceCallCli({
602
573
  program,
603
574
  config,
@@ -1,2 +1 @@
1
- import { t as createVoiceCallRuntime } from "./.setup/runtime-entry-STjFmi_B.mjs";
2
- export { createVoiceCallRuntime };
1
+ export { t as createVoiceCallRuntime } from "./.setup/runtime-entry-BqhtjzE-.mjs";
@@ -1,2 +1 @@
1
- import { n as stateMigrations, t as packageName } from "./.setup/state-retention-api-ONgqE-_V.mjs";
2
- export { packageName, stateMigrations };
1
+ export { t as packageName, n as stateMigrations } from "./.setup/state-retention-api-ONgqE-_V.mjs";
@@ -12,7 +12,7 @@ export function FromObject(context, type, value) {
12
12
  return value;
13
13
  const additionalProperties = GetAdditionalProperties(type);
14
14
  for (const key of Guard.Keys(value)) {
15
- if (Guard.HasPropertyKey(type.properties, key)) {
15
+ if (Guard.HasPropertyKey(type.properties, key) && IsSchema(type.properties[key])) {
16
16
  value[key] = FromType(context, type.properties[key], value[key]);
17
17
  continue;
18
18
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "typebox",
3
3
  "description": "Json Schema Type Builder with Static Type Resolution for TypeScript",
4
- "version": "1.3.34",
4
+ "version": "1.3.35",
5
5
  "keywords": [
6
6
  "typescript",
7
7
  "jsonschema"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/voice-call",
3
- "version": "2026.10.5-beta.4",
3
+ "version": "2026.10.10-beta.1",
4
4
  "description": "OpenClaw voice-call plugin for Twilio, Telnyx, and Plivo phone calls.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -8,11 +8,11 @@
8
8
  },
9
9
  "type": "module",
10
10
  "dependencies": {
11
- "typebox": "1.3.34",
11
+ "typebox": "1.3.35",
12
12
  "zod": "4.6.5"
13
13
  },
14
14
  "peerDependencies": {
15
- "openclaw": ">=2026.10.5-beta.4"
15
+ "openclaw": ">=2026.10.10-beta.1"
16
16
  },
17
17
  "peerDependenciesMeta": {
18
18
  "openclaw": {
@@ -31,10 +31,10 @@
31
31
  "minHostVersion": ">=2026.4.10"
32
32
  },
33
33
  "compat": {
34
- "pluginApi": ">=2026.10.5-beta.4"
34
+ "pluginApi": ">=2026.10.10-beta.1"
35
35
  },
36
36
  "build": {
37
- "openclawVersion": "2026.10.5-beta.4"
37
+ "openclawVersion": "2026.10.10-beta.1"
38
38
  },
39
39
  "release": {
40
40
  "publishToClawHub": true,
@@ -1,53 +0,0 @@
1
- import { asOptionalRecord, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
2
- import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
3
- import { root } from "openclaw/plugin-sdk/security-runtime";
4
- import { resolveAgentIdentity, resolveAgentWorkspaceDir } from "openclaw/plugin-sdk/agent-runtime";
5
- //#region extensions/voice-call/src/realtime-agent-context.legacy.ts
6
- function limitLegacyContextText(text, maxChars) {
7
- if (text.length <= maxChars) return text;
8
- const truncated = `${truncateUtf16Safe(text, Math.max(0, maxChars - 32)).trimEnd()}\n[truncated]`;
9
- return truncateUtf16Safe(truncated, maxChars);
10
- }
11
- async function buildLegacyRealtimeVoiceAgentContext(params) {
12
- const { config, coreConfig, agentId } = params;
13
- if (!config.enabled) return;
14
- const capsule = [
15
- "OpenClaw agent voice context:",
16
- `- Agent id: ${agentId}`,
17
- "- Use this context to match the OpenClaw agent's personality and standing preferences on fast voice turns.",
18
- "- Treat this as compact context only; call openclaw_agent_consult when the caller needs the full agent brain, tools, memory, or workspace state."
19
- ];
20
- if (config.includeIdentity) {
21
- const identity = asOptionalRecord(resolveAgentIdentity(coreConfig, agentId));
22
- const lines = [
23
- ["Name", identity?.name],
24
- ["Emoji", identity?.emoji],
25
- ["Vibe", identity?.vibe],
26
- ["Theme", identity?.theme],
27
- ["Creature/persona", identity?.creature]
28
- ].flatMap(([label, value]) => {
29
- const text = normalizeOptionalString(value);
30
- return text ? [`- ${label}: ${text}`] : [];
31
- });
32
- if (lines.length > 0) capsule.push(`Configured identity:\n${lines.join("\n")}`);
33
- }
34
- if (config.includeWorkspaceFiles) {
35
- const workspace = await root(resolveAgentWorkspaceDir(coreConfig, agentId)).catch(() => null);
36
- if (workspace) {
37
- const sections = [];
38
- let remaining = config.maxChars;
39
- for (const file of config.files) {
40
- if (remaining <= 0) continue;
41
- const trimmed = (await workspace.readText(file).catch(() => void 0))?.trim();
42
- if (!trimmed) continue;
43
- const section = `### ${file}\n${limitLegacyContextText(trimmed, Math.max(0, remaining - file.length - 16))}`;
44
- sections.push(section);
45
- remaining -= section.length;
46
- }
47
- if (sections.length > 0) capsule.push(`Workspace voice context:\n${sections.join("\n\n")}`);
48
- }
49
- }
50
- return limitLegacyContextText(capsule.join("\n\n"), config.maxChars);
51
- }
52
- //#endregion
53
- export { buildLegacyRealtimeVoiceAgentContext };
@@ -1,2 +0,0 @@
1
- import { getRealtimeVoiceProvider, listRealtimeVoiceProviders, resolveConfiguredRealtimeVoiceProvider } from "openclaw/plugin-sdk/realtime-voice";
2
- export { getRealtimeVoiceProvider, listRealtimeVoiceProviders, resolveConfiguredRealtimeVoiceProvider };