Repository navigation
App data (W7): a launch's seeded refresh is not logged as a completed sync - #42
Merged
Merged
Conversation
… (W07-016) The launch seed of lastSyncedAt is an emission of the same published property a HISTORY_COMPLETE stamps, so every launch logged "refreshing dashboard cache from completed sync" and a trigger=post-offload re-score before the strap had connected. LiveState now carries a LastSyncOrigin beside the value, written only by seedLastSynced and stampCompletedSync, each before the value. The sink reads it after the debounce and the seeded pass logs its own line and trigger=launch-seed. The pass itself is kept: on a background launch it is the only one. The completed-sync strings are unchanged.
An independent V2 of W01-006 holds with caveats, so the row moves to Verified. Its caveats are recorded as W01-017 … W01-019 and W02-021 … W02-023, all S4. The board gains the Phase 1 census: no S1 or S2 in scope is open, and 46 Reported rows still carry no V0.
The owner closed Phase 1 on 2026-10-03. The census stands: no S1 or S2 open in its scope. W6's 34 rows without a V0 are Phase 3 scope; the 12 W1 and W2 rows are V0'd with the first later batch touching their file, or by Phase 6. Phase 2 is claimed for W3, W4 and W7, with W07-002 (S2) fixed first. PLAN's phase table now gives AD-1 to Phase 2, which W7 already binds.
Workflow run wf_b77f01a5-1aa: six reviewers (W3 sleep, scores, activity and ECG; W4; W7 orchestration, resolution) and one adversary per workstream. P5 refuted none. Recorded W03-023 … W03-042, W04-002 … W04-018 and W07-017 … W07-031; the single pass offset and the pass-global sleep parameters, found by both W3 and W7, are one W7 row each. AD-1, AD-7 and AD-8 settle as Amend; AD-14 and AD-4 gain evidence. W07-001 re-scoped, W07-015's cause named, W07-004's CPU figures noted as process-wide.
…t from the shared namespace (W07-002) The engine writes every scored day under the canonical my-whoop-noop, whichever strap supplied it. Since W02-005's follow-up, deleteAllData left that namespace alone whenever another device was registered, so deleting a re-added strap's data, or the canonical strap's with a second one registered, kept every score computed from it on screen. scoreInputProvenance already names the day owner for each scored day's recovery, strain and Rest-point cells. deleteAttributedCanonicalRows runs first in the delete transaction: each cell naming the device goes; a day's row goes when no recovery or strain cell names another source, together with the day's unattributed cells; cells naming another source or an estimator stay. Sleeps and detected workouts carry no day key and go when only the device's heart rate covers them; edited nights stay. Days scored before provenance existed have no cells and stay.
…002) 0xFF is FIT's uint8 invalid value, written whenever the HR sensor drops out. The record, lap and session heart-rate fields passed it to validHr, which accepts 1...300, so a ride with dropouts stored a per-second stream with 255 bpm samples and an avg/max of 255, which ryanbr#137 then used for day Effort on a strap-less day. FitParser.heartRateField drops 0xFF before validHr; the shared validHr is unchanged for GPX and TCX.
sleepDaily keyed every sleep-stage interval by the local day of its own end. Apple Watch writes a staged night as many short intervals, so the part before midnight landed on the bed day and each stored day held the tail of one night plus the head of the next. Intervals are now joined into nights first (a gap of two hours or more starts a new one) and each night is keyed by the day it ends on.
…ot a cross-source sum (W04-005) The aggregator summed active and basal energy and sleep minutes across every source, the double count ryanbr#589 fixed for steps only. A watch and another app (the WHOOP app, or NOOP's own iOS write-back) both writing the same day doubled its energy and its night. Energy sums within a source and takes the max source per day, as steps do. Each wake day's sleep is accumulated per source, and the day takes the source with the most sleep.
…import filled (W04-003) Every writer of the apple-health daily rows replaced the whole row, so a Shortcut payload, which carries only steps, sleep, resting HR and a few others, nulled the deep/REM/light minutes, SpO2, respiration, SDNN, basal kcal and max and walking HR an export.zip import had filled for the same days. They came back only by re-importing the zip. upsertDailyMetrics and upsertAppleDaily take keepingStoredValuesForNil: a nil column keeps the stored value. The Shortcut ingest uses it; every other writer keeps the whole-row replace.
…dex scan (W07-032) W07-002's sleep and workout delete asked whether any other source had heart rate inside each canonical session with a deviceId != predicate, which cannot use the (deviceId, ts) key: SQLite scanned the whole heart-rate index once per session, inside the delete's write transaction. On 115 days of 1 Hz rows that was 25 to 53 s with offload inserts and every store read waiting behind it. The other heart-rate sources are now read once by a skip scan over the key, and each session is probed with key lookups.
On an install whose active strap id is not my-whoop (any remove and re-add, ryanbr#814), Repository's computed sibling is whoop-<uuid>-noop, but the engine banks every detected night, and so every edit of one, under the canonical my-whoop-noop. selfHealEditedStages read only the active sibling, found no edited nights, and the day's totals, Rest and Charge kept the detected bounds. It now reads every id computedReadIds unions, returns one row per night (the active strap's copy winning), and writes each re-staging back to the namespace its row came from.
…03-023) detectSleep took the sparse-gravity flag, the fragmentation rescue and the HR baseline over its whole read, about 54 h. A gravity gap or the heart rate half a day after a night decided whether a pre-sleep fragment was bridged into it, so the same night read by two windows of different reach came out with two starts, and a night first scored in the morning moved when re-scored after midnight. SleepStager.LocalContext gives every decision at time t only its own neighbourhood: the bridge judges sparseness and fragmentation within 6 h of each pair, and run building, the bridge's HR band, the HR confirmation and the morning guard use the median HR within 12 h of t. A read that covers a session's 12 h neighbourhood gives the same session. The trace counts locally sparse gaps instead of asserting one read-wide flag.
…s banked once (W03-024) Each past day's sleep read stopped at its next local midnight. A night that began with an evening fragment and a short stir before running across midnight was joined into one session by the next day's read, but the earlier day saw only the fragment and banked it as its own sleep: two rows with one start and two ends, and an evening nap scored on the wrong day. Every day now reads 12 h past its next midnight (capped at now), the neighbourhood W03-023 gives each detection decision, so a session ending on the day is detected whole and the earlier day filters out a night that ends after it. StreamReadCap.forwardSeconds carries the span, so the stream caps grow with it; the skin cap is sized like gravity's. The day-owner probe stays on the day's own span.
…n motion (W03-044) stagingSparse, stored on every computed night and shown as the Sleep tab's "may be incomplete" caption, came from isGravitySparse over the whole read, as did the Rest confidence guard and the sleep-motion trace. A dense night was stamped sparse for a gravity gap half a day away: 8 of 12 banked nights on the 2026-09-30 backup, none with a gap inside its own span. Each session now takes LocalContext.sparse at its midpoint, the bridge's 6 h neighbourhood; the day's value is whether any session of its main night is sparse.
…indow (W03-046) W03-024 widened the sleep read to 66 hours, but the steps, respiration, raw SpO2 and v18 auxiliary reads kept a literal 200,000 limit, below a full window at one row a second (about 230,000 step rows on the 2026-09-30 backup). A read that reaches its limit drops its newest rows without a word. StreamReadCap now names a cap for each, sized like gravity.
…n cap (W03-045) daySliceFromNight (ryanbr#997) declines a night read at or above its limit, taking it for a truncated read, and the limit defaulted to 200,000. W03-024's 66-hour window holds about 237,600 rows of a 1 Hz stream, so on almost every past day the shortcut declined and the HR, steps and gravity day streams were read again: about 60 extra store reads per pass, with no data difference. The limit is now required and each call passes the cap its night read used, so a read that came back at its cap still declines and a complete one is sliced.
After W03-023 the sparseBridge line still printed the read-wide fragmentedToNothing value the bridge no longer used, and gated its own emission on it, while its sparseGaps=k/m count always read k = m. Each SparseBridgeAttempt now carries enabledBy (sparse, fragmented, or none for a pair the local rule did not open, recorded as reason=dense). The pair line prints it, the night line counts pairs by it, and the unused read-wide value is gone.
…ow (W03-048) W03-023's LocalContext doc claimed any read covering a session's 12 h neighbourhood gives the same session. That holds for the decisions it localises; classifyStill's window size, the overnight acceptance chain and the off-wrist gap gate still read the whole window. The doc now names them. Comments that still described the night window as ending at the next midnight or at dayStart + 12 h describe the 66-hour read.
…a corrupt reading (W03-051) LocalContext.baseline sized its per-call histogram by the largest bpm in the read, so a single corrupt value would make every baseline call allocate that many counters. The histogram now covers 0 to 511 and anything outside it is counted from a sorted list, as negatives were; the median is unchanged.
…y snapshot it supplied is cleared (W07-035) W07-002 kept a mixed day's whole dailyMetric row whenever any recovery or strain cell named another source. A row whose strain the deleted strap scored, beside a recovery a legacy snapshot kept from an older source, therefore kept the deleted strap's strain, sleep, HRV and resting HR, and lost its source badge as its provenance went. The row now belongs to the source its strain cell names (the pass that scored it), else its recovery cell. The owner's delete removes the row; when the deleted strap supplied only the legacy snapshot on another source's row, just the snapshot's columns are cleared.
… (W07-036) On a day only the deleted strap supplied, the sweep removed fitness_age but kept vo2max_est, because the VO2max cell's provenance names its estimator rather than a device. Both come from one Fitness Age computation over the same week, so the strap's VO2max stayed on screen beside a missing Fitness Age and kept feeding Body Age. The sweep now treats the estimator-named cell as unattributed.
… under the canonical id (W07-037) The attributed workout delete selected every workout under my-whoop-noop with no source filter, so a legacy workout the user had entered there, covered only by the deleted strap's heart rate, went with the detected bouts while edited nights were spared. Only detected bouts (sport "detected" or a computed source) are attributed now; a manual row stays.
…of another source (W07-038) The attributed delete kept a canonical night or detected workout when any other source had a single heart-rate sample inside it, so a sparse second source (a chest strap, an imported activity file) protected every overlapping session the deleted strap recorded. A session now goes when the device's heart rate inside it outnumbers every other source's; an even split stays.
…elete (W07-040) W07-002 added canonicalComputedId as "my-whoop" + suffix beside a bare "my-whoop" in exclusiveComputedSibling, and tested "the canonical namespace is this device's alone" as exclusiveSibling != canonicalComputedId. One canonicalDeviceId constant now feeds both, and the condition has a name. No behaviour change; the delete tests pass unchanged. W07-039 (the repeated provenance delete) is kept on purpose.
…egistered strap (W07-041, W07-042, W07-043) Three gaps W07-023 left in one function, fixed together: - Edited nights came from the active and canonical siblings only, while the Sleep tab shows every registered strap's computed nights; two re-adds later a nap under the first re-added strap was shown but never scored. The heal now reads through rawComputedReadIds, the Sleep tab's resolver, active first. - Only the copy scoring takes was healed, so a second edited copy the Sleep tab still shows went stale. Every copy is healed now. - restageFromRaw read raw under the active strap only, so a night the old strap recorded was never re-staged and a partial active read could overwrite it. The raw now comes from the strap with the most gravity inside the night.
… writers (W07-045) W07-016 documented that seedLastSynced and stampCompletedSync are the only writers of lastSyncedAt, each setting its origin first, but the property kept a public setter, so a direct write would leave the origin stale. The setter is private now; SyncChipStateTests stamps through stampCompletedSync.
… a completed sync (W07-044) W07-016's sink defaulted an unrecorded origin to completedSync, so a value whose writer it could not name would be logged as a completed sync with trigger=post-offload, the claim W07-016 removed. Such a value is now reported as unattributed, with its own neutral line and trigger.
…046) The persisted last-sync time is seeded from bootstrapStore, which beginBackfill retries on a later tick when the first open fails, for example on a relaunch while the phone is locked. The "Launch:" line and trigger=launch-seed could then appear after Connected, asserting a launch the code did not see. The line now reads "Seed: …" and the trigger is stored-seed.
…h chain (W07-047) W07-016's test subscribed to $lastSyncedAt directly, not to the chain the app runs (drop first, de-duplicate, 2-second debounce, origin read after it), so a change that broke the chain would still pass. The chain is now AppModel.lastSyncRefreshes(_:debounce:), which the app sinks and the test drives: a seed followed by a HISTORY_COMPLETE settles once as a completed sync, a seed alone as the stored seed.
This was referenced Oct 3, 2026
Analytics (W3): sleep detection reads only a night's own neighbourhood, and each night is banked once
Import (W4): FIT invalid heart rate, Apple Health night keying, one source per day, Shortcut merge
App data (W7): attributed strap-data delete, sleep edits on a re-added strap, launch-seed attribution
Review tracker: close Phase 1, Phase 2 review passes for W3, W4 and W7
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.
Whole-project review, W7, from the owner's 2026-09-30 strap export (recorded in #41). Two commits: the fix, and the Phase 1 census with W01-006's independent V2.
W07-016 (S3): a launch is not logged as a completed sync
At launch,
LiveStateseedslastSyncedAtfrom its persisted value. The seed is an emission of the same published property a HISTORY_COMPLETE stamps, so every launch logged "refreshing dashboard cache from completed sync" and ran atrigger=post-offloadre-score before the strap had connected. On a background launch that line was the only pass, and it read as a sync that never happened.LiveStatecarries aLastSyncOriginbesidelastSyncedAt, written only byseedLastSyncedandstampCompletedSync, each before the value.AppModelsink reads the origin after its debounce. A seeded pass logsLaunch: …andtrigger=launch-seed.Verification: V0 (the test fails with the fix reverted),
LastSyncAttributionTests; StrandTests 2,132 pass;Strand(macOS) andNOOPiOS(without the watch app) build. Strap check still due: a launch's log reads the launch line beforeConnected, andpost-offloadonly after a HISTORY_COMPLETE.Tracker: W01-006 V2 and the Phase 1 census
PLAN.md's scope: no S1 or S2 open in W1, W2 or the safe-trim and backfill part of W6.