NOOP AI 9.3.2 DX Beta
NOOP AI 9.3.2 DX Beta
A measurement-correctness release. Three separate faults were feeding wrong numbers into HRV — one
introduced by this fork, two found upstream in the Oura path — and all three are fixed here. On top of
that: the fork's own untranslated strings are done, and 60-odd upstream commits land, including the
sleep-edit date fix people have been asking for since ryanbr#970.
Fixed — measurement correctness
- Same-second heartbeats are read in emission order again (ryanbr#830). Swift's
sorted(by:)is not a
stable sort, so re-sorting R-R intervals by timestamp was free to reorder beats sharing a second
into value order. RMSSD is the sum of successive differences, so that reordering changes the
result directly — on the reference case, 34.85 ms read as 12.72 ms. Upstream fixed this in
34c66b66; this fork reverted it at six call sites during the Today redesign, and the 9.3.1 merge
resolved the conflict in favour of the reverted side, so it stayed broken here after being correct
upstream. Restored inSleepStager,SleepStagerV2,AnalyticsEngine(both deep-sleep HRV
windows),HRVFreqDomainandResonanceEngine. If you wear a WHOOP 5.0/MG, whose R-R is dense
enough for several beats to share a second, your nightly HRV and the Charge derived from it were
affected. - An Oura ring's beats are no longer stored twice (ryanbr#1071). The ring reports the same heartbeat on
more than one optical channel and every one was kept, so an unfiltered read returned ~2.06x the
beats the measured heart-rate curve allows. Mean rate and resting HR survive that; RMSSD and a
~200 ms nocturnal SDNN (against a 40–100 ms physiological range) do not. Scoring now reads one
channel; the other stays on disk as the cross-check. - An Oura record's beats keep their order (ryanbr#1072). The Oura path enqueued one event per beat, so
the batch-local ordering counter was recomputed as 0 for every row and the emission order was never
recorded at all — the storage-side root cause of the same problem. Records are handed to the store
whole. - SpO2 samples stop colliding (ryanbr#1070). 12 of every 13 samples shared a primary key and were
discarded. - A capture known to be over-counted no longer reports SDNN (ryanbr#1085). SDNN is a spread over every
interval, so a double-counted night inflates it directly. The nightly diagnostic now withholds the
figure and says why, rather than printing a number beside the verdict that it cannot be trusted.
RMSSD and mean rate are unaffected and still shown. - A night missing beats stops reading as clean (ryanbr#977). The coverage classifier had a ceiling but
no floor.
Fixed — sleep
- Sleep edits preserve the wake calendar date (ryanbr#970/ryanbr#1059). Correcting a late-tracked night's bed
time kept the calendar date, so the "corrected" bed could land on the coming evening — a
future-dated night that staged as all-awake and blanked the Sleep tab. Three layered guards now:
a cross-midnight bed auto-correct, an explicit confirm when the corrected window has no recorded
data to stage from, and a persistence clamp that refuses an inverted or future-ending window. - Naps count toward sleep debt (ryanbr#1041). A separately-recorded nap repays debt with its actual
asleep minutes, including for days already in your history. Rest and the sleep headline still
describe the main night only. - Unmeasured respiration is represented explicitly (ryanbr#847) rather than implied by a zero.
Fixed — this fork
- Coach settings opened from More poisoned the tab. The screen built its own navigation stack
inside the one More already owns, so SwiftUI updated two navigation authorities in the same frame
and the outer path stopped accepting pushes: after visiting Coach settings, no other More row
navigated until you left the tab and came back. It now reuses More's stack; the standalone sheet
path from the chat is unchanged. - The fork's own strings are translated. 25 strings in the design system — the chart colour
styles (Aurora, Forest, Sunset, Titanium, Apple Health), the sleep-editor labels, the day
navigation — were extracted but never translated, so German, Spanish, French and Portuguese users
saw English there. All four locales are now complete across every catalog. Two Watch entries that
were only punctuation and the product name are marked non-translatable instead of being given fake
translations.
Changed
- Today is denser on both shells. Tighter card padding, gutters and corner radii, and a smaller
header cluster — the visible circles shrink to 34pt while their 36pt hit targets stay exactly as
they were, so nothing became harder to tap. More of the screen's content sits above the fold. - Activity-file steps add up across a day (ryanbr#1058). A second file imported for the same day
overwrote the day's step count instead of adding to it. - iOS widgets show Charge, Effort and Rest as score rings (ryanbr#1022).
- Low Power Mode stops everything decorative (ryanbr#909), including the tilt sensor.
- WHOOP MG ECG packets are decoded (ryanbr#896), with a hand-run turn-on probe that is default OFF.
Unvalidated instrumentation for protocol research — not a measurement, and not a diagnosis. - The Live Activity widget anchor and the HR-zone set are memoized across live-HR ticks
(ryanbr#1091/ryanbr#1092), so a connected strap does less work per beat. - Version identity is 9.3.2 DX Beta (build 217; Android
versionCode309).
Verification
swift test: WhoopProtocol 531, WhoopStore 362, OuraProtocol 168, StrandAnalytics 1259 — all green.xcodebuildof both app targets —Strand(macOS) andNOOPiOS(iOS) — both BUILD SUCCEEDED.python3 Tools/i18n_audit.py --ciexits 0: all four String Catalogs complete in de/es/fr/pt-PT,
the non-focus ratchet unchanged.- Not verified on hardware. Nothing here touches the CoreBluetooth connection path, but the HRV
and Oura fixes change numbers derived from a real capture — the honest check is to sync a strap or
ring and compare a night's HRV against what 9.3.1 reported for the same night. - Translation provenance: German is first-hand; 15 of the 25 new strings reuse wording already
shipping elsewhere in the app. The remaining new terms in es/fr/pt-PT/it/ru/zh are considered but
not native-speaker reviewed.
Thanks
Nearly everything in the "measurement correctness" section above came from outside this fork:
- @pipiche38 — the entire Oura beat-duplication investigation and its fixes (ryanbr#1071, ryanbr#1072, ryanbr#1070,
ryanbr#1084) plus the SDNN-withholding follow-through (ryanbr#1085). The R-R double-count was found, root-caused
and fixed by them. - @Creanathan — reported and fixed the sleep-edit wake-date bug (ryanbr#970, ryanbr#1059).
- @vishk23 — the coverage-classifier floor (ryanbr#977), unmeasured respiration (ryanbr#847) and the MG ECG
decoding (ryanbr#896). - @SpurgtBror — naps toward sleep debt, reported and fixed (ryanbr#1041, ryanbr#1042).
- @akeschmidi — the iOS widget score rings (ryanbr#1022).
- @whisp0 — foreground-service analytics caching (ryanbr#1051).
- @tigercraft4 — custom AI gateway auth (ryanbr#765, ryanbr#766).
- @theLastOfCats — reported the Coach settings UI and DeepSeek error handling (ryanbr#1074).
- @Quaii — iOS widgets showing only sample data (ryanbr#887).
- @digitalerdude, @TAKEOFF69, @ben-tsk, @rube-de — Android, CI and documentation
fixes carried in this merge.
And to ryanbr/noop, whose 9.3.1 line this builds on.
Install
- iOS: add the AltStore/SideStore source, or download
NOOP-ios-unsigned-v9.3.2-dx-beta.ipaand
sign it yourself with your own free Apple ID. - macOS: unzip
NOOP-macos-v9.3.2-dx-beta.zip, then right-click → Open the first time — the
bundle is ad-hoc signed, not notarised. macOS may ask you to re-approve Bluetooth after an update,
because the code identity changes with every build.