Skip to content

Alert Flow Review

Last updated: 2026-04-02 09:30 EDT Status: current implementation review baseline Owner: BionicLoop engineering

Purpose

This document is the review-oriented companion to Alert Inventory and Mapping.

Use it when you want to inspect the implemented alert flow end-to-end:

  1. what triggers an alert
  2. how it is normalized into AppAlert
  3. where it is presented to the user
  4. how it clears, retracts, or requires acknowledgement

Scope

This document covers the current implemented alert path for:

  • pump-native alerts
  • pump synthesized state alerts
  • CGM state-driven and fallback alerts
  • runtime interruption alerting
  • app-policy alerts

This document does not treat preview-only alert injection as production behavior.

Defined in AppAlertCode but not currently raised by production alert flows:

  • ALERT-LOOP-AWAITING-FIRST-STEP
  • ALERT-LOOP-COMMAND-BLOCKED
  • ALERT-MEAL-NOT-AVAILABLE-NOW

Those codes currently exist for preview/test support and routing definitions, but not for live user alert issuance.

End-to-End Flow

flowchart LR
    A["Pump / CGM / Runtime / App Policy Trigger"] --> B["Mapper / Monitor creates AppAlert or retracts dedupe key"]
    B --> C["AppAlertCenter"]
    C --> D["Active Alerts (persisted)"]
    C --> E["Recently Cleared Timeline (persisted)"]
    C --> F["Background Local Notification (severity + cooldown gated)"]
    D --> G["Home Alert Carousel / Banner"]
    D --> H["Home Bell Badge"]
    D --> I["Alert Center Active List"]
    E --> J["Alert Center Recently Cleared List"]
    F --> K["Notification Tap Routing"]

Shared Review Rules

Review topic Current behavior Primary implementation
Canonical model All live alerts are normalized to AppAlert with source, sourceCode, normalizedCode, severity, message, recommendedAction, ackState, dedupeKey, and optional countdown metadata. BionicLoop/Runtime/AppAlertSupport.swift, BionicLoop/Runtime/AppAlertDomainSupport.swift
Ordering / top-alert selection Active alerts are ordered by severity first, then timestamp, then dedupe key. Home always shows the current top alert first. AppAlertPrioritizer in BionicLoop/Runtime/AppAlertSupport.swift
Dedupe / replace behavior AppAlertCenter replaces an existing active alert when the dedupe key matches; otherwise it appends a new active alert. AppAlertCenter.upsertNormalizedAlert(_:)
Clear / retract behavior Retract removes the active alert, clears any pending/delivered OS notification, and appends a ClearedAppAlert entry to the recently-cleared timeline. AppAlertCenter.retractNormalizedAlert(dedupeKey:)
Acknowledge behavior Only ackState == .requiresAcknowledge exposes an explicit acknowledge button. Most required-ack alerts clear on acknowledge, record reason acknowledged, and propagate closure back into the source lifecycle store through a source-specific handler. ALERT-CGM-URGENT-LOW is the current exception: acknowledge stamps clinician-review state (acknowledgedAt) but leaves the alert active until trustworthy glucose recovery clears it. If a source handler is not registered yet during startup, AppAlertCenter persists a pending acknowledgement request and replays it when the handler registers later. AppAlertCenter.acknowledgeAlert(dedupeKey:), HomeAlertBannerView, AlertCenterView
Background notification channel Only non-CGM actionable and safetyCritical alerts can schedule background local notifications, and only when the app is backgrounded. Cooldown is 5 minutes for actionable, 60 seconds for safetyCritical. CGM alerts are in-app informational only. AppAlertCenterNotificationSupport, UserNotificationAppAlertScheduler
Notification tap routing Notification tap routes by normalized code: pump alerts -> pump modal, CGM alerts -> CGM modal, app/runtime alerts -> Home. The CGM route remains defined for in-app navigation/preview support even though production CGM alerts do not schedule background notifications. AppAlertNotificationRouter in BionicLoop/App/BionicLoopApp.swift
Persistence AppAlertCenter persists active alerts and recently-cleared alerts across relaunch. Pump/CGM delegate lifecycle stores preserve issued/unretracted/retracted source alerts and restore them on attach. AppAlertCenterPersistenceSupport, AppPumpManagerDelegate, AppCGMManagerDelegate
Time-sensitive refresh Countdown-backed alerts refresh displayed message text once per minute while active. Current production countdown alerts are ALERT-PUMP-EXPIRING and ALERT-PUMP-EXPIRED. AppAlertCenter.refreshTimeSensitiveAlerts()
Telemetry Alert issue/update, retract/acknowledge, notification schedule/clear/tap are all emitted as telemetry events. AlertLifecycleTelemetryEventMapper, UserNotificationAppAlertScheduler, AppAlertNotificationRouter

User-Visible Presentation Surfaces

Surface What the user sees File path Review note
Home top alert Severity-colored banner for the current top alert. If multiple alerts are active, Home shows a vertical carousel with count and step-through controls. BionicLoop/Features/Home/HomeAlertCenterView.swift This is the main foreground alert surface.
Home bell badge Bell button with active-alert count badge. BionicLoop/Features/Home/HomeTopOverlayBar.swift Fast path to Alert Center.
Settings entry Alert Center row in Settings with active-alert count. BionicLoop/Features/Home/HomeSettingsNavigationSectionsView.swift Secondary path to Alert Center.
Alert Center active list Sorted active alerts, including acknowledge button when required. BionicLoop/Features/Home/HomeAlertCenterView.swift Review active ordering and acknowledge affordance here.
Alert Center recently-cleared list Timeline of cleared alerts with pagination (More / Less). BionicLoop/Features/Home/HomeAlertCenterView.swift Review closure behavior here.
Background local notification OS notification for background non-CGM actionable / safetyCritical alerts. BionicLoop/Runtime/UserNotificationAppAlertScheduler.swift Review cooldown, dedupe, and tap routing here.

Source Flow Review

Pump Alert Flows

Pump alerts come from two paths:

  1. native LoopKit/Omni alerts entering AppPumpManagerDelegate.issueAlert(_:) / retractAlert(identifier:), normalized by PumpAlertMapper
  2. synthesized pump-state conditions entering AppAlertCenter.updatePumpSignalLossCondition(...) and updatePumpNoActivePodCondition(...) through Home/runtime pump-state evaluation

Pump lifecycle state is also restored on delegate attach from the persisted alert store, and pod-expiration state can be synthesized from current pump status so relaunch does not miss expiration/grace-window alerts.

Normalized code Trigger to issuance Presentation to user Close / clear behavior Primary implementation
ALERT-PUMP-SIGNAL-LOSS Armed loop, active pod, established pump session, and missing/unknown pump status for longer than the 5 minute debounce window. Home top alert/carousel, Alert Center, Home bell count, background local notification when app is backgrounded. Notification tap routes to pump. Auto-clears when the condition clears. Retract also clears matching OS notification. PumpSignalLossAlertEvaluator, HomeActionCoordinator.applyPumpAlertPresentation, AppAlertCenter.updatePumpSignalLossCondition(...)
ALERT-PUMP-NO-ACTIVE-POD Armed loop, first successful step exists, and hasActivePod == false for longer than the 5 minute debounce window. Same user surfaces as other active pump alerts; routes to pump when notification is tapped. Auto-clears once an active pod is restored. No-active-pod cleanup retracts only non-critical pod-tied alerts. PumpNoActivePodAlertEvaluator, HomeActionCoordinator.applyPumpAlertPresentation, AppAlertCenter.updatePumpNoActivePodCondition(...)
ALERT-PUMP-LOW-RESERVOIR LoopKit pump alert lowReservoir. Home, Alert Center, background local notification when app is backgrounded; notification tap routes to pump. Auto-clears on lifecycle retraction / recovery. AppPumpManagerDelegate.issueAlert(_:), PumpAlertMapper.lowReservoirAlert(...)
ALERT-PUMP-EXPIRING LoopKit userPodExpiration, or state-driven synthesis from expiresAt when pod is within 1 hour of expiry and still in the future. Home, Alert Center, background local notification; notification tap routes to pump. Message refreshes as countdown advances. Auto-clears on lifecycle retraction / recovery or when replaced by expired state. PumpAlertMapper.expiringAlert(...), pod-expiration sync support, AppAlertCenter.refreshTimeSensitiveAlerts()
ALERT-PUMP-EXPIRED LoopKit podExpiring / podExpireImminent, or state-driven synthesis when expiresAt has passed and pod still has remaining post-expiration service time. Home, Alert Center, background local notification; notification tap routes to pump. Message refreshes through the remaining delivery-stop countdown after nominal expiration. Auto-clears on lifecycle retraction / recovery, once pod service time is exhausted, or when pod is detached. PumpAlertMapper.expiredAlert(...), pod-expiration sync support, AppAlertCenter.refreshTimeSensitiveAlerts()
ALERT-PUMP-FAULT Pump fault/alarm signal or unexpectedAlert mapping. Home, Alert Center, background local notification. Requires explicit acknowledge in foreground UI. Notification tap routes to pump. Does not auto-clear solely because there is no active pod. Clears on lifecycle retraction/recovery or explicit acknowledge. Required-ack closure is now written back to the persisted pump alert store so the alert does not restore on relaunch once acknowledged. PumpAlertMapper.faultAlert(...), AppAlertCenter.acknowledgeAlert(dedupeKey:), AppPumpManagerDelegate
ALERT-PUMP-INCOMPATIBLE Incompatible pod signal/text in pump alert mapping. Same as pump fault: Home, Alert Center, background local notification, acknowledge required, notification tap routes to pump. Same retention rule as pump fault, including persisted acknowledge closure preventing relaunch restore after the user acknowledges it. PumpAlertMapper.incompatibleAlert(...), AppAlertCenter.acknowledgeAlert(dedupeKey:), AppPumpManagerDelegate
ALERT-PUMP-SETUP-INCOMPLETE LoopKit finishSetupReminder. Home, Alert Center, background local notification when app is backgrounded; notification tap routes to pump. Auto-clears on lifecycle retraction / setup completion. PumpAlertMapper.setupIncompleteAlert(...)
ALERT-PUMP-SUSPEND-IN-PROGRESS LoopKit suspendInProgress. Home, Alert Center, background local notification; notification tap routes to pump. Auto-clears on lifecycle retraction / delivery resume. PumpAlertMapper.suspendInProgressAlert(...)
ALERT-PUMP-SUSPEND-ENDED LoopKit suspendEnded. Home, Alert Center, background local notification; notification tap routes to pump. Auto-clears on lifecycle retraction / recovery. PumpAlertMapper.suspendEndedAlert(...)
ALERT-PUMP-TIME-OFFSET-DETECTED LoopKit timeOffsetChangeDetected. Home, Alert Center, background local notification; notification tap routes to pump. Auto-clears on lifecycle retraction / recovery. PumpAlertMapper.timeOffsetDetectedAlert(...)

CGM Alert Flows

CGM alerts are intentionally state-driven first. AppCGMManagerDelegate.syncG7Alerts(for:) rebuilds current CGM alert state from live AlgorithmState and G7SensorLifecycleState. LoopKit issueAlert fallback mapping remains in place for compatibility and persisted-alert plumbing, but it does not drive failed/expired escalation from message-only payload text.

Operational policy: the FDA-cleared Dexcom application is the source of truth for CGM alarming. BionicLoop CGM availability/failure alerts are informational in-app state only on Home / Alert Center and do not schedule OS notifications. BionicLoop also derives an in-app urgent-low review alert from trustworthy G7 glucose <55 mg/dL; that alert is for clinician review/telemetry only, does not represent Dexcom-app acknowledgement, and still does not schedule OS notifications.

Normalized code Trigger to issuance Presentation to user Close / clear behavior Primary implementation
ALERT-CGM-UNAVAILABLE Live G7 state reports temporary error, warmup, or no reliable glucose; fallback LoopKit keyword mapping only classifies unavailable-temporary states. Home, Alert Center. No background local notification is scheduled by BionicLoop. Auto-clears once live CGM state recovers; restored from live state on attach rather than replayed from persisted LoopKit alert payloads. AppCGMManagerDelegate.syncG7Alerts(for:), CGMAlertMapper.normalizedG7Alert(...), CGMAlertMapper.normalizedAppAlert(...)
ALERT-CGM-FAILED-OR-EXPIRED Live G7 lifecycle is failed / expired, or live algorithm state reports failed / expired. Fallback LoopKit mapping only classifies failed/expired from source identifiers, not free-text message keywords. Home, Alert Center. No background local notification is scheduled by BionicLoop and no app-side acknowledge button is shown. Operators are expected to rely on the Dexcom app for CGM alarming. Clears on live-state recovery/replacement. Not replayed from persisted LoopKit payloads on attach. AppCGMManagerDelegate.syncG7Alerts(for:), CGMAlertMapper.normalizedG7Alert(...), CGMAlertMapper.normalizedAppAlert(...)
ALERT-CGM-URGENT-LOW Trustworthy live G7 reading <55 mg/dL (hasReliableGlucose == true and fresh timestamp) from current state sync. This is app-derived from CGM data and mirrors the Dexcom urgent-low threshold for clinician review, but it is not Dexcom-app acknowledgement. Home, Alert Center. No background local notification is scheduled by BionicLoop. Active alert can show reviewed state after acknowledge. Acknowledge records clinician-review state and telemetry but does not clear the active alert while glucose remains <55. Active alert auto-clears only on a later trustworthy reading >=55 mg/dL; stale, unreliable, or missing readings do not clear the episode. Acknowledged active state persists across reset / delegate reattach until recovery clears the episode. AppCGMManagerDelegate.syncG7Alerts(for:), CGMAlertMapper.normalizedUrgentLowAlert(...), AppAlertCenter.acknowledgeAlert(dedupeKey:), CGMAlertPersistenceStore

Runtime Alert Flow

Currently the only production runtime-issued user alert is the step-interruption alert.

Normalized code Trigger to issuance Presentation to user Close / clear behavior Primary implementation
ALERT-ALGORITHM-STEPPING-INTERRUPTED Armed loop has not completed a successful algorithm step before the computed interruption deadline. LoopRuntimeEngine recomputes the monitoring decision after execution publication and on state refresh; AppAlertCenter schedules a future local notification for the deadline and upserts the alert when the deadline passes. Home, Alert Center, background local notification. Notification tap routes to Home. Remains distinct from informational CGM state surfaces. Clears when successful stepping resumes, when the loop is disarmed/reset, or when monitoring is disabled. Pending future notification is cleared when monitoring is disabled or replaced. LoopRuntimeEngine.refreshAlgorithmSteppingInterruptionMonitoring(), LoopRuntimeEngineStepInterruptionMonitoringSupport, AppAlertCenter.updateAlgorithmSteppingInterruptionMonitoring(...)

App-Policy Alert Flows

These alerts are not device-native; they come from app/session policy and cloud-facing workflow checks.

Normalized code Trigger to issuance Presentation to user Close / clear behavior Primary implementation
ALERT-AUTH-LOGIN-REQUIRED ContentView.syncAuthRecoveryAlert() determines the user is unauthenticated while an active algorithm state remains and remote-monitoring login recovery is required. Home, Alert Center, background local notification when app is backgrounded. Home banner exposes primary action Log In. Notification tap routes to Home. Clears when the user re-authenticates or when there is no longer an active algorithm state requiring recovery. AppAuthRecoveryPolicy, ContentView.syncAuthRecoveryAlert()
ALERT-SUBJECT-ID-CONFLICT CloudTelemetryReporter detects a permanent subject-claim conflict from the telemetry/device-cloud path and upserts the normalized alert. Home, Alert Center, background local notification when app is backgrounded. Notification tap routes to Home. Clears when the operator successfully saves corrected clinical settings after subject claim succeeds, and also clears when Home later re-validates the currently persisted subject ID on launch, foreground, or settings reopen and the backend claim now succeeds; both paths retract the normalized alert dedupe key. CloudTelemetryReporter.subjectIDConflictAlert(timestamp:), HomeSettingsView.confirmSaveReview(_:), HomeView.recheckSubjectIDConflictIfNeeded(now:)
ALERT-APP-CLOCK-SKEW DeviceClockSyncMonitor performs a UTC drift check on launch, foreground (when last success is older than 24 hours), or time/timezone change. If abs(skew_seconds) > 600, it issues the alert, limited to once per 24 hours. Home, Alert Center, background local notification when app is backgrounded. Notification tap routes to Home. Auto-clears when a later successful check returns ok. Unavailable checks do not raise or spam alerts. DeviceClockSyncMonitor.applyWarningPolicy(...)

Alert Center Review Notes

Use the Alert Center to review lifecycle behavior, not just current foreground presentation.

  • Active section shows all active normalized alerts sorted by severity, then recency.
  • Recently Cleared section shows the clear history sorted by clearedAt descending.
  • requiresAcknowledge alerts expose acknowledge affordance in both Home banner/carousel and the active Alert Center row.
  • Recently-cleared retention is capped at 100 entries in persistence.
  • Recently-cleared UI pagination is 10 rows at a time with More / Less.

Primary files:

  • BionicLoop/Features/Home/HomeAlertCenterView.swift
  • BionicLoop/Runtime/AppAlertCenter.swift
  • BionicLoop/Runtime/AppAlertCenterPersistenceSupport.swift

Background Notification Review Notes

Review background alert behavior separately from foreground UI.

  • Notifications are scheduled only for actionable and safetyCritical alerts.
  • Informational alerts do not schedule background notifications.
  • Category identifier is the normalized alert code.
  • Notification identifier is derived from the dedupe key, so retract/replace paths clear the matching OS notification.
  • Safety-critical notifications use iOS time-sensitive interruption level when available.
  • Notification tap routing is by normalized alert code:
  • pump -> pump modal
  • CGM -> CGM modal
  • app/runtime -> Home

Primary files:

  • BionicLoop/Runtime/UserNotificationAppAlertScheduler.swift
  • BionicLoop/App/BionicLoopApp.swift

Persistence and Relaunch Review Notes

  • AppAlertCenter persists both active alerts and recently-cleared alerts in UserDefaults.
  • Pump and CGM delegate persisted-alert stores preserve issued/unretracted/retracted source alerts across relaunch.
  • On delegate attach:
  • pump alerts are restored from unretracted, unacknowledged persisted alerts
  • CGM state-driven alerts are rebuilt from current live G7 state, not replayed from stale persisted payloads
  • No-active-pod relaunch cleanup retracts only non-critical pod-bound alerts; ALERT-PUMP-FAULT and ALERT-PUMP-INCOMPATIBLE remain until explicit closure.
  • Explicit acknowledge of required-ack pump alerts now persists closure into the pump lifecycle store, so acknowledged fault/incompatible alerts do not rehydrate on delegate reattach.
  • If a required-ack pump alert is acknowledged before the pump acknowledgement handler registers on startup, AppAlertCenter persists that pending acknowledgement locally and the pump delegate drains it before computing restore candidates.
  • If ALERT-CGM-URGENT-LOW is acknowledged before the CGM acknowledgement handler registers on startup, AppAlertCenter persists that pending acknowledgement locally and the CGM delegate drains it before rebuilding live urgent-low presentation.

Current Verification Anchors

Primary trace anchors for this flow:

Useful direct review evidence already called out in the SVVP:

  • testSignalLossDebounceAddsAndClearsAlert
  • testSignalLossDebounceSuppressesTransientCondition
  • testAlertCenterTracksRecentlyClearedAlerts
  • testAlertCenterRestoresPersistedActiveAndClearedAlerts
  • testPumpPersistedAlertStoreReturnsIssuedAndRetractedAlerts
  • testCGMPersistedAlertStoreReturnsIssuedAndRetractedAlerts
  • testCGMUrgentLowAcknowledgePersistsAcrossAlertCenterResetUntilRecovery
  • testPumpExpirationAlertSyncPlannerReturnsRetractsWhenNoExpirationAlertsApply
  • testTimeSensitivePumpExpiringAlertRefreshesMessageWithoutReschedulingNotification
  • testCGMAlertsNeverScheduleBackgroundNotifications
  • testUI007_HomeAlertCenterButtonOpensAlertCenter
  • testUI008_AlertCenterAcknowledgeMovesAlertToRecentlyCleared
  • testUI009_AlertCenterPersistsAcrossRelaunch

Review Outcome Summary

As of this baseline:

  • the implemented production alert path is unified through AppAlertCenter
  • the main user-visible channels are Home foreground presentation, Alert Center, and background local notifications for non-CGM alerts
  • pump, CGM, runtime interruption, and app-policy alert flows are all reviewable from source trigger through user presentation
  • three alert codes remain defined but not currently issued by production flows:
  • ALERT-LOOP-AWAITING-FIRST-STEP
  • ALERT-LOOP-COMMAND-BLOCKED
  • ALERT-MEAL-NOT-AVAILABLE-NOW
  • ALERT-SUBJECT-ID-CONFLICT now has both an issue path from telemetry conflict handling and retract paths in the successful Clinical Settings save flow plus the Home auto-revalidation flow for stale conflicts