Skip to content

Sync deadlocks on a band whose oldest stored records predate kMinPlausibleUnix (revived WHOOP 4.0) #601

Open

Description

@RyanBeckett

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):

  1. The band's flash still held records from 13–14 Aug 2022, left over from the official app. GET_DATA_RANGE showed 2022-08-06 / 2022-08-13 / now, and the drain started from 2022.
  2. RecordGate drops all of them because they are below kMinPlausibleUnix = 1700000000 (Nov 2023, lib/sync/sync_policy.dart:343). They are counted in droppedBelowFloor.
  3. With nothing durable buffered and droppedThisBurst > 0, TrimAckPolicy.evaluate returns blockedNoDurableProgress (lib/ble/ble_state.dart around L719). The HISTORY_END token is never echoed.
  4. 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 raises syncClockLost. 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's GET_DATA_RANGE oldest is also below the floor, archive the raw frames to raw_archive and 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

  1. 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).
  2. Pair it in OpenStrap and wear it.
  3. 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

  1. RyanBeckett commented on Oct 14, 2026

    @RyanBeckett
    Author

    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:

    • OpenStrap only auto-continues after a productive offload (productive = recordsThisOffload > 0 && lastTrimAdvanced in _onOffloadFinished, lib/ble/ble_engine.dart). Event-only bursts are unproductive, so each sync drained a few batches and stopped. EmptySyncTracker then raised syncClockLost ("Syncs are finishing with no data in them"), but the RTC was fine; live records carried correct timestamps the whole time.
    • research_playground.py's run_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:

    • An OpenStrap DB export (0.10.x) showed sync_ledger: 142 historical_batch rows acked, all with 0 records, plus 2 trim_refused (the pre-2023 batches from the original report). decoded_onehz/decoded_rr were empty, raw_archive held 26 × 88-byte v12 rows gate_dropped (2022), and events had 4,208 rows.
    • A drain that counted any new batch as progress, and re-sent SEND_HISTORICAL_DATA whenever 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. FirmwareAwareR24Decoder decodes them fine.
    • With that backlog gone, OpenStrap synced normally straight away: "Synced through" moved from 1 Jan 1970 to 14:01 today.

    Suggestions:

    1. Treat an ACKed burst that advanced the cursor as progress for auto-continue, even with 0 records, so console/event backlogs drain in one session.
    2. Only raise clockLost when the RTC or record timestamps are actually implausible; "N empty syncs" alone also fires for a log backlog.
    3. Same idle fix in research_playground.py's run_sync().

    Minor, related: the band page showed "Charging" with no pack attached. The band's own event log had a CHARGING_ON with no matching CHARGING_OFF. Attaching and removing the pack once cleared it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions