Repository navigation
docs(adr): ADR 0009, an Apple Watch that works without the phone - #1621
Conversation
An Apple Watch cannot open the WebSocket the phone uses, so it reaches the box with one HTTPS request per step through a second relay socket. Core keeps exchange sessions apart from the four stream sessions, moves the command log from the session to the box, and adds a watch role that an owner phone adds and that never holds more than that phone. Proposed only. Paired with srcfl/ftw-webapp docs/watch-exchange.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 46454796ec
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| push that follows an applied command on the stream. Every frame is lane 0, | ||
| 512 B: |
There was a problem hiding this comment.
Keep on-demand exchange traffic out of lane 0
When a watch opens an exchange or sends a command, these frames exist only in response to that request, so they cannot have lane 0's required fixed cadence and expose exactly when an exchange occurs. Use a distinct exchange framing class or explicitly redesign the cadence and its privacy model instead of labeling the on-demand messages as lane 0.
AGENTS.md reference: AGENTS.md:L71-L73
Useful? React with 👍 / 👎.
| 3. **The command log belongs to the box.** It moves out of the handler, keyed | ||
| by root device and `cmdId`, and keeps each entry 24 h of uptime, as | ||
| protocol.md already promises. A watch's root device is the phone that added | ||
| it, so one intent sent by both paths acts once. This also closes the gap | ||
| for phones: a retry after a reconnect returns the first outcome instead of | ||
| acting again. |
There was a problem hiding this comment.
Atomically claim command IDs across both paths
When the watch exchange and its phone fallback deliver the same cmdId concurrently, moving the existing log to box scope is insufficient: the current onCmd performs cmds.lookup separately from the later acceptCmd call to cmds.remember, so both handlers can observe a miss and dispatch the command. The shared log needs an atomic lookup-or-reserve operation keyed by root device and cmdId before either handler may act; otherwise the stated both-paths guarantee and test 8 can still double-execute an intent.
Useful? React with 👍 / 👎.
Fredrik said yes to A–F on 14 October 2026 and asked that the watch work fully on its own, for example on LTE with the phone away. Reads now go through a one-shot Noise read at FTW Cloud, with `now` on the exchange when FTW Cloud is off. The watch gets the plan and alert scopes, its own token and push registrations, and one identity that the box's device list removes in both doors. The ADR now orders the work as ten pull requests across four repositories, each with its proof. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Accepted design, no code. Paired with srcfl/ftw-webapp#83, which holds the exchange's wire, the relay route and the client changes; neither works without the other.
Decision
Fredrik decided on 14 October 2026 that a watch controls the box directly as well as through the phone, that an unlocked watch on the wrist counts as unlocked, and that each watch has its own key. On this design he answered "ja på alla" (yes to A–F) and asked that the watch work fully on its own, for example on LTE with the phone away. The ADR's status is now accepted.
watchOS allows HTTPS and no WebSocket (TN3135), and both the relay and FTW Cloud's
/v1/appare WebSocket. So the watch gets two one-shot doors:/r/<epoch>/<handle>/exchange. Each step is one HTTPS request carrying one Noise IK message. The relay reads only an id it wrote.POST /v1/app/once. That is the app session's existing handshake without the WebSocket: message 1 asks, message 2 carries the snapshot. FTW Cloud never wakes the box and still never controls. With FTW Cloud off, the watch readsnowthrough the exchange.What Core changes
cmdandnowonly.CmdRetentionMsand ftw-webapp'sdocs/protocol.mdpromise 24 h.watch(ftw.live.read,ftw.plan.read,ftw.events.read,ftw.dispatch.write), added only by an owner phone over its session with step-up, never holding more than that phone. Removing a phone removes its watches.cmd.exchange, and watches in the roster sent to FTW Cloud.FTW Cloud gains the one-shot read, role
watchin the roster,apponpush.registerso alerts and widget pushes reach the watch, and a token per watch. The box's device list is the one place to remove a watch from both doors.Order of work
cmdIdin a second session acts once; another device's id is new; pruning at 24 hwatch, capabilitycmd.exchangenow,hello_ok.cloud, capability saidcloudwireand protocol-cloud.md: one-shot read,push.register.app, rolewatch/v1/app/once, watch push topics (after ADR 0008's token,/v1/nowand widget push)/v1/app, removal within a second, token scope, identical alert payloads, nothing held after an answernowfallback, complications, widget push, alertsStep 1 needs nothing else; steps 2 and 4 can run beside it. Step 7 waits for FTW Cloud's token and widget push from ADR 0008.
For W1, being built now
Let the watch mint
cmdIdand pass it to the phone, so a command that tried both paths runs once.What I checked
Read against
appuplink/session.go,appproto/handler.go,appproto/command.go,appproto/ev.go,appwire/transport.go,api/api_app_link.go,docs/protocol-cloud.md, ftw-cloud'sinternal/appsessionandinternal/server, and the relay source. Apple's rule that identical payloads to a watch and its phone show once is on theregisterForRemoteNotifications()page. Nothing was run; this is a document.🤖 Generated with Claude Code