Skip to content

P1: Per-day reuse cache serves HRV-less night scans permanently when R-R lands after HR; force refresh does not bypass it #29

Description

@bhelm

Found by an independent review of the integrated fork state (2026-08-18). The code arrived with
the upstream range d4b7cc8..f2271cd (ryanbr#1005 / ryanbr#1395 / ryanbr#1396 per-day reuse cache); affects
upstream equally. Filed here first — forwarding upstream is a separate decision.

Problem

The per-day reuse cache in Strand/Data/IntelligenceEngine.swift:780 (same design in the
Kotlin twin, IntelligenceEngine.kt:617-643) keys a day's scan on owner + HR fingerprint
(count, maxTs) + skin anchor + config signature only. R-R / respiratory / SpO2 rows arriving
with no HR-row delta produce the identical key, so a stale cached DayScan is served.
Force-refresh does not bypass the day cache — force=true only bypasses the whole-pass
watermark gate.

Failure scenario

WHOOP history offload delivers channels independently, and offloaded HR rows duplicating live
rows are dropped by ON CONFLICT DO NOTHING. A banked night whose R-R packets land after its
HR is scored once with rmssd=nil and cached; every subsequent pass — including user-initiated
force refreshes — recomputes the same key and re-serves the HRV-less scan. Night HRV /
respiratory rate stay blank until an unrelated HR row, a config change, or an app restart
changes the key.

Possible directions

Fold the other stream fingerprints (R-R / resp / SpO2 counts + maxTs) into the day key, or let
force=true bypass the day cache as users already expect it to.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions