Repository navigation
fix(sleep): collapse a near-adjacent pre-onset fragment in the dedup heal (#1284) - #1338
Merged
Merged
Conversation
…heal (#1284) The #899 SleepSessionDedup overlap rule requires overlap > 0, so it misses the Oura SleepNet pre-onset fragment: a short session that ends seconds BEFORE the 0x49-anchored night begins with no overlap (measured 69 s on 08-10/11; the pre-onset run is ~7 min / 14 codes). Extend isDuplicate so two DISJOINT sessions within a 15-min edge gap are also one night — a gap that small can only be one interrupted night, never two real sleeps (a nap is hours out). The existing overlap branch and survivor sort (userEdited > fresh > longest) are unchanged, so the fuller/edited row still wins and WHOOP behaviour is untouched. Swift + Kotlin twins; 3 new tests each (fragment collapses, real nap stays separate, gap > 15 min stays separate). Full Android suite green (3834). Heal-side (post-hoc): this completes the sweep so no duplicate survives to the next analyze pass. It does NOT stop duplicate GENERATION or the transient phantom-nap window before the heal runs — that needs the separate generation-side 0x49/sleep-day keying (#1284 residual 3, hardware-gated).
overlapSeconds is internal and WhoopStoreTests imports WhoopStore without @testable, so the disjointness sanity assertions didn't compile in swift-packages CI (Kotlin's internal is same-module, so it passed locally). isDuplicate/dedupe (public) already cover the behaviour; drop the redundant overlapSeconds calls.
This was referenced Aug 15, 2026
ryanbr
added a commit
that referenced
this pull request
Aug 15, 2026
…log-only) (#1339) * feat(oura): cross-connection duplicate-generation diagnostic (#1284, log-only) The in-source duplicate-gen(#1284) line compares a PER-CONNECTION memory list, so it is blind to the common case pipiche measured: an overnight with link drops mints the duplicate across DIFFERENT connections. At persist, read the day's stored sessions instead (cross-connection, survives an app restart) and log — via the same SleepSessionDedup.isDuplicate rule the heal uses (#1338) — when this persist duplicates one. Emits one compact line carrying startΔ (≈ the 0x49 onset jitter, since a session's startTs is its anchored onset), the edge-gap/overlap, and both code counts. Log-only: behaviour is unchanged (still upserts). This is the corpus the generation-side 0x49/sleep-day keying will be designed + validated against, and it survives the log head-clip far better than the multi-line burst trace. Swift + Kotlin twins. * style(oura): ASCII the #1284 diagnostic log line for grep/parity with the Kotlin twin
ryanbr
added a commit
that referenced
this pull request
Aug 16, 2026
…al 3) The durable fix for the duplicate-generation bug, behind a default-OFF Test Centre toggle for hardware validation. The heal (#1338/#1382) only collapses duplicates AFTER an analyze pass; this stops them being banked at the source, closing the window where the wrong night shows until the next pass / app restart. Mechanism (per @pipiche38's 08-16 capture: the 0x49 onset is stable to 21 s across 11 re-serves while the END chases wall-clock): - OuraLiveSource keys the session's startTs on the ROUNDED 0x49 onset (SleepSessionDedup.keyedStart, 60 s grid) instead of the end-anchored first-code time — so re-serves of one night share a PK, and the shown bedtime becomes the true onset (fixing the current end-anchor drift). - The persist closure runs a completeness guard (SleepSessionDedup.planBank) before banking: same collapse rule as the heal (longest, then latest-end wins), so a partial re-drain is suppressed and a fuller/later copy supersedes and deletes the rows it replaces. A rare onset-bucket straddle is caught here by overlap, not the PK. Failure falls back to a plain upsert — never loses the night. No schema migration (startTs is still the PK, set to the onset). Pure core (keyedStart + planBank, reusing the shared rank + isDuplicate) is unit- tested both platforms (SleepSessionDedupTest 23/23). The persist wiring is BLE-path — compile-only here; it must be validated on a real ring. Open for a hardware run: the 60 s grid, and whether to keep startTs exact + add a sessionKey column (migration) later. Off by default; no effect for a user with no Oura ring.
ryanbr
added a commit
that referenced
this pull request
Aug 16, 2026
…al 3) The durable fix for the duplicate-generation bug, behind a default-OFF Test Centre toggle for hardware validation. The heal (#1338/#1382) only collapses duplicates AFTER an analyze pass; this stops them being banked at the source, closing the window where the wrong night shows until the next pass / app restart. Mechanism (per @pipiche38's 08-16 capture: the 0x49 onset is stable to 21 s across 11 re-serves while the END chases wall-clock): - OuraLiveSource keys the session's startTs on the ROUNDED 0x49 onset (SleepSessionDedup.keyedStart, 60 s grid) instead of the end-anchored first-code time — so re-serves of one night share a PK, and the shown bedtime becomes the true onset (fixing the current end-anchor drift). - The persist closure runs a completeness guard (SleepSessionDedup.planBank) before banking: same collapse rule as the heal (longest, then latest-end wins), so a partial re-drain is suppressed and a fuller/later copy supersedes and deletes the rows it replaces. A rare onset-bucket straddle is caught here by overlap, not the PK. Failure falls back to a plain upsert — never loses the night. No schema migration (startTs is still the PK, set to the onset). Pure core (keyedStart + planBank, reusing the shared rank + isDuplicate) is unit- tested both platforms (SleepSessionDedupTest 23/23). The persist wiring is BLE-path — compile-only here; it must be validated on a real ring. Open for a hardware run: the 60 s grid, and whether to keep startTs exact + add a sessionKey column (migration) later. Off by default; no effect for a user with no Oura ring.
ryanbr
added a commit
that referenced
this pull request
Aug 16, 2026
…al 3) The durable fix for the duplicate-generation bug, behind a default-OFF Test Centre toggle for hardware validation. The heal (#1338/#1382) only collapses duplicates AFTER an analyze pass; this stops them being banked at the source, closing the window where the wrong night shows until the next pass / app restart. Mechanism (per @pipiche38's 08-16 capture: the 0x49 onset is stable to 21 s across 11 re-serves while the END chases wall-clock): - OuraLiveSource keys the session's startTs on the ROUNDED 0x49 onset (SleepSessionDedup.keyedStart, 60 s grid) instead of the end-anchored first-code time — so re-serves of one night share a PK, and the shown bedtime becomes the true onset (fixing the current end-anchor drift). - The persist closure runs a completeness guard (SleepSessionDedup.planBank) before banking: same collapse rule as the heal (longest, then latest-end wins), so a partial re-drain is suppressed and a fuller/later copy supersedes and deletes the rows it replaces. A rare onset-bucket straddle is caught here by overlap, not the PK. Failure falls back to a plain upsert — never loses the night. No schema migration (startTs is still the PK, set to the onset). Pure core (keyedStart + planBank, reusing the shared rank + isDuplicate) is unit- tested both platforms (SleepSessionDedupTest 23/23). The persist wiring is BLE-path — compile-only here; it must be validated on a real ring. Open for a hardware run: the 60 s grid, and whether to keep startTs exact + add a sessionKey column (migration) later. Off by default; no effect for a user with no Oura ring.
ryanbr
added a commit
that referenced
this pull request
Aug 16, 2026
…al 3) The durable fix for the duplicate-generation bug, behind a default-OFF Test Centre toggle for hardware validation. The heal (#1338/#1382) only collapses duplicates AFTER an analyze pass; this stops them being banked at the source, closing the window where the wrong night shows until the next pass / app restart. Mechanism (per @pipiche38's 08-16 capture: the 0x49 onset is stable to 21 s across 11 re-serves while the END chases wall-clock): - OuraLiveSource keys the session's startTs on the ROUNDED 0x49 onset (SleepSessionDedup.keyedStart, 60 s grid) instead of the end-anchored first-code time — so re-serves of one night share a PK, and the shown bedtime becomes the true onset (fixing the current end-anchor drift). - The persist closure runs a completeness guard (SleepSessionDedup.planBank) before banking: same collapse rule as the heal (longest, then latest-end wins), so a partial re-drain is suppressed and a fuller/later copy supersedes and deletes the rows it replaces. A rare onset-bucket straddle is caught here by overlap, not the PK. Failure falls back to a plain upsert — never loses the night. No schema migration (startTs is still the PK, set to the onset). Pure core (keyedStart + planBank, reusing the shared rank + isDuplicate) is unit- tested both platforms (SleepSessionDedupTest 23/23). The persist wiring is BLE-path — compile-only here; it must be validated on a real ring. Open for a hardware run: the 60 s grid, and whether to keep startTs exact + add a sessionKey column (migration) later. Off by default; no effect for a user with no Oura ring.
simoncad7
pushed a commit
to simoncad7/noop
that referenced
this pull request
Aug 17, 2026
…heal (ryanbr#1284) (ryanbr#1338) * fix(sleep): collapse a near-adjacent pre-onset fragment in the dedup heal (ryanbr#1284) The ryanbr#899 SleepSessionDedup overlap rule requires overlap > 0, so it misses the Oura SleepNet pre-onset fragment: a short session that ends seconds BEFORE the 0x49-anchored night begins with no overlap (measured 69 s on 08-10/11; the pre-onset run is ~7 min / 14 codes). Extend isDuplicate so two DISJOINT sessions within a 15-min edge gap are also one night — a gap that small can only be one interrupted night, never two real sleeps (a nap is hours out). The existing overlap branch and survivor sort (userEdited > fresh > longest) are unchanged, so the fuller/edited row still wins and WHOOP behaviour is untouched. Swift + Kotlin twins; 3 new tests each (fragment collapses, real nap stays separate, gap > 15 min stays separate). Full Android suite green (3834). Heal-side (post-hoc): this completes the sweep so no duplicate survives to the next analyze pass. It does NOT stop duplicate GENERATION or the transient phantom-nap window before the heal runs — that needs the separate generation-side 0x49/sleep-day keying (ryanbr#1284 residual 3, hardware-gated). * test(sleep): use only public dedup API in the ryanbr#1284 near-adjacent tests overlapSeconds is internal and WhoopStoreTests imports WhoopStore without @testable, so the disjointness sanity assertions didn't compile in swift-packages CI (Kotlin's internal is same-module, so it passed locally). isDuplicate/dedupe (public) already cover the behaviour; drop the redundant overlapSeconds calls.
simoncad7
pushed a commit
to simoncad7/noop
that referenced
this pull request
Aug 17, 2026
…1284, log-only) (ryanbr#1339) * feat(oura): cross-connection duplicate-generation diagnostic (ryanbr#1284, log-only) The in-source duplicate-gen(ryanbr#1284) line compares a PER-CONNECTION memory list, so it is blind to the common case pipiche measured: an overnight with link drops mints the duplicate across DIFFERENT connections. At persist, read the day's stored sessions instead (cross-connection, survives an app restart) and log — via the same SleepSessionDedup.isDuplicate rule the heal uses (ryanbr#1338) — when this persist duplicates one. Emits one compact line carrying startΔ (≈ the 0x49 onset jitter, since a session's startTs is its anchored onset), the edge-gap/overlap, and both code counts. Log-only: behaviour is unchanged (still upserts). This is the corpus the generation-side 0x49/sleep-day keying will be designed + validated against, and it survives the log head-clip far better than the multi-line burst trace. Swift + Kotlin twins. * style(oura): ASCII the ryanbr#1284 diagnostic log line for grep/parity with the Kotlin twin
ryanbr
added a commit
that referenced
this pull request
Aug 17, 2026
…al 3) The durable fix for the duplicate-generation bug, behind a default-OFF Test Centre toggle for hardware validation. The heal (#1338/#1382) only collapses duplicates AFTER an analyze pass; this stops them being banked at the source, closing the window where the wrong night shows until the next pass / app restart. Mechanism (per @pipiche38's 08-16 capture: the 0x49 onset is stable to 21 s across 11 re-serves while the END chases wall-clock): - OuraLiveSource keys the session's startTs on the ROUNDED 0x49 onset (SleepSessionDedup.keyedStart, 60 s grid) instead of the end-anchored first-code time — so re-serves of one night share a PK, and the shown bedtime becomes the true onset (fixing the current end-anchor drift). - The persist closure runs a completeness guard (SleepSessionDedup.planBank) before banking: same collapse rule as the heal (longest, then latest-end wins), so a partial re-drain is suppressed and a fuller/later copy supersedes and deletes the rows it replaces. A rare onset-bucket straddle is caught here by overlap, not the PK. Failure falls back to a plain upsert — never loses the night. No schema migration (startTs is still the PK, set to the onset). Pure core (keyedStart + planBank, reusing the shared rank + isDuplicate) is unit- tested both platforms (SleepSessionDedupTest 23/23). The persist wiring is BLE-path — compile-only here; it must be validated on a real ring. Open for a hardware run: the 60 s grid, and whether to keep startTs exact + add a sessionKey column (migration) later. Off by default; no effect for a user with no Oura ring.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Extends the #899
SleepSessionDedupheal to close the one Oura duplicate mode it misses (#1284 residual 3).What
SleepSessionDedup.isDuplicate(Swift + Kotlin twin) requiredoverlap > 0, so it missed the Oura SleepNet pre-onset fragment: a short session ending seconds before the0x49-anchored night starts, with no overlap (measured 69 s on 08-10/11; the pre-onset run is ~7 min / 14 codes perOuraLiveSource). Now two disjoint sessions within a 15-min edge gap are also treated as one night. A gap that small can only be one interrupted night — a real nap is hours out (tested).The overlap branch and the survivor sort (
userEdited > fresh > longest) are unchanged, so the fuller/edited row still wins and WHOOP behaviour is untouched.Tests
Swift + Kotlin, 3 each: fragment-collapses-despite-no-overlap, real-nap-stays-separate, gap-beyond-15-min-stays-separate. Full Android suite green (3834 tests, 0 failures); Swift via swift-packages.
Scope — honest framing
This is heal-side (post-hoc): it completes the sweep so no duplicate survives to the next analyze pass. It does not stop duplicate generation or the transient phantom-nap window before the heal runs — @pipiche38 was clear that residual 3 needs the generation side (
0x49/sleep-day keying), which is separate and hardware-gated. This ships as the heal-completeness half; the generation-side fix + the logging to design it are tracked on #1284.Relates to #1284. Measurements: @pipiche38.