diff --git a/docs/plugins/voice-call.md b/docs/plugins/voice-call.md index 590988f5d0..aba6355502 100644 --- a/docs/plugins/voice-call.md +++ b/docs/plugins/voice-call.md @@ -126,6 +126,35 @@ Notes: - `tunnel.allowNgrokFreeTierLoopbackBypass: true` allows Twilio webhooks with invalid signatures **only** when `tunnel.provider="ngrok"` and `serve.bind` is loopback (ngrok local agent). Use for local dev only. - Ngrok free tier URLs can change or add interstitial behavior; if `publicUrl` drifts, Twilio signatures will fail. For production, prefer a stable domain or Tailscale funnel. +## Stale call reaper + +Use `staleCallReaperSeconds` to end calls that never receive a terminal webhook +(for example, notify-mode calls that never complete). The default is `0` +(disabled). + +Recommended ranges: + +- **Production:** `120`–`300` seconds for notify-style flows. +- Keep this value **higher than `maxDurationSeconds`** so normal calls can + finish. A good starting point is `maxDurationSeconds + 30–60` seconds. + +Example: + +```json5 +{ + plugins: { + entries: { + "voice-call": { + config: { + maxDurationSeconds: 300, + staleCallReaperSeconds: 360, + }, + }, + }, + }, +} +``` + ## Webhook Security When a proxy or tunnel sits in front of the Gateway, the plugin reconstructs the diff --git a/extensions/voice-call/README.md b/extensions/voice-call/README.md index 6ac2dd602a..88328b6a33 100644 --- a/extensions/voice-call/README.md +++ b/extensions/voice-call/README.md @@ -87,6 +87,26 @@ Notes: - Telnyx requires `telnyx.publicKey` (or `TELNYX_PUBLIC_KEY`) unless `skipSignatureVerification` is true. - `tunnel.allowNgrokFreeTierLoopbackBypass: true` allows Twilio webhooks with invalid signatures **only** when `tunnel.provider="ngrok"` and `serve.bind` is loopback (ngrok local agent). Use for local dev only. +## Stale call reaper + +Use `staleCallReaperSeconds` to end calls that never receive a terminal webhook +(for example, notify-mode calls that never complete). The default is `0` +(disabled). + +Recommended ranges: + +- **Production:** `120`–`300` seconds for notify-style flows. +- Keep this value **higher than `maxDurationSeconds`** so normal calls can + finish. A good starting point is `maxDurationSeconds + 30–60` seconds. + +Example: + +```json5 +{ + staleCallReaperSeconds: 360, +} +``` + ## TTS for calls Voice Call uses the core `messages.tts` configuration (OpenAI or ElevenLabs) for diff --git a/extensions/voice-call/src/webhook.test.ts b/extensions/voice-call/src/webhook.test.ts index 2b4b611c3f..51afdb7eba 100644 --- a/extensions/voice-call/src/webhook.test.ts +++ b/extensions/voice-call/src/webhook.test.ts @@ -80,6 +80,24 @@ describe("VoiceCallWebhookServer stale call reaper", () => { } }); + it("skips calls that are younger than the threshold", async () => { + const now = new Date("2026-02-16T00:00:00Z"); + vi.setSystemTime(now); + + const call = createCall(now.getTime() - 10_000); + const { manager, endCall } = createManager([call]); + const config = createConfig({ staleCallReaperSeconds: 60 }); + const server = new VoiceCallWebhookServer(config, manager, provider); + + try { + await server.start(); + await vi.advanceTimersByTimeAsync(30_000); + expect(endCall).not.toHaveBeenCalled(); + } finally { + await server.stop(); + } + }); + it("does not run when staleCallReaperSeconds is disabled", async () => { const now = new Date("2026-02-16T00:00:00Z"); vi.setSystemTime(now);