Skip to content

Split app layer into open_health - #7

Draft
Th0rgal wants to merge 79 commits into
mainfrom
split-open-health
Draft

Split app layer into open_health#7
Th0rgal wants to merge 79 commits into
mainfrom
split-open-health

Conversation

@Th0rgal

@Th0rgal Th0rgal commented Jul 8, 2026

Copy link
Copy Markdown
Owner

Summary

Splits the app/product layer out of open_oura now that open_health exists at https://github.com/Th0rgal/open_health.

open_oura keeps the reusable Oura protocol/library surface:

  • oura-protocol
  • oura-link
  • oura-store
  • oura-analysis
  • protocol/debug oura CLI commands

The moved surfaces now live in open_health:

  • web dashboard
  • iOS app
  • DNA explorer
  • blood PDF import
  • app summary/FFI crates
  • model-running product code

Sync correctness and maintainability

  • fixes extended-envelope timestamp state so each envelope starts from its own absolute timestamp
  • requires persistence callbacks to succeed before acknowledging a history batch
  • preserves (captured_unix, insertion id) order so ring reboots remain detectable even when thousands of rows share a capture second
  • separates pure legacy/extended history decoding from transport and cursor-checkpoint orchestration
  • gates app-stream setup by ring generation

Notes

The open_health repository was created with history preserved from this repository using git-filter-repo, then bootstrapped to consume core crates from open_oura as Git dependencies.

Validation

  • cargo test --workspace
  • dedicated legacy, extended-envelope, unterminated-batch, timestamp-reset, and capture-order tests
  • consumed by open_health build 8 in device and simulator builds

Th0rgal added 30 commits June 26, 2026 22:09
A single-page, local-first health dashboard following notes/dashboard-v2-brainstorm.md.

- Rust (crates/oura-cli/src/dashboard.rs) owns the DB + non-model calcs: per-night
  HRV/RHR/skin-temp, SpO2 % via Oura's R→% calibration, baselines + deltas, the
  Device & Data Health panel, and the digest. It serves the page + /api/summary.
- The torch models (sleep hypnogram, activity, CVA) run via the Python runners,
  shelled out like `oura sessions`. Added `--json` to run_sleep_model.py and
  run_cva_model.py for machine-readable output.
- Frontend in a separate dashboard/web/ folder: vanilla HTML/CSS/JS, no build, no
  external deps. Auto light/dark via prefers-color-scheme, skeleton loading,
  reduced-motion safe, one accent + one radius, system + mono type. Activity grouped
  by day, per-night hypnograms, vitals tiles with sparklines.
- New `oura dashboard --port --tz-offset --sex/--age/--height/--weight` subcommand
  (loopback-only). Default build includes it; models still run via Python.

Verified in Playwright: light, dark, and mobile all render correctly.
- Display serif (system-only, offline) for the digest 'what changed' statement and
  the big hero numbers (vascular age, SpO2); data/tables stay mono.
- Small UPPERCASE letter-spaced labels for panel headings, tile labels, day headers
  (the scientific/editorial meta voice).
- Italic serif sub-lines under the hero numbers; more air on panels.
Verified in Playwright (light, dark, mobile). No external/Google fonts — keeps the
dashboard fully offline.
Functional:
- Sync the ring from the header (POST /api/sync runs `oura sync` as a subprocess,
  using the dashboard's --name/--key-file), then auto-refreshes. CSRF-guarded.
- Battery in the header + Device panel, read offline from stored
  `battery_level_changed` debug events (battery % + voltage).
- Editable user profile (age/sex/height/weight/ring) the ring can't measure, stored
  in a gitignored profile.json next to oura.db and used by the CVA model + runners
  (GET/POST /api/profile, CSRF-guarded; run_cva_model.py reads it as defaults).
  Fixes the wrong "-11.2 yr vs your age" — now driven by your real age.

Visual:
- Vendored Phosphor icons (MIT, offline) on panel headers, actions, chips.
- Leaner palette: neutral outline tags, subtle status dots instead of filled pills,
  monochrome thin sparklines with a faint area fill.

Verified in Playwright (light/dark): battery, icons, profile edit → CVA re-run.
- Device & data health now shows the ring's identity, read offline from the
  `device` table: Ring ID (serial), firmware, API version, MAC. Added
  Store::device_info() (device row joined with sync_state).
- "Last sync" now uses the real sync_state.last_sync_unix instead of the event
  anchor, so it reflects the actual last drain.
- Sync errors are surfaced in a visible toast with an actionable hint
  ("Couldn't find your ring. Take it off the charger…") instead of only a
  tooltip that read "Failed". Success shows the synced summary line.

Verified in Playwright: identity renders, sync toast shows the real reason.
… fixes

- estimate active kcal per detected session; show it in the session popover
- cache build_summary (invalidate on db/profile mtime); run sleep/CVA/activity
  models concurrently; batch all nights through one sleep-model process
- resolve --db to an absolute path so Rust and the Python runners open the same
  file (Bugbot: Python models use wrong DB)
- HRV digest %: divide the delta by the prior-night mean baseline, matching the
  vitals tile (Bugbot: HRV digest percent wrong)
- recovery line keys off HRV direction (RHR-down as fallback), not the raw sign
  of an HR-rise fragment (Bugbot: recovery text ignores resting HR)
- surface profile-save errors instead of closing the dialog on a 200+{error}
  response (Bugbot: profile save ignores server errors)
- vivid-but-glassy hypnogram colors; dot-style toasts; activity icons
…nvert, decode notes

- `oura sleep-score`: SleepNet hypnogram → durations/efficiency + calibrated
  contributor curves & combiner weights (tools/score_sleep.py, fit_*.py)
- score-weights doc + data-recovery-map: the app (not cloud) is the analytics tier
- run_stress_model.py runner
- Ring Runner: persist horizontal/vertical invert toggles across restarts
- real_steps decode: byte-layout notes confirmed vs the native parser
- route API on the path only, stripping any query string, so /api/summary?cb=…
  no longer 404s (Bugbot: API path ignores query string)
- guard the vitals trend against a zero baseline → no Inf/NaN in the JSON
  (Bugbot: vitals delta divides by zero)
- "Real steps" capability tile checks feature_1 OR feature_2, matching the event
  the Steps stream actually counts (Bugbot: real steps toggle wrong event)
- last-sync fields come only from a real recorded sync timestamp; absent → null
  (UI shows "—") instead of faking freshness from the latest event's capture time
  (Bugbot: wrong fallback for last sync)
When the scored night can't be matched to a stored bedtime, fall back to the
night's own midpoint (start_ds, end_ds) instead of indexing bts[-1] — which used
the wrong night's end and raised IndexError when bts was empty (e.g. --start/--end
with no bedtime_period rows). timing_features now takes end_ds.
A session crossing midnight had its bar sized from full duration_min while end was
clamped to 1440, so the bar ran wider than its lane and the tooltip/popover time
range didn't match the duration. Keep a true uncapped end (endTrue) for the
duration/range label, and size the bar from the day-clamped (end - start).
…ror state

- score_sleep: compute the 7-day midpoint regularity from a circular mean so it's a
  true circular delta — frame-invariant (independent of --tz / ds→clock phase) and
  correct across the midnight wrap, where the naive arithmetic mean broke
  (Bugbot: sleep timing mixes timezones)
- dashboard: on an /api/summary {error} or fetch failure, clear every panel's
  loading skeleton (not just the digest) so the page reads as errored instead of
  stuck mid-load (Bugbot: summary errors leave skeletons)
- cached_summary re-stats oura.db / profile.json after the (slow) build and only
  stores the result if they didn't change underneath it, so a sync that lands during
  a build can't be masked by a stale cache entry; also guards against a slower
  concurrent build clobbering a fresher entry (Bugbot: summary cache stale after sync)
- startup line now reads "open_oura dashboard running — open http://… in your browser"
  instead of the arrow-y debug-looking string (Bugbot: wrong dashboard startup message)
…atest, matching %)

Both the vitals tiles and the digest now read a single vital_stat: latest is the
*most recent night's* value (None if that night had no sample, so an older night is
never shown as current), baseline is the mean of the prior nights, and the percent
change comes from one shared helper.

- latest no longer slides to an older night when the newest night lacks samples
  (Bugbot: vitals "latest" skips empty nights)
- the digest HRV % is now the exact value the tile shows, including the zero-baseline
  guard (no fragment), instead of d/base.max(1.0) (Bugbot: digest HRV percent mismatches tiles)
- Advanced & debugging panel: export the 16-byte ring key (copy / .key / QR) to set
  up another device without re-pairing, or import one (paste / .key / QR), written to
  --key-file (0600, lowercased, validated as 32 hex chars). New GET/POST /api/ring-key
  behind the existing CSRF + loopback guards.
- capability toggles now reflect the *real* on-ring feature mode: each sync snapshots
  SetFeatureMode status to a gitignored feature_modes.json next to oura.db; the panel
  falls back to "events seen recently" until a mode is captured.
- docs: document the advanced panel, key export/import, the feature-modes snapshot,
  the JSON API + CSRF/loopback guards, and the cached/parallel/batched summary.
…etrics

tools/fit_scores_all.py extends the Sleep two-layer approach (weights × contributor
curves) to Readiness and Activity, engineering lag-1 / trailing-mean features for the
"today vs personal baseline" contributors. Recovered weights confirmed for all three
(ceiling R²=0.84–0.998); end-to-end Sleep R²=0.97, with the Readiness/Activity gap
isolated to baseline-relative / multi-day-load contributors that need accumulated
history. docs/algorithms written up accordingly.
…ock end

- security: GET /api/ring-key discloses the ring auth key, so it now requires the
  same-origin X-Oura-Dash header like the mutating endpoints — a cross-origin page
  (or DNS-rebind attempt, already blocked by the Host guard) can't read the key; only
  the dashboard's own page can. fetchRingKey() sends the header.
- actogram: display the API's wall-clock `end` string (which already wraps past
  midnight) instead of formatting a numeric endTrue that produced invalid "24:xx"
  times; the day-clamped endMin is kept only for bar geometry.
Compute the 7-day midpoint regularity with a circular mean + circular distance on
the 24h clock, like tools/score_sleep.py, so a schedule that straddles a wrap isn't
scored as wildly irregular. Equivalent to the previous arithmetic mean on normal
(contiguous) bedtimes, so the calibration is unchanged.
…n them)

A successful /api/feature toggle now writes the new mode (0 off / 1 automatic) into
feature_modes.json right away, and the summary cache keys on that file's mtime too —
so a reload shows the new capability state instead of the pre-toggle one held until
the next sync re-snapshots. (Bugbot: capability toggles stale until sync)
- the "Steps" stream now counts both real_step_event_feature_1 and _2 (paired
  halves), and "Skin temp" counts sleep_temp_event alongside temp_event — matching
  the events the per-night stats already consume (Bugbot: steps/skin-temp streams undercount)
- load() carries a sequence id and drops a response if a newer load() has started,
  so a slow earlier /api/summary can't overwrite fresher vitals/CVA/digest after a
  sync or profile save (Bugbot: overlapping summary loads race)
…ura-ffi

Move build_summary and its helpers out of the CLI into a reusable `oura-summary`
crate behind a `ModelRunner` trait (the dashboard supplies a Python runner; a native
client can supply an on-device one). Add `oura-core` (UniFFI-exported protocol/summary
surface) and `oura-ffi` (C-ABI) so the same tested Rust logic backs the native app.
dashboard.rs now just wires the Python runner + HTTP server; no behavior change.
apps/ios: SwiftUI client (OuraApp) + an on-device model spike, built on
OuraCore.xcframework generated from oura-core via UniFFI. Build outputs (the
xcframework, Xcode build/ dirs, bundled libtorch, *.db) are gitignored and
regenerated by the build scripts — only source + generated bindings are tracked.
…lines, docs

Add score_readiness.py and the calibrate_scores/export_mobile/build_daily tooling
that turns raw metrics into the daily Sleep/Readiness/Activity summaries with
personal baselines, and document the approach (daily-summaries-and-baselines.md).
Put `pub mod ble` and the btleplug `From` impl behind `#[cfg(feature = "ble")]` so
oura-link (and oura-core/oura-ffi on top of it) can build without btleplug — BLE
isn't available on iOS, where transport is supplied natively. The CLI keeps `ble`
on by default, so its behavior is unchanged.
- write() now awaits the GATT write-with-response ack via a continuation resumed in
  didWriteValueFor, so the sequential Rust client knows a frame landed before sending
  the next (Bugbot: BLE write never awaits response)
- connect(timeout:) actually uses the timeout — fails with .timedOut instead of
  hanging when no ring advertises or Bluetooth is off (Bugbot: connect timeout unused)
- resume connect/write continuations on disconnect too, and take/resume them under a
  lock so the success and timeout paths can't double-resume (a crash). Class marked
  @unchecked Sendable accordingly.
…ript

- BLETransport: store the connect-timeout as a DispatchWorkItem and cancel it in
  finishConnect, so a stale timer from a finished/replaced attempt can't abort a
  newer connection with a false timeout (Bugbot: stale BLE connect timers)
- BLETransport: only report "connected" once every notify subscription is confirmed
  (didUpdateNotificationStateFor), so Rust sync can't start before inbound frames
  flow and miss early ring responses (Bugbot: connect OK before notify ready)
- OuraApp: pass the phone's real UTC offset to summaryJson instead of a hardcoded 1,
  so labels/windows/digest match the wearer's clock (Bugbot: iOS timezone mismatch)
- resume_libtorch_ios.sh: drive CMake directly like build_libtorch_ios.sh — PyTorch
  2.9 removed scripts/build_ios.sh (Bugbot: resume script uses removed build)
- BLETransport: reject a second connect() while one is in flight (.busy) instead of
  overwriting the continuation and stranding the earlier caller (Bugbot)
- BLETransport: honor the CoreBluetooth error in didDiscoverServices /
  didDiscoverCharacteristicsFor — fail the connect with it rather than proceeding on
  an incomplete characteristic set (Bugbot)
- Info.plist: add NSBluetoothAlways/PeripheralUsageDescription, required for
  CoreBluetooth on device (Bugbot)
- OuraApp: round the device UTC offset to the nearest whole hour (the offset is
  integer-hours across the web CLI, Python runners, and FFI) (Bugbot)
Th0rgal added 5 commits July 4, 2026 11:23
Connect fixes (iPhone scan stalled while desktop worked):
- accept any device matching the proprietary Oura service filter even
  before its name arrives — the ADV packet is full, so the name only
  comes in the scan response, which a worn ring answers lazily; the old
  name gate could reject the ring's single coalesced report
- scan with allow-duplicates so a late scan response is re-delivered
- raise the connect budget 20s -> 50s (desktop allows 25s scan + 30s GATT)
- fail fast when Bluetooth is off/unauthorized instead of burning the
  whole timeout; serial (not concurrent) CoreBluetooth delegate queue
- timeouts now name the stage that stalled (scanning / GATT / subscribe)

Diagnostics: RingDiag transcript (timestamped, tagged, 4000-line ring
buffer, cleared per attempt) fed by every pipeline step — state changes,
adv reports (deduped), characteristics + properties, subscribe errors,
full frame hex both ways — with a live tail + "copy logs" button in the
sync screen. os.log mirror uses privacy: .public so Console shows values
instead of <private> on an untethered device.

Also fix backwards pairing advice: the ring advertises reliably ON its
charger, and the phone running the official app holds its single link.
…c refinements

DNA explorer — new `oura-dna` crate reads genome VCF/gVCF (`.vcf.gz`) and scores
curated single-SNP traits (`dna/catalog.json`) plus polygenic scores: illustrative
built-ins and real PGS Catalog scoring files fetched on demand from EBI. Rigorous:
strict effect+other-allele matching with strand-flip resolution and palindromic
exclusion, `weight_type` (OR/HR→ln), coverage stats; gVCF reference-block resolution
(single-pass per-chromosome cursor) so 30x WGS gets full coverage; strand-aware trait
interpretation. Served by `crates/oura-cli/src/dna.rs` → `dashboard/web/dna.{html,js,css}`.
Genomes read from a configurable directory (`--dna-files` / `$OURA_DNA_FILES`) and are
never committed; only the curated catalog is tracked.

blood panel — `/blood` lab-marker tracking (`crates/oura-cli/src/blood.rs` +
`dashboard/web/blood.{html,js,css}`).

Also: autonomic-recovery-by-stage (`oura-summary` + iOS `Reports.swift`), ring-sync /
BLE-discovery refinements (`oura-link`, `RingSync.swift`), epoch-aware time mapping
(`tools/epoch_time.py`), regenerated iOS FFI bindings.

Personal data — genomes, `oura.db` backups, and working plan drafts — stays gitignored.
Unfiltered advertisement scan that lists every nearby BLE advertiser (Oura
ones flagged). `oura scan` filters on the ring's service and only says
"not found", which can't tell apart: Bluetooth off (radio sees nothing),
the ring's single link held elsewhere / asleep / flat (radio sees other
devices), or the ring present via its charging case but itself silent. This
example pins the failure mode — it's what identified the official Android
app holding the link during the sync-debug session.

Uses the shared OURA_SERVICE constant; current-thread runtime.
@Th0rgal

Th0rgal commented Jul 8, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dbf256dcb5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/oura-link/src/client.rs Outdated
Comment on lines +532 to +534
let _ = self
.request_tag(&protocol::req_get_event_ack(start), 0x11)
.await;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid acknowledging unstored event batches

If SQLite fails while cmd_sync is inserting a batch, its on_event callback records db_err and then stops storing later events, and on_batch returns without persisting the cursor; however drain_events still sends this ring-side ack before the caller can see that error. In a disk-full/DB-locked mid-batch sync, the ring is told that all events through start were consumed while they were not written to SQLite and the local cursor remains old, so a retry can permanently miss data the ring has already acknowledged.

Useful? React with 👍 / 👎.

Comment thread crates/oura-protocol/src/events.rs Outdated
Comment on lines +960 to +962
let ms = match self.last_ms {
Some(prev) if encoded_ts < 100_000_000 => prev.saturating_add(encoded_ts),
_ => encoded_ts,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reset extended timestamp deltas per envelope

For ExtGetEvent syncs on a recently rebooted ring, absolute timestamps can be below 100_000_000 ms, but this branch treats every such value after the first packet as a delta from the previous event. Since the parser's own format says each completed envelope starts with an absolute timestamp and only subsequent events in that envelope are deltas, carrying last_ms across envelopes inflates ring_timestamp and the sync cursor for those boots, corrupting event ordering and later resume ranges.

Useful? React with 👍 / 👎.

@Th0rgal

Th0rgal commented Jul 8, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d409f9ecad

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread ci_scripts/ci_post_clone.sh Outdated
REPO="${CI_PRIMARY_REPOSITORY_PATH:-$PWD}"

# OuraCore.xcframework (device + sim) from the committed UniFFI bindings
bash "$REPO/apps/ios/build-xcframework.sh"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Remove the stale iOS build from post-clone CI

If this repo is built by Xcode Cloud, adding ci_scripts/ci_post_clone.sh makes the post-clone phase fail because this split removes the apps/ tree, yet the script still invokes apps/ios/build-xcframework.sh and later cds into apps/ios/OuraApp. I checked the tree with rg --files | rg '^apps/' and there are no matching files, so the CI hook is now guaranteed to exit before any Rust checks run.

Useful? React with 👍 / 👎.

Comment thread tools/build_daily.py Outdated
Comment on lines +82 to +83
if csv:
cmd += ["--csv", csv]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not pass an unsupported --csv flag

When build_daily.py is run with its documented --csv option, this appends --csv <file> to the score_sleep.py subprocess, but score_sleep.py's argparse setup does not register a --csv argument. That makes every sleep-score subprocess exit with an argparse error, sleep_metrics() returns None, and the daily summary is silently rebuilt with zero nights.

Useful? React with 👍 / 👎.

@Th0rgal

Th0rgal commented Jul 8, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9cc3170050

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread tools/fit_sleep_score.py
Comment thread tools/build_daily.py Outdated

print(f"daily_summary: {len(rows)} night(s) written to {db.name}")
for d in rows:
rb = f"hrvσ={d['hrv_avg']:.0f}/{d['hrv_baseline']:.0f}" if d['hrv_baseline'] else f"hrv={d['hrv_avg']:.0f}/—"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Guard daily-summary printing for missing HRV

If a synced night has no decoded HRV/IBI-derived hr_bpm data, hrv_avg remains None (the code explicitly stores None when rmssd is empty), but this status line formats it with :.0f. In that valid sparse-data case the script writes daily_summary and then exits with TypeError, making tools/build_daily.py look failed and breaking callers that require a zero exit status.

Useful? React with 👍 / 👎.

@Th0rgal

Th0rgal commented Jul 8, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fe27a2db48

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/oura-link/src/client.rs
@Th0rgal

Th0rgal commented Jul 8, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@Th0rgal

Th0rgal commented Jul 11, 2026

Copy link
Copy Markdown
Owner Author

@codex review for serious correctness regressions in Ring 5 history-envelope decoding, cursor checkpointing, and the physical regression-vector behavior.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e3ca846981

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

/// gives no progress reporting. A few thousand events (~1 min of transfer) keeps
/// the per-batch round-trip overhead negligible while bounding what a drop can
/// lose and yielding regular `bytes_left` progress updates.
const EXT_BATCH_MAX_EVENTS: u16 = 4096;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve same-tick events at capped batch boundaries

With Ring 5 extended sync this new 4096-event cap makes cursor checkpointing lossy when the ring cuts a batch between events that share the same decisecond. The physical 0x7e/0x7f real-step pairs in the added regression vector are only 1 ms apart, so if 0x7e is the last event in a capped batch, validate_batch advances to max_ts + 1 and the next ExtGetEvent starts at (T+1)*100 ms, skipping the 0x7f event at T*100+1 ms before it is ever fetched. Either avoid partial extended batches or checkpoint with enough precision/overlap to include the rest of the current decisecond.

Useful? React with 👍 / 👎.

Comment thread tools/epoch_time.py

def build_epochs(pairs):
"""pairs: iterable of (ds, captured_unix). Returns list of [min_ds, max_ds, anchor_unix]."""
order = sorted((cu, ds) for ds, cu in pairs)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep insertion order when splitting ring epochs

For reboot detection this helper needs the real sync order, but sorting by (captured_unix, ds) reorders all rows captured in the same second by timestamp. In a physical drain with thousands of rows sharing captured_unix, a reboot reset places the low post-reboot ds values before the high pre-reboot values, so the backward jump is hidden and the model runners merge distinct boot epochs under one wall-clock anchor. Include the SQLite insertion id/order (or accept an already captured_unix, id ordered stream) instead of sorting ties by ds.

Useful? React with 👍 / 👎.

@Th0rgal

Th0rgal commented Jul 11, 2026

Copy link
Copy Markdown
Owner Author

@codex review

Fixes the exact iOS Ring 5 sync trace in c6674ed:

  • ExtGetEvent no longer sends the legacy GetEvent ACK that caused late history frames to race with and be discarded by the next data flush
  • non-zero extended result codes, including the observed 0xff frame, are no longer treated as successful empty completion
  • regression tests use the exact 0xff frame and assert that extended drains never emit a 0x10 ACK

Validated with cargo test --workspace and clippy -D warnings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c6674ede0d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

5. SyncTime using the app-style counter packet (`12 09 ... f6`) where supported.
6. (optional) firmware / product / battery for metadata.
7. DataFlush, then drain history events from the persisted cursor; persist each
event, ack with `GetEvent(max_events=0)`, and advance the cursor; stop when

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restrict ACK guidance to legacy event drains

When a client follows this minimal recipe on Ring 5 with the extended ExtGetEvent path, it will still send the legacy GetEvent(max_events=0) ACK after persisting events. That is the ACK pattern the implementation now avoids because it can make late history frames race with the next flush; the documentation should distinguish that only the legacy GetEvent path is ACKed, matching the !use_extended guard in OuraClient::drain_events.

Useful? React with 👍 / 👎.

Readers (iOS models, summary_json) and the per-event sync writer share
oura.db; the default rollback journal made every autocommit insert take an
exclusive lock, so a concurrent whole-table read could come back truncated
or kill the writer. WAL gives readers a consistent snapshot during the
drain, and a 5 s busy_timeout absorbs the remaining contention. The pragma
failure is tolerated for the read-only seed DB bundled with the iOS app.
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