fix(scribe): let a microphone-setup failure be retried - #940
Merged
kraenhansen merged 1 commit intoAug 17, 2026
Merged
Conversation
A microphone-mode session whose getUserMedia call rejects can never send audio, but streamFromMicrophone only emitted an error and left the socket open holding that session. useScribe kept connectionRef pointing at it, so every later connect() short-circuited on "Already connected" and the mic UI stayed wedged until the component unmounted. Close the connection on that terminal failure. The stranded socket is released, and the hook's existing CLOSE handling clears the ref, so a retry works. onError still fires first. Guard the hook's CLOSE handler too. It nulled connectionRef for whichever connection reported a close, so a late close from a replaced socket tore down the session that had replaced it — the race a consumer hits when working around the wedge with disconnect-then-reconnect. A close is now ignored when a newer connection owns the ref; disconnect() clears the ref before its own close arrives, so that path still reports normally. Fixes elevenlabs#938
kraenhansen
approved these changes
Aug 17, 2026
Member
|
Thanks a lot @chinmayv095 for the fix 👍 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #938.
Problem
When
getUserMediarejects (user denies or dismisses the permission prompt),ScribeRealtime.streamFromMicrophoneemits the error but leaves the socket open. A microphone-mode session that never acquired a microphone can only ever be silent, so that socket is stranded holding a session that will never produce audio.useScribekeepsconnectionRef.currentpointing at it, and every laterconnect()short-circuits onconsole.warn("Already connected"). The consuming app's mic UI stays wedged until a full unmount.Fix
packages/client— close the connection on that terminal failure. This is the root cause: it releases the stranded socket, and the hook's existingCLOSEhandling already clears the ref, so a retry works with no new hook state.onErrorstill fires first, so consumers keep the error they get today.I deliberately did not clear the ref from the hook's
ERRORhandler as the issue suggests.RealtimeEvents.ERRORis also emitted for recoverable conditions —commit_throttled,rate_limited,queue_overflow,input_error— so releasing the ref there would orphan a live, healthy socket and let a second one open alongside it. Closing at the one site that is genuinely terminal keepsERRORsemantics untouched.packages/react— guard theCLOSEhandler. It nulledconnectionRef.currentfor whichever connection reported a close, without checking that connection was still the current one, so a late close from a replaced socket tore down the session that replaced it. That is exactly the race a consumer hits working around the wedge with disconnect-then-reconnect.The guard is
if (connectionRef.current && connectionRef.current !== connection) return;rather than a plain!==.disconnect()releases the ref synchronously, before the socket's close arrives, so a plain!==would swallowonDisconnecton the ordinary disconnect path. The null case has to keep running.Behaviour change worth a ruling
A microphone-setup failure now ends as
status: "disconnected"witherrorpopulated, where it previously stopped at"error"with the socket still open.onErrorfires first either way. That reads correct to me — the session really is over — but it is a visible change, so flagging it rather than burying it.Tests
closes the connection when microphone setup failsCLOSEnever firesignores a close from a connection that has been replacedonDisconnectstill reports a close for the current connection after disconnect()!==guardThe third exists specifically to pin the
connectionRef.current &&clause; I verified it fails when that clause is dropped.Full suites:
@elevenlabs/client180 passing (179 before),@elevenlabs/react140 passing (138 before).lint:es,lint:prettierandcheck-typesclean. Changeset included peragents.md.Note
Medium Risk
Changes real-time connection lifecycle and hook status transitions on mic failure and reconnect races; behavior is intentional but visible to consumers tracking status vs errors.
Overview
Fixes
useScribegetting stuck after a denied or dismissed mic permission prompt, soconnect()can be retried without remounting.In
@elevenlabs/client, when microphone setup fails (getUserMediarejects), the client still emittedonErrorbut left the WebSocket open.useScribekeptconnectionRef, so laterconnect()hit "Already connected". The client now **close()**s the connection after the error so the session ends and the hook’s existingCLOSEhandling clears the ref.In
@elevenlabs/react, theCLOSEhandler cleared state for any connection that closed, so a late close from a replaced socket (e.g. disconnect then reconnect) could tear down the active session. Closes are ignored whenconnectionRefpoints at a newer connection;disconnect()still runs teardown when the ref is already null.Visible behavior: a mic-setup failure now tends to end as
disconnectedwitherrorset (afteronError), noterrorwith an open socket. Tests cover mic-failure close, stale-close ignore, and post-disconnect()close handling.Reviewed by Cursor Bugbot for commit b937449. Bugbot is set up for automated code reviews on this repo. Configure here.