Skip to content

docs(adr): ADR 0009, an Apple Watch that works without the phone - #1621

Merged
frahlg merged 3 commits into
masterfrom
claude/watch-direct-adr
Oct 14, 2026
Merged

frahlg merged 3 commits into
masterfrom
claude/watch-direct-adr

Conversation

@frahlg

@frahlg frahlg commented Oct 14, 2026 •

Copy link
Copy Markdown
Member

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/app are WebSocket. So the watch gets two one-shot doors:

  • Commands go to the box through a second relay socket, /r/<epoch>/<handle>/exchange. Each step is one HTTPS request carrying one Noise IK message. The relay reads only an id it wrote.
  • Reads go to FTW Cloud in one request, 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 reads now through the exchange.

What Core changes

  1. A second relay socket and exchange sessions apart from the four stream sessions, taking cmd and now only.
  2. The command log moves from the session's handler to the box. This stands on its own: today a retry in a new session would act again, although CmdRetentionMs and ftw-webapp's docs/protocol.md promise 24 h.
  3. Role 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.
  4. Capability cmd.exchange, and watches in the roster sent to FTW Cloud.

FTW Cloud gains the one-shot read, role watch in the roster, app on push.register so 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

# Repository What Proof
1 ftw Command log on the box Same cmdId in a second session acts once; another device's id is new; pruning at 24 h
2 ftw-webapp, then ftw and ftw-app Registry: role watch, capability cmd.exchange Drift checks green in all three
3 ftw Watch role, enrolment, parent, removal by phone, roster Role and removal tests
4 ftw-webapp Relay exchange, limits, README claim, Caddy log Relay blindness, routing, shape, limits, epoch
5 ftw Exchange socket and sessions, now, hello_ok.cloud, capability said Replay, both paths, isolation, timing, Go vectors, end to end through a real relay
6 ftw cloudwire and protocol-cloud.md: one-shot read, push.register.app, role watch Golden vectors
7 ftw-cloud /v1/app/once, watch push topics (after ADR 0008's token, /v1/now and widget push) Routes, admission parity with /v1/app, removal within a second, token scope, identical alert payloads, nothing held after an answer
8 ftw-app Wire for watchOS Swift replays exchange and read vectors on the watchOS simulator
9 ftw-app Add watch on the phone; direct commands on the watch Device tests for commands, wrist-off and removal
10 ftw-app Watch on its own: reads, now fallback, complications, widget push, alerts Device tests on LTE with the phone off

Step 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 cmdId and 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's internal/appsession and internal/server, and the relay source. Apple's rule that identical payloads to a watch and its phone show once is on the registerForRemoteNotifications() page. Nothing was run; this is a document.

🤖 Generated with Claude Code

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>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-14T06:41:27.486605Z 4645479 PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread docs/adr/0009-watch-exchange.md Outdated
Comment on lines +63 to +64
push that follows an applied command on the stream. Every frame is lane 0,
512 B:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment thread docs/adr/0009-watch-exchange.md Outdated
Comment on lines +74 to +79
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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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>
@frahlg frahlg changed the title docs(adr): propose ADR 0009, watch commands through a one-shot exchange docs(adr): ADR 0009, an Apple Watch that works without the phone Oct 14, 2026
@frahlg
frahlg merged commit 7081097 into master Oct 14, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant