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
The baseline on top of the charge page says 57. At the bottom it is 48. This is rather my HRV. Could be a coincidence?
What you expected instead
Same value.
Steps to reproduce
Go to charge screen
Strap log
Before you submit
I attached my exported NOOP .zip (not a screenshot of the Live screen). If my bug is a clearly reproducible UI issue I described exactly how to see it instead.
I attached or pasted a strap log above — or my bug is a clearly reproducible UI issue that doesn't need one (e.g. a typo, a button that does nothing) and I've described exactly how to see it.
I checked the Troubleshooting guide — especially "history syncs but no data" (most often the strap isn't banking history to flash; charge it to 100% and reconnect).
I checked the FAQ and — if I'm on a WHOOP 5.0 / MG — the 5.0/MG status page, or — if I'm on an Oura ring — the Oura ring support table, and this isn't an already-known limit (e.g. SpO₂, recovery/sleep on the 5/MG, or no Recovery / HRV on a ring-only day).
I've left out anything personal (strap serial, account email, raw export).
Thanks @bartmuskala, and the second screenshot is what makes this diagnosable. Three
numbers on three surfaces is not a coincidence: there are four separate HRV baseline
computations in the app, and you are seeing three of them.
The four
#
Where
How it is computed
1
The engine, which scores Charge
Baselines.foldHistory WITH the recalibration epoch. Winsorized at 3x spread, hard-rejects at 5x, gated on 4 seed / 14 trust nights
2
"What shaped it" driver rows
The same fold WITHOUT the epoch (TodayScoring.kt:70, ChargeBreakdownWiring.swift:38)
3
Coach
A plain mean of the trailing 30 days excluding today, no winsorization and no gating (CoachSuggestions.kt:94, CoachSuggestions.swift:97)
4
The Today "X% over baseline" insight
A plain mean of whatever priors the caller passes (TodayView.swift:3212)
Only the first is the baseline the Charge score actually rides.
Why the two numbers on one page differ
The headline reads #1 and the rows read #2, and the only difference between them is one
argument:
The comment above the second one already states the condition:
byte-identical to the engine's whole-history fold when no manual Recalibrate epoch is
set, the common case
noop.hrvBaselineEpoch is written by Settings, Recalibrate HRV baseline. Once it is set,
the engine discards every night dated before it while the driver rows keep folding the
whole history, so the two numbers separate and stay separated. Apple has the identical
split, so this is not Android-only.
The one thing that would confirm it: have you ever used Recalibrate HRV baseline in
Settings? No log needed, just yes or no. If yes, that is 57 against 48 explained. If no,
then #1 and #2 should agree and the interesting number is the pair that does not, which
would point somewhere else and I would want the export after all.
Coach's 52 ms needs no recalibration to differ from either. A plain 30-day mean with no
winsorization will sit apart from a winsorized, outlier-rejected baseline for anyone with
one unusual night, which is most people.
Proposed fix
1. Thread the epoch into the driver folds.TodayScoring.kt:70 and ChargeBreakdownWiring.swift:38 take hrvBaselineEpoch exactly as the headline path does.
That makes the two numbers on the Charge page the same number by construction rather than
by coincidence, and it is a small change on both platforms with an existing twin test shape
to follow.
2. Give Coach the real baseline. Route CoachSuggestions through Baselines instead of
its own mean. A user cannot reconcile advice against a number nothing else in the app
agrees with. If Coach genuinely needs a different window, then it should not be called a
baseline in the copy.
3. One definition, with the others named. The deeper problem is that "baseline" is four
things wearing one word. hrvBaselineDeltaPct already says so:
Proper fix = tag HRV provenance/method per row and isolate baselines (separate follow-up)
Worth doing after 1 and 2, since those two remove the visible contradiction while this one
is a refactor with its own risk.
I would not start on any of it before the Recalibrate answer, because it decides whether
step 1 is the fix for what you are seeing or just a correctness cleanup that leaves your
actual complaint untouched.
Activity
bartmuskala commentedon Sep 18, 2026
Coach says yet another value. 52 Ms.
ryanbr commentedon Sep 18, 2026
Thanks @bartmuskala, and the second screenshot is what makes this diagnosable. Three
numbers on three surfaces is not a coincidence: there are four separate HRV baseline
computations in the app, and you are seeing three of them.
The four
Baselines.foldHistoryWITH the recalibration epoch. Winsorized at 3x spread, hard-rejects at 5x, gated on 4 seed / 14 trust nightsTodayScoring.kt:70,ChargeBreakdownWiring.swift:38)CoachSuggestions.kt:94,CoachSuggestions.swift:97)TodayView.swift:3212)Only the first is the baseline the Charge score actually rides.
Why the two numbers on one page differ
The headline reads #1 and the rows read #2, and the only difference between them is one
argument:
The comment above the second one already states the condition:
noop.hrvBaselineEpochis written by Settings, Recalibrate HRV baseline. Once it is set,the engine discards every night dated before it while the driver rows keep folding the
whole history, so the two numbers separate and stay separated. Apple has the identical
split, so this is not Android-only.
The one thing that would confirm it: have you ever used Recalibrate HRV baseline in
Settings? No log needed, just yes or no. If yes, that is 57 against 48 explained. If no,
then #1 and #2 should agree and the interesting number is the pair that does not, which
would point somewhere else and I would want the export after all.
Coach's 52 ms needs no recalibration to differ from either. A plain 30-day mean with no
winsorization will sit apart from a winsorized, outlier-rejected baseline for anyone with
one unusual night, which is most people.
Proposed fix
1. Thread the epoch into the driver folds.
TodayScoring.kt:70andChargeBreakdownWiring.swift:38takehrvBaselineEpochexactly as the headline path does.That makes the two numbers on the Charge page the same number by construction rather than
by coincidence, and it is a small change on both platforms with an existing twin test shape
to follow.
2. Give Coach the real baseline. Route
CoachSuggestionsthroughBaselinesinstead ofits own mean. A user cannot reconcile advice against a number nothing else in the app
agrees with. If Coach genuinely needs a different window, then it should not be called a
baseline in the copy.
3. One definition, with the others named. The deeper problem is that "baseline" is four
things wearing one word.
hrvBaselineDeltaPctalready says so:Worth doing after 1 and 2, since those two remove the visible contradiction while this one
is a refactor with its own risk.
I would not start on any of it before the Recalibrate answer, because it decides whether
step 1 is the fix for what you are seeing or just a correctness cleanup that leaves your
actual complaint untouched.
fix(charge): score the breakdown against the baseline the ring rides (#…
bartmuskala commentedon Sep 19, 2026
No. I never recalibrated. Thanks for looking into your suggested solutions.
feat(health): add local monitor and reliable wellness alerts
fix(health): bind trusted readings to source and scoring snapshot
fix(health): bind respiratory trust to fresh source evidence
Pin health ranges and alert provenance to Swift oracle output
Bind health monitor values and provenance to one snapshot
Link GRDB directly in hosted store tests