fix(stt): Soniox dies for the rest of a meeting after a hidden/idle stretch — self-heal on resumed audio - #513
Open
averatec0773 wants to merge 1 commit into
Conversation
|
| Filename | Overview |
|---|---|
| electron/audio/SonioxStreamingSTT.ts | Adds exhaustion tracking and safely revives active sessions after resumed audio, while preserving the user-stop latch. |
| electron/audio/sttReconnectPolicy.mjs | Defines the pure cooldown and lifecycle guards governing reconnect revival. |
| electron/audio/tests/SonioxReviveWiring.test.mjs | The previous silent-skip issue is fixed by importing the compiled class at module load so a missing build fails explicitly. |
| electron/audio/tests/SttReconnectRevival.test.mjs | Covers cooldown boundaries, stopped sessions, connection guards, and repeated exhaustion cycles. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Soniox reconnect budget exhausted] --> B[Latch reconnect off and record timestamp]
B --> C{Audio arrives after cooldown?}
C -->|No| D[Remain latched off]
C -->|Yes, session active| E[Reset reconnect attempts]
E --> F[Reconnect Soniox WebSocket]
C -->|Session stopped| G[Do not reconnect]
Reviews (2): Last reviewed commit: "fix(stt): Soniox STT dies for the rest o..." | Re-trigger Greptile
…le stretch — self-heal on resumed audio ## The bug Leave a Soniox meeting running, hide the app for a while, come back and speak — transcription is dead and the UI is stuck on "STT reconnecting". Only ending and restarting the meeting recovers it. Root cause: after RECONNECT_MAX_ATTEMPTS (10) failed reconnects, scheduleReconnect() latches `shouldReconnect = false` permanently for the life of the session. During a silent/hidden stretch the exponential-backoff ladder quietly burns all 10 attempts (~2 min). When audio flows again, write()'s lazy-reconnect checks `shouldReconnect` (false) and never reconnects. The latch was designed to stop a flapping endpoint from storming forever — but it also dooms a session whose failures were a transient blip the user has since moved past. ## The fix Genuine audio arriving at write() means the capture pipeline is alive and the user wants transcription again. Grant the session ONE fresh reconnect budget instead of staying dead: - scheduleReconnect() records `reconnectExhaustedAt` when it gives up. - write() calls a pure predicate (shouldReviveExhaustedReconnect) and, when an EXHAUSTED-but-still-ACTIVE session sees audio after a cooldown, resets the attempt budget, re-enables reconnect, and connects. - Cooldown (15 s) rate-limits revival so a genuinely-dead endpoint can't be re-stormed on every chunk; a second exhaustion re-arms its own cooldown. - A user stop() (isActive=false) is never revived; a plain stop-latch with no exhaustion timestamp stays dead. Both are guarded in the predicate. The reconnect-attempt cap, its backoff ladder, and the F-203 stale-socket guards are unchanged. ## Tests (no network, no Electron) - electron/audio/sttReconnectPolicy.mjs — pure, unit-tested decision helper (same pattern as the repo's other .mjs logic helpers). - SttReconnectRevival.test.mjs — 8 pure-function tests: the reported scenario, cooldown boundary (>= inclusive, -1 waits), user-stop never revives, stop-latch-with-null stays dead, in-flight/live-socket guards, second-exhaustion re-arms cooldown. - SonioxReviveWiring.test.mjs — 4 integration tests that drive the REAL compiled SonioxStreamingSTT.write() (connect() stubbed, no socket) and assert it revives after the cooldown, does not before it, and never revives a stopped session. 12/12 pass; strict (TS7) typecheck and build:electron clean.
averatec0773
force-pushed
the
fix/soniox-reconnect-revival
branch
from
August 26, 2026 23:32
c287646 to
104a405
Compare
averatec0773
marked this pull request as ready for review
August 26, 2026 23:54
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.
Summary
Soniox STT silently dies for the rest of a meeting after the app is hidden/idle for a while — come back, speak, and transcription is gone with the UI stuck on "STT reconnecting". Only ending + restarting the meeting recovers it.
Root cause: after
RECONNECT_MAX_ATTEMPTS(10) failed reconnects,scheduleReconnect()latchesshouldReconnect = falsepermanently. During a silent/hidden stretch the exponential-backoff ladder burns all 10 attempts (~2 min), so when audio flows againwrite()'s lazy-reconnect sees the latch and never reconnects.Fix: genuine audio at
write()means the pipeline is alive and the user wants transcription again — grant the session one fresh reconnect budget instead of staying dead. A 15 s cooldown rate-limits revival so a genuinely-dead endpoint can't be re-stormed every chunk; a userstop()is never revived. The attempt cap, backoff ladder, and F-203 stale-socket guards are unchanged.Fixes #
Type of Change
Testing & Environment
electron/audio/sttReconnectPolicy.mjs(same pattern as the repo's other.mjslogic helpers).SttReconnectRevival.test.mjs— 8 pure-function tests: the reported hidden→exhausted→resume scenario, cooldown boundary (>=inclusive,-1waits), user-stop never revives, stop-latch-with-null stays dead, in-flight/live-socket guards, second-exhaustion re-arms its own cooldown.SonioxReviveWiring.test.mjs— 4 integration tests driving the real compiledSonioxStreamingSTT.write()(connect() stubbed, no socket/network): revives after cooldown, does not before it, never revives a stopped session.build:electronclean.Scope
RECONNECT_MAX_ATTEMPTSlatch pattern; the puresttReconnectPolicy.mjshelper is reusable and applying it to them is a clean follow-up, kept out of this PR to keep the change focused.