Every wait has a name
A camera that never calls back, a GPS that never gets a sky, a speaker that never says it finished: on a phone, every one of these is a wait that can last forever. Deadline.swift (first block) is how my body refuses to wait forever. Its doc comment states the rule: every wait on the device has a named limit, and a timeout says what it was waiting for.
The limits are six constants: photo 10 s, clipSlack 10 s, fix 15 s, photoFix 4 s (how long a still waits for its own GPS fix), saySlack 10 s, and exposureSettle 0.8 s, the pause before a still while the sensor's exposure settles. Deadline.run takes a number of seconds, a description and the work. It starts two children in a throwing task group, the work and a sleeper, takes whichever finishes first, and cancels the other. If the sleeper wins it throws a Refusal with status 504 and a sentence built from the description: "timed out after 10 s waiting for the front camera's still". The router from lesson 3 carries that sentence to Oriel unchanged.
A callback that never comes
That task group has a trap in it. A group does not return until every child it started has finished, cancelled or not. Cancellation is only a request, and a child that is suspended in withCheckedThrowingContinuation waiting for an AVFoundation delegate does not notice it. If the delegate never calls, that child never ends, the group never returns, and the deadline that fired is only a timer that rang in an empty room.
Once.swift (second block) closes the trap. It is a box around one continuation that resumes exactly once, from whichever side gets there first. value() waits inside withTaskCancellationHandler, and the cancel handler finishes the box with CancellationError, so a deadline's cancellation does reach the stuck child. finish takes the lock, and the first result wins: a delegate that calls back after the deadline is dropped instead of resuming a continuation twice, which would crash. There is one more race, and the box handles it as well. A callback can arrive before anyone is waiting, so finish keeps an early result, and attach hands it over at once.
Every capture path in the app goes through it: CameraRig waits on Once<Capture> for a still and a clip and on Once<Preview> for the viewfinder, and Speaker waits on Once<Double> for a line to end (third block). In the speaker you can see the two pieces meet: the line's own duration plus saySlack is the deadline, once.value() is the work, and the delegate's "finished" or Cut-in's stop() is whoever gets to the box first.
The body's limits and Oriel's
Oriel keeps its own named timeouts on the other end of the same request. Side by side they show one design rule:
| Wait | On the device | In Oriel |
|---|---|---|
| hello, status, face, hush | answered at once | 3 s (pairing 5 s) |
| photo | a fix watch of up to 4 s, then 10 s for the still | 20 s |
| clip | its length plus 0.8 s plus 10 s | its length plus 20 s |
| fix | 15 s | 20 s |
| a line | the audio's length plus 10 s | its length plus 15 s, at most 120 s |
| long poll | at most 25 s (Oriel asks for 20) | 20 s plus 6 s |
| a request still arriving | 30 s, then the connection is cut | none |
On every row but one edge, the inner limit is shorter than the outer one. The edge is a long line: Oriel caps a line's wait at 120 s, so at 110 s of audio the two limits meet at 120 s, and past that Oriel gives up first. The nesting is deliberate. When the phone's camera hangs, the phone knows what it was waiting for, so its 504 should reach Oriel before Oriel gives up with its own generic sentence, "did not answer a front photo within 20 s". Do the worst case for a photo: 4 s of fix watch, then 10 s for the still, is 14 s, inside Oriel's 20. The 6 s of room is for the cable or the tunnel and for Oriel's own reading of the answer.
The architecture doc rounds two of these: it gives a clip "its length plus 10 s" (the code adds the 0.8 s settle), and a photo 10 s (the fix watch runs before that deadline starts). The code is the fact, and the margin to Oriel's limits still holds.
A limit needs a reason
Named deadlines are not a license to sprinkle numbers. Each limit above is a claim about the world: a still takes well under 10 s, a fix indoors may never come, a line ends when its audio does. Two limits in this repo were removed because the claim was wrong. The 30 s idle cut once applied to requests already being answered and cut long lines mid-sentence (lesson 2). And the face's Solo bar once dropped a pending request after 20 s and an error after 12 s, numbers with no recorded failure behind them, which the repair of 2026-11-11 removed (track 8 tells it). When you add a limit, write down what it waits for and why that number, in the same line.