Skip to content

feat: wake-triggered derivation + RR-from-minute HRV + TTL cache + D1-hot/R2-sealed storage#9

Merged
abdulsaheel merged 13 commits into
mainfrom
feat/wake-trigger
Jun 20, 2026
Merged

feat: wake-triggered derivation + RR-from-minute HRV + TTL cache + D1-hot/R2-sealed storage#9
abdulsaheel merged 13 commits into
mainfrom
feat/wake-trigger

Conversation

@abdulsaheel

@abdulsaheel abdulsaheel commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

feat/wake-trigger → main

Wake-triggered architecture (v2)

  • Cron does ONE cheap job — incremental isUserAwake ladder (wake_cron.ts) → fires close_day once per physiological day at wake.
  • HRV from minute.rr at close (biometrics_minute.ts) — zero R2 re-decode (RR un-banned).
  • TTL read_cache (no watermark); D1-hot / R2-sealed minute storage (minute_store.ts); naps-as-sleep + per-nap hypnograms.
  • Storage/migrations: minute.rr (v11), read_cache (v12), cursor wake-state (v13), minute_day (v14).
  • RR-aware staging at close so stored sleep duration matches the on-read view.

New in this update

  • Menstrual cycle: cycle_log (v15) + opt-in users.track_cycle (v16); GET/POST/DELETE /cycle gated on explicit consent; biometric overlay; profile carries track_cycle.
  • Live HRV spot-check: POST /spotcheck decodes RR from posted live frames (realtimeRr) + timeDomainHrv (stateless).
  • Skin-temp + SpO₂ registered in /trend; skin_temp added to /day/heart.
  • Irregular-rhythm served for the always-on on-screen card.

Deploys to the isolated openstrap-backend-v2 worker via wrangler.v2.toml (local-only); prod v1 untouched. ops/ + wrangler.v*.toml stay untracked.

🤖 Generated with Claude Code

…inute, D1-only loadDayRr) + TTL read-cache + v11/v12 migrations
…alytics), cron isUserAwake ladder (wake_cron), close_day queue job (processUser + HRV-from-minute + cache invalidate), demote nightly to maintenance + retry-net, v13 cursor migration
… per-nap hypnograms on /day/v2/sleep (on-read, no migration)
…) — all minute-reading day endpoints now cached
…acks days >3d into gzipped R2 objects (put-before-delete), day-detail reads fall back to R2; cuts D1 storage + per-row prune-deletes + dodges the 10GB cap. Ingest/processUser paths unchanged (hot window).
…gzipped blob, RMW merge mirroring the old ON CONFLICT) replaces row-per-minute as the hot store; all readers (analytics/daydetail/query/workouts/biometrics/seed) + seal + prune go through minute_store. The D1 write-cost lever (~1,440→~1 write/day). v14 migration.
…ging

loadMinutes/stageNight now carry MinuteRec.rr through to stageSleep so the
autonomic deep/REM split (RMSSD) runs on /day/sleep and /day/v2/sleep. Pairs
with the analytics stageSleep RR change. ops/ + wrangler.v*.toml stay local.
…ake-only

detectSessions and autoCloseStaleWorkouts ran only inside the once-a-day
close_day / every */10 tick, so an auto-detected workout didn't surface until
the next wake and the frequent cron wasn't purely wake-detection.

- new workouts.ensureTodayWorkouts(db,user): closes this user's forgotten LIVE
  workouts + (re)detects TODAY's auto sessions, mirroring processUser Pass-2's
  session block EXACTLY (delete auto-non-deleted, insert done/auto; manual/
  edited/deleted untouched). THROTTLED via read_cache (~120s) so a burst of reads
  costs one detect, and only for users actively opening the app.
- called on-read from listWorkouts, getSessions, and getDayStrain(today).
- autoCloseStaleWorkouts now takes an optional userId scope (on-read = one user).
- scheduled(): removed autoCloseStaleWorkouts from the every-*/10-tick path; the
  */10 cron now does ONLY runWakeLadder. Kept autoCloseStaleWorkouts on the nightly
  30:3 tick as a safety net for users who never open the app.

Validated on v2: /workouts + /sessions 200, detection runs + throttles (wkscan
read_cache sentinel set), 0 false workouts on a non-workout night.
… drop steps_imu

Steps had three code paths: a per-record r10Motion heuristic (-> minute.steps,
fed the chart), the full AN-2554 (calcSteps in ingest_signals -> computed then
DISCARDED), and an R2 recompute (steps_imu -> daily.steps at close, lagged all day).

Collapse to one: AN-2554, counted once at ingest, live on read.
- minute_store.writeBatch: minute.steps += ingest_signals.steps (AN-2554), additive
  (idempotent via edge hex-dedup). rollup no longer sums the r10Motion s.steps_inc.
- processUser folds daily.steps = SUM(this day's minute.steps) into the close (no R2),
  so past days keep a permanent total after minutes prune.
- reads serve TODAY live: getToday + getDayStrain(today) = SUM(minute.steps) from the
  hot day blob (zero R2); past days use the stored daily.steps.
- REMOVED steps_imu.ts entirely + the 'steps_full' queue job + runStepsIncremental from
  close_day/sweep + the /admin/run-steps endpoint. No step work in any cron now.

Validated on v2: /today + /day/strain return live steps (104) from minute.steps; tsc clean.
Ingest aggregates per-minute red/IR/temp ADCs (wrist-on) into the minute
blob; biometrics_minute derives a RELATIVE red/IR SpO2 index (confidence-
gated) + skin-temp index vs rolling EWMA baselines, feeds temp into illness.
Zero extra R2 (reads the D1 minute blob). admin/enqueue forwards onset/wake
for close_day verification. Validated end-to-end on v2.
…er + cache /day/sleep

Drive the hypnogram, stage breakdown, duration, awake and efficiency from ONE source
(analytics stageHypnogram — v1 Cole-Kripke method) so the graph and breakdown can't
disagree; pass per-minute RR so the REM tiebreaker reclaims REM mislabeled as awake.
Add the fractal sleep cycles (detectSleepCycles) to the response.

Wrap getDaySleep in the TTL read-cache: compute the night ONCE, serve from read_cache
(past day immutable, today <=60s, close clears it) — no more recomputing the hypnogram
on every read.
…creen

The RR REM-tiebreaker lived only in the on-read /day/sleep path, so the STORED
sleep.duration_min (what /today reads) stayed HR-only — Today showed 4.9h while the
Sleep screen showed 5h43m for the same night. processUser now runs the same
stageHypnogram (RR tiebreaker) at close and stores its duration/efficiency/stages, so
both screens read one consistent value. Verified: /today and /day/sleep both 343min.
…ways-on irregular

- Cycle tracking: cycle_log table (migrate_v15) + opt-in users.track_cycle
  (migrate_v16); GET/POST/DELETE /cycle gated on explicit consent; biometric overlay.
- Live HRV spot-check: POST /spotcheck decodes RR from posted live frames
  (realtimeRr) + timeDomainHrv (stateless).
- Skin-temp + SpO2 registered in /trend; skin_temp added to /day/heart.
- Irregular-rhythm served for the always-on on-screen card.
- Profile GET/PATCH carry track_cycle.
@abdulsaheel abdulsaheel merged commit 3d1b3aa into main Jun 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant