Skip to content

SportMode: Loop changes needed to run autonomously on the watch - #2545

Open
jeremybarnum wants to merge 54 commits into
LoopKit:next-devfrom
jeremybarnum:sportmode/next-dev
Open

jeremybarnum wants to merge 54 commits into
LoopKit:next-devfrom
jeremybarnum:sportmode/next-dev

Conversation

@jeremybarnum

@jeremybarnum jeremybarnum commented Oct 1, 2026 •

Copy link
Copy Markdown

Commits 1 and 2 below, the refactoring of stock Loop, have landed upstream as #2645, so the diff no longer shows them.

Sport Mode: the phone lends the Omnipod to the watch. The watch runs Loop itself (its own G7 glucose, its own stores, the same Loop algorithm and loop cycle as the phone) until it hands the pod back. The phone stays the home of the user's data and can take the pod back at any time. It's behind SPORT_MODE_ENABLED, off by default; with it off, neither app runs any of this.

How the PR is organised

Eight commits, each building with its tests passing, to be read in order. 1–7 are the engine; 8 is what the user sees.

  1. Loop: move the types the loop runs on into LoopCore (a move, no change).
  2. Loop: one loop cycle that both apps can run.
  3. Sport Mode: the phone–watch protocol and who controls the pod (with the control document).
  4. Sport Mode: the phone lends the pod.
  5. Sport Mode: the phone's hooks in stock files, together in one place.
  6. Watch: one set of stores, and one display state for the screens.
  7. Sport Mode: the watch borrows the pod.
  8. Sport Mode: what the user sees.

Refactoring of stock Loop

On the phone, the refactoring shares code with the watch and changes no behaviour:

  • The loop moves into LoopCore. The body of LoopDataManager.loop() becomes LoopCycle, which both apps run; LoopDataManager is the phone's host for it and keeps the parts only the phone knows (overrides, the algorithm experiments, the dosing strategy). The manual bolus recommendation, cancelling the loop's temp basal, the display run and the remote recommendation go through the cycle too. LoopDataManager is about 500 lines shorter.
  • The types it runs on move with it, unchanged: the algorithm input, the dosing decision record, LoopConstants, LoopError (with its messages and translations), LoopWarning, SimpleInsulinDose, CarbEntryChange, the four store protocols, SettingsProvider and three extensions. About a dozen files gain import LoopCore and nothing else.
  • Delivery: DeviceDataManager's pump-facing delivery members (the pump's state, rounding, the checks before a dose is enacted) become PumpManagerDeliveryDelegate in LoopCore, which the watch shares.
  • The watch bolus record: WatchDataManager builds the dosing record of a bolus asked for on the watch with BolusDosingDecision.watchBolus, which the watch also uses. The one difference: the record takes every field from the display state read at the start, where before it read some fields before computing the recommendation and some after.

On the watch, the stock app is reshaped so that Sport Mode builds on it rather than running beside it. Without Sport Mode the code changes, but the watch app looks and works as before, apart from one fix:

  • One set of stores. The watch's LoopDataManager opens dose and dosing-decision stores beside its glucose and carb stores, so that its screens and, during a loan, its loop use the same ones.
  • One display state for every screen. Upstream used it for the glucose reading alone (the phone's summary, with the watch's newer reading in its place); now every screen reads it, and during a loan the watch's own loop is laid over it.
  • The fix: the carb list asks the phone for its carbs when it opens. Since the carb list moved to SwiftUI, nothing has asked, so the list could stay empty.
  • Stock code compiled on the watch instead of copied: GlucoseAlertManager for the glucose alarms during a loan, and DoseEnactor.

How it works

  • Start: the phone releases the pump and sends the watch a grant: the pump's exported configuration, the settings and dosing strategy (including the algorithm experiments: integral retrospective correction, glucose-based partial application, guard + runway with the phone's volatility baseline, and slow retrospective correction), and recent insulin, carbs, glucose and overrides. The watch builds its own pump manager from what the phone exports. For glucose the watch reads the G7 itself, with Loop's own watch support (the G7's Read on Apple Watch setting), which Sport Mode requires: Start is refused, naming the setting, while it is off. The watch cancels any running temp at a Start; its first loop cycle comes with the next sensor reading. A Start the watch never confirms is taken back by the phone at its first cycle after the grant's 60-second lease. On the watch, G7SensorKit is named only in WatchApp Extension/Managers/WatchDeviceManagers.swift and OmnipodKit only in WatchApp Extension/StockLoop/WatchPumpManagers.swift; everything else goes through LoopKit protocols. Until the watch can load device kits as plugins, these two files are the list.
  • Slow retrospective correction: it keeps no state and reads 72 hours. With it on, the grant's insulin, carbs and glucose cover its windows, and the watch stores them as the phone does. The watch records glucose under the phone app's identity, so one sensor's readings form one series however they reach it. A large grant travels compressed: about 30 KB with the 72 hours.
  • During the loan: the watch runs the same loop cycle as the phone and doses. The phone's loop keeps computing to the extent it can still reach the pump and sensor but never acts on it. Doses, carbs and overrides from the watch come back to the phone as records, sent after each of the watch's cycles and stored once each as they arrive (a temp basal still running, once it has ended). The phone's banner says when they last came. The watch also keeps its own dosing decisions, alert history and device log; they go home to the phone in one file at hand-back. The phone stores no dosing decisions of its own during a loan.
  • The watch glance: a Sport Mode page on the watch, after the watch app's usual two, shows glucose and trend, the loop status ring, IOB, COB and the running temp, with Start and End. The watch moves to it when a loan starts, when the app relaunches into a loan, and after carbs or a bolus are entered during a loan; outside a loan the app opens where it always has. The ring is tappable to open/close loop.
  • Carbs on the watch: during a loan, carbs can be added and also deleted (swipe left in the Active Carbs list). A carb entry drives the dose, so the user needs a way to correct a wrong one while the watch has the pod; deleting gives that now, and editing is left for later. Both reach the phone with the next checkpoint. Outside a loan the watch's carb list is Loop's usual read-only one.
  • Hand-back: the watch hands the pod back when the user taps End (a "hand-back"), or the user takes the pod back from the phone (a "take-back"). A take-back reaches for the pod first. If the pod answers, the phone tells the watch to stop, imports the records the watch has already sent, checks the pod's delivered total against them and resumes looping, without waiting for the watch. If the pod doesn't answer within a few seconds, nothing changes and the user is asked to try again. With both devices available, records are synced and dosing should be exact. Otherwise, the safety mitigants described below kick in.
  • Nobody takes the pod while it is giving a bolus, or while a command to it is unconfirmed: a Start while the phone's bolus is being given, and an End or a Take Back while the watch's is, are refused with the seconds left. A bolus counts from the moment it is sent. While an End is waiting for the phone, the watch refuses a new bolus instead. A take-back on silence (below), where nobody can retry, waits for the pod to finish the bolus. While a command to the pod is unconfirmed, the pod driver keeps the pod when asked to let it go, and a Start or an End is refused the same way (SportMode: OmnipodKit on the watch, and handing a pod between controllers loopandlearn/OmnipodKit#177). A phone that stands aside for a loan the watch started without it, holding such a command, reads the pod at once, which settles the command so the driver lets go.
  • When the watch goes quiet: if the watch's report is overdue and the phone is near the looper, which it determines by checking for a fresh sensor reading, the phone asks the watch whether it still holds the pod. If no answer has come by the phone's next cycle, at least 30 seconds later, the phone takes the pod back on its own (a take-back on silence), counts the insulin the watch gave, and says so ("Watch Not Responding"). A watch that comes back afterwards checks with the phone ("Checking with your iPhone…") and stands aside before any dose. This is partly a safety mitigation on the premise that looping is better than not looping as long as dosing is conservative. This scenario continues looping from the phone if the looper comes home with a dead watch having started sport mode with the phone in range initially. These abnormal situations involve judgement calls; the control document sets out the framework for them (see Managing edge cases below).
  • Without the phone: the watch can start from a stored copy of the grant when the phone is unreachable: after 10 seconds without an answer to Start, it offers "Start without it?". The scenario here is forgetting the phone at home, or having the phone battery die, and starting Sport Mode on the watch as a way to continue looping. When the phone comes back, it sees the watch's loan (from the watch's messages, or from insulin in the pod's total its own records can't explain, after asking the watch), adopts it and stands aside; the loan continues and the banner shows. If the watch is gone by then, the phone books the insulin it can't explain and keeps looping.
  • Phone UI during a loan: a "Sport Mode Active" banner (tap to take the pod back), and the pod tile offers the same. Carbs, bolus and presets are disabled, and charts show history only. Settings is also disabled, as a change in settings during a loan where the watch isn't reachable would be problematic. Any attempt to tap action buttons on the phone UI offers the "Reclaim Pod?" option.
  • Alerts: the watch raises Loop's usual glucose alarms itself, plus pod and Loop-failure alerts. The phone's own glucose alerts and missed-meal detection pause during a loan, since its dose history is incomplete. Acknowledging a pod alert on the wrist reaches the pod. The phone posts a notice if the watch reports a pod fault. Worth noting how the phone's notifications reach the watch. Sport Mode is for times the watch is out of the phone's Bluetooth range; within it, there's no need for Sport Mode. Out of Bluetooth range but on the same Wi-Fi, the phone and the watch still talk to each other through WatchConnectivity. Beyond Wi-Fi they don't, since WatchConnectivity doesn't work over cellular, but the phone's notifications still reach the watch as push notifications. So if the phone didn't suppress its notifications during a loan, its Loop Failure warnings and others about a loop it isn't running would reach the wrist. That's why the phone suppresses them during a loan. After a start without the phone, though, the phone doesn't know about the loan until it hears from the watch again, so until then its nuisance notifications can still reach the wrist. A future PR may fix that.
  • A pod that faults during a loan: while the watch holds a faulted pod, its glance and the alarm's notification offer Deactivate Pod, which runs the pod settings' own deactivation and silences the alarm. Sport Mode then ends, and the phone forgets the pod instead of reconnecting. The pod's suspend at the fault travels to the phone, so no scheduled basal is counted after it. A take-back of a faulted pod finishes on the pod's last delivered total, since the pod refuses reads after a fault. All the pod's alerts, its fault alarm included, close before the final offer.
  • Relaunch: loan state is saved as one value per side, so either app can be relaunched mid-loan. A watch that restarts (say, after a dead battery and a recharge) first asks the phone whether the loan is still its own, then rebuilds the pump manager. Without an answer it carries on at its first sensor reading at least 30 seconds later, unless the phone has stopped it. It doses nothing before the pod's first reading, which counts any insulin given while it was off. If it was off for longer than insulin's action time, it doesn't resume: its records go to the phone and it shows "Sport Mode Ended".
  • Managing edge cases, failure modes and associated state challenges Documentation/SportMode/Control.md sets out the framework. A comprehensive inventory of conceivable physical configurations of devices, connectivity and failure modes, the principles that should guide the decisions about the system's behavior, and where each decision lives in code. Each decision is one function in LoanControl.
  • Remote uploads during a loan are not in this PR. On its own, the phone keeps uploading its own glucose while it's in range, and the loan's treatments and loop status reach Nightscout and Tidepool from the phone at hand-back. Uploading from the watch during a loan comes in two stacked PRs: SportMode - Nightscout uploads from the watch during a loan #2554 (Nightscout) and SportMode - Tidepool uploads from the watch during a loan #2555 (Tidepool). Their limitations, such as cellular-only uploads waiting for the app to be in front, are described there.

Safety

The insulin audit. At every hand-back the phone reads the pod's own delivered-insulin total and compares it with what the records say was delivered during the loan (the doses plus the scheduled basal, counted in whole pod pulses). Important to understand that the pod knows how much it delivered but not when it delivered it.

  • If the pod delivered more than the records explain (beyond ±0.2 U, amount to be discussed): after an ordinary hand-back the watch's records are complete, so the phone reports the difference and books nothing. Any differences here should be rounding errors and other corner cases. We don't expect them. After a take-back from the phone, meaning the phone is not getting records from the watch because the watch is not available, records may be missing, so the phone books the difference as a placeholder bolus delivered now and reminds the user at 2 and 4 hours. That's always conservative because the IOB can't be any higher than what is implied by a dose booked right now, and on a dose taken long ago it can be quite conservative. When the watch's records arrive they replace the placeholder. The loop stays closed in both cases.
  • If the records claim more than the pod delivered, it alerts ("Insulin On Board May Be Overstated") but doesn't open the loop. This is the scenario where during Sport Mode a low temp was running (this might be quite common during exercise) and then the watch dies before sending records. The risk is therefore having less IOB than Loop believes, which is safe albeit undesirable. As soon as the watch comes back into range/gets charged, predictions are exact again.
  • During a loan the watch sends checkpoints. This means that to the extent the phone is in range during a loan, the "knowledge gap" about carbs and insulin declared on the watch is minimized. Imagine for example starting a loan, walking around the house or doing some yardwork, and having the watch deliver some additional insulin, then departing the area and leaving the phone behind and then having the watch die. Upon returning in range of the phone, the gap is limited to the last sync, rather than to everything that has happened since the start of the loan. Also helpful that the sync can happen over Wi-Fi, not just Bluetooth.
  • If the phone takes the pod back without the watch's records (the watch is gone), it holds dosing until the audit has run. When the watch's records arrive later, they replace the placeholder.
  • The watch does the same at takeover: insulin the phone's copy can't explain (say, a bolus given on the phone while the watch was on the charger in another room, not connected over Wi-Fi, followed by leaving the phone behind and starting a loan on the watch without the phone present) is booked before the watch's first loop cycle. This should be a very rare scenario given the frequency of the sync.
  • The phone does the same at every reading of the pod, on a phone set up for Sport Mode. On both devices the check runs as the pump reports the reading, before the dose that reading feeds is decided. A watch that restarts mid-loan does the same at its first reading, so it never doses without counting insulin given while it was off, for instance by the phone. This covers a loan started on the watch away from the phone, where the watch then dies: the phone never heard about that loan and would otherwise resume control with the watch's insulin missing. The difference is booked as a placeholder bolus, as after a take-back, and the watch's records replace it if they cover the gap. Each estimate stands for its own stretch of time and is replaced only by records from that stretch. One older than insulin's action time stops being tracked; its dose stays in the history.
  • A record of a bolus still being given is stored at once as a bolus in progress (mutable, as Loop's own drivers report one), so insulin on board counts it immediately; the finished record replaces it. A temp basal still running is kept back and stored once it has ended.

Duplicate carbs. Extra carbs mean extra insulin, so this is the dangerous direction.

  • Every carb entered on the wrist carries a fixed identity (the sync identifier NewCarbEntry carries since CarbStore: carry the sync identifier on new carb entries LoopKit#654), and the phone stores it under that identity, so a resent entry is stored once, however many times it arrives.
  • Records are resent until acknowledged, and the phone saves which ones it has stored before it acknowledges them. A relaunch at the wrong moment can't store one twice.
  • Records from a loan that's already over can't add carbs.
  • Carb deletions travel as records too, so a carb deleted on the wrist doesn't come back.
  • During a loan, entering carbs on the wrist needs the same confirmation as a bolus.

Known limitations

  • Nightscout issue in an edge case: after a take-back without the watch's records, the placeholder uploads as an ordinary bolus and is replaced when the watch's records arrive. If the watch returns more than a day later, Nightscout may keep it.
  • Install or update only when no loan is running. No migration from earlier Sport Mode builds.
  • The watch reads the G7 in its own slot, which Dexcom's watch app also uses, so the two are not expected to work together (untested). The Dexcom app on the phone is unaffected.

Worth a look

  • Stock files changed: 27 on the phone, of which about a dozen only gain import LoopCore; 16 on the watch. The phone's Sport Mode hooks are in commit 5, its UI in commit 8.
  • The goal, and the expectation, is no difference in behavior, predictions or dosing: without Sport Mode the phone behaves as stock Loop does, and during a loan the watch runs the phone's own loop cycle with the phone's settings.
  • Threading: Sport Mode's code runs on the main actor on both devices, as the phone's managers do. On the watch the loop's work runs one step at a time, in the order asked, as the phone's does.
  • Logs: Sport Mode logs one line per event a field report needs, through DiagnosticLog on the phone and the watch app's OSLog on the watch.
  • The watch's entitlements come from a build setting, LOOP_WATCH_ENTITLEMENTS: empty by default, so with the flag off the watch signs as it does without Sport Mode. Sport Mode builds set it next to SPORT_MODE_ENABLED; without it, a Sport Mode watch can't keep itself running during a loan (unless it has a workout keepalive, which is a separate discussion). The Bluetooth background mode stays in the watch's Info.plist either way.

Testing

  • Smoke tests on real hardware: a real phone and watch, with an emulated pod and G7. Earlier rounds also ran with real DASH pods and G7 sensors.

  • A pod fault on real hardware: the fault flow (the fault alarm on the watch, Deactivate Pod on the watch, Sport Mode ending, and the phone forgetting the pod) has been run on a real phone and watch, with the faults triggered from the pod emulator.

  • Every end-to-end test in the Simulator passes. The real phone and watch apps run in a paired iPhone and Apple Watch Simulator, talking over Bluetooth to a simulated pod and a simulated G7 on the Mac:

    • the simulators are LoopKit's own, through the ImpossiBLE Bluetooth stand-in;
    • additions for Sport Mode: the pod delivers insulin over time as a real one does; the phone and the watch share one pod and one sensor; and either device can go out of range, or the watch shut down, on command;
    • scripted tests tap through both apps' screens and check both apps' logs: starting and ending a loan, taking the pod back, starting without the phone, the watch restarting mid-loan, the insulin check, the bolus rules, carbs and presets during a loan, a lost answer from the pod, and the watch without Sport Mode, also on a build with Sport Mode off.

    Harness and test list: https://github.com/jeremybarnum/sportmode-sim.

  • Unit tests pass. The phone (LoopTests) and watch (WatchAppTests) suites pass on this PR. Both apps also build with the Sport Mode flag off. The stacked PRs are run with it again once they are redone on this history.

LoopKit/LoopKit#626 and LoopKit/G7SensorKit#80 have landed; needs loopandlearn/OmnipodKit#177 and LoopKit/LoopKit#686. Draft.

Built on this PR (drafts, being redone on this history)

jeremybarnum pushed a commit to jeremybarnum/Loop that referenced this pull request Oct 9, 2026
…ications-v2

# Conflicts:
#	Loop.xcodeproj/project.pbxproj
EJM0 added a commit to EJM0/Trio that referenced this pull request Oct 18, 2026
… to stop

A take-back revoked the watch and took the pump at once. A phone that then
couldn't reach the pod left a pump nobody dosed. The phone now takes
control and waits up to 5 s for the pod's link; only with it up does it
revoke the watch and take over. Without it, it lets go again, nothing
changes and the watch keeps running, with a note to bring the phone close.
Dosing stays held meanwhile, as the session is not idle. As LoopKit/Loop#2545.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
EJM0 added a commit to EJM0/Trio that referenced this pull request Oct 18, 2026
The watch doses with the settings that came with its grant; a change on
the phone during a session never reached it. The screens whose values the
grant carries now show "Locked While the Watch Loops" until the hand-back:
basal, ISF, carb ratio and target editors, meal, bolus calculator, dynamic
ISF, autosens, SMB, target behaviour, advanced algorithm settings and units
and limits. Settings › Apple Watch, Nightscout and the rest stay open, so
the session can still be ended. As LoopKit/Loop#2545 locks Settings during
a loan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
EJM0 added a commit to EJM0/Trio that referenced this pull request Oct 18, 2026
…ne ends it

Set again after a hand-over, a temp runs at least Omnipod's 30 minutes,
longer than it had left, should the new side not loop: the risky way for a
high temp. A temp above the scheduled rate now ends at the hand-over
instead, as LoopKit/Loop#2545 does:

- Phone to watch: the phone cancels it at the release and hands nothing
  over; the schedule runs until the watch's first loop.
- Watch to phone: the watch leaves it running and reports it with the
  hand-back; once the phone has read the pod it cancels it, and ends the
  watch's record of it where it stopped it, final, so IOB and the audit
  don't count the planned rest. The schedule runs until the phone's loop.

Low and zero temps are handed over as before, cancelled and set again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
EJM0 added a commit to EJM0/Trio that referenced this pull request Oct 18, 2026
…tch has the pump

On 2026-10-18 the watch went without readings from 09:24 to 10:39: a
sensor connect hung while its app was suspended, and only opening it put
that right. A phone in reach now wakes it, as LoopKit/Loop#2545 does with
every phone cycle:

- Phone: 45 s after its own reading, clear of the watch's link to the same
  sensor, a small application context ("untetheredWake"); at most one per
  four minutes, as storing the watch's readings fires the same publisher.
- Watch: that context re-checks the sensor's acquisition and the alarms
  and sends the history. A WatchConnectivity wake is now held until what it
  brought is delivered (at most 20 s, never past the system's expiry), so
  the callbacks run before the app is suspended again; it was completed on
  arrival.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
EJM0 added a commit to EJM0/Trio that referenced this pull request Oct 18, 2026
The watch stopped dosing at the hand-back, let go of the pod, and the phone
then waited up to 60 s for its last records before it judged the pump.
Now, as LoopKit/Loop#2545:

- Drain: in the ending phase the watch keeps looping and sends its
  backlog, paced around the sensor as before, until the phone has
  confirmed all of it (at most 3 min; the rest goes with the last records).
- Last stage: no new loop or bolus (isFinalizing), the temp handled as at
  any hand-over, the pod read, the pump status recorded, the last records
  sent and confirmed (at most 30 s), then the pump offered.
- The pod is let go only when the phone acknowledges. The phone takes
  control before it acknowledges, so its connect is standing as the watch
  lets go, and the history it judges is complete. Without an
  acknowledgement for a minute the watch lets go anyway and keeps
  offering, as before; it never takes the pump back on its own.
- A relaunch mid-hand-back goes on draining, or stays in the last stage
  without loops when the pump was offered already.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
EJM0 added a commit to EJM0/Trio that referenced this pull request Oct 18, 2026
…e on its word

While it has the pump, the phone keeps a standing grant queued on the watch: the full grant, issued
expired, resent after carbs, boluses and adjustments (30 s floor), on any settings change and every
30 min. With the phone out of reach the watch starts from it on an epoch of its own, if the copy is
younger than the insulin's action time, and books what the pod delivered past the copy's history as
a bolus before its first loop. The phone takes on a newer epoch it hears of before storing its
records, lets go of the pod and stops dosing; the watch tells it at once when it is back and asks
the wrist once to hand back. As LoopKit/Loop#2545.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
EJM0 added a commit to EJM0/Trio that referenced this pull request Oct 18, 2026
…insulin no record explains

Each loop keeps the pod's delivered total; when the next comes ten minutes or more later, on the
same pod, the difference is judged against the history like a return's audit, and what no record
explains is booked and shown. This catches a watch that started without the phone and then failed.
A return starts the check over. As LoopKit/Loop#2545.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kristinkand pushed a commit to kristinkand/Trio that referenced this pull request Oct 18, 2026
… to stop

A take-back revoked the watch and took the pump at once. A phone that then
couldn't reach the pod left a pump nobody dosed. The phone now takes
control and waits up to 5 s for the pod's link; only with it up does it
revoke the watch and take over. Without it, it lets go again, nothing
changes and the watch keeps running, with a note to bring the phone close.
Dosing stays held meanwhile, as the session is not idle. As LoopKit/Loop#2545.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kristinkand pushed a commit to kristinkand/Trio that referenced this pull request Oct 18, 2026
The watch doses with the settings that came with its grant; a change on
the phone during a session never reached it. The screens whose values the
grant carries now show "Locked While the Watch Loops" until the hand-back:
basal, ISF, carb ratio and target editors, meal, bolus calculator, dynamic
ISF, autosens, SMB, target behaviour, advanced algorithm settings and units
and limits. Settings › Apple Watch, Nightscout and the rest stay open, so
the session can still be ended. As LoopKit/Loop#2545 locks Settings during
a loan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kristinkand pushed a commit to kristinkand/Trio that referenced this pull request Oct 18, 2026
…ne ends it

Set again after a hand-over, a temp runs at least Omnipod's 30 minutes,
longer than it had left, should the new side not loop: the risky way for a
high temp. A temp above the scheduled rate now ends at the hand-over
instead, as LoopKit/Loop#2545 does:

- Phone to watch: the phone cancels it at the release and hands nothing
  over; the schedule runs until the watch's first loop.
- Watch to phone: the watch leaves it running and reports it with the
  hand-back; once the phone has read the pod it cancels it, and ends the
  watch's record of it where it stopped it, final, so IOB and the audit
  don't count the planned rest. The schedule runs until the phone's loop.

Low and zero temps are handed over as before, cancelled and set again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kristinkand pushed a commit to kristinkand/Trio that referenced this pull request Oct 18, 2026
…tch has the pump

On 2026-10-18 the watch went without readings from 09:24 to 10:39: a
sensor connect hung while its app was suspended, and only opening it put
that right. A phone in reach now wakes it, as LoopKit/Loop#2545 does with
every phone cycle:

- Phone: 45 s after its own reading, clear of the watch's link to the same
  sensor, a small application context ("untetheredWake"); at most one per
  four minutes, as storing the watch's readings fires the same publisher.
- Watch: that context re-checks the sensor's acquisition and the alarms
  and sends the history. A WatchConnectivity wake is now held until what it
  brought is delivered (at most 20 s, never past the system's expiry), so
  the callbacks run before the app is suspended again; it was completed on
  arrival.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kristinkand pushed a commit to kristinkand/Trio that referenced this pull request Oct 18, 2026
The watch stopped dosing at the hand-back, let go of the pod, and the phone
then waited up to 60 s for its last records before it judged the pump.
Now, as LoopKit/Loop#2545:

- Drain: in the ending phase the watch keeps looping and sends its
  backlog, paced around the sensor as before, until the phone has
  confirmed all of it (at most 3 min; the rest goes with the last records).
- Last stage: no new loop or bolus (isFinalizing), the temp handled as at
  any hand-over, the pod read, the pump status recorded, the last records
  sent and confirmed (at most 30 s), then the pump offered.
- The pod is let go only when the phone acknowledges. The phone takes
  control before it acknowledges, so its connect is standing as the watch
  lets go, and the history it judges is complete. Without an
  acknowledgement for a minute the watch lets go anyway and keeps
  offering, as before; it never takes the pump back on its own.
- A relaunch mid-hand-back goes on draining, or stays in the last stage
  without loops when the pump was offered already.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kristinkand pushed a commit to kristinkand/Trio that referenced this pull request Oct 18, 2026
…e on its word

While it has the pump, the phone keeps a standing grant queued on the watch: the full grant, issued
expired, resent after carbs, boluses and adjustments (30 s floor), on any settings change and every
30 min. With the phone out of reach the watch starts from it on an epoch of its own, if the copy is
younger than the insulin's action time, and books what the pod delivered past the copy's history as
a bolus before its first loop. The phone takes on a newer epoch it hears of before storing its
records, lets go of the pod and stops dosing; the watch tells it at once when it is back and asks
the wrist once to hand back. As LoopKit/Loop#2545.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kristinkand pushed a commit to kristinkand/Trio that referenced this pull request Oct 18, 2026
…insulin no record explains

Each loop keeps the pod's delivered total; when the next comes ten minutes or more later, on the
same pod, the difference is judged against the history like a return's audit, and what no record
explains is booked and shown. This catches a watch that started without the phone and then failed.
A return starts the check over. As LoopKit/Loop#2545.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
EJM0 added a commit to EJM0/Trio that referenced this pull request Oct 24, 2026
… to stop

A take-back revoked the watch and took the pump at once. A phone that then
couldn't reach the pod left a pump nobody dosed. The phone now takes
control and waits up to 5 s for the pod's link; only with it up does it
revoke the watch and take over. Without it, it lets go again, nothing
changes and the watch keeps running, with a note to bring the phone close.
Dosing stays held meanwhile, as the session is not idle. As LoopKit/Loop#2545.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
EJM0 added a commit to EJM0/Trio that referenced this pull request Oct 24, 2026
The watch doses with the settings that came with its grant; a change on
the phone during a session never reached it. The screens whose values the
grant carries now show "Locked While the Watch Loops" until the hand-back:
basal, ISF, carb ratio and target editors, meal, bolus calculator, dynamic
ISF, autosens, SMB, target behaviour, advanced algorithm settings and units
and limits. Settings › Apple Watch, Nightscout and the rest stay open, so
the session can still be ended. As LoopKit/Loop#2545 locks Settings during
a loan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
EJM0 added a commit to EJM0/Trio that referenced this pull request Oct 24, 2026
…ne ends it

Set again after a hand-over, a temp runs at least Omnipod's 30 minutes,
longer than it had left, should the new side not loop: the risky way for a
high temp. A temp above the scheduled rate now ends at the hand-over
instead, as LoopKit/Loop#2545 does:

- Phone to watch: the phone cancels it at the release and hands nothing
  over; the schedule runs until the watch's first loop.
- Watch to phone: the watch leaves it running and reports it with the
  hand-back; once the phone has read the pod it cancels it, and ends the
  watch's record of it where it stopped it, final, so IOB and the audit
  don't count the planned rest. The schedule runs until the phone's loop.

Low and zero temps are handed over as before, cancelled and set again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
EJM0 added a commit to EJM0/Trio that referenced this pull request Oct 24, 2026
…tch has the pump

On 2026-10-18 the watch went without readings from 09:24 to 10:39: a
sensor connect hung while its app was suspended, and only opening it put
that right. A phone in reach now wakes it, as LoopKit/Loop#2545 does with
every phone cycle:

- Phone: 45 s after its own reading, clear of the watch's link to the same
  sensor, a small application context ("untetheredWake"); at most one per
  four minutes, as storing the watch's readings fires the same publisher.
- Watch: that context re-checks the sensor's acquisition and the alarms
  and sends the history. A WatchConnectivity wake is now held until what it
  brought is delivered (at most 20 s, never past the system's expiry), so
  the callbacks run before the app is suspended again; it was completed on
  arrival.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
EJM0 added a commit to EJM0/Trio that referenced this pull request Oct 24, 2026
The watch stopped dosing at the hand-back, let go of the pod, and the phone
then waited up to 60 s for its last records before it judged the pump.
Now, as LoopKit/Loop#2545:

- Drain: in the ending phase the watch keeps looping and sends its
  backlog, paced around the sensor as before, until the phone has
  confirmed all of it (at most 3 min; the rest goes with the last records).
- Last stage: no new loop or bolus (isFinalizing), the temp handled as at
  any hand-over, the pod read, the pump status recorded, the last records
  sent and confirmed (at most 30 s), then the pump offered.
- The pod is let go only when the phone acknowledges. The phone takes
  control before it acknowledges, so its connect is standing as the watch
  lets go, and the history it judges is complete. Without an
  acknowledgement for a minute the watch lets go anyway and keeps
  offering, as before; it never takes the pump back on its own.
- A relaunch mid-hand-back goes on draining, or stays in the last stage
  without loops when the pump was offered already.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
EJM0 added a commit to EJM0/Trio that referenced this pull request Oct 24, 2026
…e on its word

While it has the pump, the phone keeps a standing grant queued on the watch: the full grant, issued
expired, resent after carbs, boluses and adjustments (30 s floor), on any settings change and every
30 min. With the phone out of reach the watch starts from it on an epoch of its own, if the copy is
younger than the insulin's action time, and books what the pod delivered past the copy's history as
a bolus before its first loop. The phone takes on a newer epoch it hears of before storing its
records, lets go of the pod and stops dosing; the watch tells it at once when it is back and asks
the wrist once to hand back. As LoopKit/Loop#2545.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
EJM0 added a commit to EJM0/Trio that referenced this pull request Nov 7, 2026
While the CGM's Read on Apple Watch is on, the phone passes its configuration and the watch builds its own manager
from it (as LoopKit/Loop#2545's WatchCGMHost). The watch reads the sensor all the time but shows its own readings
only once the phone's have stopped coming (7 min since the phone's newest), and alarms by the phone's thresholds only
then. The glucose bubble shows where the reading comes from, a yellow triangle for a sensor problem, and the sensor's
state in place of the minutes while no readings come.

CGMs the watch can read are one protocol, WatchReadableCGM, in WatchDeviceManagers; the G7 is the first. The watch
settings show Read on Apple Watch for any CGM that shares its configuration, with a link to its settings, and a
Critical Alerts switch for the watch's urgent low.

The watch app's Info.plist (bluetooth-central) is now its INFOPLIST_FILE; the watch targets need watchOS 10.6 for
LoopKit and G7SensorKit, and the app's module is Trio for the shared glucose helpers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@marionbarker

Copy link
Copy Markdown
Contributor

Test Continued

Same configuration as previous comment.
❌ Several times phone took over control from the watch; I think because watch was not looping

  • supply sysdiagnose in a private folder visible to Pete and Jeremy

Battery check:

  • Kept watch on since about noon on 6-Nov until 8 am on 7-Nov with watch mostly in control of looping
  • Battery is at 57%

Test Details

Yesterday there was a rapid transfer test (inadvertent) to phone then back to watch:

  • 2026.11.06_17-53: Rapid Transfer (link) - I manually transferred control to phone then tried to return before things had settled

Tried to keep the watch in control but several times the phone took over control from the watch with Watch Not Looping message.

When I was awake and aware when this happened, I captured a sysdiagnose once phone was in control.

  • 2026.11.06_22-32: I was watching a video on my phone wearing earbuds and wearing the watch
  • 2026.11.07_05-14: Recent bathroom break at which I noticed the phone had taken over control while I was sleeping; I transferred back to the watch. Before I went back to sleep, I felt the watch alert that phone had taken over again so grabbed another sysdiagnose.
  • 2026.11.07_07-19: Awake, doing puzzles on my phone, got the notification that phone was taking over Pod (grabbed screenshot of message and sysdiagnose)

Dexcom G7 Ends

Grace period for Dexcom G7 ends this morning at 08:30
08:00 Turn on watch control so that it is in charge when sensor ends
08:05 Insert a new sensor

  • monitor it with the receiver; old sensor will die before new sensor completes warmup
  • decided to use abdomen for Sensor site to see if watch sees sensor more reliably

Will monitor over the next hour and report back in a new comment.

Screenshot of Watch Not Looping message

IMG_9592

List of sysdiagnose files in folder

sysdiagnose_2026.11.06_12-11-11-0800_iPhone-OS_iPhone_24B119.tar.gz
sysdiagnose_2026.11.06_17-53-59-0800_iPhone-OS_iPhone_24B119.tar.gz
sysdiagnose_2026.11.06_22-32-31-0800_iPhone-OS_iPhone_24B119.tar.gz
sysdiagnose_2026.11.07_05-14-52-0800_iPhone-OS_iPhone_24B119.tar.gz
sysdiagnose_2026.11.07_07-19-54-0800_iPhone-OS_iPhone_24B119.tar.gz

For all but the first sysdiagnose, I have notes above about what was happening.

  • I am not sure why I captured the first sysdiagnose, but it is in the folder anyway.

@jeremybarnum

Copy link
Copy Markdown
Author

thank you! Will take a look. The behavior of phone taking over is designed to ensure looping continues if a loan has started, the pod and sensor is back in range, the phone is able to loop and, critically, the phone verifies that the watch is not looping. Your experience is not consistent with that so sounds like a bug. I'll report back.

Jeremy Barnum and others added 2 commits November 7, 2026 15:50
…e pod

When the watch stops reporting, the phone asks whether it still holds the pod. Until now silence, or "I hold it but cannot reach it", made the phone take the pod back. But a watch that misses its sensor readings sleeps, and watchOS often declines to wake it for the phone's message, so the phone could take the pod from a watch that may still be looping. The phone now tells the looper once and leaves the take-back to a tap. A watch that answers that it holds nothing has let the pod go, so the phone still takes it.

The control document's principle 4 and rows 10 and 14 are updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@marionbarker

Copy link
Copy Markdown
Contributor

Test continued

Test what happens when G7 session ends and new sensor has not completed warmup?
✅ got expected no glucose on watch

  • had to end the watch control in order to pair new sensor

✅ paired new sensor on phone and it was picked up by watch

  • after pairing new sensor on watch, started watch control again and it worked fine

This test happened on 2026-11-07

Dexcom G7 Ends

Grace period for Dexcom G7 ends this morning at 08:30
08:00 Turn on watch control so that it is in charge when sensor ends
08:05 Insert a new sensor

  • monitor it with the receiver; old sensor will die before new sensor completes warmup
  • decided to use abdomen for Sensor site to see if watch sees sensor more reliably

08:30ish - there was a haptic on my watch but I did not see the alert
Dexcom Receiver shows 31 minutes remaining for warmup
08:35 put phone in faraday bag and move far away

09:14 Watch shows no recent glucose
retrieve phone, connect to new sensor (which is now fully warmed up)
watch connects to G7 within 5 minutes and asks to pair

@marionbarker

Copy link
Copy Markdown
Contributor

Test new Configuration

Several updates were added to the configuration. The main one being tested is that the app warns when not looping on the watch instead of grabbing control back.

  • ✅ got 2 warnings, but control remained on watch
  • final warning - did not wait for it to resolve - used phone to return control to phone

Configuration

git log --oneline -1
087d17a (HEAD -> next-dev-SportMode-PRs, origin/next-dev-SportMode-PRs) Loop: a silent watch is reported to the looper instead of taken from

Built on personal iPhone 15 pro with SE 3 watch on 2026-11-07 but left Loop on phone control.

Test Details

This test happened on 2026-11-08

Turned on watch control at 5 am and went back to sleep until 6:30. I have had 2 warnings of watch not Looping. One while asleep and one just past 7 am. Checked watched and it went from no recent G7 to picking one up.

I tried again to get a watch sysdiagnose. I got the haptic without the picture flash.

Got one more watch not looping warning (so 3 total since 5 am).

At 07:57, tried to tap End on watch to transfer back to phone and it "could not connect to phone"
Switched to phone and tapped take control back and that worked.

Loop Report

Loop Report 2026-11-08 08-02-10-08-00.md.zip

Watch sysdiagnose

The file shows as incomplete on the phone. I will check later send DM to Jeremy when/if it finishes.

@jeremybarnum

Copy link
Copy Markdown
Author

thanks again for this very useful testing. For now, that "watch appears to not be looping" warning on the phone will be moved to be less frequent until I get all the comms nailed down. Also going to make logging a bit easier during testing so we don't rely on sysdiagnose as much, which is tedious.

Jeremy Barnum and others added 3 commits November 8, 2026 12:56
Problem:
- The phone asks a silent watch whether it holds the pod once the watch's report is 6 minutes overdue, and tells the looper if no answer comes by its next cycle.
- A watch that misses two sensor readings in a row sleeps through the question, so two missed readings are enough for a "Watch May Not Be Looping" notice, at night too.

Solution:
- The phone still asks at 6 minutes, and still takes the pod at once from a watch that answers that it holds nothing.
- It tells the looper only once the watch has been silent 20 minutes.
- The control document's rows 10 and 14 say so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ing a loan

Problem: the watch app reopens on whatever page it was last left on while
its process is alive. After a loan ends, or after the watch has gone back
to the clock, the looper can land on a stale page (often the Sport Mode
glance) instead of the main screen.

Solution: track the scene phase. When the app returns to the front after
being in the background (clock face shown or crown pressed), select the
Sport Mode glance if a loan is in progress, otherwise the main page
(chart page if Start on Chart Page is set). Wrist-down inactivity within
Return to Clock is not the background, so a quick glance keeps its page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: the chart paused its scene after drawing, but its view kept
drawing frames many times a second for as long as the app ran. While a
loan's keep-alive holds the app open, that drained the watch's battery
several times faster.

Solution: the scene says when it starts and stops drawing, and the view
pauses with it (at most one frame a second while idle).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@marionbarker

marionbarker commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

Retrospective

There are some discrepancies in insulin record when looking at Loop, Apple Health, Nightscout and Tidepool.

Pretty sure the missing insulin in my Apple Health are from times when the watch was in charge.

I will go back and determine which times I had the watch running (or the watch branch running). The data should be in the comments above. But I thought I'd make a note of the discrepancy now so it can be investigated.

Apple Health and Loop Insulin Record Screenshot

health-missing-insulin-pr2545

Table from various sources

Day Date NS: basal NS:bolus NS:total TP: basal TP:bolus TP:total
Sun 8-Nov 12.6 21.8 34.3 12.7 15.5 28.2
Sat 7-Nov 11.3 18.0 29.3 13.8 17.8 31.6
Fri 6-Nov 11.6 15.6 27.2 14.7 15.6 30.3
Thu 5-Nov 12.4 16.2 28.6 13.1 16.2 29.3
Wed 4-Nov 11.0 20.5 31.4 11.0 20.5 31.4
Tues 3-Nov 11.8 23.4 35.2 11.8 23.4 35.2

Configuration Changes

There were several configuration changes that are not listed in comments above.

  • 2026-11-06 15:12 (PST), installed a special branch that supports TandemKit (dffc934) and then transitioned from Pods to Mobi at 16:00 PST
  • 2026-11-06 08:00 (PST), installed the current version of LoopWorkspace next-dev branch (9a35873)

@jeremybarnum

Copy link
Copy Markdown
Author

Retrospective

There are some discrepancies in insulin record when looking at Loop, Apple Health, Nightscout and Tidepool.

Pretty sure the missing insulin in my Apple Health are from times when the watch was in charge.

I will go back and determine which times I had the watch running (or the watch branch running). The data should be in the comments above. But I thought I'd make a note of the discrepancy now so it can be investigated.

Apple Health and Loop Insulin Record Screenshot

health-missing-insulin-pr2545 ### Table from various sources Day Date NS: basal NS:bolus NS:total TP: basal TP:bolus TP:total Sun 8-Nov 12.6 21.8 34.3 12.7 15.5 28.2 Sat 7-Nov 11.3 18.0 29.3 13.8 17.8 31.6 Fri 6-Nov 11.6 15.6 27.2 14.7 15.6 30.3 Thu 5-Nov 12.4 16.2 28.6 13.1 16.2 29.3 Wed 4-Nov 11.0 20.5 31.4 11.0 20.5 31.4 Tues 3-Nov 11.8 23.4 35.2 11.8 23.4 35.2 ### Configuration Changes There were several configuration changes that are not listed in comments above.
  • 2026-11-06 15:12 (PST), installed a special branch that supports TandemKit (dffc934) and then transitioned from Pods to Mobi at 16:00 PST
  • 2026-11-06 08:00 (PST), installed the current version of LoopWorkspace next-dev branch (9a35873)

Looking at this, likely to be a bug

Jeremy Barnum and others added 4 commits November 9, 2026 23:03
Co-Authored-By: Claude <noreply@anthropic.com>
…one way

Apple Health showed less insulin than Nightscout and Tidepool on days
with loans, and a take-back shortly after a watch bolus could drop that
bolus from insulin on board.

The phone stored the watch's records two ways: as pump events, and by
writing the dose cache directly. A bolus still being given was stored
as a pending pump event, which Loop does not replace with its finished
record.

Now the phone holds a dose that is still running and stores it once it
has finished: at the watch's next check-in, or at the end of the loan.
The records go in one way, as a pump driver's finished events, and Loop
derives the cache, Health and the uploads from them. The direct cache
write and the rewrite at the end of a loan are removed.

Each save also tells Loop how far the pump's history is complete.
Loop takes that time as final: over any stretch before it with no
temp basal on record, it records the scheduled basal. Two faults
there:

- A check-in said "complete up to now" while a temp basal was still
  running and held back. Loop recorded scheduled basal over the
  running temp: wrong in Health, doubled in Tidepool. The time now
  stops at the start of a temp basal still running.
- A late batch, or one from an older loan, could move the time
  backwards. It now never moves back, and is never earlier than what
  the phone's own pump reports have covered.

A take-back stores the records the phone holds the same way, before
the phone doses again. Its save's result used to be ignored. If the
save fails, the phone now holds automatic dosing and retries each
cycle, so it never doses on insulin on board that is missing the
watch's doses; it refuses a new Start until the save has succeeded.

With the phone in reach during a loan, its insulin on board lags the
watch's by a dose that is still running. The messages between watch
and phone are unchanged.

Co-Authored-By: Claude <noreply@anthropic.com>
… are finished

A loan started without the phone leaves the phone's own bolus or temp basal pending. Two cases were
still under-counted after the loan:

- The loan ended before the dose's programmed end. The save that ends a loan, a hand-back's or a
  take-back's, now stores such a dose finished, ending there, a bolus in full. A take-back saves
  even when it holds no watch records.
- The watch cancels a temp basal it has no record of. It now records that cancel as a zero-length
  rate record, so the phone's temp basal ends at the takeover.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
htenlik pushed a commit to htenlik/Loop that referenced this pull request Nov 10, 2026
With Read on Apple Watch on in the G7 settings, the phone puts the
CGM's exported configuration in the watch context
(DeviceConfigurationSharing, from LoopKit/LoopKit#626). The watch
builds its own G7 manager from it, in the sensor's watch slot, keeps
it across launches, and lets go of the sensor when the phone stops
sharing. Readings continue on the watch when the phone isn't there.

- WatchCGMHost hosts the manager. It rebuilds only when the export
  names a different sensor or code, saves the manager's state, and is
  started at launch so a relaunch for the sensor's link finds it.
- The watch's readings go into its glucose store beside the phone's;
  the same reading from both carries the same sync identifier, so the
  store keeps one. The glance and the complications show the watch's
  reading while it is newer than the phone's, the rest of the phone's
  context as before, and the chart redraws on each one.
- Glucose backfill from the phone starts no later than the phone's
  own last reading, so the watch's readings don't hide a gap.
- Wakes: a Bluetooth wake task is held 25 s, through the reading. The
  app opening, each phone context and a background refresh 60 s after
  each reading re-check that a connect is standing, in case watchOS
  froze the app during the G7's 35 s wait before it lodged one; the
  refresh comes after that wait, so it never connects early.
- Wakes are handled on the main queue: watchOS can deliver the
  Bluetooth wake on CoreBluetooth's queue.
- A third page, shown while the watch reads a sensor, has the
  sensor's name, the G7's note when it needs the code or is
  searching, the last reading and connection, and a countdown to the
  next reading.
- The watch app links and embeds G7SensorKit and LoopKit, and declares
  bluetooth-central with a Bluetooth usage description.

The registry, the context field and the project entries use the names
and IDs of the Sport Mode Loop PR (LoopKit#2545), which can drop
its own copies once this lands. Needs LoopKit/LoopKit#626 and
LoopKit/G7SensorKit#80.
Jeremy Barnum and others added 11 commits November 12, 2026 14:08
On the watch the cycle runs on the main actor, and a run over the long history takes
seconds, during which the screen and incoming phone messages waited. The display run
already runs off it; the cycle's own run now does too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Overnight the watch's cycles fell behind: every copy of a glucose reading asked for a
display run of several seconds, and the cycle waited behind all of them.

Until now the glucose store's change announcement asked for a display run every time,
including for writes that stored nothing, such as a reading the store already had. Now
that announcement asks for none: a new reading runs the cycle, and the cycle ends with its
own display run. Carb and dose changes, an override and the loop switch still ask for one,
but only one request waits, and it is left to a cycle already waiting. The bolus
recommendation's algorithm run moves off the caller's actor, as the display run's already is.

Worth a look upstream: the store announces a change for a write that changed nothing, and
whatever listens does its work again for it. This commit only stops the watch's display
from following that announcement; the store itself is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… screen, ahead of the display update

While the watch held the pod, the recommended bolus on the carb screen refreshed very slowly:
4 to 15 s on the bench after a change. Each cycle ran the algorithm three times (the dose, the
display, and a recommended bolus for a screen that was usually closed), and the open screen's
own request waited behind all three.

The per-cycle recommendation is gone. An open carb or bolus screen asks for its own when it opens and
at each cycle's decision, and that request runs before the cycle's display update. One request is out
at a time; one made meanwhile runs once when it returns.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After 10 s with no answer, the watch offered to start without the phone. The phone was
often still reading the pod and building its grant, so the offer came too early, and the
regular start then completed behind it: the offer to start without the phone was on screen
for a start that was in fact going ahead.

The phone now answers a Start request as soon as it arrives, before the pod read and the
grant. Silence now means only that the phone has not heard the request; once it has
answered, the watch waits up to 30 s for the grant. Both sides log the timing so the waits
can be set from measurements.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ived", then 20 s for the grant

Follows from the previous commit, which split the Start's one wait in two: one for the phone
to say it has the request, one for its grant. Over the bench Starts of 11 Nov the phone said it
had the request within 0.1 to 2.9 s and the grant followed within about 10 s. Each wait is twice
the slowest seen, so a watch with the phone away offers the start without it after 6 s instead
of 10. The phone ignores a Start request older than the same 6 s. End and the other on-screen
waits stay at 10 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… cycle

During a loan, a reading the watch's store takes for the first time should be
offered to the loop, whichever way it came. At every wake the watch asks the
phone for the readings it missed. The answer was stored by a call that tells
nobody, so when it carried the newest reading a moment before the watch's own
copy, the watch's copy counted as already stored and no cycle ran: three
readings in one overnight loan.

The phone's readings now go to the store one way, whether they come in the
phone's regular update or in its answer to that request: the store call the
watch's own readings use, which reports what it took. The newest reading taken
is offered to the loop as before. Nothing changes without a loan except that
catch-up readings are stored as the watch's records of them rather than copies
of the phone's records; nothing on the watch reads the fields that differ.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The algorithm's arithmetic runs off the caller's actor only by a Swift default. On the watch
that matters: watchOS ends an app whose main thread keeps a phone message waiting, and a
build that ran the algorithm there was ended three times in one night. A project setting in
newer Swift reverses the default; with this check the unit tests fail that day.

Debug builds and unit tests only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts: the phone's pump-event report takes upstream's async form and still
tells Sport Mode the pump reported; the watch's CGM host takes upstream's async
form and keeps its handling of unreliable sensor data.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… phone

Upstream now gives every dose its own id and keeps what Loop knows about it (its
origin, device and dosing decision) in a dose metadata store under that id. The
watch does the same: it opens its own dose metadata store beside its dosing
decision store, and each dose it sends, automatic or from the watch's bolus
screen, gets a new id with its metadata stored first, as DeviceDataManager does on
the phone.

The loan's dose records carry the dose's id in place of its decision id, so the
phone's copy of a watch dose has the watch's id. Each record also carries the
dose's metadata, and the phone stores it in its own metadata store as the records
arrive: the watch's dosing decisions already reach the phone at the loan's end,
and this is how the phone's insulin delivery log still finds the decision behind
a dose the watch sent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Upstream's pump managers now call their delegate with async methods, in order,
from any context, and have no delegate queue. The watch's loan controller adopts
the same form as the phone's DeviceDataManager: a pump report returns once it is
booked instead of calling a completion, and the watch no longer sets a delegate
queue on the pod's manager.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lder ones

After a long loan with the phone out of reach, the End took almost three
minutes: the watch had queued one copy of its records per cycle, and when
the phone came back in reach it worked through them one at a time, with the
End's offer queued behind them. Each copy carries every record the phone has
not acknowledged, and the End's offer carries them too, so only the newest
needs to arrive. A queued copy now cancels older queued copies from the same
loan, and a queued End offer cancels them as well. Records stay on the watch
until the phone acknowledges them, so nothing is lost.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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