Repository navigation
Sync deadlocks on a band whose oldest stored records predate kMinPlausibleUnix (revived WHOOP 4.0) #601
Description
What happened?
A WHOOP 4.0 that had been flat since August 2022 never syncs any data. Every sync completes empty, the band page shows "Syncs are finishing with no data in them", and Home stays on "Synced through Thu 1 Jan, 01:00" (epoch 0). Re-pairing, charger restarts and hours of wear make no difference.
Root cause (from a capture with research_playground.py):
- The band's flash still held records from 13–14 Aug 2022, left over from the official app.
GET_DATA_RANGEshowed 2022-08-06 / 2022-08-13 / now, and the drain started from 2022. RecordGatedrops all of them because they are belowkMinPlausibleUnix = 1700000000(Nov 2023,lib/sync/sync_policy.dart:343). They are counted indroppedBelowFloor.- With nothing durable buffered and
droppedThisBurst > 0,TrimAckPolicy.evaluatereturnsblockedNoDurableProgress(lib/ble/ble_state.dartaround L719). The HISTORY_END token is never echoed. - The band re-offers the same 2022 burst on every connection, so the drain never reaches today's records. Three completed console-only syncs trip
EmptySyncTracker, which raisessyncClockLost. That status is misleading: the band's RTC was fine, and live records carried the correct 2026 timestamps.
The no-progress gate is right not to trim records the app never stored. But when every drop is below the absolute floor, the batch is genuinely old data, not a wandering clock, and nothing will ever make it bankable. The band stays wedged for good.
Workaround that unblocked it. On Linux, I ran research_playground.py sync (with storage on, so the 2022 data was kept in its whoop.db). It ACKed 9 batches, 4aba… through 28bb… in about 10 s, 40 R12 records plus events from 2022-08-13/14, then reached today. After that, wrist on/off events fired normally. (Pairing from Linux needed a single bluetoothctl session with agent NoInputNoOutput + default-agent; without an agent, bluetoothd cancels the Just Works confirmation and reports AuthenticationFailed.)
Possible fixes:
- If a burst's drops are all below the floor (
timeBaseNotWallClock-style signature) and the session'sGET_DATA_RANGEoldest is also below the floor, archive the raw frames toraw_archiveand treat that as durable progress, so the ACK can go out. Nothing is lost, and the data is never derived. - At minimum, surface a distinct band status ("Old pre-2023 data on the band is blocking sync") rather than
clockLost, whose fix text ("leave it connected… pair it again") can't help here.
Steps to reproduce
- Take a WHOOP 4.0 whose flash still holds records older than Nov 2023 (e.g. a band left flat since 2022 after its last official-app sync).
- Pair it in OpenStrap and wear it.
- Sync. Every sync completes with zero banked records, "Synced through" stays at 1 Jan 1970, and after 3 syncs the band page shows "Syncs are finishing with no data in them".
App version
0.10.x (TestFlight). 0.11.3 wasn't on TestFlight yet, but the floor and the blockedNoDurableProgress gate are unchanged on main at v0.11.3 (1f1025a).
Platform
iOS (TestFlight)
Phone model and OS version
iPhone 16 Pro, iOS 26.6.1
Which band?
WHOOP 4.0
Quick checks
- The official WHOOP app is not also connected to this band
- I've searched existing issues for this
Activity
RyanBeckett commentedon Oct 14, 2026
Update: confirmed fixed on this band. Clearing the pre-2023 backlog was necessary but not sufficient; there's a second cause.
After the workaround above, OpenStrap still showed "Synced through 1 Jan" for a couple more hours, and short research-client syncs returned zero sensor records, which made it look as if the band had stopped recording. It hadn't.
Second cause: drains give up on batches that carry no sensor records. After a long power loss the band's history held hours of its own console-log (
0x32) and event frames ahead of the 1 Hz records. Each burst was valid and ACKable, but contained 0 records:productive = recordsThisOffload > 0 && lastTrimAdvancedin_onOffloadFinished,lib/ble/ble_engine.dart). Event-only bursts are unproductive, so each sync drained a few batches and stopped.EmptySyncTrackerthen raisedsyncClockLost("Syncs are finishing with no data in them"), but the RTC was fine; live records carried correct timestamps the whole time.research_playground.py'srun_sync()has the same shape: it declares idle after 8 s with no new records and sends ABORT, even while event-only batches are still arriving.Evidence:
sync_ledger: 142historical_batchrowsacked, all with 0 records, plus 2trim_refused(the pre-2023 batches from the original report).decoded_onehz/decoded_rrwere empty,raw_archiveheld 26 × 88-byte v12 rowsgate_dropped(2022), andeventshad 4,208 rows.SEND_HISTORICAL_DATAwhenever batches stalled for 20 s (the band stops offering until re-asked), recovered ~6,750 1 Hz v12 records (11:22–13:21 local) that had been queued behind the logs.FirmwareAwareR24Decoderdecodes them fine.Suggestions:
clockLostwhen the RTC or record timestamps are actually implausible; "N empty syncs" alone also fires for a log backlog.research_playground.py'srun_sync().Minor, related: the band page showed "Charging" with no pack attached. The band's own event log had a
CHARGING_ONwith no matchingCHARGING_OFF. Attaching and removing the pack once cleared it.