Skip to content

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
Natively-AI-assistant:mainfrom
averatec0773:fix/soniox-reconnect-revival
Open

fix(stt): Soniox dies for the rest of a meeting after a hidden/idle stretch — self-heal on resumed audio#513
averatec0773 wants to merge 1 commit into
Natively-AI-assistant:mainfrom
averatec0773:fix/soniox-reconnect-revival

Conversation

@averatec0773

Copy link
Copy Markdown

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() latches shouldReconnect = false permanently. During a silent/hidden stretch the exponential-backoff ladder burns all 10 attempts (~2 min), so when audio flows again write()'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 user stop() is never revived. The attempt cap, backoff ladder, and F-203 stale-socket guards are unchanged.

Fixes #

Type of Change

  • 🐛 Bug Fix

Testing & Environment

  • New pure decision helper electron/audio/sttReconnectPolicy.mjs (same pattern as the repo's other .mjs logic helpers).
  • SttReconnectRevival.test.mjs — 8 pure-function tests: the reported hidden→exhausted→resume 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 its own cooldown.
  • SonioxReviveWiring.test.mjs — 4 integration tests driving the real compiled SonioxStreamingSTT.write() (connect() stubbed, no socket/network): revives after cooldown, does not before it, never revives a stopped session.
  • 12/12 pass; strict (TS7) typecheck and build:electron clean.
  • Automated only — no manual audio session was run; the tests reproduce the exact state transitions.

Scope

  • Fixes Soniox (the provider that exhibited the bug). Deepgram / ElevenLabs / nvidia share the same RECONNECT_MAX_ATTEMPTS latch pattern; the pure sttReconnectPolicy.mjs helper is reusable and applying it to them is a clean follow-up, kept out of this PR to keep the change focused.

@greptile-apps

greptile-apps Bot commented Aug 26, 2026

Copy link
Copy Markdown

Greptile Summary

The PR allows an active Soniox transcription session to recover after exhausting its reconnect budget when audio resumes after a cooldown.

  • Records when automatic reconnect attempts become exhausted.
  • Grants a fresh reconnect budget when later audio proves the session remains active.
  • Adds pure policy tests and compiled-class wiring coverage.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

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]
Loading

Reviews (2): Last reviewed commit: "fix(stt): Soniox STT dies for the rest o..." | Re-trigger Greptile

Comment thread electron/audio/__tests__/SonioxReviveWiring.test.mjs Outdated
…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
averatec0773 force-pushed the fix/soniox-reconnect-revival branch from c287646 to 104a405 Compare August 26, 2026 23:32
@averatec0773
averatec0773 marked this pull request as ready for review August 26, 2026 23:54
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.

1 participant