Repository navigation
Fix profile loading in boost_simulator.html - #18
Open
luborjurena wants to merge 448 commits into
Open
luborjurena wants to merge 448 commits into
luborjurena wants to merge 448 commits into
Conversation
Brings Boost V1 plugin to parity with V3MLG3 v4.4.4 acting layer. Fix A v2 (v4.4.4): - recentLowBG45Min computed in plugin from existing 60-min recentBgReadings - inPostRescueWindow = recentLowBG45Min < 75 - Guards T3 (UAM_BOOST), T4 (UAM_HIGH_BOOST), and T5 (PERCENT_SCALE) Fix B (v4.4.3): - recentSmbVolume60Min queried from PersistenceLayer (BS.Type.SMB, isValid) - CUMULATIVE_SMB_CAP tier suppresses microBolus when cumulative ≥ cap Fix D NOT ported: V1's fast-carb logic uses only velocityOverride, no eventualBgOverride — the bug Fix D addressed doesn't exist in V1. Backtest rationale: see DetermineBasalBoostV3MLG3 comments and prior commits. V1 inherits the same parameter defaults (cap 1.5U, window 45 min, threshold 75).
Bring V1 on Boost-V4.4.2-tier-release (Boost-AAPS-core) to literal file
identity with V1 on Boost-ML-Beta (AndroidAPS). 5 files now bit-identical
across the two repos; RT.kt gains the matching isfShadow_* fields.
What V1 picks up (delta from prior Boost-AAPS-core V1 state):
- Layer A: local BoostRiskModel + new BoostMealModel (mlMealLikely)
with directionNum bucketing matching training-time encoding
- Layer C: G3 Pre-UAM Uncertainty Hold gating T5/T6/T7/T8 with 3
release conditions (delta_accl>10, BG>160+delta>5, mlMealLikely>0.50)
- Layer D: V4.4.2 tier refinements (T4 sustained-velocity fallback
`delta>8`, T6 BG floor `bg>110`, velocityOverride lowered 15→10,
eventualBgOverride with Fix D gating on recentLowBG>=75)
- BoostIsfShadow: V4.4.2-style EMA(τ=3h) TDD sensitivity ratio computed
in parallel for observability (does not affect dosing)
- V5 silent shadow hand-off: every V1 cycle invokes
boostV5Plugin.runShadow() so V5 sees parallel data
- RT.minGuardBG populated for V5 hard-gate
Models in openAPSBoost (local) coexist with their twins in
openAPSBoostV3ML — different fully-qualified names, no DI conflict.
V3MLG3 plugin (the V4.4.4 acting algorithm) is unaffected.
New SleepStateDetector with 3-state machine: AWAKE → PRE_SLEEP → SLEEPING.
Enter PRE_SLEEP (time-based) when within ApsBoostPreSleepLeadMin (default
60 min) before nightStart. PRE_SLEEP engages night-mode SMB suppression
early so the user doesn't carry excess IOB into the night.
Enter SLEEPING (HR + steps + time + meal-gate) when all hold for
ApsBoostSleepHysteresisMin (default 10 min):
- avgHr ≤ HRrest × 1.15
- steps_15min < 50
- clock ∈ outer night window
- mlMealLikely < 0.30 (or null)
Exit SLEEPING → AWAKE requires BOTH (per 2026-06-02 spec — BG trending
up alone is NOT sufficient; REM sleep can drive HR rises without wake):
- steps_15min ≥ 100
- avgHr > HRrest × 1.25 sustained ≥ ApsBoostWakeHrHysteresisMin
OR clock exits outer window (hard morning fallback).
Wired into isNightModeActiveImpl() via new BooleanKey
ApsBoostNightModeAutoBySleep (default false, opt-in). When on, sleep
state can both extend night-mode past nightEnd and engage it early via
PRE_SLEEP.
State persisted to StringKey ApsBoostSleepState across plugin restarts.
Evaluated at end of invoke() once mlMealLikely is available; cached for
the next isNightModeActive() call.
New keys:
BooleanKey.ApsBoostNightModeAutoBySleep
IntKey.ApsBoostPreSleepLeadMin
IntKey.ApsBoostSleepHysteresisMin
IntKey.ApsBoostWakeHrHysteresisMin
StringKey.ApsBoostSleepState (persisted JSON)
NS observability: sleep state appended to rT.reason (e.g. "sleep=SLEEPING; ").
Telemetry can drive retrospective tuning once HR upload to NS lands.
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>
…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)
…ard-low forecast
The plateau-nudge shadow scraped oref's forward-low guard out of the formatted reason with
Regex("minGuardBG ([0-9.]+)"). That character class has no minus sign, so on a NEGATIVE
minGuardBG — a deep forward-low forecast, i.e. exactly the dangerous case — the match failed,
the value was null, and the veto `minGuardMgdl != null && minGuardMgdl < 85.0` short-circuited
to false. The floor whose stated job is "never nudge into a low" was disabled precisely when
the forecast was worst.
Verified on four live cycles (2026-08-02 16:26-16:41, minGuardBG -25.1..-18.6 mmol) where the
live dose path was HARD-blocking on min_guard_bg while this shadow reported floor="ok". That
descent ended at 55 mg/dL.
Now reads rT.minGuardBG — the typed mg/dL value the engine already publishes — so there is no
parsing and no mmol magnitude heuristic (which would itself have multiplied a genuine sub-30
mg/dL value by 18). A null now VETOES. Also adds the missing lower bound on the trigger:
`trend <= 1.7` was unbounded below, so a -25 mg/dL/5min freefall counted as a plateau.
SHADOW ONLY — doses nothing. But every plateau evaluation banked to date used the broken floor,
so existing shadow data over-counts safe nudges and must not be used for pricing until it has
been re-collected.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BTN6FX7LQH621CQC1bhk8C
…d say where they live User feedback: "'Sensitivity' doesn't exist as a setting — assume it means 'Meal-detection Sensitivity' in advanced settings — might be worth changing in the Readme as it's easier to work out settings if there is an exact match of terms used in the app and documentation." Correct, and the same mismatch applied to a second dial. README said app actually says where Aggression Aggression Boost V6 HypoCaution Hypo Caution (two words) Boost V6 Sensitivity Meal-detection Sensitivity Boost V6 -> Advanced Settings Renames both, adds a "Where" column with the in-app path, and states that the labels used are the exact ones on those screens. Also corrects the description of the third dial rather than just its name. Checked against the code: the value reaches AggressionBudget as `sensitivityUserKnob` and scales the per-cycle insulin budget. So it has nothing to do with meal DETECTION — the README's "overall strength" description was right and the app's own title is the inaccurate part. The table now says so explicitly, since a reader who trusts the label would otherwise reach for it to change meal detection. NB the in-app title is the thing that is actually wrong here. Renaming the setting is a separate decision (it would move users' mental model and touch translations) and is not done here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BTN6FX7LQH621CQC1bhk8C
… simulator The primer has been live-dosing since 2026-07-20 and none of the three tools knew it existed, so a user reading the guide could not find out what the setting did, the analyser silently ignored the primer breadcrumb, and the simulator's dose arithmetic did not match the engine's whenever a primer fired. Guide: the two settings a user can actually reach, named exactly as the app labels them. The base is additive and the velocity-scaled excess is netted off the commit-shot, which is the part that decides whether it costs insulin or only moves it, so that is stated rather than left implied. The routing flag is auto-config managed and is described as such rather than as a dial. Analyser: the primer writes a breadcrumb into the reason rather than a structured field, so it is parsed from the text. All four forms are handled, and the two that compute a primer without delivering it are counted separately, since a primer that was computed and then subsumed says something different about the configuration than one that never fired. It is reported apart from the paired dose comparison, because folding it into a V6 total would double-count insulin the commit-shot has already given up. Simulator: implements the primer against the same constants and the same gate order as DetermineBasalBoostV5, applied after the dose is finalised and clamped to the remaining IOB headroom. Exposes the base as a slider and the once-per-session guard as a toggle, and states in the breakdown why the primer did not fire when it did not. Checked rather than assumed: scale 1.25 on acceleration 15 gives 0.25 U from a 0.20 U base; acceleration 60 clamps at twice the base; each gate blocks with the right reason; and with 0.05 U of headroom left the total stays at 0.05 U rather than adding on top. Analyser tests 17 to 19, including a regression that pairing is untouched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BTN6FX7LQH621CQC1bhk8C (cherry picked from commit dadd4c7)
Some Nightscout profiles store basal values as text instead of numbers, which crashed the page when it tried to format them. Also fixed ISF and target not being converted to mg/dL when the profile is set to mmol/L.
tim2000s
added a commit
that referenced
this pull request
Oct 8, 2026
…#12, #18) All changes sit in the plugin seam and its helpers. Nothing is added to any determine_basal method and RT gains no field; telemetry rides in the reason string. #11 (regression from the overnight gate fix). Activity classification moved into the pure classifyBoostActivity and runs whatever the Boost gate says, so exercise inside the night window keeps its reduced profile and 150 mg/dL target, and an evening session crossing night start is no longer cut off (which also started post-exercise recovery early). The INACTIVE raise still needs the gate open, as well as its existing sleep, lie-in and night-window exclusions. #12. With HR integration on, the INACTIVE +30% raise now needs positive HR evidence of rest: inactivityRaiseBlockedByHr returns true for a null classification (no HR in the window, or a stuck value), state HR_UNAVAILABLE. Failing closed was chosen over leaving it open because the user enabled HR as evidence for the sedentary claim the raise rests on, the same reasoning F1 applies to a dark pedometer, and because the open direction carried 3,997 of B's 4,146 raised cycles. With HR integration off the raise is step-only, unchanged. The frozen-HR comment that called the fallback safe-side is corrected. #18. sleepSignals holds the sleep exclusions for the sleep hysteresis plus one cycle after the detector leaves SLEEPING by its clock boundary. A pre-window sleep was ended by the boundary rule on the next cycle, which left one AWAKE cycle that opened INACTIVE and, for V1 users with sleep-driven night mode, the gate. The hold also applies to the morning boundary exit, delaying Boost by about 15 min there. The detector is unchanged. #1. The confirm-tranche release is held to the rest of the V6 dose's bounds through ConfirmTrancheController.onCycleBounded: dropped unless the state is COMMITTED (a CONFIRMED cycle replaces the hold), when a phase-3 hard gate fired, or inside the post-rescue window, and clamped to what is left of min(boost maxIOB, maxIOB) minus IOB and of the CONFIRMED cap after the cycle's own dose. A hold is also dropped on any cycle where the override block does not run. Logged as trancheGate=dropped:<why>,<U> or trancheGate=clamped:<granted>-><delivered>; the tranche= tag is unchanged. #8. The lowered pre-meal target applies only outside the configured night window, with the detector neither SLEEPING nor PRE_SLEEP, with no temp target and outside the post-rescue window (logged "V6 pre-meal SUPPRESSED (<why>)"). The learner records a session only when it starts outside the night window with the detector AWAKE, and learned events whose local time falls in the configured window are dropped from the stored history (checked each cycle, persisted when anything is dropped). A window that wraps midnight is the union of [start, 24:00) and [00:00, end); equal times are an empty window, as NightWindow has it. #9. When V6 is active and returns no decision, V1's SMB is capped at the base oref SMB. baseOrefSmb and its tests are ported from 2b7e93675a (tier2-v6-oref-ceiling); the IDLE ceiling and obsOref shadow are not. #10. The primer temp-basal route reads the temp V1 leaves running when it returns "no temp required" with no rate, so a running zero or low temp below basal now wins, and a running temp at or above the primer rate is left alone. Tests: ActivityGateClassificationTest, SeamSafetyGatesTest, and additions to V6StandDownTest, MealTimeLearnerTest, HrActivityCalculatorTest and ConfirmTrancheControllerTest. With the pre-fix behaviour restored inside the new helpers, 19 of the 98 tests in these classes failed, at least one per 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 d80847f41554d626bbf95fc0a59098363c0b7d00)
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
…#12, #18) All changes sit in the plugin seam and its helpers. Nothing is added to any determine_basal method and RT gains no field; telemetry rides in the reason string. moved into the pure classifyBoostActivity and runs whatever the Boost gate says, so exercise inside the night window keeps its reduced profile and 150 mg/dL target, and an evening session crossing night start is no longer cut off (which also started post-exercise recovery early). The INACTIVE raise still needs the gate open, as well as its existing sleep, lie-in and night-window exclusions. HR evidence of rest: inactivityRaiseBlockedByHr returns true for a null classification (no HR in the window, or a stuck value), state HR_UNAVAILABLE. Failing closed was chosen over leaving it open because the user enabled HR as evidence for the sedentary claim the raise rests on, the same reasoning F1 applies to a dark pedometer, and because the open direction carried 3,997 of B's 4,146 raised cycles. With HR integration off the raise is step-only, unchanged. The frozen-HR comment that called the fallback safe-side is corrected. plus one cycle after the detector leaves SLEEPING by its clock boundary. A pre-window sleep was ended by the boundary rule on the next cycle, which left one AWAKE cycle that opened INACTIVE and, for V1 users with sleep-driven night mode, the gate. The hold also applies to the morning boundary exit, delaying Boost by about 15 min there. The detector is unchanged. bounds through ConfirmTrancheController.onCycleBounded: dropped unless the state is COMMITTED (a CONFIRMED cycle replaces the hold), when a phase-3 hard gate fired, or inside the post-rescue window, and clamped to what is left of min(boost maxIOB, maxIOB) minus IOB and of the CONFIRMED cap after the cycle's own dose. A hold is also dropped on any cycle where the override block does not run. Logged as trancheGate=dropped:<why>,<U> or trancheGate=clamped:<granted>-><delivered>; the tranche= tag is unchanged. night window, with the detector neither SLEEPING nor PRE_SLEEP, with no temp target and outside the post-rescue window (logged "V6 pre-meal SUPPRESSED (<why>)"). The learner records a session only when it starts outside the night window with the detector AWAKE, and learned events whose local time falls in the configured window are dropped from the stored history (checked each cycle, persisted when anything is dropped). A window that wraps midnight is the union of [start, 24:00) and [00:00, end); equal times are an empty window, as NightWindow has it. base oref SMB. baseOrefSmb and its tests are ported from 2b7e93675a (tier2-v6-oref-ceiling); the IDLE ceiling and obsOref shadow are not. returns "no temp required" with no rate, so a running zero or low temp below basal now wins, and a running temp at or above the primer rate is left alone. Tests: ActivityGateClassificationTest, SeamSafetyGatesTest, and additions to V6StandDownTest, MealTimeLearnerTest, HrActivityCalculatorTest and ConfirmTrancheControllerTest. With the pre-fix behaviour restored inside the new helpers, 19 of the 98 tests in these classes failed, at least one per 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 d80847f41554d626bbf95fc0a59098363c0b7d00) (cherry picked from commit a33752c)
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
…#12, #18) All changes sit in the plugin seam and its helpers. Nothing is added to any determine_basal method and RT gains no field; telemetry rides in the reason string. #11 (regression from the overnight gate fix). Activity classification moved into the pure classifyBoostActivity and runs whatever the Boost gate says, so exercise inside the night window keeps its reduced profile and 150 mg/dL target, and an evening session crossing night start is no longer cut off (which also started post-exercise recovery early). The INACTIVE raise still needs the gate open, as well as its existing sleep, lie-in and night-window exclusions. #12. With HR integration on, the INACTIVE +30% raise now needs positive HR evidence of rest: inactivityRaiseBlockedByHr returns true for a null classification (no HR in the window, or a stuck value), state HR_UNAVAILABLE. Failing closed was chosen over leaving it open because the user enabled HR as evidence for the sedentary claim the raise rests on, the same reasoning F1 applies to a dark pedometer, and because the open direction carried 3,997 of B's 4,146 raised cycles. With HR integration off the raise is step-only, unchanged. The frozen-HR comment that called the fallback safe-side is corrected. #18. sleepSignals holds the sleep exclusions for the sleep hysteresis plus one cycle after the detector leaves SLEEPING by its clock boundary. A pre-window sleep was ended by the boundary rule on the next cycle, which left one AWAKE cycle that opened INACTIVE and, for V1 users with sleep-driven night mode, the gate. The hold also applies to the morning boundary exit, delaying Boost by about 15 min there. The detector is unchanged. #1. The confirm-tranche release is held to the rest of the V6 dose's bounds through ConfirmTrancheController.onCycleBounded: dropped unless the state is COMMITTED (a CONFIRMED cycle replaces the hold), when a phase-3 hard gate fired, or inside the post-rescue window, and clamped to what is left of min(boost maxIOB, maxIOB) minus IOB and of the CONFIRMED cap after the cycle's own dose. A hold is also dropped on any cycle where the override block does not run. Logged as trancheGate=dropped:<why>,<U> or trancheGate=clamped:<granted>-><delivered>; the tranche= tag is unchanged. #8. The lowered pre-meal target applies only outside the configured night window, with the detector neither SLEEPING nor PRE_SLEEP, with no temp target and outside the post-rescue window (logged "V6 pre-meal SUPPRESSED (<why>)"). The learner records a session only when it starts outside the night window with the detector AWAKE, and learned events whose local time falls in the configured window are dropped from the stored history (checked each cycle, persisted when anything is dropped). A window that wraps midnight is the union of [start, 24:00) and [00:00, end); equal times are an empty window, as NightWindow has it. #9. When V6 is active and returns no decision, V1's SMB is capped at the base oref SMB. baseOrefSmb and its tests are ported from 2b7e93675a (tier2-v6-oref-ceiling); the IDLE ceiling and obsOref shadow are not. #10. The primer temp-basal route reads the temp V1 leaves running when it returns "no temp required" with no rate, so a running zero or low temp below basal now wins, and a running temp at or above the primer rate is left alone. Tests: ActivityGateClassificationTest, SeamSafetyGatesTest, and additions to V6StandDownTest, MealTimeLearnerTest, HrActivityCalculatorTest and ConfirmTrancheControllerTest. With the pre-fix behaviour restored inside the new helpers, 19 of the 98 tests in these classes failed, at least one per 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 d80847f41554d626bbf95fc0a59098363c0b7d00) (cherry picked from commit a33752c)
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.
Some Nightscout profiles store basal values as text instead of numbers, which crashed the page when it tried to format them. Also fixed ISF and target not being converted to mg/dL when the profile is set to mmol/L.
prof.basal[].value comes as a string ("0.5552", not 0.5552) from my Nightscout.
