Skip to content

[P0] HealthKit anchor persisted before ingestion runs — observer sync can silently lose days #1

Closed

Description

@bhelm

Summary

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.

Affected code (main @ d4b7cc8, Release 10.1.0)

  • StrandiOS/Health/HealthKitBridge.swift:324 — anchor persisted in the anchored-query callback
  • StrandiOS/Health/HealthKitBridge.swift:356 — guard auth == .authorized, !syncing else { return } (silent bail)
  • 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.

Activity

  1. ryanbr commented on Aug 17, 2026

    @ryanbr

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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