Repository navigation
SportMode: Loop changes needed to run autonomously on the watch - #2545
jeremybarnum wants to merge 54 commits into
Conversation
…ications-v2 # Conflicts: # Loop.xcodeproj/project.pbxproj
… 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>
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>
…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>
…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>
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>
…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>
…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>
… 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>
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>
…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>
…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>
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>
…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>
…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>
… 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>
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>
…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>
…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>
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>
…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>
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>
Test ContinuedSame configuration as previous comment.
Battery check:
Test DetailsYesterday there was a rapid transfer test (inadvertent) to phone then back to watch:
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.
Dexcom G7 EndsGrace period for Dexcom G7 ends this morning at 08:30
Will monitor over the next hour and report back in a new comment. Screenshot of Watch Not Looping message
List of sysdiagnose files in foldersysdiagnose_2026.11.06_12-11-11-0800_iPhone-OS_iPhone_24B119.tar.gz For all but the first sysdiagnose, I have notes above about what was happening.
|
|
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. |
…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>
Test continuedTest what happens when G7 session ends and new sensor has not completed warmup?
✅ paired new sensor on phone and it was picked up by watch
This test happened on 2026-11-07 Dexcom G7 EndsGrace period for Dexcom G7 ends this morning at 08:30
08:30ish - there was a haptic on my watch but I did not see the alert 09:14 Watch shows no recent glucose |
Test new ConfigurationSeveral 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.
ConfigurationBuilt on personal iPhone 15 pro with SE 3 watch on 2026-11-07 but left Loop on phone control. Test DetailsThis 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" Loop ReportLoop Report 2026-11-08 08-02-10-08-00.md.zip Watch sysdiagnoseThe file shows as incomplete on the phone. I will check later send DM to Jeremy when/if it finishes. |
|
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. |
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>
Looking at this, likely to be a bug |
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>
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.
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>



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.
Refactoring of stock Loop
On the phone, the refactoring shares code with the watch and changes no behaviour:
LoopDataManager.loop()becomesLoopCycle, which both apps run;LoopDataManageris 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.LoopDataManageris about 500 lines shorter.LoopConstants,LoopError(with its messages and translations),LoopWarning,SimpleInsulinDose,CarbEntryChange, the four store protocols,SettingsProviderand three extensions. About a dozen files gainimport LoopCoreand nothing else.DeviceDataManager's pump-facing delivery members (the pump's state, rounding, the checks before a dose is enacted) becomePumpManagerDeliveryDelegatein LoopCore, which the watch shares.WatchDataManagerbuilds the dosing record of a bolus asked for on the watch withBolusDosingDecision.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:
LoopDataManageropens 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.GlucoseAlertManagerfor the glucose alarms during a loan, andDoseEnactor.How it works
WatchApp Extension/Managers/WatchDeviceManagers.swiftand OmnipodKit only inWatchApp 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.LoanControl.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.
Duplicate carbs. Extra carbs mean extra insulin, so this is the dangerous direction.
NewCarbEntrycarries 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.Known limitations
Worth a look
import LoopCore; 16 on the watch. The phone's Sport Mode hooks are in commit 5, its UI in commit 8.DiagnosticLogon the phone and the watch app'sOSLogon the watch.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:
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)