Skip to content

fix(sleep): recover unmatched off-wrist tails from sustained HR - #2585

Merged
ryanbr merged 5 commits into
mainfrom
stack/2375-offwrist-recovery
Sep 29, 2026
Merged

ryanbr merged 5 commits into
mainfrom
stack/2375-offwrist-recovery

Conversation

@ryanbr

@ryanbr ryanbr commented Sep 29, 2026

Copy link
Copy Markdown
Owner

Stacked rebase of #2375 by @fadkeprasad, replayed onto current main so their four commits keep their
authorship, plus one commit from me repinning a contract test.

Their work is unchanged. Unmatched WRIST_OFF tails are closed from sustained valid HR (five minutes,
30 to 220 bpm, no gap over five seconds, invalid samples reset the run), returning the first observed
sample of the confirmed run rather than inventing a boundary, with the Kotlin twin line for line
equivalent. The history repair is one-shot, reentrancy-guarded, foreground-only, and marks its flags
only inside the post-persistence callback, so a busy, cancelled or failed attempt leaves the work
pending. preserveUnscoredHistory also stops the existing Effort repair replacing an entire historical
window, which is a data-loss fix riding along inside a sleep change.

The parity authority is theirs as well: reciprocal twin claims re-formed the offWristIntervals pair at
its new arity and paired both WristWearRecovery files, so the identity change took no debt.

Why this branch rather than a merge of #2375

parity-governance was red on test_rr_legacy_preservation_contract, which pinned
dailyMetrics: persistedDailies as a literal. Per-day windowing replaced that argument with a ternary.
The invariant was never broken: dailiesByDay groups from persistedDailies, there is exactly one
persistComputedScores call site, and the assertNotIn over the derivations never stopped passing.

The repin cannot land on its own, because main still has the old literal and the new assertions would
not match there. It has to travel with the Swift change, and a contributor should not have to carry a
workflow defect, so I added it here instead of asking for another push.

Why it stayed hidden for eight days

That test reads product source but sits behind parity-governance.yml's Tools/parity_* path filter,
and the core tools-python step collects with find . -maxdepth 1 from Tools, so Tools/tests/**
is not in it either. The ternary landed on 2026-09-21 and the test did not run once until
Tools/parity_dispositions.json was touched seven days later. Same shape as #2582 and #1691, tracked
separately and deliberately not widened here.

Verification, on this exact tree

  • test_rr_legacy_preservation_contract: 2/2 OK, mutation-checked both ways. Grouping from raw
    dailies, and writing raw dailies in the wide branch, each turn it red. The literal it replaces
    only ever pinned the wide branch, so this is wider.
  • parity repository-acceptance suite: 50/50 OK
  • parity_ratchet.py --base origin/main: errors=0
  • rebase onto 49e2237 was clean

Still unmeasured, and carried over from review: elapsed time for a full foreground historyDays: 4000
repair on WHOOP 5 or MG with deep history, where R-R window reuse is disabled for the whole family.
Correct and retrying either way, so not gating, but it wants a real device.

Credit to @fadkeprasad.

fadkeprasad and others added 5 commits September 29, 2026 17:52
…ence

The contract pinned `dailyMetrics: persistedDailies` as a literal. Per-day
windowing for preserveUnscoredHistory replaced that argument with a ternary, so
the literal went missing and the test went red while the invariant it exists to
protect was intact: both branches still read only the snapshot-protected copy.

Pin both branches instead. That dailiesByDay groups from persistedDailies covers
the per-day path, and the ternary's else covers the wide path. Mutation-checked
in both directions: grouping from raw dailies, and writing raw dailies in the
wide branch, each turn the test red. That is wider than the literal it replaces,
which only ever pinned the wide branch.

The assertNotIn over the derivations is untouched. That is the assertion keeping
a legacy snapshot from masquerading as a value recalculated against today's
baselines, and it never stopped passing.
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.

2 participants