From 80dae2e5e88d948c8d6bafc52843bc136cd010b0 Mon Sep 17 00:00:00 2001 From: Ghost Date: Sun, 18 Jan 2026 20:27:23 -0800 Subject: [PATCH] Voice-call: avoid streaming on notify callbacks --- extensions/voice-call/src/providers/twilio.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/extensions/voice-call/src/providers/twilio.ts b/extensions/voice-call/src/providers/twilio.ts index 6e68556e3..06115d662 100644 --- a/extensions/voice-call/src/providers/twilio.ts +++ b/extensions/voice-call/src/providers/twilio.ts @@ -64,6 +64,8 @@ export class TwilioProvider implements VoiceCallProvider { /** Storage for TwiML content (for notify mode with URL-based TwiML) */ private readonly twimlStorage = new Map(); + /** Track notify-mode calls to avoid streaming on follow-up callbacks */ + private readonly notifyCalls = new Set(); /** * Delete stored TwiML for a given `callId`. @@ -73,6 +75,7 @@ export class TwilioProvider implements VoiceCallProvider { */ private deleteStoredTwiml(callId: string): void { this.twimlStorage.delete(callId); + this.notifyCalls.delete(callId); } /** @@ -137,7 +140,7 @@ export class TwilioProvider implements VoiceCallProvider { */ private async apiRequest( endpoint: string, - params: Record, + params: Record, options?: { allowNotFound?: boolean }, ): Promise { return await twilioApiRequest({ @@ -286,6 +289,9 @@ export class TwilioProvider implements VoiceCallProvider { if (!ctx) return TwilioProvider.EMPTY_TWIML; const params = new URLSearchParams(ctx.rawBody); + const type = + typeof ctx.query?.type === "string" ? ctx.query.type.trim() : undefined; + const isStatusCallback = type === "status"; const callStatus = params.get("CallStatus"); const direction = params.get("Direction"); const callIdFromQuery = @@ -297,13 +303,21 @@ export class TwilioProvider implements VoiceCallProvider { // Handle initial TwiML request (when Twilio first initiates the call) // Check if we have stored TwiML for this call (notify mode) - if (callIdFromQuery) { + if (callIdFromQuery && !isStatusCallback) { const storedTwiml = this.twimlStorage.get(callIdFromQuery); if (storedTwiml) { // Clean up after serving (one-time use) this.deleteStoredTwiml(callIdFromQuery); return storedTwiml; } + if (this.notifyCalls.has(callIdFromQuery)) { + return TwilioProvider.EMPTY_TWIML; + } + } + + // Status callbacks should not receive TwiML. + if (isStatusCallback) { + return TwilioProvider.EMPTY_TWIML; } // Handle subsequent webhook requests (status callbacks, etc.) @@ -385,6 +399,7 @@ export class TwilioProvider implements VoiceCallProvider { // We now serve it from the webhook endpoint instead of sending inline if (input.inlineTwiml) { this.twimlStorage.set(input.callId, input.inlineTwiml); + this.notifyCalls.add(input.callId); } // Build request params - always use URL-based TwiML.