Skip to content

fix(scribe): let a microphone-setup failure be retried - #940

Merged
kraenhansen merged 1 commit into
elevenlabs:mainfrom
chinmayv095:fix/scribe-retry-after-mic-failure
Aug 17, 2026
Merged

fix(scribe): let a microphone-setup failure be retried#940
kraenhansen merged 1 commit into
elevenlabs:mainfrom
chinmayv095:fix/scribe-retry-after-mic-failure

Conversation

@chinmayv095

@chinmayv095 chinmayv095 commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Fixes #938.

Problem

When getUserMedia rejects (user denies or dismisses the permission prompt), ScribeRealtime.streamFromMicrophone emits 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.

useScribe keeps connectionRef.current pointing at it, and every later connect() short-circuits on console.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 existing CLOSE handling already clears the ref, so a retry works with no new hook state. onError still fires first, so consumers keep the error they get today.

I deliberately did not clear the ref from the hook's ERROR handler as the issue suggests. RealtimeEvents.ERROR is 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 keeps ERROR semantics untouched.

packages/react — guard the CLOSE handler. It nulled connectionRef.current for 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 swallow onDisconnect on 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" with error populated, where it previously stopped at "error" with the socket still open. onError fires 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

Test Package Without this change
closes the connection when microphone setup fails client fails — CLOSE never fires
ignores a close from a connection that has been replaced react fails — stale close calls onDisconnect
still reports a close for the current connection after disconnect() react fails against a plain !== guard

The third exists specifically to pin the connectionRef.current && clause; I verified it fails when that clause is dropped.

Full suites: @elevenlabs/client 180 passing (179 before), @elevenlabs/react 140 passing (138 before). lint:es, lint:prettier and check-types clean. Changeset included per agents.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 useScribe getting stuck after a denied or dismissed mic permission prompt, so connect() can be retried without remounting.

In @elevenlabs/client, when microphone setup fails (getUserMedia rejects), the client still emitted onError but left the WebSocket open. useScribe kept connectionRef, so later connect() hit "Already connected". The client now **close()**s the connection after the error so the session ends and the hook’s existing CLOSE handling clears the ref.

In @elevenlabs/react, the CLOSE handler 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 when connectionRef points 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 disconnected with error set (after onError), not error with 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.

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
kraenhansen merged commit a068144 into elevenlabs:main Aug 17, 2026
5 checks passed
@kraenhansen

Copy link
Copy Markdown
Member

Thanks a lot @chinmayv095 for the fix 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[react] useScribe retains connectionRef after mic-setup failure, wedging retry with "Already connected"

2 participants