feat(voice-call): bidirectional Telnyx Media Streams for realtime voice#79575
feat(voice-call): bidirectional Telnyx Media Streams for realtime voice#79575happydog-bot wants to merge 1 commit into
Conversation
|
Thanks for the context here. I swept through the related work, and this is now duplicate or superseded. Close: the central Telnyx realtime voice feature is now implemented on current main by the merged canonical PR, and this branch’s distinct post-answer So I’m closing this here and keeping the remaining discussion on the canonical linked item. Review detailsBest possible solution: Keep the merged inline Telnyx realtime implementation from #81024 as the canonical path, and handle late-attach Do we have a high-confidence way to reproduce the issue? Not applicable: this is a feature PR, not a bug report. The PR body supplied live Telnyx logs, and current main now contains source and test coverage for the same user-facing Telnyx realtime capability through the merged canonical design. Is this the best way to solve the issue? No for this branch as a merge target. The owner-boundary feature landed through #81024 using inline Telnyx dial/answer streaming, while this branch’s late-attach design is now superseded and still has a token-logging defect. Security review: Security review needs attention: The branch is superseded, but its diff would expose a short-lived realtime stream token in gateway logs if merged as-is.
What I checked:
Likely related people:
Codex review notes: model gpt-5.5, reasoning high; reviewed against 1f1a5c205c82; fix evidence: commit 4272ce15978d, main fix timestamp 2026-05-12T21:23:50+05:30. |
|
Tested by a human here, works local. |
|
Cross-linking from the parallel Telnyx Media Streaming PR: #81024. The implementations took different paths on carrier-side kickoff: this PR uses
Concrete limitations the
|
Summary
Adds bidirectional Telnyx Media Streaming support to the realtime voice path so users can run sub-second-latency voice on Telnyx + a realtime LLM (Gemini Live, OpenAI Realtime, etc.) end-to-end. Previously
realtime.enabled: truerequiredprovider: "twilio"; this lifts that restriction by adding the small amount of provider-specific transport plumbing Telnyx needs (it negotiates the stream via thestreaming_startCall Control action rather than via TwiML).The same
RealtimeCallHandlerWebSocket bridge serves both providers. Telnyx and Twilio Media Streams use a near-identical wire format (event: "media"with base64 audio,clear,mark,stop); the only differences are inbound field names (stream_id/call_control_idvsstreamSid/callSid) and the fact that Telnyx emits aconnectedframe beforestart. Both shapes are now accepted.Layered changes
extensions/voice-call/src/config.ts):provider !== "twilio"becomesprovider !== "twilio" && provider !== "telnyx"whenrealtime.enabledis true.extensions/voice-call/src/providers/base.ts+types.ts): adds optionalstartMediaStream/stopMediaStreammethods +StartMediaStreamInput/StopMediaStreamInputshapes. Twilio doesn't implement them; its TwiML path is unchanged.extensions/voice-call/src/providers/telnyx.ts): implementsstartMediaStream(issuesstreaming_startwithstream_bidirectional_mode: "rtp"+stream_bidirectional_codec: "PCMU"defaults) andstopMediaStream(idempotentstreaming_stopviaallowNotFound).extensions/voice-call/src/manager.ts): addssetRealtimeStreamStarterwhich fires oncall.answeredfor non-TwiML realtime providers.extensions/voice-call/src/webhook/realtime-handler.ts): WS handler now ignores Telnyx'sconnectedframe and accepts either Twilio (start.streamSid/start.callSid) or Telnyx (top-levelstream_id/start.call_control_id) start-event shapes. AddsmintStreamUrl()so non-TwiML providers can construct the wss URL.extensions/voice-call/src/runtime.ts): wires the manager's stream-starter callback for any provider withstartMediaStream. Includes a per-call dedup set so that providers (notably Telnyx) which firecall.answeredtwice — once for their auto-answer and once for our explicitanswerPOST — only issue onestreaming_start. The dedup is reset on failure so retries still work.Codec choice
Default is PCMU 8 kHz (mu-law) to match the existing audio pacer (
extensions/voice-call/src/webhook/realtime-audio-pacer.ts), which encodes the Live model's PCM output to 8 kHz mu-law for Twilio Media Streams. Telnyx accepts the same shape, so realtime providers work over Telnyx with no resample changes.StartMediaStreamInputexposescodec+samplingRateoverrides for callers that prefer L16 16 kHz (recommended by Telnyx for AI integrations) once a higher-rate audio pacer lands.Verification
pnpm test extensions/voice-call extensions/google— 21 test files / 206 tests pass, including 6 new tests:config.test.ts— accepts telnyx + realtime; still rejects plivo + realtimeproviders/telnyx.test.ts—startMediaStreamdefaults; track/codec/sampling-rate overrides;stopMediaStreamidempotent cleanupwebhook/realtime-handler.test.ts—mintStreamUrlreturns null without public origin and a fresh-token wss URL with one; WS handler accepts Telnyxconnected+start { stream_id, call_control_id }shapepnpm tsgo --project tsconfig.extensions.projects.json— cleanpnpm exec oxfmt --checkon all 10 changed files — cleanReal behavior proof
Behavior or issue addressed: Realtime voice mode (
realtime.enabled: true) previously requiredprovider: "twilio"; users on Telnyx had no path to sub-second voice latency. This PR wires Telnyx'sstreaming_startCall Control action and the WebSocket dialect into the sameRealtimeCallHandlerbridge that Twilio uses, so realtime voice now works on Telnyx end-to-end. Also fixes acall.answereddouble-fire from Telnyx that, without dedup, would issue twostreaming_startcommands per call.Real environment tested: OpenClaw gateway 2026.5.7 running locally on Linux 6.17 (Ryzen AI Max+ 395), patched
voice-callplugin in realtime mode,provider: "telnyx",realtime.provider: "google"→ Gemini Live modelgemini-2.5-flash-native-audio-preview-12-2025, real Telnyx number+1 (651) 422-0083with bidirectional media streaming over an ngrok WebSocket tunnel (stretch-unclaimed-foe.ngrok-free.dev). Inbound calls placed from a real US cell phone over PSTN.Exact steps or command run after this patch:
openclaw gateway restartjournalctl --user -u openclaw-gateway -n 50 | grep "Realtime voice provider"should showRealtime voice provider: googlejournalctl --user -u openclaw-gateway -f | grep -E "voice-call|telnyx call|realtime"Starting realtime media streamline per call, even though Telnyx firescall.answeredtwiceEvidence after fix: Redacted runtime logs from
journalctl --user -u openclaw-gatewayduring a real live call. TheStarting realtime media stream for telnyx call ... -> wss://...line is the new code path (extensions/voice-call/src/runtime.tsstream-starter wiring →extensions/voice-call/src/providers/telnyx.ts:startMediaStream);Realtime bridge startingis the existingRealtimeCallHandler.handleCallaccepting Telnyx'sstream_id+start.call_control_idshape that this PR added support for;realtime tool call/realtime tool resultlines are realopenclaw_agent_consultround-trips through the openclaw agent.Observed result after fix: Inbound Telnyx phone calls connect to the realtime bridge over the bidirectional WebSocket, the caller hears the model's greeting within ~1 second of pickup, and the conversation runs full-duplex with sub-second voice turn latency. Tool calls round-trip through the full openclaw agent (
openclaw_agent_consult) in 3.5–9 seconds with the existing working-on-it filler bridging the gap. Per-call dedup observed: exactly oneStarting realtime media streamline per real call even with Telnyx's twocall.answeredevents. Zero regressions observed on the Twilio realtime path during the same session.What was not tested: Outbound calls from openclaw to a real PSTN destination via Telnyx (only inbound was exercised; outbound shares the same code path but the dial is initiated by
provider.initiateCallwhich is unchanged); theL16/16000codec/samplingRate overrides onStartMediaStreamInput(only thePCMU/8 kHz default was exercised because the existing realtime audio pacer is locked to mu-law/8 kHz today); Plivo realtime (Plivo provider does not implementstartMediaStreamso the new wiring no-ops there, but no Plivo number was on hand to confirm).Sources