Skip to content

Oura: fix #91 on the clean stack - (lost when refactoring the oura ble stack) - #291

Merged
ryanbr merged 1 commit into
ryanbr:mainfrom
pipiche38:oura-getevents-cursor-fix
Jul 12, 2026
Merged

ryanbr merged 1 commit into
ryanbr:mainfrom
pipiche38:oura-getevents-cursor-fix

Conversation

@pipiche38

Copy link
Copy Markdown

I forgot that fix when refactoring the BLE Oura stack

App (OuraLiveSource):

  • The drain now runs until bytes_left == 0. The response byte-count is NEVER persisted.
  • The durable resume point (open_oura nextEventToSync) is the newest STORED history sample's ring-time (maxStoredRingTime), advanced only by samples that resolved a REAL anchored time (incl. drained pending-anchor parks; never wall-clock fallbacks), bounded by a plausibility ceiling, and committed only at drain end when it resolves under the current anchor.
  • Genuine ring reboot detection: a stored sample OLDER than the seek cursor (sawPreResumeData) -> full pull next connect.
  • Backstop guards only (a healthy drain ends at 0): bytes_left stall (3 flat summaries) and a 300s wall deadline; a forced stop keeps banked forward progress, never resets to 0.
  • A persisted cursor above the ceiling (pre-fix garbage) resets to a full pull at load.
  • Log marker [cursor-fix v1] so the running build is identifiable.

swift test: 91/91. macOS app builds (BUILD SUCCEEDED). To validate on-device: connect twice - 1st drain should end "caught up (bytes_left 0)" + "resume cursor advanced", 2nd should fetch only the delta (no phantom regression, no full re-dump), and the previous night's sleep data should finally arrive.

Both logs are a textbook pass — the fix works exactly as designed:

1st connect:

  • ✓ [cursor-fix v1] running, no phantom "regression detected"
  • ✓ Drain ran to bytes_left 0 and recovered the previously-abandoned tail — today's data appeared for the first time (15:23 → 19:07, the post-charger afternoon)
  • ✓ resume cursor advanced to 3453828 [2026-07-11 19:13:46] — a real ring-time resolving to the fetch moment
  • ✓ Estimate now has day=2026-07-11 through 19:07 (17 active min — plausible for your afternoon)

2nd connect (2 min later):

  • ✓ Resumed from 3453828, summary 43 00 00 00 00 00 = 67 events, bytes_left 0 — an instant incremental delta, no full re-dump. That's the first true incremental sync the Oura path has ever done.

…, drain to bytes_left==0

The 0x11 GetEvents summary was misread on main (the old ryanbr#91 fix never
merged): body[0] is events_received (a batch COUNT, not a status - the
2026-07-11 18:53 drain stopped on events==0 with 400,873 bytes STILL
LEFT, abandoning the newest data incl. the night's hypnogram), and
bytes[2..5] is bytes_left (a remaining-BYTE count, not a cursor -
persisting it and comparing across sessions as clocks minted a phantom
"ring-time regression" -> reset-to-0 -> full history re-dump on every
connect; both morning/evening logs show it byte-for-byte).

Package:
- parseGetEventsResponse -> (eventsReceived, bytesLeft, moreData) per
  open_oura's EventBatchSummary; moreData = bytesLeft > 0.
- 3 golden tests from the real captured summaries, incl. the exact
  events==0/bytes_left==400,873 case that must continue.

App (OuraLiveSource):
- The drain now runs until bytes_left == 0. The response byte-count is
  NEVER persisted.
- The durable resume point (open_oura nextEventToSync) is the newest
  STORED history sample's ring-time (maxStoredRingTime), advanced only
  by samples that resolved a REAL anchored time (incl. drained
  pending-anchor parks; never wall-clock fallbacks), bounded by a
  plausibility ceiling, and committed only at drain end when it resolves
  under the current anchor.
- Genuine ring reboot detection: a stored sample OLDER than the seek
  cursor (sawPreResumeData) -> full pull next connect.
- Backstop guards only (a healthy drain ends at 0): bytes_left stall
  (3 flat summaries) and a 300s wall deadline; a forced stop keeps
  banked forward progress, never resets to 0.
- A persisted cursor above the ceiling (pre-fix garbage) resets to a
  full pull at load.
- Log marker [cursor-fix v1] so the running build is identifiable.

swift test: 91/91. macOS app builds (BUILD SUCCEEDED). To validate
on-device: connect twice - 1st drain should end "caught up (bytes_left
0)" + "resume cursor advanced", 2nd should fetch only the delta (no
phantom regression, no full re-dump), and the previous night's sleep
data should finally arrive.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bj38eZ4JMejA813XhYvMk6
@ryanbr
ryanbr merged commit b17525e into ryanbr:main Jul 12, 2026
6 checks passed
ryanbr added a commit that referenced this pull request Jul 12, 2026
…tested core (#291 follow-up)

The #91 drain guards + resume-cursor logic lived untested inline in OuraLiveSource
(app-target BLE) — which is exactly why it silently regressed when the Oura BLE
stack was refactored (#291: "lost when refactoring"). Move the DECISIONS into a
pure OuraHistoryDrain in OuraProtocol so they're pinned by unit tests, not just
on-device observation:

- drain-to-zero + stall guard (bytes_left must shrink; stop after 3 flat summaries)
  + 300s deadline guard, keeping banked progress on a forced stop
- stored-ring-time high-water mark + reboot flag (a real sample older than the seek)
- resume-cursor commit (advance only if forward AND resolves; reboot -> 0 full pull)
- loaded-cursor plausibility-ceiling sanitize

OuraLiveSource keeps ALL I/O — anchor resolution, persistence, logging, the
historyCursorAdvanced emit — and now delegates every decision to the drain core.
Behaviour-preserving: each site maps 1:1 to the extracted logic (verified by diff).
Also de-versions the [cursor-fix v1] log marker to [cursor-fix].

14 new OuraHistoryDrainTests + the existing 91 pass under swift test (105/105, run
on Linux — OuraProtocol is pure). OuraLiveSource compile: app-build.
ryanbr added a commit that referenced this pull request Jul 12, 2026
…tested core (#291 follow-up) (#306)

The #91 drain guards + resume-cursor logic lived untested inline in OuraLiveSource
(app-target BLE) — which is exactly why it silently regressed when the Oura BLE
stack was refactored (#291: "lost when refactoring"). Move the DECISIONS into a
pure OuraHistoryDrain in OuraProtocol so they're pinned by unit tests, not just
on-device observation:

- drain-to-zero + stall guard (bytes_left must shrink; stop after 3 flat summaries)
  + 300s deadline guard, keeping banked progress on a forced stop
- stored-ring-time high-water mark + reboot flag (a real sample older than the seek)
- resume-cursor commit (advance only if forward AND resolves; reboot -> 0 full pull)
- loaded-cursor plausibility-ceiling sanitize

OuraLiveSource keeps ALL I/O — anchor resolution, persistence, logging, the
historyCursorAdvanced emit — and now delegates every decision to the drain core.
Behaviour-preserving: each site maps 1:1 to the extracted logic (verified by diff).
Also de-versions the [cursor-fix v1] log marker to [cursor-fix].

14 new OuraHistoryDrainTests + the existing 91 pass under swift test (105/105, run
on Linux — OuraProtocol is pure). OuraLiveSource compile: app-build.
pipiche38 added a commit to pipiche38/noop that referenced this pull request Jul 13, 2026
…t, PR ryanbr#325 review)

ryanbr is right that the Android twin exists and must not diverge - it carried every bug
Swift fixed this cycle. This ports the complete chain, file by file, with the same fixes,
the same comments, and JVM twin tests mirroring the Swift suites (same fixtures/values):

- Framing.kt: 0x11 summary corrected to open_oura EventBatchSummary
  (events_received/progress/bytes_left - the old cursor misread WAS the ryanbr#91 full re-dump
  loop, still live here); 0x13 SyncTime response parse (deterministic anchor pair);
  lenient parseRecord + ONE-packet-per-notification reassembler (twin of dae3d7a, the
  phantom-storm fix - the old buffering loop was still live here too).
- Commands.kt: syncTime -> u64 seconds LE + tz half-hours (on-device-proven layout; the
  open_ring token/counter/0xF6 guess never anchored real hardware).
- OuraDriver.kt: continuation fetch at the ADVANCED cursor (max=255, open_oura
  drain_events; the max=0 "ack" caused the same-window re-serve loop);
  syncTimeAnchorCandidate (raw-ticks vs seconds x10 disambiguation against the persisted
  cursor) + adoptSyncTimeAnchor (same 2020-2035 plausibility gate); 0x4B routed to the
  validated phase decode.
- EventTags.kt: SLEEP_SUMMARY_B(0x4B) -> SLEEP_PHASE_B, Tier A (twin of dddbe95 - the
  divergence flagged in the PR ryanbr#325 review).
- OuraEvents.kt: envelopeRingTimestamp (feeds the in-session continuation cursor).
- Decoders.kt: decodeGreenIBIAmpCandidate (ringverse p_green_ibi_and_amp @0x503960,
  Tier-B ryanbr#287, log-only).
- NEW OuraHistoryDrain.kt: seen/stored two-cursor drain core (ryanbr#91/ryanbr#291) - guards,
  progress test, reboot detection, cursor commit rules.
- NEW HypnogramAssembler.kt: burst grouping + 30s/code backward time-axis reconstruction.
- ble/OuraLiveSource.kt: sync_time now sent at streaming (was never sent!); 0x13 anchor
  handler; quiet-window batch continuation + no-progress stop; drain-end cursor commit
  (stored-anchored only) + self-chained passes (reboot/deadline backlog); hypnogram
  burst persistence with park-until-anchor + honest teardown drop + the 0x49 true-sleep-
  end refinement; 0x71 fixture-capture log with candidate decode; per-session resets.

STORED-DATA PARITY: OURA_SLEEP_PHASE rows now get the same reconstructed time axis on
both platforms (per-code envelope-time rows were the old Android behavior - the whole
night collapsed onto the write moment).

Verified: compileFullDebugKotlin BUILD SUCCESSFUL; testFullDebugUnitTest 2647 tests -
all com.noop.oura + OuraStreamMapping suites green incl. 30 new/updated twin tests
(OuraHistoryDrainTest, HypnogramAssemblerTest, 0x71 round-trip/gates, 0x13/candidate,
one-packet reassembler, continuation-shape pins). The 3 remaining failures
(AiCoachContextTest, StandardHrSensorFormatTest x2) fail identically on the untouched
tree - pre-existing locale/env issues on this machine, not this change. BLE session
choreography is inspection-only (no Android device on hand) - same validation ask as
the Swift side had before its on-device passes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bj38eZ4JMejA813XhYvMk6
DX23876 pushed a commit to DX23876/noop that referenced this pull request Jul 22, 2026
…, drain to bytes_left==0 (ryanbr#291)

The 0x11 GetEvents summary was misread on main (the old ryanbr#91 fix never
merged): body[0] is events_received (a batch COUNT, not a status - the
2026-07-11 18:53 drain stopped on events==0 with 400,873 bytes STILL
LEFT, abandoning the newest data incl. the night's hypnogram), and
bytes[2..5] is bytes_left (a remaining-BYTE count, not a cursor -
persisting it and comparing across sessions as clocks minted a phantom
"ring-time regression" -> reset-to-0 -> full history re-dump on every
connect; both morning/evening logs show it byte-for-byte).

Package:
- parseGetEventsResponse -> (eventsReceived, bytesLeft, moreData) per
  open_oura's EventBatchSummary; moreData = bytesLeft > 0.
- 3 golden tests from the real captured summaries, incl. the exact
  events==0/bytes_left==400,873 case that must continue.

App (OuraLiveSource):
- The drain now runs until bytes_left == 0. The response byte-count is
  NEVER persisted.
- The durable resume point (open_oura nextEventToSync) is the newest
  STORED history sample's ring-time (maxStoredRingTime), advanced only
  by samples that resolved a REAL anchored time (incl. drained
  pending-anchor parks; never wall-clock fallbacks), bounded by a
  plausibility ceiling, and committed only at drain end when it resolves
  under the current anchor.
- Genuine ring reboot detection: a stored sample OLDER than the seek
  cursor (sawPreResumeData) -> full pull next connect.
- Backstop guards only (a healthy drain ends at 0): bytes_left stall
  (3 flat summaries) and a 300s wall deadline; a forced stop keeps
  banked forward progress, never resets to 0.
- A persisted cursor above the ceiling (pre-fix garbage) resets to a
  full pull at load.
- Log marker [cursor-fix v1] so the running build is identifiable.

swift test: 91/91. macOS app builds (BUILD SUCCEEDED). To validate
on-device: connect twice - 1st drain should end "caught up (bytes_left
0)" + "resume cursor advanced", 2nd should fetch only the delta (no
phantom regression, no full re-dump), and the previous night's sleep
data should finally arrive.


Claude-Session: https://claude.ai/code/session_01Bj38eZ4JMejA813XhYvMk6

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
DX23876 pushed a commit to DX23876/noop that referenced this pull request Jul 22, 2026
…tested core (ryanbr#291 follow-up) (ryanbr#306)

The ryanbr#91 drain guards + resume-cursor logic lived untested inline in OuraLiveSource
(app-target BLE) — which is exactly why it silently regressed when the Oura BLE
stack was refactored (ryanbr#291: "lost when refactoring"). Move the DECISIONS into a
pure OuraHistoryDrain in OuraProtocol so they're pinned by unit tests, not just
on-device observation:

- drain-to-zero + stall guard (bytes_left must shrink; stop after 3 flat summaries)
  + 300s deadline guard, keeping banked progress on a forced stop
- stored-ring-time high-water mark + reboot flag (a real sample older than the seek)
- resume-cursor commit (advance only if forward AND resolves; reboot -> 0 full pull)
- loaded-cursor plausibility-ceiling sanitize

OuraLiveSource keeps ALL I/O — anchor resolution, persistence, logging, the
historyCursorAdvanced emit — and now delegates every decision to the drain core.
Behaviour-preserving: each site maps 1:1 to the extracted logic (verified by diff).
Also de-versions the [cursor-fix v1] log marker to [cursor-fix].

14 new OuraHistoryDrainTests + the existing 91 pass under swift test (105/105, run
on Linux — OuraProtocol is pure). OuraLiveSource compile: app-build.
@pipiche38
pipiche38 deleted the oura-getevents-cursor-fix branch August 3, 2026 09:49
simoncad7 pushed a commit to simoncad7/noop that referenced this pull request Aug 17, 2026
…, drain to bytes_left==0 (ryanbr#291)

The 0x11 GetEvents summary was misread on main (the old ryanbr#91 fix never
merged): body[0] is events_received (a batch COUNT, not a status - the
2026-07-11 18:53 drain stopped on events==0 with 400,873 bytes STILL
LEFT, abandoning the newest data incl. the night's hypnogram), and
bytes[2..5] is bytes_left (a remaining-BYTE count, not a cursor -
persisting it and comparing across sessions as clocks minted a phantom
"ring-time regression" -> reset-to-0 -> full history re-dump on every
connect; both morning/evening logs show it byte-for-byte).

Package:
- parseGetEventsResponse -> (eventsReceived, bytesLeft, moreData) per
  open_oura's EventBatchSummary; moreData = bytesLeft > 0.
- 3 golden tests from the real captured summaries, incl. the exact
  events==0/bytes_left==400,873 case that must continue.

App (OuraLiveSource):
- The drain now runs until bytes_left == 0. The response byte-count is
  NEVER persisted.
- The durable resume point (open_oura nextEventToSync) is the newest
  STORED history sample's ring-time (maxStoredRingTime), advanced only
  by samples that resolved a REAL anchored time (incl. drained
  pending-anchor parks; never wall-clock fallbacks), bounded by a
  plausibility ceiling, and committed only at drain end when it resolves
  under the current anchor.
- Genuine ring reboot detection: a stored sample OLDER than the seek
  cursor (sawPreResumeData) -> full pull next connect.
- Backstop guards only (a healthy drain ends at 0): bytes_left stall
  (3 flat summaries) and a 300s wall deadline; a forced stop keeps
  banked forward progress, never resets to 0.
- A persisted cursor above the ceiling (pre-fix garbage) resets to a
  full pull at load.
- Log marker [cursor-fix v1] so the running build is identifiable.

swift test: 91/91. macOS app builds (BUILD SUCCEEDED). To validate
on-device: connect twice - 1st drain should end "caught up (bytes_left
0)" + "resume cursor advanced", 2nd should fetch only the delta (no
phantom regression, no full re-dump), and the previous night's sleep
data should finally arrive.


Claude-Session: https://claude.ai/code/session_01Bj38eZ4JMejA813XhYvMk6

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
simoncad7 pushed a commit to simoncad7/noop that referenced this pull request Aug 17, 2026
…tested core (ryanbr#291 follow-up) (ryanbr#306)

The ryanbr#91 drain guards + resume-cursor logic lived untested inline in OuraLiveSource
(app-target BLE) — which is exactly why it silently regressed when the Oura BLE
stack was refactored (ryanbr#291: "lost when refactoring"). Move the DECISIONS into a
pure OuraHistoryDrain in OuraProtocol so they're pinned by unit tests, not just
on-device observation:

- drain-to-zero + stall guard (bytes_left must shrink; stop after 3 flat summaries)
  + 300s deadline guard, keeping banked progress on a forced stop
- stored-ring-time high-water mark + reboot flag (a real sample older than the seek)
- resume-cursor commit (advance only if forward AND resolves; reboot -> 0 full pull)
- loaded-cursor plausibility-ceiling sanitize

OuraLiveSource keeps ALL I/O — anchor resolution, persistence, logging, the
historyCursorAdvanced emit — and now delegates every decision to the drain core.
Behaviour-preserving: each site maps 1:1 to the extracted logic (verified by diff).
Also de-versions the [cursor-fix v1] log marker to [cursor-fix].

14 new OuraHistoryDrainTests + the existing 91 pass under swift test (105/105, run
on Linux — OuraProtocol is pure). OuraLiveSource compile: app-build.
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