Skip to content

Stale alerts on CGM delete, remove stuck-value detector, rename to Libre 3 / 3+ - #13

Open
ps2 wants to merge 5 commits into
mainfrom
fix/stale-alerts-and-stuck-detector
Open

Stale alerts on CGM delete, remove stuck-value detector, rename to Libre 3 / 3+#13
ps2 wants to merge 5 commits into
mainfrom
fix/stale-alerts-and-stuck-detector

Conversation

@ps2

@ps2 ps2 commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Three small fixes.

1. Stale alerts after the CGM is deleted

A user removed the Libre CGM after their sensor expired and switched back to a G7, then kept getting "sensor about to expire" alerts a week later — once per app launch.

Nothing retracted the alerts on the deletion path:

  • retractExpiryAlerts was reachable only from discardSensor, which only Replace Sensor calls.
  • The Delete CGM button went straight to notifyDelegateOfDeletion, skipping LibreLoopCGMManager.delete entirely.
  • Loop's AlertStore keeps alerts for the whole local-cache window (90 days in this build), and launch-time playback rebuilds any past-due .delayed alert as .immediate and presents it again (StoredAlert.swift:158-166, AlertManager.swift:515). So the scheduled expiry alerts re-fired on every launch until acknowledged.

delete now retracts and ends with notifyDelegateOfDeletion instead of a bare completion(). That was wrong in the other direction too — overriding delete without re-issuing the notification meant the debug-menu delete tore down BLE but never actually removed the manager from Loop.

Retraction covers every alert the manager can issue, not just the expiry set: playback replays anything left unacknowledged and unretracted, so a standing sensorAttention or reconnectNeedsReScan notice produces the same symptom. discardSensor retracts the same set.

Workaround for anyone already affected: tap OK on the alert rather than dismissing it. That records acknowledgement and drops it from playback permanently.

Covering this needed the test target to actually run, so it also adds a shared LibreLoop.xcscheme. The target was wired up correctly but unreachable — the project had no shared scheme, so its schemes were per-user and invisible at workspace level, and building the project standalone can't resolve LibreCRKit / LoopKit / LoopAlgorithm.

2. Remove the stuck-value detector

Every realtime frame already logs the full decrypted plaintext plus the decoded word, DQ error, sensor condition and actionability — enough to review a frozen-value report directly. Classifying the anomaly in the app on top of that earns nothing, so the STUCK: run-length check is gone.

3. Rename to "FreeStyle Libre 3 / 3+"

Both strings Loop uses to identify the plugin: the bundle display name in the Add CGM picker, and localizedTitle on the configured-CGM settings row.

Screen titles, alert body text and the HKDevice name keep the plain "FreeStyle Libre 3" — the slash reads badly in prose and truncates in a nav bar, and the HKDevice name is written into existing HealthKit samples.

Testing

  • Workspace builds clean (Loop scheme, iPhone 17 simulator).
  • 8 tests pass via the new scheme:
xcodebuild -workspace LoopWorkspace.xcworkspace -scheme LibreLoop \
  -destination 'platform=iOS Simulator,name=iPhone 17' test
  • The 3 new LibreLoopAlertRetractionTests assert that both exit paths retract every identifier in allAlertIdentifiers, that delete still notifies the delegate, and that the identifier list stays complete. Verified against the pre-fix delete — it fails on both the missing retractions and the missing delegate notification.

Not runtime-verified end to end: confirming the alert fix on device needs a paired sensor, deleting the CGM, and checking that no alert appears on subsequent launches.

ps2 added 4 commits September 8, 2026 01:08
The detector fired on any current-glucose word repeated across 3 advancing
frames, which is ordinary flat glucose at 1 mg/dL resolution. A healthy
5-hour field capture produced 18 hits at 167, 165, 164, 131, 130 and 91,
every one with the surrounding frame bytes and the sensor's historic series
advancing normally. At that noise level it could never single out the hold
it exists to catch, which pinned one value for the better part of an hour.

Raise the threshold to 12 advancing frames, clearing the longest run
observed in that capture, and repeat only every 5th frame afterwards so an
hour-long hold costs ~10 lines instead of ~55.

Run length alone still can't separate a hold from flat glucose, so every
report now carries the sensor's committed 5-min historic series as a second
opinion: its value, lifeCount, lag, drift since the run opened, and its gap
from the pinned value. That series is produced independently and lands ~15
minutes behind, so once a run outlasts its own lag the two records describe
the same minutes and should agree. When they don't, the live value is the
suspect and the line is labelled STUCK-LATCH. When they do agree — which is
what the false-low field report looked like — that is itself the finding,
and points at the sensor rather than at our decode.

Also report the step that ends a hold (flat glucose resumes by a point or
two, a released hold jumps), and stop counting runs of error words, which
are a different failure already surfaced by the quality-assessment path and
never forwarded to Loop.

Decision logic moves into a pure StuckGlucoseDetector so it can be tested
directly. Replaying all 293 realtime frames from the field capture through
it now emits nothing, down from 18 lines.
A user removed the Libre CGM after their sensor expired and switched back to
a G7, then kept getting "sensor about to expire" alerts a week later, once
per app launch.

Nothing retracted the alerts on the deletion path. retractExpiryAlerts was
reachable only from discardSensor, which only Replace Sensor calls; the
Delete CGM button went straight to notifyDelegateOfDeletion and skipped the
manager's delete entirely. Loop's AlertStore keeps alerts for the whole
local-cache window (90 days in this build), and launch-time playback rebuilds
any past-due delayed alert as .immediate and presents it again — so the three
scheduled expiry alerts re-fired on every launch until acknowledged.

delete now retracts and ends with notifyDelegateOfDeletion instead of a bare
completion. That was wrong in the other direction too: overriding delete
without re-issuing the notification meant the debug-menu delete tore down BLE
but never actually removed the manager from Loop.

Retraction covers every alert the manager can issue, not just the expiry set.
Playback replays anything left unacknowledged and unretracted, so a standing
sensorAttention or reconnectNeedsReScan notice produces the same symptom.
They are listed in one place, allAlertIdentifiers, so a future alert can't be
missed. discardSensor retracts the same set and clears the re-scan and
sensor-attention state, since those notices belong to the sensor going away.

Retraction is issued before the delegate notification and captures the
delegate strongly, so it still lands once Loop releases the manager.
retractAlert needs only the identifier, and both unschedules the pending user
notification and records the retraction — which closes the launch-replay path
and the rescheduleMutedAlerts path together.
The test target was wired up correctly but unreachable. The project had no
shared scheme, so its schemes were per-user and invisible at workspace level,
and building the project standalone can't resolve LibreCRKit, LoopKit or
LoopAlgorithm — those come from the workspace. Tests could only be verified
by extracting the type under test and running it as a script.

Add a shared LibreLoop.xcscheme carrying build and test actions, following
G7SensorKit's layout, so the tests run through the workspace:

  xcodebuild -workspace LoopWorkspace.xcworkspace -scheme LibreLoop \
    -destination 'platform=iOS Simulator,name=iPhone 17' test

With that in place, cover the deletion bug that prompted this: a stale
sensor-expiry alert re-firing on every launch for a week after the CGM was
removed. LibreLoopAlertRetractionTests asserts that both exit paths — delete
and discardSensor — retract every identifier in allAlertIdentifiers, that
delete still notifies the delegate, and that the identifier list stays
complete. Verified against the pre-fix delete: it fails on both the missing
retractions and the missing delegate notification.

15 tests pass.
Every realtime frame already logs the full decrypted plaintext along with the
decoded word, DQ error, sensor condition and actionability, which is what is
needed to review a frozen-value report. Classifying the anomaly in the app on
top of that earns nothing, so remove the detector, its type and its tests.

Also trim the comments on the alert-retraction change.
@ps2 ps2 changed the title Fix stale expiry alerts after CGM deletion, retune stuck-glucose detector Retract standing alerts when the CGM is deleted Sep 9, 2026
Covers both strings Loop uses to identify the plugin: the bundle display name
in the Add CGM picker, and localizedTitle on the configured-CGM settings row.

Screen titles, alert body text and the HKDevice name keep the plain "FreeStyle
Libre 3": the slash reads badly in prose and truncates in a nav bar, and the
HKDevice name is written into existing HealthKit samples.
@ps2 ps2 changed the title Retract standing alerts when the CGM is deleted Stale alerts on CGM delete, remove stuck-value detector, rename to Libre 3 / 3+ Sep 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant