Repository navigation
feat(once): carry a watch's exchange to its box as x - #49
Conversation
The relay that carried a watch's commands stopped on 17 October 2026. A
watch now sends each exchange message in a one-shot request as x {d}. The
service sends x {dev, id, d} down the box's uplink, with the device id its
own handshake matched and a number of its own, and answers with the box's
d, unread and unchanged.
- Moves the go/protocol pin to srcfl/ftw 657f84ae, which adds x.
- /v1/app/once admits x by the roster entry's tun, so commands work with
the readable copy off. The other types now need a non-empty effective
scope, as protocol-cloud.md step 3 says; before, a tun device with the
copy off got an empty snapshot.
- No box, a box without x in hello.caps, 8 exchanges open at it, or 5 s
without an answer: an empty d, never an error. The one-shot limits hold
for the whole wait.
- The uplink routes the box's x by id and dev. One nobody waits for is
dropped, counted and charged to the box's message rate. When the box's
connection ends, every waiting exchange gets an empty d at once.
- Tests replay cloud-once-x-vectors.json through the real handler and
uplink, and cover admission, no answer, routing and an end-to-end round
trip.
Pairs with srcfl/ftw 657f84ae (go/protocol and the golden vectors).
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: 0fef724c65
ℹ️ 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".
| if c.send(exchange{Dev: dev, ID: id, D: d}) != nil { | ||
| return nil | ||
| } | ||
| t := time.NewTimer(u.xWithin) |
There was a problem hiding this comment.
Bound the uplink send by the exchange deadline
When the box stops reading or another uplink write holds writeMu, c.send blocks before either the exchange timer or context cancellation can take effect. The production send path uses writeFrame, which waits for that mutex and then allows another 10 seconds for the write, so an exchange can exceed both its advertised 5-second bound and ServeOnce's 10-second response deadline. In that case the watch gets a failed HTTP response instead of the promised empty d, while the request continues holding its admission slots. Apply the exchange deadline and cancellation to the send as well as the answer wait.
Useful? React with 👍 / 👎.
A roster entry with tun but another role, a phone's, has its own tunnel and no business on the watch's door. Review of #49 (N1). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…o Core master 21e3d169 (#1818 merged) The uplink keeps both the exchange waiters and the tiles resync clock. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Why
A watch's commands reached its box through the relay's exchange (ADR 0009), and the relay stopped on 17 October 2026. A watch has HTTPS and nothing lower, so its one-shot request now carries each exchange message, and the service hands it to the box on the uplink, unread.
What
x./v1/app/oncetakesx {d}:store.Forwith the key the handshake proved, as before.xthen needs the roster entry'stunand rolewatch, so commands work with the readable copy off. A phone withtungets 403 forx: its own session carries its commands.tunand no scopes now gets 403 for a read, where before it got an empty 200 snapshot. A watch with the copy off is such a device.x {dev, id, d}:devcomes from the service's own handshake, andidis a number per connection. It returns the box'sdwithin 5 s.xcomes back empty, never as an error, in four cases. The first three answer at once:x;idanddev. An answer nobody waits for is dropped, counted, and charged to the box's message rate. When a box's connection ends, every waiting exchange gets an emptydat once.xamong what the service sends a box and what a watch may do.The service grows by about 160 lines of Go, not counting tests.
Proof
TestOnceCarriesTheExchangeVectorsreplayscloud-once-x-vectors.jsonthrough the real handler and uplink, byte for byte.tunis refusedx(403), and so is a watch withouttun. No-answer cases, routing, end of uplink, malformedx, the one-at-a-time turn, the downlink's closed set, andTestAWatchExchangesWithItsBoxEndToEnd.go vet ./...,go test -race -count=1 -p 2 ./..., the same with-tags ftwclouddev, andscripts/check-imports.sh. Each fuzz target ran 30 s. linux/arm64 builds.ftwclouddfrom this branch, the box from feat(cloud): a watch's exchange rides FTW Cloud ftw#1819, and the watch from feat(watch): the watch on its own through FTW Cloud ftw-app#99, with the paired iPhone simulator shut down. The watch read its home and charged the car.Nothing deployed.
Not done
/v1/app(the session socket) still admits a key whose roster role iswatch. The box answers such a key on the exchange alone (TestAWatchKeyGetsNoStreamin srcfl/ftw#1819), so the session would carry nothing. Refusing it at the door as well is a one-line change, left out to keep this PR tox.Pairs
srcfl/ftw#1818 (merge first, then move the pin to its merge commit), srcfl/ftw#1819, srcfl/ftw-app#99.
🤖 Generated with Claude Code