"The payload never leaves the device on a hope."
Why the Watch Keeps Its Own Queue
transferUserInfo is already queued, ordered and guaranteed by the system, so keeping a second copy on the watch looks redundant. It is not, for two reasons. The local write is the capture, and the transfer is a separate act that can fail or take hours, so the words must be safe on the wrist before anything is sent. And a visible queue is honest: "3 waiting" is something the wearer can see, instead of being told everything is already filed. The watch's queue follows the phone's outbox rules one hop out. An entry is removed only in the transfer's didFinish, never at send time. Everything still waiting is offered again whenever the session activates, because the receiver deduplicates on the id. And each capture carries the moment it was spoken, with its time zone, stamped on the wrist; the phone passes that through untouched, because restamping on arrival would record a lie after a phone that was off for hours.
An Unreadable Queue Is Kept, Never Replaced
The first version of the queue loaded with try? decode … else pending = []. That starts empty whenever the file cannot be read, and the next capture's save writes that empty list over whatever was waiting. On the watch those entries are the only copy. One added field in a new build is enough to make an old file unreadable. The kit's queue now moves an unreadable file aside with its bytes intact and names it in a restore failure the app can show.
Idempotent Where It Counts: The Store
Deduplication at a door only covers what is still standing in it. The training app's phone store appended every offer, the watch offered its captures again on each activation by design, and a payload offered twice became two entries with one id. One acknowledgement settled one; the other sat at the head of the phone's queue for eight TestFlight builds. The store itself must be idempotent on the capture id.
The phone's door has an ordering rule of its own. It commits the capture first and records the id in its seen ledger after. Written the other way, a commit that throws leaves the id marked as seen, the watch's next re-offer is dropped at the door, and the capture, whose only other copy the watch was holding for exactly this case, is gone once the watch lets go. Written this way, a crash between commit and record costs one redundant commit, which the idempotent store absorbs. The ledger is a bounded cache that stops honest echoes early; it is never the only defence.
Counts That Mean What They Say
A waiting count read from the watch's queue alone is the transfer daemon's state, not the phone's: "3 waiting" was true of the transfers and misleading about captures the phone already held. When the phone answers the doorbell with a receipt, those ids count as handed over until their transfers finish, and the status line folds them in.