Watch five overnight signals for signs of strain together - #126
Open
ak710 wants to merge 2 commits into
Open
Conversation
restingHRDrift has been a CoachAnomalyKind since the proactive-alert path landed, with a comment explaining why nothing raised it: the detector reads a 12-hour context packet, and drift is only meaningful against a multi-day baseline. But the baseline already exists — RestingHRBaselineService learns and persists it — so the packet just needed to carry it in. The night and the baseline are both measured as the interpolated 10th percentile of heart rate, sharing RestingHRBaselineService's own percentile helper. Comparing a night's mean against a 30-day percentile would have produced a difference that was mostly an artefact of the two formulas. The night is bounded by the sleep session rather than a fixed clock window, so a late night or a shift schedule is measured over the hours actually slept. Fires at +5 bpm and only upward — a resting HR below baseline is usually good news and not worth an unprompted alert. Gated on an established baseline, at least 10 overnight samples (a YCBT ring floors its interval at 30 minutes, so a full night is only ~14 there against ~84 on a 5-minute Colmi), and a night no more than two days old. Ordered last of the three detectors: a short night usually raises resting HR too, so when both trip the sleep alert names the cause while drift would only restate its consequence. Every threshold is documented in docs/project/anomaly-alerts.md, which also covers the two detectors that were already shipping but undocumented. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The gap analysis put illness early-warning as the single biggest thing the premium apps have that PulseLoop didn't — Oura's Symptom Radar, Ultrahuman's Sleep Screener. Every input already existed; nothing read them together. HealthWatch judges five overnight signals against their own 30-day baselines: skin temperature, resting heart rate, HRV, breathing rate and blood oxygen. Only the strain direction of each counts — a resting HR below baseline or an HRV above it is good news, and flagging it would turn that into an alert. The scoring exists to make it wait for agreement. Each signal scores 0, 1 or 2; a total of 2 is minor and 3 is major. A single notable signal therefore never counts on its own — one reading past its knot is a warm duvet or one restless hour. A single strong signal reaches minor but not major: a full degree of overnight temperature rise isn't noise, but it isn't enough to interrupt for unaccompanied either. Only major fires an alert. Minor still reaches the Today card and the coach, but two signals nudging past their knots happens after a glass of wine often enough that alerting on it would train people to dismiss the ones that matter. Baselines are the median of preceding nights' figures, not a mean of every sample. One night the ring recorded four times as often as usual would otherwise dominate, and a single feverish night would drag the very baseline it needs to be judged against. Heart rate uses the night's 10th percentile so it is the same quantity as the resting-HR baseline it's compared with; the rest use the mean. Nights are bounded by the sleep session, not a clock window. Signals without a baseline are simply not judged, so a ring with fewer sensors still gets a useful answer from the three it has — and fewer than two judgeable signals returns clear rather than a warning built on one reading. Ordered above restingHRDrift, because drift is one of its own signals: when both trip, the multi-signal result is the better-corroborated message about the same night. Drift still fires alone when nothing corroborated it, or when it was the only signal with a baseline at all. The Today card is conditional — a clear night renders nothing. A permanent "all clear" tile would be clutter, and would train people to stop reading it. Nothing here names a condition, and the copy names alcohol, heat and yesterday's session before anything else; a test asserts it. Thresholds and rationale in docs/project/anomaly-alerts.md, which also now documents the two detectors that were already shipping undocumented. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #117
Merge order: 7 of 9.⚠️ Stacked on #121 — this diff also contains that commit. Merge #121 first.
What
Oura's Symptom Radar, Ultrahuman's Sleep Screener. Every input already existed; nothing read them together.
Five overnight signals against their own 30-day baselines: skin temperature, resting HR, HRV, breathing rate, blood oxygen. Only the strain direction of each counts — a resting HR below baseline or an HRV above it is good news, and flagging it would turn that into an alert.
The scoring exists to make it wait for agreement
Each signal scores 0/1/2; total 2 is minor, 3 is major.
Baselines are medians of per-night figures
Not a mean of every sample. One night the ring recorded 4× as often would otherwise dominate, and a single feverish night would drag the very baseline it needs judging against —
testBaselineIsAMedianOfNightsNotOfSamplesbuilds exactly that case.Heart rate uses the night's 10th percentile so it's the same quantity as the resting-HR baseline it's compared with; the rest use the mean. Nights are bounded by the sleep session, not a clock window.
Ring coverage
Signals without a baseline simply aren't judged, so a ring with fewer sensors still gets a useful answer from the three it has. Fewer than two judgeable signals returns clear rather than a warning built on one reading.
Safety
Ordered above
restingHRDrift, because drift is one of its own signals — when both trip, the multi-signal result is the better-corroborated message about the same night.Nothing names a condition, and the copy names alcohol, heat and yesterday's session before anything else.
testFactsNameTheMundaneExplanationsAndNoDiagnosisasserts both.The Today card is conditional — a clear night renders nothing, so it never becomes an "all clear" tile people learn to ignore.
Testing
HealthWatchTests— 18 tests, most of them pinning the cases where it deliberately stays quiet. Full suite green (900). Docs:docs/project/anomaly-alerts.md.