"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. Two edges matter as much as the main case. A file that exists but will not read is the same danger as one that will not decode, so the queue does not treat a read error as a fresh install. And if the move itself fails, the queue leaves the file where it is and refuses every write, because writing anything there would replace it.
The same rule runs the other way for a new capture. The local write is the capture, so a capture whose save failed is not in the queue: the entry is taken back out of memory and the error goes to the caller. Otherwise a retry finds the id already pending, returns success without writing, and the words are gone at the next launch.
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.