Repository navigation
SportMode - Tidepool uploads from the watch during a loan - #2555
Open
jeremybarnum wants to merge 102 commits into
Open
jeremybarnum wants to merge 102 commits into
jeremybarnum wants to merge 102 commits into
Conversation
This was referenced Oct 5, 2026
The watch needs the same types to run the same loop as the phone. They move unchanged from the Loop app to LoopCore: the algorithm input (StoredDataAlgorithmInput), the dosing decision record (BolusDosingDecision), LoopConstants, LoopError with its messages and translations, LoopWarning, SimpleInsulinDose, CarbEntryChange, the four store protocols, SettingsProvider, the DosingDecisionStore, PumpManagerError and TempBasalRecommendation extensions, and LoopDataManager's conversions of carb entries and glucose samples for the algorithm. The algorithm input gains a public initializer, because the Loop app still builds it until the next commit. Files that use them import LoopCore. Nothing else changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lending the pod to the watch needs a protocol both apps share, and one place that decides, in each situation, which device loops, which stands aside and which asks. - LoopCore/PodLoan: the messages (the request and grant, the takeover, records, the hand-back offer and its acknowledgement, the take-back, a status question and its answer), how they travel (live when the watch is reachable, and queued as well where a message must arrive), and the loan's records. - LoanControl: the control decisions, one function per situation. Each is a row of the control document, Documentation/SportMode/Control.md, which sets out the situations, the principles behind each decision and the known limits. - The SPORT_MODE_ENABLED build setting, off by default. With it off, neither app runs any of this. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sport Mode runs Loop's loop on the watch. Rather than a second copy of LoopDataManager.loop() there, the loop moves into LoopCore, where both apps run it. - LoopCycle holds the body of loop(): the input checks, the algorithm run, the dosing decision and its record, and the enact. It asks its host only for what the device knows: the stores, the settings and overrides, the algorithm experiments, the dosing strategy, whether dosing is held and whether the loop is closed. LoopDataManager is the phone's host. - The manual bolus recommendation, cancelling the loop's temp basal, the display run and the remote recommendation go through the cycle too. - The pump-facing members DeviceDataManager had for delivery (the pump's state, rounding, the checks before a dose is enacted) become PumpManagerDeliveryDelegate in LoopCore; each device sends the checked dose its own way. - The dosing record of a bolus asked for on the watch comes from one builder, BolusDosingDecision.watchBolus, which WatchDataManager now calls. The phone behaves as before, and its tests pass. One difference: the watch bolus 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. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The phone's side of a loan, in its own files, on the main actor like the phone's managers. - Start: the phone lends only when it may (no bolus being given, no command to the pod unconfirmed, settings complete). It releases the pump and sends a grant: the pump's exported configuration, the settings, the glucose alert settings and dosing strategy, and recent insulin, carbs, glucose and overrides. - During the loan it doses nothing, stores the watch's records once each as they arrive, a temp basal still running once it has ended, and checks the pod's delivered total against them. - The pod comes back at a hand-back, when the user takes it back, or when the watch goes quiet and does not answer. The phone imports the records, checks the insulin and books an estimate for any the records cannot explain; later records replace it. - A loan the watch started without the phone, from the copy of the grant the phone keeps on the watch for that: when the phone hears from the watch again (its records, a hand-back offer, or its answer to the phone's question) about a loan this phone never granted, it stands aside, holding its dosing and releasing the pump. If the loan began before this phone last took the pod back because the watch went quiet, the phone tells the watch to stop instead. If its pod driver keeps the pod because the phone's last command is unconfirmed, the phone reads the pod, which settles the command, and the driver lets go. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Everything Sport Mode changes in the phone's stock files, together, so it can be read in one place. Each hook does nothing on a phone that never lends its pod, except the Predicted Glucose screen's redraw below. - DeviceDataManager: a pump report runs the loan's insulin check before it completes; Loop's diagnostic report includes the loan's state. - LoopDataManager: automatic dosing is held while the pod is lent. - AlertManager: the Loop Failure warnings stop while the pump is released. - GlucoseAlertManager and MealDetectionManager: while the watch raises this user's glucose alerts, the phone evaluates none, and starts its episodes afresh afterwards. - WatchDataManager: loan messages go to the loan controller; a bolus sent from the watch while the pod is lent is refused. - LoopAppManager: the wiring, and banners in the foreground for the loan's notices. - PredictionTableViewController: the Predicted Glucose screen redraws when insulin, carbs or the forecast change, as the status screen does. During a loan the watch's records arrive without a glucose reading, so the open screen would otherwise fall behind until the next one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Until now the watch app has been a display for the phone's loop. Its screens show the summary the phone sends after each cycle (insulin and carbs on board, the forecast, when the loop last ran), with the watch's own glucose reading in place of the phone's when it is newer, and the app keeps glucose and carb stores only to draw the chart and the carb list. A watch that runs the loop itself, as Sport Mode's does in the next commit, needs its doses and dosing decisions in stores too, and its screens need to show that loop instead of the phone's summary. This commit prepares both, so that Sport Mode builds on the stock watch app rather than running beside it with stores and screens of its own. - The watch's LoopDataManager opens dose and dosing-decision stores, with the alert history and device log, beside its glucose and carb stores, in the watch's usual locations. In Sport Mode the loop and the screens use the same stores. - Every screen reads one display state, which until now was used for the glucose reading alone: the phone's summary, with the watch's newer reading in its place. Sport Mode, in the next commit, lays the watch's own loop over it. - A fix: the carb list asks the phone for its carbs when it opens and when the phone's summary changes. Since the carb list moved to SwiftUI, nothing has asked, so the list could stay empty. Without Sport Mode the code changes, but the watch app looks and works as before, apart from that fix to the carb list. The simulator tests check the watch app with Sport Mode built in and with it off. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After the previous commits the watch can run Loop's loop cycle on stores of its own, but it has no pump to dose with: the pod belongs to the phone. This commit adds the watch's side of a loan, in its own folder, on the main actor. - WatchLoopManager runs the shared loop cycle on the watch, over the pump the phone lends. Its cycle work runs one step at a time, in the order it was asked for, as on the phone. - PodLoanWatchController handles the loan from the watch's side: Start, with the phone or, when the phone can't be reached, from the copy of the grant the phone keeps on the watch; the takeover of the pod; records sent to the phone as checkpoints during the loan; End and the hand-back; standing aside when the phone takes the pod back; and resuming after the watch app relaunches, which checks with the phone first and counts any insulin given while the watch was off before it doses again. - During a loan the watch raises the glucose alarms (with Loop's own GlucoseAlertManager, compiled for the watch) and the pod and Loop Failure alerts. It keeps its dosing decisions, alert history and device log, which go to the phone in one file when the loan ends. - Hooks in stock watch files: the extension delegate builds the Sport Mode stack and holds loan messages that arrive before it is ready; LoopDataManager hands each stored glucose reading to the loop, and shows the loop's state and override on the screens during a loan; WatchCGMHost reports unreliable sensor data, so the loop cancels a high temp basal, as the phone does. - The watch's entitlements come from a build setting, empty by default, so without Sport Mode the watch signs as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After the previous commits a loan runs end to end, but nothing on either screen starts one, ends one or says one is running: the watch's pages and the phone's status screen look as they do without Sport Mode. - Watch: the Sport Mode glance, a page after the watch app's usual two, which shows glucose and trend in the user's unit, the loop ring, insulin and carbs on board and the running temp basal, 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. - Watch: during a loan, carbs can be deleted from the carb 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. A bolus or an End that is refused says why on the screen. With Sport Mode built in, the check that the phone is set up applies page by page, so the glance stays reachable when it is not. - Phone: a "Sport Mode Active" banner, which says when the watch's records last arrived, and a pod tile that say the watch has the pod and offer to take it back; carbs, bolus, presets and settings unavailable while it does; the loop's status ring stops ageing, and its explanation says the watch is running the loop. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
While the watch holds the pod it runs stock's RemoteDataServicesManager, compiled into the watch target unchanged, so glucose, treatments and loop status reach Nightscout with the phone asleep or out of reach. - The Start grant carries each active remote service's shared configuration (DeviceConfigurationSharing), never the standing copy, never logged. The wrist adopts the ones it knows (LoanRemoteUploads), in memory only, and drops them at the pump's teardown. - Glucose is exclusive for the loan: the phone holds its own glucose uploads while the pod is away (one guard in DeviceDataManager), because Nightscout matches readings only by their timestamp to the second and each device stamps a G7 reading from its own receipt. The wrist follows its CGM's upload setting, else the phone CGM's, carried in the grant. - Hand-back confirms: the watch flushes, waits up to 5 s for each service's bookmarks to reach its stores, and reports per service which types it uploaded in full. The phone moves its own bookmarks past exactly those (glucose, and the watch's dosing decisions it imports) and uploads everything else itself; a force reclaim skips nothing. RemoteDataServicesManager's UserDefaults anchor helpers lose fileprivate so the phone can move a bookmark at hand-back. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The grant's shared service configurations and the phone's glucose-upload answer lived only in memory, so a watch relaunched during a loan resumed dosing with no uploads. They now sit in the loan's saved GrantedSettings, beside the pump's own state (which already holds the pod's pairing key), and resume stages them before ACTIVE, which starts them as a grant's would. Teardown already clears GrantedSettings, so they go with the loan. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The watch adopts the phone's Tidepool service as it does Nightscout's, uploading into the phone's data set as the same user. The session comes with its refresh token, so the watch renews its own access token during a loan: an access token lasts about 12 minutes, a loan hours, and the phone may be out of reach. Tidepool does not rotate refresh tokens, so the two renew independently. When the upload starts, one log line says whether the watch can renew: refresh token present, client ID present, minutes left on the access token. No token values are logged. At the loan's end the watch drops the session rather than logging out, which would end the phone's. TidepoolServiceKit is linked and embedded in the watch target, as NightscoutServiceKit is. Needs TidepoolService built for watchOS and sharing its session with its refresh token, and TidepoolKit's watchOS build (tidepool-org/TidepoolKit). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In the background, watchOS refuses cellular to the app's requests and leaves them waiting until a timeout or a route change. An upload type is now held only in that case — app in the background, cellular the only route — and goes when the app opens or the route changes. On opening, every type is sent, not only the held ones: the held list lives in memory and a relaunch forgets it, while the uploader resumes from its own saved position. Phone link, Wi-Fi and app in front upload as before. Two log lines come with it: the watch's internet route at each change (which the gate reads), and how each upload settled. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The WatchApp target runs Apply Info Customizations, as the Loop target already does, so its BuildDetails carry TidepoolServiceClientId. With the session's refresh token (TidepoolService "Share the Tidepool session WITH its refresh token"), the watch renews its Tidepool access token itself during a loan; without both, it uploads until the shared token expires (~12 min) and the phone catches up at hand-back. Optional with that commit: drop the two together. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jeremybarnum
force-pushed
the
sportmode/tidepool-uploads
branch
from
October 28, 2026 05:40
67ab620 to
0c7c44e
Compare
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jeremybarnum
marked this pull request as ready for review
October 28, 2026 18:32
…oan the watch started without the phone Follows up the review point that, during Sport Mode, the phone's screens leave out what the watch has done, even when the two are in touch. The earlier commit answered it by saving the watch's records on the phone as they arrive and showing on the banner how current they are. It missed two things: - The banner's time moved whenever the watch said anything, even when it sent no records. So the banner could say the phone was more current than it was. Now it moves only when the watch's records are saved on the phone, and reads "Insulin and carbs as of …". - If the watch started Sport Mode while the phone was away, the phone ignored the watch's records even after the two were back in touch, until Sport Mode ended. Its insulin and carbs stayed as they were when it left, and the banner showed no time. Now, once they are in touch, the phone takes the watch's records and saves them as they arrive, as it does when Sport Mode starts with the phone nearby. For this, the watch includes the phone's offline-start credential with its records. Until the first records arrive, the banner shows when the phone last read the pod itself. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…of it We're narrowing down which Apple Watch generations work with Sport Mode: an SE and a Series 9 do, a Series 5 doesn't. On a Series 7 the grant never reached the watch while smaller messages got through, and the phone's log couldn't say why: the grant goes urgently and queued at once, and a failed urgent copy wasn't logged. The phone now logs the grant's size and whether it went urgently or queued only, and a failed urgent send of any message that also rides the queue. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
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>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
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>
# Conflicts: # Loop/Managers/PodLoanPhoneController.swift
…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>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Sport Mode: Tidepool uploads from the watch during a loan. Stacked on the Nightscout watch-uploads PR (linked below), which is merged in as it moves; the net diff shows only this PR's changes. From my fork so you can push edits.
The watch adopts the phone's Tidepool service the way it adopts Nightscout's, and uploads into the phone's data set as the same user. Everything else (exclusive glucose, the hand-back confirmation, relaunch) is the Nightscout PR's machinery, unchanged.
81ef1baf("Watch: Tidepool client ID in the watch app's build details") runs Apply Info Customizations on the WatchApp target, so the watch has Tidepool's client ID. Together with SportMode - TidepoolService builds for watchOS and shares its configuration TidepoolService#40's last commit, which shares the session's refresh token, the watch renews its own access token and Tidepool updates in real time for the whole loan. The token lives where the rest of the grant does: the loan's saved state in the watch app's files, not the Keychain. Drop both commits and the watch uploads until the shared access token expires (about 12 minutes), then fails without logging out; that service goes unconfirmed at hand-back and the phone uploads the window.Draft until TidepoolKit's watchOS build lands (tidepool-org/TidepoolKit, linked below): TidepoolService pulls TidepoolKit from its
devbranch, so until then TidepoolService doesn't build for watchOS. I bench-tested with a local TidepoolKit override: uploads with the phone off, through a watch relaunch, past the access token's expiry (with both optional commits), and a hand-back where Tidepool confirmed both glucose and loop status.Related PRs (Sport Mode watch uploads)
🤖 Generated with Claude Code