Fix stale expiry alerts after CGM deletion, retune stuck-glucose detector - #13
Open
ps2 wants to merge 3 commits into
Open
Fix stale expiry alerts after CGM deletion, retune stuck-glucose detector#13ps2 wants to merge 3 commits into
ps2 wants to merge 3 commits into
Conversation
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.
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.
Two independent fixes from field reports. Separate commits; happy to split into two PRs if preferred.
1. Retract standing alerts when the CGM is deleted (
b2ecddf)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:
retractExpiryAlertswas reachable only fromdiscardSensor, which only Replace Sensor calls.notifyDelegateOfDeletion, skippingLibreLoopCGMManager.deleteentirely.AlertStorekeeps alerts for the whole local-cache window (90 days in this build), and launch-time playback rebuilds any past-due.delayedalert as.immediateand presents it again (StoredAlert.swift:158-166,AlertManager.swift:515). So the three scheduled expiry alerts re-fired on every launch until acknowledged.deletenow retracts and ends withnotifyDelegateOfDeletioninstead of a barecompletion(). That was wrong in the other direction too — overridingdeletewithout 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
sensorAttentionorreconnectNeedsReScannotice produces the same symptom. They're listed in one place (allAlertIdentifiers) so a future alert can't be missed.discardSensorretracts the same set and clears the re-scan / 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.
retractAlertneeds only the identifier, and both unschedules the pending user notification and records the retraction — closing the launch-replay path and therescheduleMutedAlertspath together.Workaround for anyone already affected: tap OK on the alert rather than dismissing it. That records acknowledgement and drops it from playback permanently.
2. Retune the stuck-glucose detector (
b415f2e)The detector fired on any current-glucose word repeated across 3 advancing frames — 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 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.
STUCK-LATCH. When they do agree — which is what the false-low field report looked like — that's itself the finding, and points at the sensor rather than at our decode.Decision logic moved into a pure
StuckGlucoseDetectorso it can be tested directly.3. Make
LibreLoopTestsrunnable (58a63ce)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 / LoopAlgorithm — those come from the workspace.
Adds a shared
LibreLoop.xcschemewith build and test actions, following G7SensorKit's layout:Testing
Loopscheme, iPhone 17 simulator).StuckGlucoseDetectorTestsand 4LibreLoopAlertRetractionTestsadded here.LibreLoopAlertRetractionTestsasserts both exit paths (deleteanddiscardSensor) retract every identifier inallAlertIdentifiers, thatdeletestill notifies the delegate, and that the identifier list stays complete. Verified against the pre-fixdelete: it fails on both the missing retractions and the missing delegate notification.Still 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.
🤖 Generated with Claude Code
https://claude.ai/code/session_0111hASHbYQcnPZSUrafNLaA