openclaw - 💡(How to fix) Fix Bug: Realtime voice bridge fails on outbound calls — status callback race overwrites <Connect><Stream> TwiML [1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#68712Fetched 2026-04-19 15:08:22
View on GitHub
Comments
0
Participants
1
Timeline
1
Reactions
0
Timeline (top)
closed ×1

When making outbound calls with realtime.enabled = true, the OpenAI Realtime voice bridge consistently fails with "WebSocket was closed before the connection was established". The root cause is a race condition between the status callback (regular path) and the TwiML-fetch callback (realtime path).

Error Message

[voice-call] realtime voice error: WebSocket was closed before the connection was established [voice-call] Failed to connect realtime bridge: Error: WebSocket was closed before the connection was established at WebSocket.close (.../ws/lib/websocket.js:300:7) at OpenAIRealtimeVoiceBridge.close (.../realtime-voice-provider-.js:119:12) at WebSocket.<anonymous> (.../realtime-handler-.js:110:39)

Root Cause

When Twilio initiates an outbound call, two webhook callbacks arrive nearly simultaneously:

OrderCallbackQuery ParamsPath TakenEffect
1Status callback?callId=X&type=statusRegular (shouldShortCircuitToRealtimeTwiml returns false for type=status)Registers call in call manager, triggers speakInitialMessage
2TwiML-fetch?callId=XRealtime shortcircuitReturns <Connect><Stream> TwiML

The race condition:

  1. TwiML-fetch correctly returns <Connect><Stream url="wss://..."> via the realtime shortcircuit. Twilio begins connecting the media stream WebSocket.
  2. Status callback goes through the regular path. processEvent registers the call as call.answered, which triggers maybeSpeakInitialMessageOnAnsweredspeakInitialMessageplayTts.
  3. In playTts (Twilio provider), since no legacy media stream is active (streaming.enabled = false), it falls back to Twilio REST API: POST /Calls/<sid>.json with <Say><Gather> TwiML.
  4. This REST API call overwrites the in-progress <Connect><Stream> TwiML. Twilio tears down the media stream WebSocket and sends a stop event.
  5. The stop event triggers bridge.close() on the OpenAI Realtime bridge, which is still mid-connect (~1-2s handshake). This throws "WebSocket was closed before the connection was established".

Log evidence (consistent across all 3 observed failures):

:57.619  Status callback (type=status, in-progress) → REGULAR path
:57.628  Starting max duration timer (call registered)
:57.630  Speaking initial message (mode: conversation)
:57.635  WARN: Using TwiML <Say> fallback
:57.642  TwiML-fetch (type=undefined) → REALTIME TwiML returned
:58.185  Starting max duration timer AGAIN (realtime handler also registers)
:58.194  ERROR: WebSocket was closed before established

Code Example

[voice-call] realtime voice error: WebSocket was closed before the connection was established
[voice-call] Failed to connect realtime bridge: Error: WebSocket was closed before the connection was established
    at WebSocket.close (.../ws/lib/websocket.js:300:7)
    at OpenAIRealtimeVoiceBridge.close (.../realtime-voice-provider-*.js:119:12)
    at WebSocket.<anonymous> (.../realtime-handler-*.js:110:39)

---

:57.619  Status callback (type=status, in-progress)REGULAR path
:57.628  Starting max duration timer (call registered)
:57.630  Speaking initial message (mode: conversation)
:57.635  WARN: Using TwiML <Say> fallback
:57.642  TwiML-fetch (type=undefined)REALTIME TwiML returned
:58.185  Starting max duration timer AGAIN (realtime handler also registers)
:58.194  ERROR: WebSocket was closed before established
RAW_BUFFERClick to expand / collapse

Bug: Realtime voice bridge fails on outbound calls — status callback race condition overwrites <Connect><Stream> TwiML

Description

When making outbound calls with realtime.enabled = true, the OpenAI Realtime voice bridge consistently fails with "WebSocket was closed before the connection was established". The root cause is a race condition between the status callback (regular path) and the TwiML-fetch callback (realtime path).

Environment

  • OpenClaw version: 2026.4.15
  • Provider: Twilio
  • Realtime provider: OpenAI (gpt-4o-realtime-preview)
  • streaming.enabled: false
  • realtime.enabled: true
  • Webhook via Cloudflare Tunnel

Steps to Reproduce

  1. Configure voice-call plugin with realtime.enabled = true and streaming.enabled = false
  2. Initiate an outbound call via the voice_call tool (mode: conversation)
  3. Call connects and Twilio sends webhooks

Expected Behavior

  • The TwiML-fetch callback returns <Connect><Stream url="wss://..."> via the realtime shortcircuit
  • Twilio connects the media stream WebSocket
  • OpenAI Realtime bridge connects and handles the call

Actual Behavior

The call fails every time with this error:

[voice-call] realtime voice error: WebSocket was closed before the connection was established
[voice-call] Failed to connect realtime bridge: Error: WebSocket was closed before the connection was established
    at WebSocket.close (.../ws/lib/websocket.js:300:7)
    at OpenAIRealtimeVoiceBridge.close (.../realtime-voice-provider-*.js:119:12)
    at WebSocket.<anonymous> (.../realtime-handler-*.js:110:39)

Root Cause Analysis

When Twilio initiates an outbound call, two webhook callbacks arrive nearly simultaneously:

OrderCallbackQuery ParamsPath TakenEffect
1Status callback?callId=X&type=statusRegular (shouldShortCircuitToRealtimeTwiml returns false for type=status)Registers call in call manager, triggers speakInitialMessage
2TwiML-fetch?callId=XRealtime shortcircuitReturns <Connect><Stream> TwiML

The race condition:

  1. TwiML-fetch correctly returns <Connect><Stream url="wss://..."> via the realtime shortcircuit. Twilio begins connecting the media stream WebSocket.
  2. Status callback goes through the regular path. processEvent registers the call as call.answered, which triggers maybeSpeakInitialMessageOnAnsweredspeakInitialMessageplayTts.
  3. In playTts (Twilio provider), since no legacy media stream is active (streaming.enabled = false), it falls back to Twilio REST API: POST /Calls/<sid>.json with <Say><Gather> TwiML.
  4. This REST API call overwrites the in-progress <Connect><Stream> TwiML. Twilio tears down the media stream WebSocket and sends a stop event.
  5. The stop event triggers bridge.close() on the OpenAI Realtime bridge, which is still mid-connect (~1-2s handshake). This throws "WebSocket was closed before the connection was established".

Log evidence (consistent across all 3 observed failures):

:57.619  Status callback (type=status, in-progress) → REGULAR path
:57.628  Starting max duration timer (call registered)
:57.630  Speaking initial message (mode: conversation)
:57.635  WARN: Using TwiML <Say> fallback
:57.642  TwiML-fetch (type=undefined) → REALTIME TwiML returned
:58.185  Starting max duration timer AGAIN (realtime handler also registers)
:58.194  ERROR: WebSocket was closed before established

Suggested Fix

When realtime mode is active for a call, the status callback's processEvent should either:

  1. Skip call registration when the call is already being handled by the realtime handler, OR
  2. Skip speakInitialMessage when realtime mode is active (the realtime handler's handleCallonReadytriggerGreeting handles the greeting independently), OR
  3. Guard playTts to never issue a REST API TwiML update when a realtime stream is active or pending for that call

Option 2 or 3 seems safest — it preserves status event processing for logging/state tracking while preventing the destructive TwiML overwrite.

Additional Context

  • The shouldDeferConversationInitialMessageUntilStreamConnect check in the call manager only returns true when streaming.enabled = true, which doesn't apply to realtime.enabled calls.
  • The OpenAI API key and Realtime WebSocket connection are verified working independently — standalone connection tests succeed in ~1.4s.
  • Twilio's WebSocket upgrade through Cloudflare Tunnel is also verified working (HTTP/1.1 upgrade returns 101/401 as expected).

extent analysis

TL;DR

The most likely fix is to modify the processEvent function in the status callback to skip speakInitialMessage when realtime mode is active, preventing the destructive TwiML overwrite.

Guidance

  • Identify the processEvent function in the status callback and add a conditional check to skip speakInitialMessage when realtime.enabled is true.
  • Verify that the shouldDeferConversationInitialMessageUntilStreamConnect check in the call manager does not interfere with this fix, as it only applies to streaming.enabled calls.
  • Test the modified processEvent function with realtime.enabled calls to ensure that the TwiML overwrite is prevented and the OpenAI Realtime bridge connects successfully.
  • Consider adding logging to track the status callback and TwiML-fetch callback interactions to monitor the effectiveness of the fix.

Example

if (realtime.enabled) {
  // Skip speakInitialMessage when realtime mode is active
  return;
}
speakInitialMessage();

Notes

This fix assumes that the realtime.enabled flag is accessible in the processEvent function. If not, additional modifications may be necessary to pass this flag or check the call's mode.

Recommendation

Apply the workaround by modifying the processEvent function to skip speakInitialMessage when realtime.enabled is true, as this preserves status event processing for logging and state tracking while preventing the destructive TwiML overwrite.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - 💡(How to fix) Fix Bug: Realtime voice bridge fails on outbound calls — status callback race overwrites <Connect><Stream> TwiML [1 participants]