Skip to content

Last sync belongs to a strap, not to the install - #1745

Merged
ryanbr merged 2 commits into
mainfrom
last-sync-per-strap
Aug 30, 2026
Merged

ryanbr merged 2 commits into
mainfrom
last-sync-per-strap

Conversation

@ryanbr

@ryanbr ryanbr commented Aug 30, 2026

Copy link
Copy Markdown
Owner

Reported three times before it was believed — and that is the part worth keeping. The number was plausible, so the reading was treated as mistaken rather than the label.

The defect

noop.lastSyncAtSec is one key, stamped on any strap's HISTORY_COMPLETE and read back for whichever strap is active. On a single-strap install those are the same strap, which is why it survived this long.

With two paired, it reports the other one's. From the 30 Aug capture:

Last sync:   4d ago
Days:        whoop-MGB…=0   my-whoop=37
  device id=whoop-MGB… status=ACTIVE   lastSeen=just now
  device id=my-whoop   status=paired   lastSeen=3d ago

The 5/MG had never banked a row. The timestamp on its screen was the 4.0's.

Why it was expensive

It was wrong in the reassuring direction. A strap that has never synced read as recently synced, so a pull-to-refresh that could not possibly change the number looked broken rather than inapplicable — and it sent an entire investigation after a sync regression that never existed.

The shape of the fix

This is the same defect resolveFirmware already fixed for firmware strings, so it takes the same shape:

Deliberately not migrated by writing the global onto the active strap. That is the same guess in a different place, and on a multi-strap install it would bake the wrong attribution in permanently instead of letting it self-correct at the next real sync.

The sibling line had it too

One line below, in the same capture: Data write: rows last landed 4d ago — against a strap whose own row count was zero. Two lines that both read as evidence the 5/MG had been syncing, and neither was about the 5/MG.

Both halves of the #57 write-health pair are scoped, not just the one that was noticed. They are read as a pair — "stalled more recently than ok" is the alarm — so scoping one would compare a strap's own stall against another strap's success.

What it says now

For the strap in the capture: "never (this strap)". Not a degraded answer — the correct one, and the one nobody had been given.

Notes

  • No Swift twin needed; the Apple side has no persisted global equivalent.
  • The seed for LiveState.lastSyncAt resolves the single-strap fallback just after construction rather than in the constructor, because the paired count needs the registry and the registry is suspend. Seeding from the global first and correcting later would have shown the other strap's timestamp for as long as that took — the bug, briefly, rather than not at all.

Verification

  • 4907 tests, 0 failures
  • lintVitalFullRelease, doc-comment lint, i18n --ci clean
  • staging 392 / iOS 273

Related to #1635.

ryanbr added 2 commits August 30, 2026 23:33
Reported three times before it was believed, which is the part worth keeping. The
number was plausible, so the reading was treated as mistaken rather than the label.

`noop.lastSyncAtSec` is ONE key, stamped on any strap's HISTORY_COMPLETE and read
back for whichever strap is active. On a single-strap install those are the same
strap, which is why it survived this long. With two paired it reports the other
one's: the capture shows "Last sync: 4d ago" beside "Days: whoop-MGB...=0" and a 4.0
last seen three days earlier. The 5/MG had never banked a row. The timestamp on its
screen was the 4.0's.

Wrong in the reassuring direction, which is what made it expensive. A strap that has
never synced read as recently synced, so a pull-to-refresh that could not possibly
change the number looked broken instead of inapplicable - and it sent this whole
investigation after a sync regression that never existed.

This is the same defect resolveFirmware fixed for firmware strings, so it takes the
same shape: stamped per BLE address, read per strap, and the legacy global honoured
ONLY when exactly one strap is paired - the one case where it cannot be ambiguous,
which keeps single-strap installs reading correctly across the upgrade. Deliberately
NOT migrated by writing the global onto the active strap: that is the same guess in a
different place, and on a multi-strap install it would bake the wrong attribution in
permanently instead of self-correcting at the next real sync.

The #57 write-health pair had it too, one line below in the same capture: "Data
write: rows last landed 4d ago" against a strap whose own row count was zero. Two
lines that both read as evidence the 5/MG had been syncing, neither about the 5/MG.
Both halves are scoped, not just the one that was noticed - they are read as a pair
("stalled more recently than ok" is the alarm), so scoping one would compare a strap's
own stall against another strap's success.

For the strap in the capture the answer is now "never (this strap)". Not a degraded
answer - the correct one, and the one nobody had been given.

No Swift twin needed: the Apple side has no persisted global equivalent.

Staging 392 / iOS 273. 4907 tests, lintVital, doc lint and i18n clean.
The bug this PR removes, surviving inside the fix for it. Caught by a question
about what the screen would actually show.

The constructor seeded LiveState.lastSyncAt from NoopPrefs.lastDevice, which
records the last strap to BOND - and `bonded` is set for a 5/MG by the live-HR
path, so on a two-strap install it is whichever strap was worn most recently, not
the one the screens are scoped to. Wear the 4.0 for an evening and the next launch
would put its sync time on the 5/MG's Today screen: the same misattribution, one
layer down, and it would have shipped inside the change whose entire purpose is
that it must not happen.

Seeded from the registry's ACTIVE row now, resolved exactly as the strap-log
header resolves firmware and last-sync. That read is suspend, so the constructor
seeds NOTHING and the value arrives a moment later. A blank moment is honest;
another strap's timestamp is not, and there is no cheap synchronous guess that is
not the bug in miniature - which is why there is no fallback here at all.

The async fill never overwrites a value the session earned: a HISTORY_COMPLETE
landing while the registry read is in flight is newer than anything persisted and
must win.

4907 tests, lintVital, doc lint and i18n clean.
@ryanbr
ryanbr merged commit a82e7f6 into main Aug 30, 2026
4 checks passed
@ryanbr
ryanbr deleted the last-sync-per-strap branch August 30, 2026 12:09
ryanbr added a commit that referenced this pull request Aug 31, 2026
Build numbers only - 10.6.1 stays the unreleased line, and 395 clears the staging
high-water so the APK installs over 394.

Carries the sparse-bridge change: a night interrupted by one short trip out of bed
is no longer ended at the interruption. Anyone whose nights were being truncated
that way should see fuller totals; the honest caveat is that the absorbed minutes
land in sleep rather than wake, because both stagers call wake off HR and the bridge
only absorbs spans whose HR was already in the sleep band.

Also the #1746 PENDING-ack change, so one "offsets may have moved" line per sync
stops appearing on healthy 4.0 hardware, and the per-strap Last sync / Data write
attribution from #1745.
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