Component: Flutter app — CaptureProvider, LimitlessDeviceConnection
Severity: High — recording is permanently silenced until manual intervention
Affected device: Limitless pendant
Related fix: #7725 (BT reconnect fix — same device, different failure mode)
Summary
When the Limitless pendant is actively recording and the server network connection is lost entirely (while Bluetooth remains connected), the pendant stops streaming BLE audio and never resumes after the network reconnects. The app UI continues to show deviceRecord state, giving the appearance that recording is healthy, but transcription stops permanently. The only recoveries are force-closing the app or manually disconnecting and reconnecting the pendant via Bluetooth.
Steps to Reproduce
- Pair and connect a Limitless pendant.
- Start recording (state transitions to
deviceRecord).
- Confirm transcription is appearing in the app.
- Drop network connectivity entirely (e.g. disable Wi-Fi and cellular, or toggle airplane mode while keeping Bluetooth on).
- Wait at least 30–60 seconds (long enough for the pendant's internal streaming timeout to elapse).
- Restore network connectivity.
- Observe the app.
Expected Behavior
Within ~15 seconds of network restoration:
- The transcription WebSocket reconnects (keep-alive fires).
- The Limitless pendant resumes streaming BLE audio to the app.
- Transcription resumes in the app UI.
Actual Behavior
- The app UI continues to show
deviceRecord (no visual indication of the problem).
- The transcription WebSocket reconnects via the keep-alive timer.
- However, no new transcription appears — the pendant is silently producing no audio.
- The session never recovers. The user must force-close the app or disconnect/reconnect the pendant via Bluetooth to resume transcription.
Root Cause Analysis
Why Bluetooth disconnect/reconnect fixes it
The BT reconnect fix (#7725) added _handleTransportReconnected() to LimitlessDeviceConnection, which fires when the BLE transport layer reconnects. It calls _reinitializeAfterReconnect(), which re-sends the enable-data-stream command to the pendant over BLE. This command tells the device to (re)start streaming real-time audio frames.
Why network-only disconnect does not fix itself
The Limitless pendant has an internal timeout: after an extended period without a live server-side connection, it stops emitting BLE audio frames. The BLE link itself remains up — the device appears connected — but the _audioController stream in LimitlessDeviceConnection goes silent.
When the network reconnects:
onClosed() fires on CaptureProvider when the socket drops.
- The keep-alive timer (
_startKeepAliveServices) fires every 15 seconds and successfully reconnects the transcription WebSocket via _initiateWebsocket().
_bleBytesStream (the BLE→socket audio pipe) is still active and wired to the new socket.
- But the pendant has already stopped streaming. No frames arrive in
_audioController, so nothing is sent over the newly-connected socket.
_initiateWebsocket() has no mechanism to notify the device connection that the socket has recovered — it never re-sends the enable-data-stream command.
Why the BT workaround happens to fix the network case
When the user manually disconnects/reconnects Bluetooth, _handleTransportReconnected() fires, which calls _reinitializeAfterReconnect(). This re-sends the enable-data-stream command, waking the pendant back into streaming mode — exactly what is needed after a network outage, but only triggered accidentally via BT reconnect.
Code path gap
CaptureProvider.onConnected() (the socket reconnect callback) only handles the interrupted state (phone mic). For deviceRecord state, nothing is done. Even if it did something, onConnected() is not reachable from the keep-alive path because subscribe() is called after start() (i.e., after the socket is already connected), so the onConnected event fires on an empty listeners map and never reaches CaptureProvider.
Impact
- Data loss: Audio captured during the network outage is permanently lost for the current live session (Limitless does not use the phone-side WAL).
- Silent failure: The UI shows
deviceRecord throughout, so the user has no indication that transcription stopped.
- Requires manual intervention: Force-close or Bluetooth disconnect/reconnect required to recover every time.
Fix
A fix is available in PR (branch fix/limitless-network-reconnect on formed2forge/omi). The fix adds an onNetworkSocketReconnected() hook to the DeviceConnection base class (no-op by default) that is called by _initiateWebsocket() after successfully reconnecting the socket during an active device recording session. LimitlessDeviceConnection overrides it to re-send the enable-data-stream command, mirroring exactly what _reinitializeAfterReconnect() does for BT reconnects.
A _socketReconnectPending flag in CaptureProvider tracks whether the socket closed during an active deviceRecord session, so the hook is only fired on a true network-drop reconnect — not on initial setup or explicit user-initiated socket stops.
Files changed:
app/lib/services/devices/device_connection.dart — added onNetworkSocketReconnected() virtual hook
app/lib/services/devices/limitless_connection.dart — override re-sends enable-data-stream on network reconnect
app/lib/providers/capture_provider.dart — _socketReconnectPending flag, set in onClosed(), consumed and cleared in _initiateWebsocket(), guarded by recordingState == deviceRecord
Component: Flutter app —
CaptureProvider,LimitlessDeviceConnectionSeverity: High — recording is permanently silenced until manual intervention
Affected device: Limitless pendant
Related fix: #7725 (BT reconnect fix — same device, different failure mode)
Summary
When the Limitless pendant is actively recording and the server network connection is lost entirely (while Bluetooth remains connected), the pendant stops streaming BLE audio and never resumes after the network reconnects. The app UI continues to show
deviceRecordstate, giving the appearance that recording is healthy, but transcription stops permanently. The only recoveries are force-closing the app or manually disconnecting and reconnecting the pendant via Bluetooth.Steps to Reproduce
deviceRecord).Expected Behavior
Within ~15 seconds of network restoration:
Actual Behavior
deviceRecord(no visual indication of the problem).Root Cause Analysis
Why Bluetooth disconnect/reconnect fixes it
The BT reconnect fix (#7725) added
_handleTransportReconnected()toLimitlessDeviceConnection, which fires when the BLE transport layer reconnects. It calls_reinitializeAfterReconnect(), which re-sends the enable-data-stream command to the pendant over BLE. This command tells the device to (re)start streaming real-time audio frames.Why network-only disconnect does not fix itself
The Limitless pendant has an internal timeout: after an extended period without a live server-side connection, it stops emitting BLE audio frames. The BLE link itself remains up — the device appears connected — but the
_audioControllerstream inLimitlessDeviceConnectiongoes silent.When the network reconnects:
onClosed()fires onCaptureProviderwhen the socket drops._startKeepAliveServices) fires every 15 seconds and successfully reconnects the transcription WebSocket via_initiateWebsocket()._bleBytesStream(the BLE→socket audio pipe) is still active and wired to the new socket._audioController, so nothing is sent over the newly-connected socket._initiateWebsocket()has no mechanism to notify the device connection that the socket has recovered — it never re-sends the enable-data-stream command.Why the BT workaround happens to fix the network case
When the user manually disconnects/reconnects Bluetooth,
_handleTransportReconnected()fires, which calls_reinitializeAfterReconnect(). This re-sends the enable-data-stream command, waking the pendant back into streaming mode — exactly what is needed after a network outage, but only triggered accidentally via BT reconnect.Code path gap
CaptureProvider.onConnected()(the socket reconnect callback) only handles theinterruptedstate (phone mic). FordeviceRecordstate, nothing is done. Even if it did something,onConnected()is not reachable from the keep-alive path becausesubscribe()is called afterstart()(i.e., after the socket is already connected), so theonConnectedevent fires on an empty listeners map and never reachesCaptureProvider.Impact
deviceRecordthroughout, so the user has no indication that transcription stopped.Fix
A fix is available in PR (branch
fix/limitless-network-reconnectonformed2forge/omi). The fix adds anonNetworkSocketReconnected()hook to theDeviceConnectionbase class (no-op by default) that is called by_initiateWebsocket()after successfully reconnecting the socket during an active device recording session.LimitlessDeviceConnectionoverrides it to re-send the enable-data-stream command, mirroring exactly what_reinitializeAfterReconnect()does for BT reconnects.A
_socketReconnectPendingflag inCaptureProvidertracks whether the socket closed during an activedeviceRecordsession, so the hook is only fired on a true network-drop reconnect — not on initial setup or explicit user-initiated socket stops.Files changed:
app/lib/services/devices/device_connection.dart— addedonNetworkSocketReconnected()virtual hookapp/lib/services/devices/limitless_connection.dart— override re-sends enable-data-stream on network reconnectapp/lib/providers/capture_provider.dart—_socketReconnectPendingflag, set inonClosed(), consumed and cleared in_initiateWebsocket(), guarded byrecordingState == deviceRecord