Repository navigation
Dev - #17
Open
ShaggydogUK wants to merge 452 commits into
Open
Dev#17ShaggydogUK wants to merge 452 commits into
ShaggydogUK wants to merge 452 commits into
Conversation
Add 6 new RT fields populated at end of invoke(): hrBpmLatest, hrBpmAvg5m, hrBpmAvg15m — duration-weighted HR observations hrReadingsCount15m — data-quality signal sleepState — AWAKE | PRE_SLEEP | SLEEPING sleepStateEnteredAtMs — for dwell-time computation These ride the existing devicestatus uploader to NS at one observation per Boost cycle (~5 min), sufficient for retrospective sleep-model tuning. No new sync plumbing — the RT object already flows to NS as part of openaps.suggested.
V3MLG3 is the V4.4.4 acting algorithm. Add the same sleep state evaluation + HR/sleep telemetry as the V1 plugin, sharing the same SleepStateDetector and SharedPreferences-backed state (ApsBoostSleepState StringKey). The two plugins are not active simultaneously, so they share state safely. isNightModeActiveImpl() in V3MLG3 now honours ApsBoostNightModeAutoBySleep in the same HYBRID way: sleep state extends night-mode active window past nightEnd and engages it early via PRE_SLEEP.
New SleepHistoryTracker maintains a rolling 28-day record of (sleep_start_ms, wake_ms) sessions persisted to StringKey.ApsBoostSleepHistory. Aggregation uses *circular* mean (vector sum on unit circle, atan2 back) so the wrap-around case is handled correctly: avg of [22:00, 02:00] = 00:00, not 12:00 Once ≥7 closed sessions exist in the window, the plugins override configured nightStart / nightEnd with the learned circular-mean clock times when calling SleepStateDetector. PRE_SLEEP then engages preSleepLeadMin (default 60) before the learned sleep onset, not the fixed configured time. Sessions are recorded on transitions: AWAKE→SLEEPING → onSleepStart (opens session) SLEEPING→AWAKE → onWake (closes session + trim to 28-day window) Also: SleepStateDetector now allows AWAKE→SLEEPING when the user qualifies during the PRE_SLEEP window (previously required outer window), so an unusually-early sleep onset is detected promptly. Telemetry: sleepLearnedStartMin, sleepLearnedWakeMin, sleepLearnedDurationMin, sleepLearnedSessionCount on RT for NS upload. rT.reason includes 'learned=HH:mm→HH:mm/Nd' when active.
Extend SleepHistoryTracker.Session with two p10 (10th-percentile) HR
summaries computed at SLEEPING→AWAKE transition:
sleepHrP10 — over the just-ended sleep period (deep-sleep floor)
daytimeHrP10 — over the awake period preceding this sleep
(since previous session's wakeMs)
Aggregate returns:
restingHrBpm — median of valid sleepHrP10s (≥7 sessions) → true resting
daytimeHrBpm — median of valid daytimeHrP10s (≥7 sessions) → active baseline
p10 helper requires ≥30 samples per session (≈30 min of 1-min Wear OS HR)
to be meaningful; otherwise that session contributes null.
Wiring:
- SleepStateDetector now uses (restingHrBpm ?? configuredHrResting) so
the SLEEPING qualifier and wake threshold scale to the user's actual
deep-sleep floor (default cap = restingHr × 1.15, wake = × 1.25).
- HrActivityCalculator.classify in both plugins now uses
(daytimeHrBpm ?? configuredHrResting) as the Karvonen HRR baseline.
Cached cycle-to-cycle in @volatile field (5-min lag acceptable —
baseline changes slowly).
Telemetry: hrLearnedRestingBpm, hrLearnedDaytimeBpm on RT → NS.
Storage: per-session p10 ints add ~50B/session × 28 = ~1.4KB total.
Add HealthConnectHrIngest @singleton that reads HeartRateRecord from Android Health Connect on a polling cadence and writes through PersistenceLayer.insertOrUpdateHeartRate. Why: Garmin Connect IQ data fields only execute when on the active watch screen. The user's overnight HR is invisible to AAPS even though it flows continuously into Garmin Connect on the phone. Health Connect is the convergence point — Garmin / Wear OS both write to it, AAPS just needs to read. Behaviour: - Throttled internally; default 5 min between syncs (knob: ApsBoostHealthConnectPollMin) - Each Boost cycle triggers a non-blocking syncIfDue(); coroutine on Dispatchers.IO - First run pulls last 60 min; subsequent runs use LongNonKey.ApsBoostHealthConnectLastSyncMs as the high-water mark - HC samples are 1-min weighted into AAPS HR rows (timestamp from sample, duration = 60_000 ms, device = HC metadata) - No-op if disabled, no SDK, or permission not granted; logs and never throws into the calling cycle Wiring: - Hooked at the top of both V1 and V3MLG3 invoke() - Settings UI: opt-in toggle + poll interval next to sleep prefs Manifest: - <uses-permission android:name="android.permission.health.READ_HEART_RATE" /> - <queries> entry for com.google.android.apps.healthdata
…st is on When ApsBoostHealthConnectHrEnabled is true, GarminPlugin's private receiveHeartRate sink early-exits before calling loopHub.storeHeartRate. HC becomes the single source of truth — avoids duplicate HR rows at overlapping timestamps (Garmin path vs HC path writing the same wall-clock samples with different device labels). Gate is in the private sink so both URI (HTTP) and IQ-message entry points are covered. When HC is disabled (default), Garmin side-channel behaves exactly as before.
Add HealthConnectPermissionActivity (single-purpose, translucent, no-history) that wires the standard PermissionController.createRequestPermissionResultContract ActivityResultLauncher and finishes when the user grants/denies. Add a "Grant Health Connect access" Preference under Night Mode for both V1 and V3MLG3 plugins. Tap launches the Activity which immediately shows the HC consent dialog. Toasts on grant/deny/SDK-missing. Before this commit, the only path to grant the permission was via the Health Connect app, but HC only lists apps that have actively requested permissions — chicken-and-egg. Tapping this preference is the missing "first ask" so AAPS appears in the HC permissions list. Activity registered in the APS module's AndroidManifest.xml.
Health Connect's permission flow refuses to surface the consent UI unless
the app declares an Activity it can deep-link into for "Why does this app
want this data?". Without the manifest entry the request silently denies.
Add HealthConnectPrivacyActivity (programmatically-built plain-text screen,
no resource files needed) and the two intent filters HC looks for:
- android.intent.action.VIEW_PERMISSION_USAGE
+ category android.intent.category.HEALTH_PERMISSIONS (Android 14+)
- androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE (Android 13 and below)
Activity is exported=true (required — the OS launches it on the user's
behalf, gated by START_VIEW_PERMISSION_USAGE). The displayed text states
what HR is used for and that data stays on-device.
After this commit the existing "Grant Health Connect access" button in
Boost Night Mode preferences should produce the system consent dialog
instead of the silent-deny path.
Brings the 16 upstream patch-release commits since 3.4.2.1 (merge-base 1393357, Mar 31 2026 → tag 3.4.2.2, Apr 10 2026). All changes are pump-driver bug-fixes and CI; nothing touches Boost-specific code paths. What's pulled in: - Medtrum patch activation/deactivation/priming flow improvements - Equil: setLastDataTime on decode, don't enforce password - Medtronic: getRefreshTime method - CI: tag/branch build workflows, JDK version mapping - Version bump to 3.4.2.2 Files touched: pump/medtrum/*, pump/equil/*, pump/medtronic/*, CI scripts. No conflicts. Boost-V4.4.2-tier-release branch.
…HR platforms Garmin (and other consumer-health platforms that go through Health Connect) batch HR sync overnight rather than streaming it. The result is 0–3% HR coverage during sleep hours and a 67% catchup burst at wake. The existing SleepStateDetector required continuous HR-value qualification, so SLEEPING was never reached on these devices. Two new signals using the transmission pattern itself: 1. Drought-qualified sleep candidacy: when avgHr is null AND we've seen no fresh HR sample for ≥droughtThresholdMin (default 30), AND in outer night window AND steps + meal gates pass, the cycle counts as a sleep candidate. Existing HR-value path still wins when live HR is available. 2. Transmission-resume wake: in SLEEPING, if ≥3 fresh samples arrive in the last 15 min AND the prior drought was ≥droughtThresholdMin, transition AWAKE immediately. The burst-size threshold prevents false wakes from stray mid-night syncs. State.lastFreshHrSampleMs added (persisted across cycles + restarts) to track the most recent live sample timestamp. Backwards-compatible: existing call sites unchanged, existing serialized state deserializes cleanly. Verified empirically against 48h of Tim's Nightscout data — drought signature matches actual sleep periods; 08:00 BST sync burst matches wake.
Adds an NS-uploaded field that records which qualifier promoted the most recent SLEEPING entry: "hr" (HR-value path, avgHr ≤ resting × 1.15) or "drought" (no live HR for ≥droughtThresholdMin — the batched-HR-platform fallback shipped in 95ce6f7). Lets us validate which path is actually firing in production rather than inferring from coverage patterns. Confirmed today (2026-06-06) that Tim's narrowing of Garmin's watch-side sleep window to 02:00–06:00 BST lifted target-window HR coverage from 5.5% → 69.4%, so we expect mostly "hr" entries for Tim going forward, with "drought" as the inner-window fallback. Field set at SLEEPING transitions in both AWAKE→SLEEPING and PRE_SLEEP→ SLEEPING branches; left null in AWAKE/PRE_SLEEP states (default carries through unset State() constructions at exit transitions, which also clears the prior value). Serialized/deserialized for persistence across plugin restarts. Wired into both OpenAPSBoostPlugin (V1) and OpenAPSBoostV3MLG3Plugin (V4.4.x). RT field auto-uploads via existing devicestatus encoder.
Replaces the 28-user 240m-horizon model (LOUO 0.6796) with v9-90m, trained on
32 users (+4 V5+ML users: Tim/Joost/user4/Simon) with a 90-minute hypo target
window instead of 4h.
LOUO AUC vs v8 baseline (240m horizon):
Tim 0.6213 → 0.7586 (+0.14)
Joost 0.6637 → 0.8759 (+0.21)
user4 0.5970 → 0.8218 (+0.22)
Simon 0.6827 → 0.8538 (+0.17)
mean 0.6742 → 0.817 (+0.14)
Backtest on real-world 21d data (Tim/Joost/Simon):
AUC: Tim +0.003, Joost +0.022, Simon +0.018
False-positive reduction at risk>=0.3 (riskScale damping):
Tim −59%, Joost −87%, Simon −75%
v9 strictly dominates v8 (no regressions worth blocking on)
Model file: 50 trees, depth 4, num_leaves 15 — same architecture as v8;
only the label horizon changed. JSON shape unchanged so BoostRiskModel.kt
walks it without code changes.
Mirrors the V1 wiring from the prior commit so the V4.4.x plugin uses the
same v12 model on assets/boost/ rather than getting a feature-size mismatch
and null mlHypoRisk. Same dual-path approach: 8-feature legacy fallback for
v9-vintage models, 53-feature v12 path otherwise.
Changes:
- DetermineBasalBoostV3MLG3.kt: add mlRingBuffer field, timeSinceLastSmbMin
parameter, rewrite ML call site to dual-path inference.
- OpenAPSBoostV3MLG3Plugin.kt: extend the recent-SMB persistence query to
derive both 60-min volume and minutes-since-last-SMB from a single 12-hr
window; pass both through to determine_basal.
Tim is currently the only user on V4.4.x; he's planning to switch to V1
(which is Boost-ML-Beta acting algo with the same v12 ML retrofit). This
keeps him covered with ML hypo-risk gating until the switch.
… load failures
Issue: shipped v12 53-feature model produces null mlHypoRisk on Tim's device
despite dual-path code being present in the APK. Meal model loads + fires
normally; risk model silently returns null. Suspected load-time JSON parse
failure or stuck loadAttempted flag that we can't diagnose without device
logs.
Recovery:
- Asset reverted to v9-90m (8-feature, 50-tree, ~85KB) — known working.
- Dual-path code stays in place; v9's 8 features routes to legacy branch.
- Diagnostic logging added to both BoostRiskModel copies (V1 + V3ML):
* raw byte size logged at load entry
* per-tree parse error caught + logged before re-throw
* exception class name included alongside message
- Same diagnostic logging will help us pin v12's failure mode next attempt.
Mirrored to AndroidAPS Boost-ML-Beta in same commit on that branch.
The AAPS log export only captures D and I level lines; error-level logs (used by aapsLogger.error) are filtered out. To diagnose why the v12 53-feature hypo model silently fails to load on Tim's device, mirror every diagnostic failure path to aapsLogger.info with a "DIAG" prefix so it shows up in the log export. The error-level call is retained for AAPS' internal system log. Assets reverted to v12-90m (53 features, 100 trees, depth 5) on top of the same dual-path inference code. Next time AAPS restarts, the first inference cycle that reaches the ML block will trigger lazy-load, and either: - Success: I/APS "BoostRiskModel loaded: 100 trees, 53 features" - Failure: I/APS "BoostRiskModel DIAG load FAILED ... ExceptionClass: msg" - Per-tree: I/APS "BoostRiskModel DIAG tree N parse failed: ..." Applies to V1 (openAPSBoost/) and V3MLG3 (openAPSBoostV3ML/) copies.
The 6-cycle lookback ring buffer lives on the @singleton determine-basal classes, so it survived across cycles in-process, but StringKey.ApsBoostMlRingBuffer (added to persist it across process restarts) was never read or written, and BoostMlFeatureBuilder.serialize/deserializeBuffer were dead code. After every AAPS restart the lag1..lag5 features cold-started (zero-imputed) for ~6 cycles. Wire it up following the sleep/V5-state pattern (plugin orchestrates the Preferences I/O; the compute class stays pure). Applied to BOTH the active V3MLG3 (V4.4.x) path and the openAPSBoost V1 path: - DetermineBasal*: loadMlRingBufferOnce(raw) restores from storage once per process; serializeMlRingBuffer() exposes the live buffer. - Plugins: load before determine_basal, persist in the .also block. Drop StringKey.ApsBoostMlRecentSmbLog — it had no backing data structure and recent-SMB stats are reconstructed from PersistenceLayer.getBoluses each cycle, so it never needed persisting. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ings The V1 (openAPSBoost) plugin enforces ApsBoostCumulativeSmbCap60Min (default 1.5 U) but never added it to its preference screen, so V1 users had a silently active SMB-stacking cap with no way to see or change it. V3MLG3 already exposes it. Add the same AdaptiveDoublePreference row to V1's Boost Controls list, immediately after Boost Max IOB, matching V3MLG3's placement. Strings already existed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SMB-only hybrid: V1 keeps basal, predictions, and all safety gates; V5's observe-confirm-commit finalDose REPLACES V1's SMB only on cycles V1 permits one (microBolusAllowed), via the existing runShadow hook in the .also block. Override flows through downstream applyBolusConstraints unchanged. - BooleanKey.ApsBoostV5ActiveDosing (default off, defaultedBySM) — instant revert. - runShadow() now returns V5Decision?; takes activeMode/microBolusAllowed/flatBGsDetected. - enableSmbPreChecks wired to V1's real SMB permission in active mode (outer safety envelope); shadow mode keeps the permissive value (telemetry continuity). - sensorQualityOk wired to !flatBGsDetected in active mode. - User-adjustable dose caps: DoubleKey.ApsBoostV5ConfirmedCapU (1.0) / ApsBoostV5CommittedCapU (0.25) plumbed into applyStateDoseCap so the operator can tighten V5 live during alpha. Settings + strings added to V1 Safety screen. - Reason marker records V1's would-be dose for V5-vs-V1 comparison. DEFERRED (documented fast-follows): postActionRiskCheck projected-risk lambda (damper-only; covered by active iobHeadroomBrake + maxIOB clamp + min_guard), profileSwitched/pumpDisconnected/loopSuspended/timeJump reset triggers. Built in isolated worktree off Boost-AAPS-core f0673db. AndroidAPS mirror deferred. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…MMITTED) On a two-phase meal (rise → mid-meal decel → second rise) V5 went COMMITTED→ RECOVERING on the deceleration and had no path back to active dosing, dribbling at 0.4× through the entire second climb (BG ran to 192 on 2026-06-12's first DynISF meal). Add a RECOVERING→COMMITTED transition when the meal genuinely re-accelerates while still well above target (deltaAccl>10, delta>3, offset>20, age>=1). Resumes COMMITTED (1.0×), NOT a new CONFIRMED commit-shot — committedInSession untouched, so Fix 6's multi-CONFIRMED guard is fully preserved. Purely additive; bounded by all Phase-3 gates + the COMMITTED dose cap. Hysteresis (−5..+10 accl dead-band + min-age) prevents flicker. +unit tests for re-engage + no-regression. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rward-only Garmin writes overnight HR into Health Connect as a backdated MORNING BATCH. The forward-only reader advanced its lastSyncedMs marker to "now" on every empty overnight poll, so when the batch landed (timestamps in the past) it fell before the marker and was permanently skipped — ~2/84 overnight HR cycles despite HC holding the data. Resting HR never learned; sleep fell back to the `drought` qualifier. Fix: each poll re-reads a fixed lookback window (18h) and dedupes via an in-memory ConcurrentHashMap keySet of ingested sample timestamps (pruned to the window), instead of reading forward from a high-water mark. Backdated inserts are now caught; insertOrUpdateHeartRate is idempotent so re-reads are safe. lastSyncedMs kept for diagnostics only. Caveat: this recovers resting-HR learning + schedule accuracy from the morning batch; live overnight HR-triggered sleep still depends on Garmin writing to HC in near-real-time (which it apparently does not), so that remains a Garmin limitation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ration brake Sleep gate: V5's SMB override is suppressed when sleepState==SLEEPING → V1's oref1/Boost SMB stands (already respects night mode). V5 still computes shadow telemetry; only the dose override is gated. Robust to overnight CGM artifacts tripping V5 (the 03:54 case). Reason marker "V5 suppressed (SLEEPING)". Deceleration brake (V1-anchored): re-spec decelerationBrake to ease off as delta_accl crosses below zero — the first sign insulin is biting — instead of only at delta_accl<-10. Graduated 1.0@accl=0 → 0.30@accl<=-15, with a delta>8 velocity fallback (mirrors V1's tier gating: delta_accl>0 with delta>8 fallback, DetermineBasalBoost.kt:1505/1526). IOB gate dropped (iobHeadroomBrake covers IOB). Signed `delta` plumbed into Phase3Inputs. Also: fix pre-existing CalibrationBlockTest (stale OpenAPSBoostPlugin constructor — add boostRiskModel/boostMealModel/boostIsfShadow/boostV5Plugin/healthConnectHrIngest mocks) to unblock the plugins:aps test source set. +unit tests for the brake. Per V5-vs-V1 analysis, session-cap/holding-taper dropped (V5≈V1, no excess) and the adaptive learner deferred to V6. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
90g meal 2026-06-14: V5's commit wanted budget 1.84 × 1.8 = 3.31U but was clipped to the 2.0 ceiling, delaying coverage (peak 255). Default stays 1.0U; range to 5.0U so higher-insulin-need users / big meals aren't capped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…+ wire Sensitivity knob V6 #1 — anticipatory pre-meal low target (shadow-first): - MealTimeLearner learns habitual meal times from fresh V5 CONFIRMED commits (rolling 60d, circular-clustered into trusted modes ≥6 events / ≥4 distinct days). - OpenAPSBoostPlugin applies a low target (default 72 mg/dL) during the learned pre-meal window (opens leadMax min before, closes 45 min before the meal), LOWER-ONLY, gated by exercise / post-exercise-recovery override. - ApsBoostV6PreMealTarget toggle (default OFF = shadow: logs "WOULD apply" to reason for validation, no dosing change). Settings: target mg/dL + lead min. V6 tim2000s#2 — wire the previously-unused Sensitivity knob: - ApsBoostV5Sensitivity (0.8–1.2) now multiplies the aggression budget, threaded buildInputs→V5Inputs→decide→aggressionBudget. Default 1.0 = no-op. Live per-user lever for the future nightly learner (motivated by backtest User-D over-dosing). Tests: MealTimeLearnerTest (clustering, circular wrap, confidence, window edges, prune, round-trip), AggressionBudgetSensitivityTest (no-op default, scaling, clamp, floor). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…insulin at hypo-risk The "Hypo Caution" knob (1.0–2.0) raised the mlHypoRiskScale floor (0.5×knob), which made the scale HIGHER at elevated ML hypo-risk → MORE dosing. At knob 2.0 the floor hit 1.0 and the hypo-risk damper was effectively disabled. So dialling up "Hypo Caution" for safety did the opposite. Tim runs this live. Fix: knob now scales the reduction UP and lowers the floor (0.50/knob: 0.50@1.0 → 0.25@2.0). Higher caution ⇒ deeper/wider backoff ⇒ less insulin when risk is elevated. knob 1.0 is an exact no-op vs the prior calibration. Also the per-user lever that closes the backtest's User-D pre-severe-low over-dosing (all of which sits in the mid risk band 0.30–0.60). Test: HypoCautionDirectionTest (no-op at 1.0, monotonic less-with-more-caution, lowered floor, and an explicit guard that knob 2 never doses more than knob 1). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…1/V5 preference split
V5 was a hidden showInList{false} sidecar with an unreachable settings screen, while its
active toggle/caps/V6 pre-meal settings were bolted into the V1 Safety screen. Now:
- Shared engine: OpenAPSBoostPlugin.invoke() body → runEngine(initiator, fallback, v5Active).
invoke() = runEngine(v5Active=false). The SMB override gate now keys off v5Active instead of
the ApsBoostV5ActiveDosing pref. runShadow() still runs either way (telemetry preserved).
- OpenAPSBoostV5Plugin is now selectable (showInList{config.APS}, default isEnabled). Its
invoke() delegates to Provider<OpenAPSBoostPlugin>.runEngine(v5Active=true) and copies
lastAPSResult/lastAPSRun. (Provider breaks the V1↔V5 DI cycle.)
- Preference split: extracted addBoostEngineCategories(...) shared builder. V1 screen = engine
only (V5/V6 controls stripped). V5 screen = engine + a "Boost V5/V6 controls" sub-screen
(Aggression, HypoCaution, Sensitivity, Confirmed/Committed caps, V6 pre-meal enable/target/lead).
- ApsBoostV5ActiveDosing toggle retired (selecting "Boost V5" IS active). Key kept for back-compat.
Migration: select APS = "Boost V5" instead of "Boost" + toggle. All setting values persist (keys
unchanged). No basal/prediction logic added to V5 — it still inherits the engine via OapsProfileBoost.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…se (CRITICAL safety) Incident: during zone-3 exercise (HR 104 bpm, HRR 42%) with 64 steps in the trailing windows, the loop classified RESTING, fired the INACTIVE branch and raised the profile to 130% while BG was falling 12 mg/dL per 5 min. Two defects compounded: 1. Classifier dead zone — the RESISTANCE branch required lowSteps (<30), so 30-99 steps + zone 3/4 HR matched no branch and fell through to RESTING. Widened to !moderateSteps (<100 steps), zone 3-4 → RESISTANCE. (HrActivityCalculator) 2. INACTIVE branch ignored an elevated HR — it only checked STRESS before adding insulin. Added a belt-and-braces guard keyed on the HR ZONE directly (inactivitySuppressedByElevatedHr, zone >= 3): when it holds, suppress the profile-raise entirely and raise the target to 160 instead. Never add basal while the heart says exercise, whatever the fused state says. (OpenAPSBoostPlugin) Tests: dead-zone regression (64 steps + zone3 → RESISTANCE), suppression helper (zone3+ true / zone1-2 / null false), and the RESTING-fallback test repointed to a genuinely-resting input (its old input was the buggy behaviour). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015tCPKdr97rkLLuR7FGL6TU (cherry picked from commit 12b589d)
…se (CRITICAL safety) Incident: during zone-3 exercise (HR 104 bpm, HRR 42%) with 64 steps in the trailing windows, the loop classified RESTING, fired the INACTIVE branch and raised the profile to 130% while BG was falling 12 mg/dL per 5 min. Two defects compounded: 1. Classifier dead zone — the RESISTANCE branch required lowSteps (<30), so 30-99 steps + zone 3/4 HR matched no branch and fell through to RESTING. Widened to !moderateSteps (<100 steps), zone 3-4 → RESISTANCE. (HrActivityCalculator) 2. INACTIVE branch ignored an elevated HR — it only checked STRESS before adding insulin. Added a belt-and-braces guard keyed on the HR ZONE directly (inactivitySuppressedByElevatedHr, zone >= 3): when it holds, suppress the profile-raise entirely and raise the target to 160 instead. Never add basal while the heart says exercise, whatever the fused state says. (OpenAPSBoostPlugin) Tests: dead-zone regression (64 steps + zone3 → RESISTANCE), suppression helper (zone3+ true / zone1-2 / null false), and the RESTING-fallback test repointed to a genuinely-resting input (its old input was the buggy behaviour). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015tCPKdr97rkLLuR7FGL6TU (cherry picked from commit 12b589d)
…onfirm-crash hole
2026-07-23 user-H incident (twice in one afternoon, loop then disabled by
the user): post-rescue tier block demotes T3/T5 to T7/T8, but T7/T8 apply
no fast-carb scaling, and delta-weighted dynamic ISF inflates insulinReq
mid-rebound (3.55U SMB at BG 97, 25 min after a 67 mg/dL low; second
insulin-driven hypo followed). The V6 post-rescue cap inherited the
unrestrained dose ("capped from 4.0U to V1's 3.55U").
Fix: apply the graduated scale (0.3 below BG 120, linear to 1.0 at 170)
to the FINAL microBolus whenever the 45-min post-rescue window is active,
COB == 0 and bg < 170 — independent of tier and of the delta_accl>25
trigger, skipping cycles the in-tier scaling already handled. No velocity
escape inside the window (Fix D contamination argument). Restraint-only.
DB pricing (price_composed_guard.py, refreshed to t=now, 103k dosing
cycles, 9 users): 34% [95% CI 32-37] of removed insulin sits directly
ahead of a second low <70 (07-04 cap shipped at 27%; other levers 14-19%).
Cost: 9% genuine post-hypo meals, median 0.80U under-delivery, episodes
full dosing was not containing (median peak 240). Leave-one-out floor 27%
(drop user D). Incident replay: 9.90U -> 3.05U.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nnt1MR3aRXXFHjxUjMSh8w
(cherry picked from commit 51e7663)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nnt1MR3aRXXFHjxUjMSh8w (cherry picked from commit d721e9b)
…V6 and V7 Builds the anticipation architecture spec's shadow stack (read-only, doses nothing), in the shared engine host (OpenAPSBoostPlugin.runEngine) so it runs identically for plain Boost, V5/V6, and the V7-shadow line. - AnticipationHabitModel: per-user onset-hazard table (hour-of-week + recency), circular- smoothed, per-slot confidence-blended with a cross-user prior; refit offline (not in-loop learning). Exercise pure-per-user; meal hybrid via a breakfast/lunch/dinner prior. - AnticipationOnsetStore: persisted rolling exercise/meal onset history (StringKey JSON, V7 idiom, ~56d window), tolerant deserialize. - RetractableArm: generic arm→confirm/back-out state machine (retractability = the safety mechanism); confirm = event appears, veto = post-rescue window. - AnticipationShadow: orchestrator — onset edge-detect, ~6h refit, predict p(onset) @45min, run both arms, emit anticip=... tag. Never throws. - StringKey ApsBoostAnticipHistory; extractor anticip= parser + 14 DB columns. - 11 pure unit tests green (habit model, store, arm, orchestrator). Phase 3 (per-user pricing) + Phase 4 (within-user trial) remain per the two-test bar; population gating + live action are logged-not-enforced. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nnt1MR3aRXXFHjxUjMSh8w (cherry picked from commit 5ea788a) (cherry picked from commit 43f8266)
…ly-2026 changes The four dev-facing docs/tools were ~49-72 commits behind. Brought current: - Composed post-rescue REBOUND GUARD (scales SMB in the 45-min post-low window, closes the confirm-crash) — README §3, tuning-guide safety layer, simulator v6Override (now modelled, only ever removes insulin). - HR-exercise INACTIVE safety fix (stop adding insulin during elevated-HR exercise) — README §3/§8, tuning-guide safety layer, simulator activity-state note. - Aggression ceiling 1.3 → 1.6 — README §4/§5, tuning-guide spectrum + scenarios, simulator slider max. - A2 confirm-sooner + C5 velocity-budget-floor opt-ins, and the auto-config-managed- switch convention (insulin-adding levers gated to well-controlled) — README §4/§5, tuning guide. - Per-user ANTICIPATION shadow (read-only, learns routine, doses nothing) — README §3, tuning guide, analyser note. - Corrected stale README §8 exercise-into-correction claim; neutralised a NS-host placeholder. All three tools' JS syntax-checked. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nnt1MR3aRXXFHjxUjMSh8w (cherry picked from commit 5c230e0)
…nt the 3 levers; fix accuracy Reworked the monolithic README per new-user feedback: - Slim README = what it is, one-glance how-it-works, GETTING STARTED SAFELY (shadow-first 1-2-3), and THE THREE LEVERS as a clear table (what each is + how up/down changes dosing), with everything else flagged advanced-and-auto-set. Links out to the tools + detail pages. - Split the deep sections into separate pages: docs/v6-how-it-works.md, v6-advanced-settings.md (the 'set for you on install' page — auto-config rules + every advanced setting), v6-heart-rate-and-sleep.md, v6-safety-and-validation.md. - Tuning guide now cross-links to the README levers + the advanced-settings reference where settings are discussed. Accuracy fixes (verified against code via review): - HR-exercise guard: 'stops adding insulin' -> 'suppresses the inactivity profile-raise and lifts target to ~160' (it is the inactivity branch, not a global stop). - Auto-config insulin-adding switches gated by the STRICT well-controlled cut (TBR70<1.5% AND <54<0.3%), not 'consensus targets'; composed-brake floor correctly described as a live runtime gate (TBR63<2.0% AND <70<3.5%), NOT auto-config-managed. - 'one-shot flag' -> per-knob resolution; raise-guard also names the severe TBR54>=1% co-guard. - Aggression budget = per-cycle sizing base (30% floor), not a burst ceiling; non-meal cap notes the velocity-budget path; minGuardBG = configured LGS threshold (default 80). - A2/C5 shorthand replaced with real toggle names (Aggressive early confirm / Velocity budget). - Version naming reconciled (V6 label vs V5 code/keys). 0 broken links; JS + PII checked. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nnt1MR3aRXXFHjxUjMSh8w (cherry picked from commit 4ccf256)
…rnal A2/C5 codes from user docs Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nnt1MR3aRXXFHjxUjMSh8w
…m, fix nits - Sensitivity lever: rewrite to make clear it is an OVERALL-STRENGTH dial for the whole engine, explicitly NOT insulin sensitivity/ISF despite the name (new-user confusion). - 'Moving earlier in the cycle': where the concept is introduced (how-it-works trade-off, Aggressive early confirm setting, ML meal-likelihood note) now states plainly that confirming/dosing earlier means acting on more speculative CGM evidence — a rise that might still fizzle — trading a lower peak for a higher chance of dosing into a non-meal. - Nits: 'three dials, not twenty-four' -> 'just three dials (far fewer than before)'; composed-brake-floor condition corrected to eventualBG >= target+20 (matches code). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nnt1MR3aRXXFHjxUjMSh8w (cherry picked from commit a923280)
…g to go-straight-to-V6 Per product decision: users should go straight to Boost V6, not start on the V1 engine. Auto-config already seeds V6's dials + caps from the user's own recent dosing history read from the LOCAL AndroidAPS database (present via in-place upgrade, DB restore, or NSClient backfilling ~100 days from Nightscout) — from prior AAPS/oref OR Boost V1 — so no V1-shadow warm-up is needed. - Code: move the single APS .setDefault() flag from OpenAPSSMBPlugin to OpenAPSBoostV5Plugin (Boost V6). PluginStore force-enables the default and disables other APS plugins, so a fresh DB selects V6 directly. (Exactly one default allowed.) - Docs: rewrote README 'Getting started' (go straight to V6; seed from historic DB history; make sure NSClient/DB restore is present; shadow 'Boost' plugin + Analyser now OPTIONAL not a required warm-up). Reconciled the two 'shadow is the supported mode' statements (README footer + safety page) to 'experimental algorithm; shadow optional; supported path is V6 active seeded from your history'. The dev-process 'shadow-first for heuristic CHANGES' guidance stays (different sense). Advanced- settings auto-config intro now states the data source (local DB via upgrade/restore/ NS-sync) and that there is no separate NS import inside Boost. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nnt1MR3aRXXFHjxUjMSh8w (cherry picked from commit 6b376cf)
…/table-cell emphasis Per feedback that bold was excessive throughout. Kept: bullet/numbered lead-ins, short **Label:** list intros, and the first (name) cell of tables. Stripped: mid-sentence emphasis in prose, mid-bullet bold, and non-first table cells. Bold spans cut roughly in half across all five doc pages (README 58->18, advanced 76->27, how-it-works 41->19, safety 46->21, hr-sleep 15->3). Multi-line-safe (protect-then-strip); marker balance verified, italics + links intact. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nnt1MR3aRXXFHjxUjMSh8w (cherry picked from commit e29a2b3)
…eal engine Reclaims V1's ~15-min-earlier acceleration response (backtesting/scripts/ 2026-07-v1-acceleration: delta_accl>10 leads V6 CONFIRMED by 15 min at 98% recall; the early bolus is fizzle-safe by size, pure fizzle-low +0.9% no excess). Restores it as a small primer during OBSERVING, delivered as an advance on the CONFIRMED commit-shot. Mechanics (DetermineBasalBoostV5.decide()): - ONCE per OBSERVING session, on delta_accl>10 + rising + every floor clear (recentLow >=80, awake, !exercise, !post-rescue) + maxIOB headroom. Acceleration-scaled (1 + (accl-10)/20, capped 2x base = inside V1's proven fizzle-safe envelope). - Bolus mode: folded into finalDose, exempt from the non-meal v1-cap at the seam. - ADDITIVE up to the fizzle-safe base (primerCapU); the scaled EXCESS is netted off the CONFIRMED shot then COMMITTED holds (move-not-add; net-extra converges to the base). - Session accumulators (primerAppliedU, netting residual) in V5PersistedState, persisted in V5StateStore, reset on IDLE. The follow-through that crashed V1 is guarded by V6's existing brake/RECOVERING. Routing (auto-config managed): well-controlled -> bolus; everyone else -> RETRACTABLE temp-basal fallback (additive-only raise, never cancels a protective zero-temp, expires = retracts). USER OVERRIDE ApsBoostV5PrimerBolusMode forces the bolus. primerCapU sized from the user's own routine SMB (U200 auto-scaled), smaller for hypo-prone (C/tim residual). Keys ApsBoostV5PrimerCapU/PrimerTbrFallback/PrimerBolusMode + UI + strings. 8 unit tests (PrimerTest): fires-in-observing/scaled/additive/residual, floors gate, no-accel, temp-basal-not-folded, once-per-session, netting subtracted, IDLE reset. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015tCPKdr97rkLLuR7FGL6TU (cherry picked from commit 3aa9a4d) (cherry picked from commit 3b482d8)
applyAutoConfig() writes each managed double via an EXPLICIT resolve() call; PrimerCapU was added to managedDoubleKeys/Knobs (so the derivation runs) but its resolve() call was missing, so the cap was never persisted and never resolved — it stayed at 0 and re-derived uselessly every cycle. The tbr-fallback BOOLEAN applied (boolean path loops the list), so a tbr-routed user got the routing but no cap → primer delivered nothing. Caught by Tim on-device. Add the resolve() call (NOT raise-guarded — a tbr-routed non-well-controlled user still needs a non-zero cap to size the retractable temp-basal). Update the exact-key-list test + add a regression guard asserting PrimerCapU is applied and > 0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015tCPKdr97rkLLuR7FGL6TU (cherry picked from commit 5c17220) (cherry picked from commit 65d25d8)
…ainst the commit-shot Tim's rule (tested: the one lever that survives — cap/taper both trade seeds 1:1, this loses none). Keep every primer firing, but at CONFIRM net the ACCUMULATED primer IOB beyond one base off the commit-shot, so the meal's net-extra insulin is bounded to one base no matter how many fizzles preceded it (backtesting: 87% of confirms follow >=1 primer, 57% would net off >0.1U, median 0.39U). - V5PersistedState: primerIobU (cross-session on-board primer insulin, wall-clock decayed exp(-dt/90min), NOT reset on IDLE) + primerIobUpdatedMs. V5Inputs.nowMs (dateUtil.now()). - decide(): accumulate every primer into primerIobU; at the CONFIRMED transition set the netting residual = max(0, primerIobU - primerCap) and consume the credited excess (primerIobU := min(primerIobU, cap)) so a second meal can't re-credit it. Residual spent down against CONFIRMED then COMMITTED holds (unchanged landing). Replaces the old session-scoped scaled-excess netting. - Only ever REMOVES insulin (safe-signed). Does NOT touch pure-fizzle clusters that never confirm (their mild intermediate dip is unaddressed, by design). - V5StateStore persists the two new fields. PrimerTest updated + confirm-net test added. backtesting/scripts/2026-07-primer-clustering (clustering, cap-sweep, taper, confirm-net) to follow. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015tCPKdr97rkLLuR7FGL6TU (cherry picked from commit 6c439c8) (cherry picked from commit 35831c3)
The V1-acceleration early primer shipped to V6-shadow but was never promoted to dev, so it was undocumented. Now promoting it (3 cherry-picks) + this doc entry: auto-config- sized per-user (fraction of Committed cap, capped 0.6U), default 0.0/off, confirm-net bounds a meal's total net-extra to ~one base, and safety-routed (bolus for well- controlled, retractable temp-basal otherwise). Fizzle-safe (+0.9%). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nnt1MR3aRXXFHjxUjMSh8w (cherry picked from commit b3a4cb2)
…gle source of truth) Combines the activity-driven sleep-in backstop with the night-mode state machine. Previously two parallel mechanisms with DIFFERENT thresholds: SleepStateDetector (HR+steps+clock) and StepFeed.sleepInActive (steps-only morning backstop, its own sleepInSteps pref) — they could disagree at e.g. 150 morning steps. Now unified in the detector: - SLEEPING is HELD past nightEnd through the lie-in window [nightEnd, nightEnd+ sleepInHours]; the hard boundary exit moves to lieInEnd (== nightEnd when merge off). - Strong steps-alone wake uses the UNIFIED sleepInStepsThreshold (= ApsBoostSleepInSteps), releasing the lie-in on real morning movement regardless of HR (past the alarm), and still gated on drought-established during the core night (both-required guard holds). - Plugin passes sleepInSteps/sleepInHours into the detector; the standalone StepFeed.sleepInActive gate is DEMOTED to a failsafe that only fires when auto-by-sleep is OFF (clock-only night mode). +3 lie-in tests (holds past nightEnd, releases on threshold steps, hard boundary). V7-SHADOW ONLY per Tim. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pkjoau3i62iYTq1ncXUjoe (cherry picked from commit 3111f79) (cherry picked from commit 0a91ed5)
…IOB + fix cap reset (a) IOB total now counts bolus + max(0, basal): negative temp-basal IOB (loop zero- temping a real descent) no longer shrinks the total and arms the gate during a genuine insulin-driven low. (b) Consecutive-compression counter latches while the pattern persists instead of resetting on cap-trip, so a sustained low is followed after ~15 min and cannot re-arm. +1 test (negative basal -> followed, not masked). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PBQgjnRqZuN6rKxYaLJZMV (cherry picked from commit 7dfb3f5) (cherry picked from commit f23111b)
The learned wake trained a circular mean over ~40 sessions and clamped it to ±90min of the configured NightModeEnd — but symmetrically, so for a highly-variable waker (real wake 04:30–09:30) the early-biased learned mean (05:51) dragged a sensible 07:30–08:00 configured night-end DOWN to ~06:00, lifting sleep protection ~2h before the user's own setting (the unsafe direction, into the dawn window). Real-time HR/step detection mostly holds SLEEPING past it, but a marginal-HR morning could tip a false early AWAKE. Fix: clampToConfiguredBand gains allowEarlier (default true, unchanged for night-START where later-to-bed drift is expected/safe); the night-END call passes allowEarlier=false so learning may only push the wake bound LATER than configured, never earlier. Genuine early wakes are still caught by real-time detection. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015tCPKdr97rkLLuR7FGL6TU (cherry picked from commit 660be24) (cherry picked from commit 4169a17)
…oostSleepInSteps) PRE_SLEEP had NO activity-based exit — its only route to AWAKE was the nightEnd clock boundary. So a user up and active before nightEnd (e.g. 05:00, 90 steps/5min, BG rising) was pinned in PRE_SLEEP — night-mode SMB suppression on — until the clock, even though clearly awake. High steps also block the slide to SLEEPING, so the state froze. The night-end clamp fix (660be24) made this worse by extending the boundary later. Fix: PRE_SLEEP now exits to AWAKE immediately when 60-min cumulative step growth clears the user's ApsBoostSleepInSteps threshold (constant fallback 250) — ungated by clock/drought, since PRE_SLEEP is not confirmed sleep so activity is decisive. Same signal the SLEEPING steps-alone wake uses; threshold hoisted so both branches share it. Not a genuine sleep→wake, so wakeReason stays null (won't train the wake learner). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015tCPKdr97rkLLuR7FGL6TU (cherry picked from commit 89f0740) (cherry picked from commit d356b9e)
…e shipping line All experimental shadow layers logged on ONE device (2026-07-29 review): V7 5399 rows, Twin 3325, antBackout 2566, accelMeal 262 for a single user and ZERO for the other eight. No V7/Twin/anticipation conclusion could be cross-user, so the shadow pipeline could not meet the GroupKFold bar the analysis side enforces. Ports from Boost-V7-shadow, unchanged: - openAPSBoostV7/ — residual tracker, distributional sizer, shadow seam + tests - openAPSBoostTwin/ — physiological model, EnKF, shadow, withdrawal, anticipation back-out + tests - openAPSBoostTing/ — risk-limited smoothing planner (pure) + test - OpenAPSBoostPlugin seams: V7/Twin/back-out invocation, accelMeal onset detector, post-meal plateau nudge - RT boostV7_* fields, ApsBoostV7ResidualPools key, UKF bucketing parity test READ-ONLY. Every seam is failure-swallowed twice (each shadow's own runCatching plus a belt-and-braces one at the call site) and delivered dosing is bit-identical with or without them. Twin telemetry rides in the reason string rather than an RT field, which is the existing fix for the ART VerifyError register-allocation crash. Deliberately NOT taken from V7-shadow: OpenAPSBoostV5Plugin/OpenAPSSMBPlugin, which differ only because V7-shadow predates the V6-default-engine move. Copying them would have reverted Boost V6 as the default APS plugin. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BTN6FX7LQH621CQC1bhk8C (cherry picked from commit 639280c)
…load Adds LoopHub.loopStatus (CLOSED/LGS/OPEN/SUSPEND/DISCONN/DISABLED/SUPERBOLUS from runningMode) and lastLoopEpochMs (loop.lastRun.lastAPSRun), emitted on the newest /sgv.json element alongside the existing iob/cob/tbr. Lets the Boost Garmin face show the IOB + loop tier. Read-only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PBQgjnRqZuN6rKxYaLJZMV (cherry picked from commit 2b31c67) (cherry picked from commit 12366a2)
LoopHub.variableSensInUnits = APS variable_sens (fallback profile ISF), converted to the user's glucose units; emitted as 'isf' on the newest reading. Feeds the ISF column of the Boost BG-Ring Garmin face. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PBQgjnRqZuN6rKxYaLJZMV (cherry picked from commit 6bb4fd7) (cherry picked from commit c433e0c)
Reported live: EXERCISE "INACTIVE - 130%" at 06:09 during sleep-in, BG 71 and falling, after an overnight descent from ~200. INACTIVE raises the profile (ApsBoostInactivityPct, factory 130), which scales DynISF by 100/130, lifts basal 30% and scales the Boost SMB tiers — it ADDS insulin. That is intended for a genuinely sedentary WAKING user and is left alone. It must never coincide with sleep, and it did. WHY THE EXISTING GUARDS DID NOT STOP IT. A sleeping user has near-zero steps by definition, so `recentSteps60Min < inactivitySteps` is satisfied every night; the branch reads "asleep" as "sedentary". Two shipped protections should have caught it and neither could: 1. `sleepInActive` is `nowMs in nightEndMs until (nightEndMs + sleepInMs)` — the lie-in window opens AT night end. At 06:09 with a 07:00 end it is false by construction, so the failsafe cannot engage before dawn. The core night was never its job. 2. `lieInFailsafeEngages` stood down whenever `autoBySleepActive && detectorSleeping`, deferring to night-mode suppression. But `isInNightSleepPeriod()` returns false on its FIRST line when ApsBoostNightModeEnabled is false, which ships false — so the detector is never consulted and the failsafe deferred to a suppressor that was not running. A correctly-SLEEPING detector was what disarmed the backstop. FIX. Make the exclusion a property of the predicate rather than of the gate above it, since that gate has several defeat paths: - `inactivityEligible` gains `sleepInActive` and `asleep` and returns false for either. `asleep` is SLEEPING or PRE_SLEEP — the user is in bed for both — which covers the core night that the lie-in window cannot reach. - `lieInFailsafeEngages` gains `nightModeEnabled`, so it only defers to night-mode suppression when that suppression is actually running. - Split `inactivityStepsMet` out of `inactivityEligible` so the plugin can tell "moving" from "sedentary because asleep", and breadcrumb the second into the NS reason rather than falling through silently. This also closes the 250/499 band, where sleepInSteps (250) and inactivitySteps (500) read the same blended count and disagree — the lie-in stands down as awake-enough while INACTIVE fires as sedentary-enough. The detector now covers it. Both changes only ever WITHHOLD insulin, so the direction is safe and no backtest is needed to justify it. The genuinely-sedentary-and-awake raise is unchanged, and is covered by a test. 6 tests added: suppression during lie-in, suppression on detector sleep, the 250-499 band, the raise still firing when awake, the failsafe engaging with night mode off, and it still standing down when night mode is genuinely suppressing. Existing call sites updated for both signatures. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BTN6FX7LQH621CQC1bhk8C (cherry picked from commit c18346f)
…ode is set to Follows the sleep/INACTIVE exclusion in c18346f. That fix keyed on the sleep detector, which covers irregular sleep but produces a state on only about two thirds of cycles in the field and none at all for some users. So "INACTIVE never fires overnight" still depended on the detector having data, or on the user having enabled night mode. THE SEPARATION. ApsBoostNightModeEnabled answers "should Boost dose overnight?", which is a policy choice. The start and end times answer "when is this user overnight?", which is a fact about them. isInNightSleepPeriod() returns false on its first line when the flag is off, so switching the policy off also made the system forget when night was. The INACTIVE branch raises the profile — lower ISF, higher basal, larger SMB tiers, all adding insulin — and needs the fact, not the policy. New NightWindow.contains() reads ApsBoostNightModeStart/End (defaults 22:00 and 07:00, or whatever the user has configured) REGARDLESS of the enable flag, and INACTIVE is suppressed inside it. No new preference, no sensor dependency: it works with no HR, no steps and no detector, which is what makes the guarantee hold by default rather than by configuration. Three independent exclusions now, none of which covers every case alone: inNightWindow the configured clock window — always available, covers the common case asleep detector SLEEPING/PRE_SLEEP — covers sleep outside the window, when it has data sleepInActive the morning lie-in — steps-based, covers oversleeping past the alarm isInNightSleepPeriod() now delegates its window arithmetic to the same helper, so there is one source of truth rather than two copies to drift apart. Its equal-times behaviour is unchanged: an empty window, not a whole day, since the wrap branch would otherwise union to cover everything and silently stop Boost dosing (the 2026-07-02 incident). Measured exposure this removes, from the pre-fix backtest: 6.3% of all sleeping cycles across the cohort ran on a raised profile, and for user I every sleeping cycle logged did. 5 tests added: the wrap past midnight including 06:09 from the original report, a non-wrapping window, equal times as empty, inclusive start with exclusive end, and INACTIVE suppressed inside the window. Suite 690 tests, 0 failures. The suppression reason now names which of the three exclusions fired, so it stays auditable in Nightscout. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BTN6FX7LQH621CQC1bhk8C (cherry picked from commit 2b3b986)
tim2000s
added a commit
that referenced
this pull request
Oct 8, 2026
Patch number on the released line for fixes that only withhold or bound insulin, except where noted in the individual commits: tranche release bounded by the caps and gates (#1), pre-meal target off at night, while asleep, under a temp target and after a low (#8), V6 error falls back to base oref (#9), primer respects running low temps and V6's hard gates (#10), missing heart rate blocks the INACTIVE raise (#12), the sleep boundary exit no longer reopens INACTIVE or the gate for a cycle (#18), percent-scale divisor no longer shrinks above 180 (#4), SMB clamped to max_iob (#5), G3 hold kept after a recent low (#19), spike override limited to Tier 8 outside post-rescue and ML downgrade (#20), one confirm per meal session (#7), stale meal state reset (#17), and exercise classification restored inside the night window (#11, a regression from the overnight gate fix). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014RS6G2gfvdzoJAx9wPCeA4
tim2000s
added a commit
that referenced
this pull request
Oct 8, 2026
Patch number on the released line for fixes that only withhold or bound insulin, except where noted in the individual commits: tranche release bounded by the caps and gates (#1), pre-meal target off at night, while asleep, under a temp target and after a low (#8), V6 error falls back to base oref (#9), primer respects running low temps and V6's hard gates (#10), missing heart rate blocks the INACTIVE raise (#12), the sleep boundary exit no longer reopens INACTIVE or the gate for a cycle (#18), percent-scale divisor no longer shrinks above 180 (#4), SMB clamped to max_iob (#5), G3 hold kept after a recent low (#19), spike override limited to Tier 8 outside post-rescue and ML downgrade (#20), one confirm per meal session (#7), stale meal state reset (#17), and exercise classification restored inside the night window (#11, a regression from the overnight gate fix). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014RS6G2gfvdzoJAx9wPCeA4 (cherry picked from commit a3cc1af)
tim2000s
added a commit
that referenced
this pull request
Oct 8, 2026
Patch number on the released line for fixes that only withhold or bound insulin, except where noted in the individual commits: tranche release bounded by the caps and gates (#1), pre-meal target off at night, while asleep, under a temp target and after a low (#8), V6 error falls back to base oref (#9), primer respects running low temps and V6's hard gates (#10), missing heart rate blocks the INACTIVE raise (#12), the sleep boundary exit no longer reopens INACTIVE or the gate for a cycle (#18), percent-scale divisor no longer shrinks above 180 (#4), SMB clamped to max_iob (#5), G3 hold kept after a recent low (#19), spike override limited to Tier 8 outside post-rescue and ML downgrade (#20), one confirm per meal session (#7), stale meal state reset (#17), and exercise classification restored inside the night window (#11, a regression from the overnight gate fix). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014RS6G2gfvdzoJAx9wPCeA4 (cherry picked from commit a3cc1af)
This branch has not been deployed
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.
No description provided.