Skip to content

Bug: Twilio voice-call can get stuck in hold music after failed/no-stream call #81122

@donkeykong91

Description

@donkeykong91

Bug: Twilio inbound voice-call can get stuck in hold music after failed/no-stream call leaves activeStreamCalls stale

Summary

A failed or incomplete Twilio inbound voice-call attempt can leave the voice-call Twilio provider believing a media stream is still active. The next inbound call is then routed into the built-in queue/hold path and plays Twilio hold music instead of connecting to the voice stream.

Observed behavior

After one inbound call was accepted and a call record was created, no MediaStream connected log appeared. A later inbound call heard hold music.

Recent logs showed:

[voice-call] Inbound call accepted: +17146238306 is in allowlist
[voice-call] Created inbound call record: b63fd8ac-7a1f-4321-9540-3aafeaa9f5ac from +17146238306

No corresponding:

[MediaStream] Stream started ...
[voice-call] Media stream connected ...

Then a later call was accepted but the caller heard the plugin's hold music. Twilio showed the later call completed, but OpenClaw did not log a stream connection/transcript for it.

Why this appears to happen

In the built Twilio provider, inbound TwiML policy queues new inbound calls whenever activeStreamCalls.size > 0:

if (input.direction === "inbound") {
  if (input.hasActiveStreams) return { kind: "queue" };
  if (input.canStream && input.callSid) return {
    kind: "stream",
    activateStreamCallSid: input.callSid
  };
  return { kind: "pause" };
}

generateTwimlResponse() then sets active state as soon as it returns stream TwiML:

if (decision.activateStreamCallSid) this.activeStreamCalls.add(decision.activateStreamCallSid);

But if Twilio never successfully opens the WebSocket media stream, or the stream setup fails before registerCallStream / unregisterCallStream lifecycle runs, the call SID can remain in activeStreamCalls. The next inbound call then gets QUEUE_TWIML:

<Response>
  <Say voice="alice">Please hold while we connect you.</Say>
  <Enqueue waitUrl="/voice/hold-music">hold-queue</Enqueue>
</Response>

/voice/hold-music plays Twilio's default classical hold track (BusyStrings.mp3).

Expected behavior

If an inbound call is given stream TwiML but the media stream never connects within a short timeout, the provider should clear that call SID from activeStreamCalls.

A later inbound call should not be routed to hold music because of a stale no-stream/failed-stream previous call.

Suggested fix

Add a guard/TTL around activeStreamCalls entries that are added during TwiML generation:

  • When activateStreamCallSid is added, start a short timer.
  • Clear the call SID if no media stream is registered/started within N seconds.
  • Clear the timer when the stream actually connects/registers.
  • Also clear on terminal call status callbacks where possible.

Alternatively, for single-call/no-queue configurations, consider disabling the queue path or making queueing configurable.

Environment

  • OpenClaw / @openclaw/voice-call: 2026.5.7
  • Provider: Twilio inbound voice-call streaming
  • Public URL via Tailscale Funnel
  • Streaming STT provider: Speaches
  • Voice-call webhook was reachable and returned valid TwiML when tested with a signed Twilio-style POST after the issue.

Notes

This appears separate from local testing patches around aborting stale response-model runs on caller barge-in. Those patches touched response generation/runtime abort plumbing, not Twilio's activeStreamCalls/queue/hold-music logic.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1High-priority user-facing bug, regression, or broken workflow.clawsweeper:fix-shape-clearClawSweeper found a clear likely implementation shape for this issue.clawsweeper:queueable-fixClawSweeper marked this issue as an existing queue_fix_pr work candidate.clawsweeper:source-reproClawSweeper found a high-confidence source-level issue reproduction.impact:message-lossChannel message delivery can be lost, duplicated, or misrouted.impact:session-stateSession, memory, transcript, context, or agent state can drift or corrupt.issue-rating: 🦞 diamond lobsterVery strong issue quality with high-confidence source-level or clear reproduction.staleMarked as stale due to inactivity

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions