Symptom
On a WHOOP 5/MG (iOS 11.8.2, fw 50.39.1.0), two consecutive nights show the strap-log REM funnel
and the sleep screen disagreeing by 5–7× on the same night:
| night |
funnel final= |
funnel minutes (30s epochs) |
screen REM |
| 2026-09-19 |
85 rem of 1326 sleep-epochs |
~42 min |
4h 54m |
| 2026-09-20 |
92 rem of 1230 sleep-epochs |
~46 min |
3h 51m |
Funnel line for the second night:
REM-funnel: 1230 sleep-epochs, classify=100 rem, final=92 rem (onset-guard stripped 5); resp=ABSENT;
blocked[notStill=61, noCardiac=549, respRegular=0, noRespBar=335], otherStage=185
Read together with the screen, this says the app displayed ~3h of REM it could not substantiate.
That reading is wrong — but it is the reading the funnel's own documentation instructs.
Cause
The two numbers come from two different stagers, and the funnel claims otherwise.
- The funnel replays V1:
remFunnelDiagnostic mirrors classifyOne
(Packages/StrandAnalytics/Sources/StrandAnalytics/SleepStager.swift:2627-2656, :2464-2508).
- The screen is staged by V2:
IntelligenceEngine.swift:1331 →
SleepStager.detectSleep(useSleepStagerV2:) → SleepStagerV2.stageSession, and the selector flag
is default-ON (Strand/BLE/PuffinExperiment.swift:221-225; restated at SleepStager.swift:1307-1312:
"a normal user's nights are staged by V2").
- The funnel's doc comments assert identity with the display:
SleepStager.swift:2566-2568
("shares the exact classifier seam with stageSession, so it explains the SAME hypnogram the app
shows") and :2697. Both sentences are true of V1's stageSession and false of every shipped
install.
A 5/MG makes the divergence maximal rather than incidental. V1's primary REM rule
(still && cardiacActivated && resp.meetsIrregularBar) is structurally unreachable there: the v18
layout carries no raw resp channel (SleepStager.swift:2348-2353), every resp epoch is .unmeasured,
and meetsIrregularBar is false for .unmeasured (:2456-2461) — so only the strict double-cardiac
fallback can award REM, and noRespBar/respRegular are structural rather than informative. V2 never
reads that channel at all; it recovers respiration regularity from R-R via RSA
(SleepStagerV2.swift:393-428), which is fully populated on the same night (rr=32540). So
resp=ABSENT is true for the code the funnel ran and false for the code that produced the number on
screen.
This is the shape the repo's own rules name: "a funnel free to disagree with the code it describes is
worse than no funnel, because it will be believed" (WorkoutDetector.swift:153-154), and AGENTS.md's
"a diagnostic may only assert what it can attribute" / "two readouts of one fact must not be able to
disagree".
What this is NOT claiming
The funnel's arithmetic is sound (the partition sums exactly, as SleepStagerTests.swift:1478-1480
pins), and V2 being the display stager is deliberate. The defect is the funnel's identity claim — the
sentence, not the numbers.
Fix shape
Minimum honest fix: the funnel line names which stager it replays and which one staged the screen —
e.g. append · funnel replays V1; screen staged by V2 — totals can differ at the print site
(Strand/System/DebugDataDiagnostics.swift:299-303), keyed off PuffinExperiment.experimentalSleepV2Enabled.
Better long-term: a funnel that replays whichever stager the flag selects.
The Android twin has the identical defect: AndroidDiagnostics.kt:348 prints the Kotlin
SleepStager.remFunnelDiagnostic (V1, SleepStager.kt:2962) while IntelligenceEngine.kt:1184
stages with V2.
Secondary observation (separate issue if wanted)
The screen's 37–48% REM on these ~10h nights sits well outside the 20–25% band the funnel's own
header cites (SleepStager.swift:2555), and lands in V2's measured long-night failure mode
(Tools/SleepPSG/.../main.swift:307-310: corr(total sleep, REM%) = +0.579, traced to the fixed
60-minute REM-latency penalty). No display-side gate can catch over-assignment — H9
(SleepView.swift:1019-1039) and "May be incomplete" (:996-1001) both test only for too-little /
too-short. A REM-plausibility note on the display path may be worth its own discussion; happy to file
separately rather than mix concerns here.
Environment
WHOOP MG (DIS-attested), fw 50.39.1.0, iOS app 11.8.2 (build-from-source fork at upstream
e29bfe91 + fork-only re-pointing), iPhone17,2, iOS 26.6.2. Both strap logs available on request
(funnel lines quoted verbatim above).
Refs #688, #2012, #319, #347.
Symptom
On a WHOOP 5/MG (iOS 11.8.2, fw 50.39.1.0), two consecutive nights show the strap-log REM funnel
and the sleep screen disagreeing by 5–7× on the same night:
final=85 remof 1326 sleep-epochs92 remof 1230 sleep-epochsFunnel line for the second night:
Read together with the screen, this says the app displayed ~3h of REM it could not substantiate.
That reading is wrong — but it is the reading the funnel's own documentation instructs.
Cause
The two numbers come from two different stagers, and the funnel claims otherwise.
remFunnelDiagnosticmirrorsclassifyOne(
Packages/StrandAnalytics/Sources/StrandAnalytics/SleepStager.swift:2627-2656,:2464-2508).IntelligenceEngine.swift:1331→SleepStager.detectSleep(useSleepStagerV2:)→SleepStagerV2.stageSession, and the selector flagis default-ON (
Strand/BLE/PuffinExperiment.swift:221-225; restated atSleepStager.swift:1307-1312:"a normal user's nights are staged by V2").
SleepStager.swift:2566-2568("shares the exact classifier seam with
stageSession, so it explains the SAME hypnogram the appshows") and
:2697. Both sentences are true of V1'sstageSessionand false of every shippedinstall.
A 5/MG makes the divergence maximal rather than incidental. V1's primary REM rule
(
still && cardiacActivated && resp.meetsIrregularBar) is structurally unreachable there: the v18layout carries no raw resp channel (
SleepStager.swift:2348-2353), every resp epoch is.unmeasured,and
meetsIrregularBaris false for.unmeasured(:2456-2461) — so only the strict double-cardiacfallback can award REM, and
noRespBar/respRegularare structural rather than informative. V2 neverreads that channel at all; it recovers respiration regularity from R-R via RSA
(
SleepStagerV2.swift:393-428), which is fully populated on the same night (rr=32540). Soresp=ABSENTis true for the code the funnel ran and false for the code that produced the number onscreen.
This is the shape the repo's own rules name: "a funnel free to disagree with the code it describes is
worse than no funnel, because it will be believed" (
WorkoutDetector.swift:153-154), and AGENTS.md's"a diagnostic may only assert what it can attribute" / "two readouts of one fact must not be able to
disagree".
What this is NOT claiming
The funnel's arithmetic is sound (the partition sums exactly, as
SleepStagerTests.swift:1478-1480pins), and V2 being the display stager is deliberate. The defect is the funnel's identity claim — the
sentence, not the numbers.
Fix shape
Minimum honest fix: the funnel line names which stager it replays and which one staged the screen —
e.g. append
· funnel replays V1; screen staged by V2 — totals can differat the print site(
Strand/System/DebugDataDiagnostics.swift:299-303), keyed offPuffinExperiment.experimentalSleepV2Enabled.Better long-term: a funnel that replays whichever stager the flag selects.
The Android twin has the identical defect:
AndroidDiagnostics.kt:348prints the KotlinSleepStager.remFunnelDiagnostic(V1,SleepStager.kt:2962) whileIntelligenceEngine.kt:1184stages with V2.
Secondary observation (separate issue if wanted)
The screen's 37–48% REM on these ~10h nights sits well outside the 20–25% band the funnel's own
header cites (
SleepStager.swift:2555), and lands in V2's measured long-night failure mode(
Tools/SleepPSG/.../main.swift:307-310: corr(total sleep, REM%) = +0.579, traced to the fixed60-minute REM-latency penalty). No display-side gate can catch over-assignment — H9
(
SleepView.swift:1019-1039) and "May be incomplete" (:996-1001) both test only for too-little /too-short. A REM-plausibility note on the display path may be worth its own discussion; happy to file
separately rather than mix concerns here.
Environment
WHOOP MG (DIS-attested), fw 50.39.1.0, iOS app 11.8.2 (build-from-source fork at upstream
e29bfe91+ fork-only re-pointing), iPhone17,2, iOS 26.6.2. Both strap logs available on request(funnel lines quoted verbatim above).
Refs #688, #2012, #319, #347.