Skip to content

fix(app): suppress spurious close during intentional composite socket teardown#9438

Merged
mdmohsin7 merged 1 commit into
BasedHardware:mainfrom
andreayres:fix/composite-stop-race
Jul 11, 2026
Merged

fix(app): suppress spurious close during intentional composite socket teardown#9438
mdmohsin7 merged 1 commit into
BasedHardware:mainfrom
andreayres:fix/composite-stop-race

Conversation

@andreayres

@andreayres andreayres commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Summary

CompositeTranscriptionSocket.stop() / disconnect() tore down child sockets while _status was still connected, so each child's onClosed re-entered _onSocketClosed as an unexpected close:

  • Misleading log: Primary socket closed (code: null)
  • Double-disconnect of both children
  • False onClosed to CaptureProvider → reconnect snackbar + keep-alive churn on every intentional teardown (including every BLE reconnect)

Fix

Set _status = disconnected before stopping/disconnecting children so _onSocketClosed's guard treats those callbacks as expected.

Context

Seen with custom STT + on-device Whisper composite mode: device BLE flaps cause intentional composite teardown; the spurious close path made each flap look like a transcription outage and triggered unnecessary reconnect UI.

Related plugin work (context cache so reconnects are cheaper): BasedHardware/whisper_flutter_new#3

Test plan

  • Enable composite / on-device Whisper STT
  • Start recording, stop recording → no Primary socket closed (code: null) warning for intentional stop
  • Simulate BLE disconnect/reconnect while recording (or toggle device) → no false reconnect snackbar solely from composite teardown race
  • Intentional transcription settings change still reconnects cleanly

Review in cubic

… teardown

CompositeTranscriptionSocket.stop()/disconnect() tore down the child
sockets while _status was still connected, so each child's onClosed
re-entered _onSocketClosed as an "unexpected close": a misleading
"Primary socket closed (code: null)" warning, a second disconnect of
both children, and a false onClosed propagated to CaptureProvider
(reconnect snackbar + keep-alive) on every intentional stop, e.g. each
BLE device reconnect.

Set _status = disconnected before touching the children so
_onSocketClosed's guard treats their close callbacks as expected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f33c7d4b48

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

await Future.wait([primarySocket.stop(), secondarySocket.stop()]);

// Same ordering requirement as disconnect().
_status = PureSocketStatus.disconnected;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve final custom-STT transcripts during stop

When the primary is a custom STT socket, stop() is where the final transcript can be emitted: PurePollingSocket.disconnect() flushes buffered audio before closing, and PureStreamingSttSocket.disconnect() sends CloseStream then waits for final results. Setting the composite status to disconnected here makes _forwardAsSuggestedTranscript() return immediately, so those final suggested_transcript messages never reach the backend, which in custom_stt mode does not run STT on the raw audio. This loses the tail of recordings or settings-change reconnects whenever the primary emits a final result during stop; suppress the close callback without blocking primary messages until child stops finish.

Useful? React with 👍 / 👎.

@andreayres

Copy link
Copy Markdown
Contributor Author

Review ping

Small, targeted race fix in CompositeTranscriptionSocket.

Symptom

Intentional stop()/disconnect() (including BLE reconnect teardowns) emitted:

  • Primary socket closed (code: null)
  • double child disconnect
  • false close to CaptureProvider (reconnect snackbar / keep-alive churn)

Fix

Set _status = disconnected before tearing down children so _onSocketClosed treats the callbacks as expected.

Related (the perf win)

Plugin context cache so each flap is cheap once model stays warm: BasedHardware/whisper_flutter_new#3

Would appreciate a maintainer pass when you have a moment.

@andreayres

Copy link
Copy Markdown
Contributor Author

cc @beastoin @Git-on-my-level @mdmohsin7 — small race fix; companion perf PR is BasedHardware/whisper_flutter_new#3.

@Git-on-my-level

Copy link
Copy Markdown
Collaborator

Thanks for this clean, well-scoped fix — the race you identified is real.

What I verified against main: In disconnect() and stop(), children are torn down while _status is still connected. Each child's onClosed callback re-enters _onSocketClosed, whose guard (if (_status != PureSocketStatus.connected) return;) passes, so the intentional teardown is misclassified as an unexpected close — producing the misleading Primary socket closed (code: null) log, a redundant _disconnectBothQuietly(), and a spurious onClosed(closeCode) to CaptureProvider.

The fix is correct: Setting _status = disconnected before calling _disconnectBothQuietly() / Future.wait([…stop()]) makes the existing guard short-circuit for child callbacks fired during intentional teardown. The intentional onClosed() in disconnect() still fires to the listener afterward, so the expected close signal is preserved.

One minor observation (not blocking): _disconnectBothQuietly uses Future.wait without eagerError: false, so if one child throws the other may not finish disconnecting — but that is pre-existing and outside this PR's scope.

Would appreciate a maintainer pass before merge since this touches the composite socket lifecycle. The manual test plan looks solid for validating the BLE-flap scenario.

@Git-on-my-level Git-on-my-level added the needs-maintainer-review Needs human maintainer review before merge label Jul 11, 2026

@kodjima33 kodjima33 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mobile bug fix: suppress spurious socket close during intentional composite disconnect — approve only (needs-maintainer-review gate held for Nik)

@mdmohsin7 mdmohsin7 merged commit 46ae86c into BasedHardware:main Jul 11, 2026
17 of 20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-maintainer-review Needs human maintainer review before merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants