@openclaw/voice-call 2026.6.8 → 2026.6.9-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,6 +1,6 @@
1
1
  import { fetchWithSsrFGuard } from "./runtime-api.js";
2
2
  import "./api.js";
3
- import { a as getHeader } from "./runtime-entry-DeDXZz2k.js";
3
+ import { a as getHeader } from "./runtime-entry-BJNPmzDP.js";
4
4
  import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
5
5
  import { isLoopbackHost } from "openclaw/plugin-sdk/gateway-runtime";
6
6
  import { isFutureDateTimestampMs, resolveExpiresAtMsFromDurationMs } from "openclaw/plugin-sdk/number-runtime";
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { definePluginEntry, sleep } from "./runtime-api.js";
2
2
  import "./api.js";
3
3
  import { i as resolveVoiceCallConfig, s as validateProviderConfig } from "./config-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-DeDXZz2k.js";
4
+ import { c as getTailscaleSelfInfo, l as setupTailscaleExposureRoute, o as resolveWebhookExposureStatus, p as resolveUserPath, s as cleanupTailscaleExposureRoute, t as createVoiceCallRuntime } from "./runtime-entry-BJNPmzDP.js";
5
5
  import { c as getCallHistoryFromStore, m as setVoiceCallStateRuntime } from "./store-8M2m4Isq.js";
6
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";
@@ -1,5 +1,5 @@
1
- import { a as getHeader, m as escapeXml } from "./runtime-entry-DeDXZz2k.js";
2
- import { n as reconstructWebhookUrl, r as verifyPlivoWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-tNxwxQt0.js";
1
+ import { a as getHeader, m as escapeXml } from "./runtime-entry-BJNPmzDP.js";
2
+ import { n as reconstructWebhookUrl, r as verifyPlivoWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-CwIsZeyV.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
@@ -1,5 +1,5 @@
1
1
  import { o as resolveVoiceCallSessionKey } from "./config-jaA6vOZ2.js";
2
- import { f as resolveVoiceResponseModel } from "./runtime-entry-DeDXZz2k.js";
2
+ import { f as resolveVoiceResponseModel } from "./runtime-entry-BJNPmzDP.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";
@@ -107,6 +107,16 @@ function startMaxDurationTimer(params) {
107
107
  }, maxDurationMs);
108
108
  params.ctx.maxDurationTimers.set(params.callId, timer);
109
109
  }
110
+ /** Backfill max-duration enforcement from the first live conversation signal. */
111
+ function ensureMaxDurationTimerForLiveCall(params) {
112
+ if (params.call.answeredAt) return;
113
+ params.call.answeredAt = params.liveAt;
114
+ startMaxDurationTimer({
115
+ ctx: params.ctx,
116
+ callId: params.call.callId,
117
+ onTimeout: params.onTimeout
118
+ });
119
+ }
110
120
  /** Clear and forget a pending final-transcript waiter. */
111
121
  function clearTranscriptWaiter(ctx, callId) {
112
122
  const waiter = ctx.transcriptWaiters.get(callId);
@@ -418,6 +428,14 @@ async function speak(ctx, callId, text) {
418
428
  };
419
429
  const { call, providerCallId, provider } = connected;
420
430
  try {
431
+ ensureMaxDurationTimerForLiveCall({
432
+ ctx,
433
+ call,
434
+ liveAt: Date.now(),
435
+ onTimeout: async (id) => {
436
+ await endCall(ctx, id, { reason: "timeout" });
437
+ }
438
+ });
421
439
  transitionState(call, "speaking");
422
440
  persistCallRecord(ctx.storePath, call);
423
441
  const numberRouteKey = typeof call.metadata?.numberRouteKey === "string" ? call.metadata.numberRouteKey : call.to;
@@ -795,6 +813,14 @@ function processEvent(ctx, event) {
795
813
  transitionState(call, "active");
796
814
  break;
797
815
  case "call.speaking":
816
+ ensureMaxDurationTimerForLiveCall({
817
+ ctx,
818
+ call,
819
+ liveAt: event.timestamp,
820
+ onTimeout: async (callId) => {
821
+ await endCall(ctx, callId, { reason: "timeout" });
822
+ }
823
+ });
798
824
  transitionState(call, "speaking");
799
825
  break;
800
826
  case "call.speech":
@@ -807,6 +833,14 @@ function processEvent(ctx, event) {
807
833
  }
808
834
  addTranscriptEntry(call, "user", event.transcript);
809
835
  }
836
+ ensureMaxDurationTimerForLiveCall({
837
+ ctx,
838
+ call,
839
+ liveAt: event.timestamp,
840
+ onTimeout: async (callId) => {
841
+ await endCall(ctx, callId, { reason: "timeout" });
842
+ }
843
+ });
810
844
  transitionState(call, "listening");
811
845
  break;
812
846
  case "call.silence":
@@ -857,6 +891,9 @@ function incrementRestoreStatusCount(counts, status) {
857
891
  const key = normalizeOptionalString(status) ?? "terminal";
858
892
  counts.set(key, (counts.get(key) ?? 0) + 1);
859
893
  }
894
+ function resolveRestoredMaxDurationAnchor(call) {
895
+ return call.answeredAt ?? (call.state === "speaking" || call.state === "listening" ? call.startedAt : void 0);
896
+ }
860
897
  function resolveDefaultStoreBase(config, storePath) {
861
898
  const rawOverride = storePath?.trim() || config.store?.trim();
862
899
  if (rawOverride) return resolveUserPath(rawOverride);
@@ -903,24 +940,31 @@ var CallManager = class {
903
940
  this.providerCallIdMap = /* @__PURE__ */ new Map();
904
941
  for (const [callId, call] of verified) if (call.providerCallId) this.providerCallIdMap.set(call.providerCallId, callId);
905
942
  let skippedAlreadyElapsedTimers = 0;
906
- for (const [callId, call] of verified) if (call.answeredAt && !TerminalStates.has(call.state)) {
907
- const elapsed = Date.now() - call.answeredAt;
908
- const maxDurationMs = resolveVoiceCallSecondsTimerDelayMs(this.config.maxDurationSeconds);
909
- if (elapsed >= maxDurationMs) {
910
- verified.delete(callId);
911
- if (call.providerCallId) this.providerCallIdMap.delete(call.providerCallId);
912
- skippedAlreadyElapsedTimers += 1;
913
- continue;
914
- }
915
- startMaxDurationTimer({
916
- ctx: this.getContext(),
917
- callId,
918
- timeoutMs: maxDurationMs - elapsed,
919
- onTimeout: async (id) => {
920
- await endCall(this.getContext(), id, { reason: "timeout" });
943
+ for (const [callId, call] of verified) {
944
+ const maxDurationAnchor = resolveRestoredMaxDurationAnchor(call);
945
+ if (maxDurationAnchor !== void 0 && !TerminalStates.has(call.state)) {
946
+ const elapsed = Date.now() - maxDurationAnchor;
947
+ const maxDurationMs = resolveVoiceCallSecondsTimerDelayMs(this.config.maxDurationSeconds);
948
+ if (elapsed >= maxDurationMs) {
949
+ verified.delete(callId);
950
+ if (call.providerCallId) this.providerCallIdMap.delete(call.providerCallId);
951
+ skippedAlreadyElapsedTimers += 1;
952
+ continue;
921
953
  }
922
- });
923
- console.log(`[voice-call] Restarted max-duration timer for restored call ${callId}`);
954
+ if (call.answeredAt === void 0) {
955
+ call.answeredAt = maxDurationAnchor;
956
+ persistCallRecord(this.storePath, call);
957
+ }
958
+ startMaxDurationTimer({
959
+ ctx: this.getContext(),
960
+ callId,
961
+ timeoutMs: maxDurationMs - elapsed,
962
+ onTimeout: async (id) => {
963
+ await endCall(this.getContext(), id, { reason: "timeout" });
964
+ }
965
+ });
966
+ console.log(`[voice-call] Restarted max-duration timer for restored call ${callId}`);
967
+ }
924
968
  }
925
969
  if (skippedAlreadyElapsedTimers > 0) console.log(`[voice-call] Skipped ${skippedAlreadyElapsedTimers} restored call(s) whose max-duration timer already elapsed`);
926
970
  if (verified.size > 0) console.log(`[voice-call] Restored ${verified.size} active call(s) from store`);
@@ -2442,6 +2486,11 @@ function isProviderStatusTerminal(status) {
2442
2486
  //#endregion
2443
2487
  //#region extensions/voice-call/src/webhook/stale-call-reaper.ts
2444
2488
  const CHECK_INTERVAL_MS = 3e4;
2489
+ /** States that indicate a live conversation with speech/transcription.
2490
+ * Inbound Twilio calls may never fire a call.answered event, so answeredAt
2491
+ * can be absent even while the call is actively transcribing. These states
2492
+ * prove the call is live and should not be reaped. */
2493
+ const LiveConversationStates = new Set(["speaking", "listening"]);
2445
2494
  /** Start a stale-call reaper and return its cleanup callback. */
2446
2495
  function startStaleCallReaper(params) {
2447
2496
  const maxAgeSeconds = params.staleCallReaperSeconds;
@@ -2450,7 +2499,7 @@ function startStaleCallReaper(params) {
2450
2499
  const interval = setInterval(() => {
2451
2500
  const now = Date.now();
2452
2501
  for (const call of params.manager.getActiveCalls()) {
2453
- if (call.answeredAt || TerminalStates.has(call.state)) continue;
2502
+ if (call.answeredAt || TerminalStates.has(call.state) || LiveConversationStates.has(call.state)) continue;
2454
2503
  const age = now - call.startedAt;
2455
2504
  if (age > maxAgeMs) {
2456
2505
  console.log(`[voice-call] Reaping stale call ${call.callId} (age: ${Math.round(age / 1e3)}s, state: ${call.state})`);
@@ -2478,7 +2527,7 @@ function loadRealtimeTranscriptionRuntime() {
2478
2527
  return realtimeTranscriptionRuntimePromise;
2479
2528
  }
2480
2529
  function loadResponseGeneratorModule() {
2481
- responseGeneratorModulePromise ??= import("./response-generator-39zOYri_.js");
2530
+ responseGeneratorModulePromise ??= import("./response-generator-DRjFfMHx.js");
2482
2531
  return responseGeneratorModulePromise;
2483
2532
  }
2484
2533
  function sanitizeTranscriptForLog(value) {
@@ -3055,7 +3104,11 @@ var VoiceCallWebhookServer = class {
3055
3104
  try {
3056
3105
  const pathname = buildRequestUrl(req.url).pathname;
3057
3106
  const pattern = this.realtimeHandler?.getStreamPathPattern();
3058
- return Boolean(pattern && pathname.startsWith(pattern));
3107
+ if (!pattern) return false;
3108
+ const normalizedPattern = this.normalizeWebhookPathForMatch(pattern);
3109
+ const normalizedPathname = this.normalizeWebhookPathForMatch(pathname);
3110
+ if (normalizedPattern === "/") return true;
3111
+ return normalizedPathname === normalizedPattern || normalizedPathname.startsWith(`${normalizedPattern}/`);
3059
3112
  } catch {
3060
3113
  return false;
3061
3114
  }
@@ -3162,15 +3215,15 @@ let mockProviderPromise;
3162
3215
  let realtimeVoiceRuntimePromise;
3163
3216
  let realtimeHandlerPromise;
3164
3217
  function loadTelnyxProvider() {
3165
- telnyxProviderPromise ??= import("./telnyx-YbJp3QiY.js");
3218
+ telnyxProviderPromise ??= import("./telnyx-Bkl9KKSK.js");
3166
3219
  return telnyxProviderPromise;
3167
3220
  }
3168
3221
  function loadTwilioProvider() {
3169
- twilioProviderPromise ??= import("./twilio-DBcVB36L.js");
3222
+ twilioProviderPromise ??= import("./twilio-CDM_5OfE.js");
3170
3223
  return twilioProviderPromise;
3171
3224
  }
3172
3225
  function loadPlivoProvider() {
3173
- plivoProviderPromise ??= import("./plivo-K3Nv0D8o.js");
3226
+ plivoProviderPromise ??= import("./plivo-BK14Vyzi.js");
3174
3227
  return plivoProviderPromise;
3175
3228
  }
3176
3229
  function loadMockProvider() {
@@ -1,2 +1,2 @@
1
- import { t as createVoiceCallRuntime } from "./runtime-entry-DeDXZz2k.js";
1
+ import { t as createVoiceCallRuntime } from "./runtime-entry-BJNPmzDP.js";
2
2
  export { createVoiceCallRuntime };
@@ -1,4 +1,4 @@
1
- import { i as verifyTelnyxWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-tNxwxQt0.js";
1
+ import { i as verifyTelnyxWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-CwIsZeyV.js";
2
2
  import crypto from "node:crypto";
3
3
  //#region extensions/voice-call/src/providers/telnyx.ts
4
4
  function normalizeTelnyxDirection(direction) {
@@ -1,7 +1,7 @@
1
1
  import { fetchWithSsrFGuard } from "./runtime-api.js";
2
2
  import "./api.js";
3
- import { a as getHeader, d as chunkAudio, h as mapVoiceToPolly, i as normalizeProviderStatus, m as escapeXml, n as isProviderStatusTerminal, r as mapProviderStatusToEndReason } from "./runtime-entry-DeDXZz2k.js";
4
- import { a as verifyTwilioWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-tNxwxQt0.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-BJNPmzDP.js";
4
+ import { a as verifyTwilioWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-CwIsZeyV.js";
5
5
  import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
6
6
  import crypto from "node:crypto";
7
7
  import { safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@openclaw/voice-call",
3
- "version": "2026.6.8",
3
+ "version": "2026.6.9-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.8",
9
+ "version": "2026.6.9-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.8"
17
+ "openclaw": ">=2026.6.9-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.8",
3
+ "version": "2026.6.9-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.8"
17
+ "openclaw": ">=2026.6.9-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.8"
34
+ "pluginApi": ">=2026.6.9-beta.1"
35
35
  },
36
36
  "build": {
37
- "openclawVersion": "2026.6.8"
37
+ "openclawVersion": "2026.6.9-beta.1"
38
38
  },
39
39
  "release": {
40
40
  "publishToClawHub": true,