Skip to content

Fix profile loading in boost_simulator.html - #18

Open
luborjurena wants to merge 448 commits into
tim2000s:devfrom
luborjurena:fix/boost-simulator-profile-load
Open

luborjurena wants to merge 448 commits into
tim2000s:devfrom
luborjurena:fix/boost-simulator-profile-load

Conversation

@luborjurena

Copy link
Copy Markdown

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.
Screenshot 2026-08-11 at 15 54 01

Tim Street and others added 30 commits June 1, 2026 21:53
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>
tim2000s and others added 23 commits July 28, 2026 08:39
…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

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants