You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Thanks for the report! Your respiratory data isn't lost — it's all still there in the Respiratory screen (your last reading was 22 Jul). What's happening is that recent nights aren't producing a new number: respiratory rate is worked out from your sleep, so it needs a good night of data to show up.
To help me see why your recent nights aren't giving one, could you send a diagnostic export? Settings → Advanced → (strap log / diagnostics), then paste or attach it here. That'll show whether it's a data thing or something on our side.
A couple of quick questions too: which strap (4.0 / 5.0 / Oura), and are you wearing it for full nights? Leaving this open until we get the log.
Thanks for the log, @znkandrei — it pins the cause, and it's not a data/wear problem (so my earlier "needs a good night of data" was off the mark; apologies).
What the log shows
Your recent nights are fully captured and sleep IS detected:
Night 2026-08-15: hr=26380 rr=75874 resp=26380 — HR, R-R and resp samples all present.
[sleep] sleepProvenance provenance=measured hoursAsleep=364 — the night staged fine.
What fails is the respiratory rate derivation: REM-funnel: … resp=ABSENT … noRespBar=313, and recovery drops it (nilTerm dropped=[resp,…]). Sleep staging itself is fine — the funnel has a no-resp fallback and still produced a hypnogram; only the respiratory number is missing.
Root cause — a V10 regression, tied to the WHOOP 4.0 R-R over-count
Respiration is derived by RSA from the R-R stream. In V10, #1127 added a gate that returns no-data for any R-R stream that isn't beat-accurate (written for Oura's banked overnight IBI). But WHOOP 4.0's R-R is over-counted — the #1008 artifact stamps several R-R in one coarse second — which is visible right in your log: rr=75874 against hr=26380, ~2.9× more R-R than heartbeats. That over-count is timing-indistinguishable from a banked stream, so #1127's gate catches it → RSA returns no-data → resp=ABSENT → the blank you're seeing. That's exactly why it started "the moment I switched to V10."
This is not a bug in #1127 — its gate is correct for Oura, and it correctly refuses a stream RSA can't read. The catch is that WHOOP 4.0's over-count trips the same wire. (cc the #1127 gate — /ref #1127.)
The fix
This is the same over-count behind #1008 (HRV ≠ WHOOP) and #1118 (the HRV "unverified" caveat) — and the correct fix is the same one: the two-channel R-R de-dup. De-duped beats are beat-accurate, so they pass #1127's gate legitimately and RSA runs, restoring respiratory rate. So the de-dup now fixes three symptoms at once (HRV mismatch, HRV over-count, respiratory blank), which makes it the clear next priority; it'll be validated against ground-truth R-R (there's a WHOOP-4.0-plus-Polar-H10 dataset on #1118) rather than shipped on a guess.
A note on the interim: I'd steer away from just un-gating 4.0 — RSA rebuilds beat-time from the R-R values, so the duplicated beats would read the rate low, i.e. a wrong number rather than a blank. #1332's "carry last-known respiratory" is the honest stopgap until the de-dup lands.
The latest 10.1 log (thanks @znkandrei) confirms the open facet is unchanged, and pins down why the current shadow de-dup can't close it.
Same-second collapse is structurally insufficient — the over-count is cross-second. Every night reads crossSecondOverCount, and #1352's shadow (exact 0 ms + 40 ms same-second collapses) leaves the heavy nights over-counted: cov40 stays ~1.7–2.0, beatAccurate ~0.46, so respiration stays blanked by #1127's 0.5 gate. The hrv rrsample dump shows the duplicate twins straddling the second boundary (-1s[500,500,1048] 0s[477,477,477,500,500,…]), which a same-second collapse can't reach by construction.
Per-night, the 40 ms same-second collapse restores the milder nights but not the heavy ones:
night
coverage → cov40
beatAccurate → beatAcc40
resp
08-08
1.79 → 1.48
0.50 → 0.60
✅ clears 0.5
08-14
1.80 → 1.35
0.46 → 0.62
✅
08-13
2.04 → 1.51
0.43 → 0.55
✅
08-15
2.70 → 1.95
0.36 → 0.46
❌ still blank
08-12
2.49 → 1.72
0.33 → 0.46
❌ still blank
"Two-channel de-dup" doesn't apply to WHOOP — worth flagging before it's built. RRSourceChannel is an Oura-only concept (0x80 green / 0x6E spo2 / …). The WHOOP R-R decode reads a single rr_intervals array per record and writes srcChannel = nil (the log's src=none) — both optical channels' IBIs are already merged into that one field and aren't separable at decode. So there's no channel to drop; the de-dup has to be value/time-based.
#1399 sizes the cross-second component. It adds a third shadow candidate, xsec — the 40 ms collapse widened to a 1-second window — logged beside the others (xsecN/covXsec/beatAccXsec), so a capture shows whether a cross-second collapse drives coverage → ~1.0 and clears the 0.5 gate. Instrumentation only, and a strict upper bound — deliberately not shippable: a steady real HR has ~identical intervals one second apart, so it over-merges real beats (the test pins a 10-beat steady stream dropping to 5 at window 1).
The real fix is density/timeline-based (keep beats so each ts-gap matches its own interval) and needs validating against ground truth (@artemc's H10, #1118) before it becomes the read path — xsec gives that validation its target.
The WHOOP over-count looks like two transports of one beat train, not two optical channels
Following up on the 5.0/MG numbers I posted above, and on this from 17 Aug:
"Two-channel de-dup" doesn't apply to WHOOP … RRSourceChannel is an Oura-only concept (0x80 green / 0x6E spo2 / …). The WHOOP R-R decode reads a single rr_intervals array per record … both optical channels' IBIs are already merged into that one field and aren't separable at decode.
That is right about optical channels, and I am not disputing it — a WHOOP merges those before we ever see them. But there is a second axis. The strap emits the beat train live over 0x2A37and replays the same beats inside its historical records, and those two arrive on different code paths. That is separable at decode, because the writer knows which path it is on.
What the field data shows
WHOOP 5.0/MG, firmware 50.35.1.0, iOS, continuous wear. A transport tag (whoopStandardBLE / whoopRealtime / whoopHistorical) was added on 21 Aug, and the scoring read now prefers history where it exists and falls back to live where it does not.
night
coverage
collapsedCov
beatAccurate
rrIntegrity
SDNN
08-12
1.84
1.36
0.46
crossSecondOverCount
withheld
08-16
1.84
1.23
0.47
crossSecondOverCount
withheld
08-17
1.81
1.28
0.46
crossSecondOverCount
withheld
08-21
1.78
1.23
0.44
crossSecondOverCount
withheld
08-22
0.82
0.78
0.88
underCovered
73 ms
08-23
0.92
0.84
0.85
plausible
65 ms
08-22 is the first night scored with tagged rows.
Three things in the same log that point at transport rather than value
1. Every stored second carries exactly two rows. The delivery histogram for 08-13:
95% of occupied seconds hold two R-R rows at meanNN=898 ms (~67 bpm), where ~1.1 is the physical ceiling. multiMs=97% says the two values differ, which is why exact de-dup only removes 99 of 60,611 rows.
2. The historical path alone is not doubling. Its own emission line stays at roughly one interval per stamped second:
3. src=none is doing more work than it looks. In densestSecondWindowSample that field is computed over the whole night's stream, not the sampled second — it prints none only when no beat anywhere in the night carries a channel code. So on every over-counted night, every row was untagged, and the scoring read's deliberate "keep NULL" branch passed both copies straight through. That is the mechanism, and it also explains why the fix could only ever help nights banked after the tagging landed.
Why it presents as cross-second
History is stamped from the record's embedded strap second; live is stamped at phone receive time. A public HCI capture (verified in #1505) puts the same beat 1 or 2 s apart, and a later field log shows +3 s. So the twins land in different seconds by construction — which is exactly the shape you described:
-1s[500,500,1048] 0s[477,477,477,500,500,…]
A same-second collapse cannot reach them, and a 1-second value collapse (xsec) over-merges real beats because a steady HR has near-identical intervals one second apart. Both of those follow from the twins being time-shifted rather than value-duplicated.
What I have NOT established
The before/after is real, but the log cannot yet prove the cause, and I would rather say so than claim the fix:
The build that produced it tags rows but does not print a per-night census, so "suppression removed the second copy" is inference, not measurement.
08-19 is a clean night in the untagged era — coverage 1.05, beatAccurate 0.81, plausible, and only 34,052 beats against 44–60k on its neighbours. A night where the phone banked few live rows reads clean without any fix. So thin live coverage is a live alternative explanation for 08-22/23 that this log does not exclude.
What settles it is a per-night count of stored rows by transport plus how many live rows the selector suppressed. That is cheap and I will post one from a night's wear.
If it holds
It is a decode-side change rather than a de-dup heuristic, it needs no ground truth to validate, and it should make both generations beat-accurate from one change — which would clear #1127's 0.5 gate and restore respiration. Happy to be wrong about the mechanism; the census will say.
Correction to my comment above: that mechanism is iOS-only
@Zebsi235's data on #1505 sent me back to the storage paths, and I scoped my last comment too broadly. The transport duplication I described is real, but it cannot be what most of this thread is seeing.
The standard 0x2A37 copy reaches the durable store on iOS and not on Android.
iOS:Collector.ingestStandardHR buffers an RRInterval for every plausible standard R-R, and flushStandardHR persists it via store.insert(Streams(hr:rr:), deviceId:). So the live copy and the historical replay of the same beats both land.
Android:WhoopBleClient.kt:6631 sends it to _state.update { it.withRRIntervals(rr) }, and withRRIntervals (:234) writes rrRecent — a bounded 60-item rolling buffer for the Live console. insertRr has one caller, WhoopRepository.kt:501, fed from streams.rr, which the standard profile is not part of.
(The custom REALTIME_DATA type-40 frames do feed streams.rr on both platforms when realtime streaming is on. The asymmetry is specifically the standard profile.)
What this means for the fix
Transport tagging cannot help Android. There is no second transport in the store to prefer between. @znkandrei's 4.0 numbers, @Zebsi235's 70k pairs and the original report are all Android, so the mechanism I posted does not explain them.
It is still the right fix for iOS, and the before/after I posted stands as an iOS result — with the caveat I already gave, that thin live coverage is not yet excluded as an alternative reading of those two nights.
The two platforms may be two different bugs wearing one symptom. 2.9× on Android 4.0, 1.8× on iOS 5.0/MG. I had been reading those as one cause at different severities. That was an assumption, and the storage paths do not support it.
Where the Android half probably lives
@Zebsi235's pairs are historical-against-historical by construction, which makes them a clean measurement. Against the public capture measured on #1451 — 14,171 of 18,602 v18 records carry exactly one interval, only 365 carry two or more — a second holding two ord 0 rows is two records stamped onto the same timestamp, not one record decoding into two intervals.
So on Android the over-count looks like a timestamp-assignment problem rather than a duplication one, which also fits the crossSecondOverCount verdict and your finding that a same-second collapse cannot reach it. The measurement I would want is: for seconds holding two ord 0 rows, how far apart were the two records' embedded timestamps before rounding put them in the same second.
Sorry for the over-claim. The iOS result is unchanged; the generalisation was mine and it was wrong.
Field data for the open facet here — the blank respiration, not the display-carry half that #1362 settled. From a strap log on Android (app 10.6.1, WHOOP 4.0, fw 41.17.6.0), 21 days.
@gdorgian — this may be useful against your 28 Aug correction, because it is on the platform your narrowing leaves unexplained.
The gate refuses essentially every night
Of the 21 days with a verdict, 3 are simply dataless (that phone's 5.0 has not synced in 2 days) and never reach the gate. Of the 18 that do reach it, 17 are refused:
beatAccurate
days
0.44
4
0.45
5
0.46
5
0.47
2
0.48
1
The gate is 0.50. The whole population sits underneath it — this is not a tail of poor nights being correctly rejected, it is the distribution living below the threshold. Every refusal carries the same reason: rrIntegrity=crossSecondOverCount.
The over-count, quantified
The interesting number is not beatAccurate, it is coverage, and it is remarkably stable:
Across all 18 nights coverage runs 1.58 – 1.83 — 58% to 83% more R-R intervals than there are seconds of recording — and rejected=0% throughout, so nothing is being discarded before the count. That is the over-count in one number, and it never once lands near 1.0.
Why this is Android-specific evidence
Your 28 Aug comment established that the standard 0x2A37 copy reaches the durable store on iOS but not on Android — withRRIntervals writes only the bounded rrRecent ring for the Live console. This log is Android, and the over-count is still 1.58–1.83 every night. So whatever mechanism is doubling beats here, it is not the live/historical transport duplication, which supports your narrowing rather than contradicting it.
One anomaly worth a second look
The single night that passed did not have the lowest coverage:
coverage=1.61 is mid-range, and nights at 1.58 and 1.60 scored 0.45. What that night does have is the lowest dupBeats (134 against 199–276 elsewhere). So beatAccurate is not tracking coverage alone, and dupBeats looks like the stronger lever. If the over-count has more than one component, that is where the split may show.
Not proposing a threshold change
Worth stating plainly since the numbers invite it: I do not think the answer is moving 0.50. The gate is behaving correctly on the input it is given — the input is wrong, and lowering the bar would ship a respiration number computed from a beat train we know is over-counted. This belongs with #1118.
Happy to pull anything else out of the log — per-night rr=offered/inserted, the full dupBeats series, or the same for the 5.0 once it syncs.
Activity
fix(today): carry last-known respiratory + log why it's blank (#1331)
fix(today): carry RECENT respiratory + log why it's blank (#1331)
fix(today): iOS parity for the respiratory carry + diag (#1331)
fix(today): carry last-known respiratory + diagnostic logging (#1331) (…
ryanbr commentedon Aug 15, 2026
Thanks for the report! Your respiratory data isn't lost — it's all still there in the Respiratory screen (your last reading was 22 Jul). What's happening is that recent nights aren't producing a new number: respiratory rate is worked out from your sleep, so it needs a good night of data to show up.
To help me see why your recent nights aren't giving one, could you send a diagnostic export? Settings → Advanced → (strap log / diagnostics), then paste or attach it here. That'll show whether it's a data thing or something on our side.
A couple of quick questions too: which strap (4.0 / 5.0 / Oura), and are you wearing it for full nights? Leaving this open until we get the log.
znkandrei commentedon Aug 15, 2026
Hello, i have the same issue. Whoop4 and respiratory stopped showing me data since 11 aug. the moment i switch to V10.
noop-strap-log-260815-1005.txt
ryanbr commentedon Aug 15, 2026
Thanks for the log, @znkandrei — it pins the cause, and it's not a data/wear problem (so my earlier "needs a good night of data" was off the mark; apologies).
What the log shows
Your recent nights are fully captured and sleep IS detected:
Night 2026-08-15: hr=26380 rr=75874 resp=26380— HR, R-R and resp samples all present.[sleep] sleepProvenance provenance=measured hoursAsleep=364— the night staged fine.What fails is the respiratory rate derivation:
REM-funnel: … resp=ABSENT … noRespBar=313, and recovery drops it (nilTerm dropped=[resp,…]). Sleep staging itself is fine — the funnel has a no-resp fallback and still produced a hypnogram; only the respiratory number is missing.Root cause — a V10 regression, tied to the WHOOP 4.0 R-R over-count
Respiration is derived by RSA from the R-R stream. In V10, #1127 added a gate that returns no-data for any R-R stream that isn't beat-accurate (written for Oura's banked overnight IBI). But WHOOP 4.0's R-R is over-counted — the #1008 artifact stamps several R-R in one coarse second — which is visible right in your log:
rr=75874againsthr=26380, ~2.9× more R-R than heartbeats. That over-count is timing-indistinguishable from a banked stream, so #1127's gate catches it → RSA returns no-data →resp=ABSENT→ the blank you're seeing. That's exactly why it started "the moment I switched to V10."This is not a bug in #1127 — its gate is correct for Oura, and it correctly refuses a stream RSA can't read. The catch is that WHOOP 4.0's over-count trips the same wire. (cc the #1127 gate — /ref #1127.)
The fix
This is the same over-count behind #1008 (HRV ≠ WHOOP) and #1118 (the HRV "unverified" caveat) — and the correct fix is the same one: the two-channel R-R de-dup. De-duped beats are beat-accurate, so they pass #1127's gate legitimately and RSA runs, restoring respiratory rate. So the de-dup now fixes three symptoms at once (HRV mismatch, HRV over-count, respiratory blank), which makes it the clear next priority; it'll be validated against ground-truth R-R (there's a WHOOP-4.0-plus-Polar-H10 dataset on #1118) rather than shipped on a guess.
A note on the interim: I'd steer away from just un-gating 4.0 — RSA rebuilds beat-time from the R-R values, so the duplicated beats would read the rate low, i.e. a wrong number rather than a blank. #1332's "carry last-known respiratory" is the honest stopgap until the de-dup lands.
Leaving open to track the de-dup fix.
diag(hrv): log an exact-dup reference beside the ~40ms de-dup candida…
znkandrei commentedon Aug 15, 2026
Thanks for your explication, let me know if i can help you with other logs for Whoop 4 and iOS. Thanks!
diag(hrv): shadow-log a deduped R-R stream to validate the over-count…
ryanbr commentedon Aug 15, 2026
If you can get update logs (or multiple log files of the issue), in the
10.1.0newest staging builds. It'll help debug it furtherMerge upstream ryanbr/noop through 10.1.0
fix(today): stop the classic dashboard printing a number it can't sta…
fix(today): bound the Android classic-dashboard respiratory carry + h…
fix(today): bound the classic dashboard's vital carry + honor °C/°F o…
5 remaining items
fix(today): bound the classic dashboard's vital carry + honor °C/°F o…
fix(ui): respiratory rate is an on-device estimate on BOTH gens, not …
diag(hrv): size the CROSS-second R-R over-count with a windowed shado…
ryanbr commentedon Aug 17, 2026
The latest 10.1 log (thanks @znkandrei) confirms the open facet is unchanged, and pins down why the current shadow de-dup can't close it.
Same-second collapse is structurally insufficient — the over-count is cross-second. Every night reads
crossSecondOverCount, and #1352's shadow (exact 0 ms + 40 ms same-second collapses) leaves the heavy nights over-counted:cov40stays ~1.7–2.0,beatAccurate~0.46, so respiration stays blanked by #1127's 0.5 gate. Thehrv rrsampledump shows the duplicate twins straddling the second boundary (-1s[500,500,1048] 0s[477,477,477,500,500,…]), which a same-second collapse can't reach by construction.Per-night, the 40 ms same-second collapse restores the milder nights but not the heavy ones:
"Two-channel de-dup" doesn't apply to WHOOP — worth flagging before it's built.
RRSourceChannelis an Oura-only concept (0x80 green / 0x6E spo2 / …). The WHOOP R-R decode reads a singlerr_intervalsarray per record and writessrcChannel = nil(the log'ssrc=none) — both optical channels' IBIs are already merged into that one field and aren't separable at decode. So there's no channel to drop; the de-dup has to be value/time-based.#1399 sizes the cross-second component. It adds a third shadow candidate,
xsec— the 40 ms collapse widened to a 1-second window — logged beside the others (xsecN/covXsec/beatAccXsec), so a capture shows whether a cross-second collapse drives coverage → ~1.0 and clears the 0.5 gate. Instrumentation only, and a strict upper bound — deliberately not shippable: a steady real HR has ~identical intervals one second apart, so it over-merges real beats (the test pins a 10-beat steady stream dropping to 5 at window 1).The real fix is density/timeline-based (keep beats so each ts-gap matches its own interval) and needs validating against ground truth (@artemc's H10, #1118) before it becomes the read path —
xsecgives that validation its target.diag(hrv): size the CROSS-second R-R over-count with a windowed shado…
Count deliveries per stored second across a whole night (#1331/#1008)
Retire two superseded R-R shadow measurements (#1331/#1118)
diagnostics: add the Kotlin NaN test that never made it onto the branch
gdorgian commentedon Aug 27, 2026
The WHOOP over-count looks like two transports of one beat train, not two optical channels
Following up on the 5.0/MG numbers I posted above, and on this from 17 Aug:
That is right about optical channels, and I am not disputing it — a WHOOP merges those before we ever see them. But there is a second axis. The strap emits the beat train live over 0x2A37 and replays the same beats inside its historical records, and those two arrive on different code paths. That is separable at decode, because the writer knows which path it is on.
What the field data shows
WHOOP 5.0/MG, firmware 50.35.1.0, iOS, continuous wear. A transport tag (
whoopStandardBLE/whoopRealtime/whoopHistorical) was added on 21 Aug, and the scoring read now prefers history where it exists and falls back to live where it does not.08-22 is the first night scored with tagged rows.
Three things in the same log that point at transport rather than value
1. Every stored second carries exactly two rows. The delivery histogram for 08-13:
95% of occupied seconds hold two R-R rows at
meanNN=898 ms(~67 bpm), where ~1.1 is the physical ceiling.multiMs=97%says the two values differ, which is why exact de-dup only removes 99 of 60,611 rows.2. The historical path alone is not doubling. Its own emission line stays at roughly one interval per stamped second:
So the second copy comes from somewhere else.
3.
src=noneis doing more work than it looks. IndensestSecondWindowSamplethat field is computed over the whole night's stream, not the sampled second — it printsnoneonly when no beat anywhere in the night carries a channel code. So on every over-counted night, every row was untagged, and the scoring read's deliberate "keep NULL" branch passed both copies straight through. That is the mechanism, and it also explains why the fix could only ever help nights banked after the tagging landed.Why it presents as cross-second
History is stamped from the record's embedded strap second; live is stamped at phone receive time. A public HCI capture (verified in #1505) puts the same beat 1 or 2 s apart, and a later field log shows +3 s. So the twins land in different seconds by construction — which is exactly the shape you described:
A same-second collapse cannot reach them, and a 1-second value collapse (
xsec) over-merges real beats because a steady HR has near-identical intervals one second apart. Both of those follow from the twins being time-shifted rather than value-duplicated.What I have NOT established
The before/after is real, but the log cannot yet prove the cause, and I would rather say so than claim the fix:
plausible, and only 34,052 beats against 44–60k on its neighbours. A night where the phone banked few live rows reads clean without any fix. So thin live coverage is a live alternative explanation for 08-22/23 that this log does not exclude.What settles it is a per-night count of stored rows by transport plus how many live rows the selector suppressed. That is cheap and I will post one from a night's wear.
If it holds
It is a decode-side change rather than a de-dup heuristic, it needs no ground truth to validate, and it should make both generations beat-accurate from one change — which would clear #1127's 0.5 gate and restore respiration. Happy to be wrong about the mechanism; the census will say.
hrv: multiMs overflows a 32-bit Int, so the R-R delivery census repor…
hrv: state the real pct overflow bound, and pin that the wrap could l…
gdorgian commentedon Aug 28, 2026
Correction to my comment above: that mechanism is iOS-only
@Zebsi235's data on #1505 sent me back to the storage paths, and I scoped my last comment too broadly. The transport duplication I described is real, but it cannot be what most of this thread is seeing.
The standard
0x2A37copy reaches the durable store on iOS and not on Android.Collector.ingestStandardHRbuffers anRRIntervalfor every plausible standard R-R, andflushStandardHRpersists it viastore.insert(Streams(hr:rr:), deviceId:). So the live copy and the historical replay of the same beats both land.WhoopBleClient.kt:6631sends it to_state.update { it.withRRIntervals(rr) }, andwithRRIntervals(:234) writesrrRecent— a bounded 60-item rolling buffer for the Live console.insertRrhas one caller,WhoopRepository.kt:501, fed fromstreams.rr, which the standard profile is not part of.(The custom
REALTIME_DATAtype-40 frames do feedstreams.rron both platforms when realtime streaming is on. The asymmetry is specifically the standard profile.)What this means for the fix
Transport tagging cannot help Android. There is no second transport in the store to prefer between. @znkandrei's 4.0 numbers, @Zebsi235's 70k pairs and the original report are all Android, so the mechanism I posted does not explain them.
It is still the right fix for iOS, and the before/after I posted stands as an iOS result — with the caveat I already gave, that thin live coverage is not yet excluded as an alternative reading of those two nights.
The two platforms may be two different bugs wearing one symptom. 2.9× on Android 4.0, 1.8× on iOS 5.0/MG. I had been reading those as one cause at different severities. That was an assumption, and the storage paths do not support it.
Where the Android half probably lives
@Zebsi235's pairs are historical-against-historical by construction, which makes them a clean measurement. Against the public capture measured on #1451 — 14,171 of 18,602 v18 records carry exactly one interval, only 365 carry two or more — a second holding two
ord 0rows is two records stamped onto the same timestamp, not one record decoding into two intervals.So on Android the over-count looks like a timestamp-assignment problem rather than a duplication one, which also fits the
crossSecondOverCountverdict and your finding that a same-second collapse cannot reach it. The measurement I would want is: for seconds holding twoord 0rows, how far apart were the two records' embedded timestamps before rounding put them in the same second.Sorry for the over-claim. The iOS result is unchanged; the generalisation was mine and it was wrong.
ryanbr commentedon Aug 28, 2026
Field data for the open facet here — the blank respiration, not the display-carry half that #1362 settled. From a strap log on Android (app 10.6.1, WHOOP 4.0, fw 41.17.6.0), 21 days.
@gdorgian — this may be useful against your 28 Aug correction, because it is on the platform your narrowing leaves unexplained.
The gate refuses essentially every night
Of the 21 days with a verdict, 3 are simply dataless (that phone's 5.0 has not synced in 2 days) and never reach the gate. Of the 18 that do reach it, 17 are refused:
The gate is
0.50. The whole population sits underneath it — this is not a tail of poor nights being correctly rejected, it is the distribution living below the threshold. Every refusal carries the same reason:rrIntegrity=crossSecondOverCount.The over-count, quantified
The interesting number is not
beatAccurate, it iscoverage, and it is remarkably stable:Across all 18 nights
coverageruns 1.58 – 1.83 — 58% to 83% more R-R intervals than there are seconds of recording — andrejected=0%throughout, so nothing is being discarded before the count. That is the over-count in one number, and it never once lands near 1.0.Why this is Android-specific evidence
Your 28 Aug comment established that the standard
0x2A37copy reaches the durable store on iOS but not on Android —withRRIntervalswrites only the boundedrrRecentring for the Live console. This log is Android, and the over-count is still 1.58–1.83 every night. So whatever mechanism is doubling beats here, it is not the live/historical transport duplication, which supports your narrowing rather than contradicting it.One anomaly worth a second look
The single night that passed did not have the lowest coverage:
coverage=1.61is mid-range, and nights at 1.58 and 1.60 scored 0.45. What that night does have is the lowestdupBeats(134 against 199–276 elsewhere). SobeatAccurateis not tracking coverage alone, anddupBeatslooks like the stronger lever. If the over-count has more than one component, that is where the split may show.Not proposing a threshold change
Worth stating plainly since the numbers invite it: I do not think the answer is moving 0.50. The gate is behaving correctly on the input it is given — the input is wrong, and lowering the bar would ship a respiration number computed from a beat train we know is over-counted. This belongs with #1118.
Happy to pull anything else out of the log — per-night
rr=offered/inserted, the fulldupBeatsseries, or the same for the 5.0 once it syncs.today: the Apple half of the per-field HRV / Resting HR carry
today: per-field HRV / Resting HR carry on both platforms, and stop p…