Skip to content

feat(oura): session-identity reconciler for #1284 residual 3 (pure core, draft) - #1337

Closed
ryanbr wants to merge 3 commits into
mainfrom
oura-1284-reconciler
Closed

ryanbr wants to merge 3 commits into
mainfrom
oura-1284-reconciler

Conversation

@ryanbr

@ryanbr ryanbr commented Aug 15, 2026

Copy link
Copy Markdown
Owner

Draft — the pure, testable core of #1284 residual 3 (duplicate generation), landing the design @pipiche38's four-night corpus selected. Wiring into the live path is the hardware-gated follow-up (below), matching this issue's draft → hardware-run cadence.

What this is

OuraSessionReconciler (Swift WhoopStore + byte-identical Kotlin com.noop.oura twin) — pure, no store/BLE:

  • noonAnchoredSleepDay(startTs, tz) — groups a ring's sessions by date(startTs_local + 12h).
  • reconcile(new, existing) — within a sleep-day, overlap-or-gap<60min ⇒ same session; keep the fuller decode (the completeness guard). Returns insert / skip / replace(supersededStartTs).

Why this design (from the corpus, not a guess)

  • Option (2) collapses 4/4 nights; the 30-min grid only 1/4. The grid fails on 08-11/12 (starts 6¼ min apart straddle 22:30) and spends the bedtime precision that let 08-13/14 be checked against WHOOP to −10 s/+50 s.
  • Noon-anchor, not wake-day — on 08-10/11 the 40-min fragment ends before midnight, so its wake-day (Aug 10) differs from the night's (Aug 11); the noon anchor maps both together. The test asserts exactly this (and that wake-day would not).
  • Two duplicate modes, both handled — re-anchor (same hypnogram, two onsets) and partial-drain (shorter divergent decode). The completeness guard adjudicates the partial-drain case; the fuller row is the WHOOP-matching one on every measured night.
  • Nap safety — 08-09's real 22-min afternoon nap shares the sleep-day with that night's sleep, but the ~7½ h gap keeps it a distinct row.

Validation

  • Kotlin: 6/6 green locally (OuraSessionReconcilerTest) — every fixture is one of @pipiche38's nights.
  • Swift runs under swift-packages (WhoopStoreTests, macos-15). Pure package code — no app-target Swift, so no app-build gate needed.
  • One test caught a real bug in my own test premise while writing it (wake-day is the end day, not start), which is now asserted correctly — the design itself held.

Follow-up (needs @pipiche38's ring — Q3)

Wire the reconciler into OuraLiveSource.persistHypnogramBurst: at persist time read the day's already-stored sessions (noonAnchoredSleepDay window) and insert/skip/replace on the decision. That read is the same one that lets the cross-connection check see duplicates a per-connection in-memory list can't — so it also closes the #1297 diagnostic blind spot. It's the write-only-source read dependency we discussed; it can't be unit-tested (BLE + store) and needs a hardware run confirming one row across two connections at the correct window.

Relates to #1284. Design + measurements: @pipiche38 (thank you).

ryanbr added 3 commits August 15, 2026 17:14
Duplicate GENERATION (not just the #899 heal): the ring serves >1 decode of
one night and PK=(deviceId,startTs) mints a row per 0x49 onset. @pipiche38
measured two modes over four nights — re-anchor (same hypnogram, two onsets)
and partial-drain (a shorter divergent decode with its own anchor).

This lands the PURE, testable core of the fix (option 2, the one her data
picks 4/4 vs the grid's 1/4):
- OuraSessionReconciler (Swift WhoopStore + Kotlin com.noop.oura twin):
  noonAnchoredSleepDay(startTs, tz) grouping + reconcile(new, existing) that
  treats overlap-or-gap<60min as the same session and keeps the FULLER decode
  (the completeness guard that adjudicates the partial-drain mode).
- Tests transcribing all four nights + the 08-09 nap: mode-1 collapse, mode-2
  guard, the 30-min-grid straddle that proximity still collapses, the fragment
  ending before midnight that noon-anchor groups (wake-day would not), and the
  nap that shares the sleep-day yet stays distinct. Kotlin 6/6 green locally;
  Swift runs under swift-packages.

FOLLOW-UP (hardware-gated on @pipiche38's ring, per the issue): wire this into
OuraLiveSource.persistHypnogramBurst — read the day's sessions at persist time
(the read that also lets the cross-connection check see duplicates a
per-connection list cannot) and insert/skip/replace on the decision. That's
the write-only-source read dependency; it needs a Q3 hardware run to confirm
one row across two connections.
…h platforms)

At persist time (SourceCoordinator persist closure, Swift + Kotlin) read the
day's already-stored sessions via candidateReadWindow and run the reconciler:
insert a distinct session, skip when a fuller same-night row is stored, or
delete the earlier/shorter duplicates and upsert the fuller one. The DB read
also sees cross-connection duplicates the per-connection diagnostic can't.

- Delete uses the tombstone-free row delete on both sides (Swift
  store.deleteSleepSession; Kotlin repo.deleteSleepSessionRowOnly), matching
  the #899 heal so a superseded duplicate can't suppress the survivor's
  re-detection.
- A true re-persist (same startTs) is excluded from candidates so it upserts
  idempotently rather than delete+insert (no churn, keeps userEdited).
- Kotlin reconciler timestamps moved Int -> Long (SleepSession.startTs is Long;
  avoids the 2038 32-bit overflow and matches the Swift 64-bit Int). Kotlin
  reconciler tests still 6/6 green.

Runtime is hardware-gated on @pipiche38's ring (Q3: one row across two
connections). Swift app-target validated via the app-build gate; reconciler via
swift-packages; Android via local testFullDebugUnitTest.
@ryanbr ryanbr closed this Aug 15, 2026
@ryanbr

ryanbr commented Aug 15, 2026

Copy link
Copy Markdown
Owner Author

Closing — superseded. The persist-time reconciler here had a TOCTOU race (non-atomic read-modify-write on the BLE thread) and a userEdited data-loss edge, and a tracker comparison showed it was a divergent second dedup rule: WHOOP has the same moving-startTs problem and handles it with the #899 post-hoc SleepSessionDedup heal, which already collapses 3 of pipiche's 4 nights with the right survivor.

Split into:

@ryanbr
ryanbr deleted the oura-1284-reconciler branch August 15, 2026 06:38
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