The minute of one photo
On 2026-10-09 at 23:47, in Seville, Dad pasted one photo into the native app and sent it. The phone talked to the office in Korea over the tailnet, about 285 ms per round trip. Measured afterwards on his cabled iPhone and in office's access log: the upload of 407 KB took 2.02 s, of which 0.28 s was opening a new connection, and the first byte of the turn arrived 5.1 s after that. In that same minute the app made 200 requests and pulled 5.84 MB, every one on a new connection, the open conversation's page re-fetched every one or two seconds. A cold launch did 78 requests and 2.19 MB in its first minute, 77 of them on new connections.
Nothing was broken. Every one of those requests was correct on its own. That is the trap this lesson is about: on a desk wired to the tailnet, a chatty client is invisible, and on a phone abroad the same client spends most of its minute asking questions whose answers it already has. The house sign now carries it as a rule: the phone talks to the engine through one kept-alive pool and asks conditionally.
Three fixes, in the order Dad approved them
One kept-alive pool. The client now builds every transport and the turn streamer from one connection pool in the shared kit, so the upload's connection carries the turn that follows it, and background reads keep the pool warm for the next send. The server keeps idle connections for 75 seconds, so a kept connection survives between turns.
Ask only when changed. A new middleware, app/api_conditional.py, gives every single-shot 200 application/json reply under /api a weak ETag computed from the body, and answers a matching If-None-Match with an empty 304. It sits inside the gzip layer so the tag is over the uncompressed bytes, and the route still runs on a match, so a 304 always means identical bytes, never a guess and never a per-route version counter someone forgets to bump. Streams and private conversations get no tag. On the phone, a 304 comes back to callers as the body it already holds, so nothing above the transport changed.
Background sync stands aside for a send. Automatic refreshes run one at a time (later triggers join the running one), at most every 30 seconds unless forced, and never while a turn is being sent or streamed; they are owed and run once the send settles. The offline library walk skips a conversation whose last update has not moved and pauses when a send starts. The network monitor refreshes only when the network comes back, not on every path change.
The send path, one round trip at a time
The same measurements took the send apart. Before the upload could start, the phone made three reads in a row: the lane's settled check, the turn-status read (has the engine already seen this turn, so it is never sent twice), and a second settled check (is the conversation free of another device's unfinished turn). Each cost about 0.6 s from Seville. The last two are independent, so they now run side by side; each keeps its own rule, and no check was dropped.
A server-side turn clock went in at the same time: one log line when the route hands over the stream, split into resolve, speaker check, location, the user row, healing, compaction, history, retrieval and prompt, and one when the first event leaves. It found the scheduled turns spending 15 to 16 seconds in retrieval, because the server Mac's reranker scores one candidate at a time and the long rendered cron prompt made each one slow, and the 15-second interactive limit then threw the ranking away anyway. Scheduled turns on the Claude route now get a 60-second ranking budget; Dad's own turns keep the interactive one, and the six variant routes, which pass no budget, still give a scheduled turn 15 seconds.
The outbox learned lanes
Lesson 2's outbox drained the whole queue in one serial pass. On 2026-10-05 a wrist turn hung on a tool call held a new chat Dad had dictated for 1 minute 43 seconds; it went out one second after the stuck turn finished. Now each conversation drains in its own lane: in order inside a conversation, side by side across conversations, with the request id still the idempotency key. A send also waits for a photo still on its way into the composer, so a pasted picture no longer slips into the next message, and recordings and photos upload side by side.