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
{{ message }}
Repository navigation
[P0] HealthKit anchor persisted before ingestion runs — observer sync can silently lose days #1
fetchTouchedDayWindow persists the advanced HKQueryAnchor to UserDefaults immediately in the query callback — before the subsequent ingestion is known to run. If sync(days:) then bails silently, the anchor has already moved past the new samples and the window is never re-fetched.
StrandiOS/Health/HealthKitBridge.swift:378 — guard let store = await repo.storeHandle() else { return } (second silent bail, after the anchor commit)
StrandiOS/Health/HealthKitBridge.swift:586 — writeBackAfterNewData() sets the shared syncing flag
Failure scenario
Watch-only user, app in background. A strap write-back finishes and sets the shared syncing flag. In exactly that window HealthKit delivers an observer wake with new watch data (e.g. last night's SDNN): the anchor is persisted, sync() returns immediately because of !syncing, and the night never reaches the store. The next observer wake sees "nothing new".
Mitigation today: the foreground catch-up (health.sync() with its default 30-day window, StrandiOSApp.swift:244) usually re-covers the day. The loss becomes permanent once the day ages out of that 30-day window without the app being foregrounded — which is precisely the population the background ingestion path exists for.
Suggested direction
Persist the anchor only after ingestion has committed, or roll the stored anchor back when sync(days:) bails on any of its guards.
Found in a multi-pass code review; each claim was independently re-verified against current main before filing.
Confirmed, @bhelm — good find. fetchTouchedDayWindow committed the advanced HKQueryAnchor in the anchored-query callback, before sync(days:) ran; if sync then bailed on !syncing (a strap write-back holds that flag) or the store handle was nil, the cursor had already moved past the new samples and the night was never re-fetched. Fixed by returning the anchor and persisting it only after the sync actually commits (or when there was nothing to ingest) — a bailed sync now leaves the prior anchor for the next wake to re-fetch (idempotent). iOS-only; Android's Health Connect importer reads a fixed time window, so no twin. Thanks for the pre-verified report.
Activity
ryanbr commentedon Aug 17, 2026
Confirmed, @bhelm — good find.
fetchTouchedDayWindowcommitted the advancedHKQueryAnchorin the anchored-query callback, beforesync(days:)ran; if sync then bailed on!syncing(a strap write-back holds that flag) or the store handle was nil, the cursor had already moved past the new samples and the night was never re-fetched. Fixed by returning the anchor and persisting it only after the sync actually commits (or when there was nothing to ingest) — a bailed sync now leaves the prior anchor for the next wake to re-fetch (idempotent). iOS-only; Android's Health Connect importer reads a fixed time window, so no twin. Thanks for the pre-verified report.today: put scan and connect on the Today header (ryanbr#2178)