Device routing, reconnect, and recovery
Registration and discovery, the reconnect grace, what a reconnecting phone reloads, how timeline events reach only interested phones, push registration, and why the relay runs as one process.
Checked against the source on 17 September 2026
On this page
Registration and discovery
| Client | WebSocket headers | Cross-checked against |
|---|---|---|
| Desktop | X-Client-Type: desktop | A desktop device row the same account registered over HTTP. |
| Phone | X-Client-Type: mobile, plus X-Target-Desktop-Device-ID for the selected desktop | A mobile device row; the target desktop is read from the header or payload on every registration, never from a stored session. |
Reconnect grace and the reconnect snapshot
- Desktop requests-32011 · -32010
Requests recorded for the closed connection get -32011, “Desktop is reconnecting”, at once, and so does every new request during the wait. After it, new requests get -32010. The relay counts a silent socket as closed only after 180 seconds.
- Reconnect waitreconnect_timeout
It holds presence only, and phones see the desktop as reconnecting. At 90 seconds the relay marks it offline and broadcasts device-status disconnected, unless a new connection registered first.
- Phone requests(user, desktop, id)
The record outlives the phone’s socket, so a reply that arrives after the phone registers again is delivered. A final reply that lands during the gap is dropped, and the phone’s call ends at its own timeout.
- Phone leasechat:timeline-interest
Bound to the phone’s connection, so it ends when the socket closes and nothing from the gap is kept. The phone sends it again once the relay answers registered or resumed, pings the desktop, and loads the latest window.
A client that registers with its resume token still passes the protocol and device-ownership checks, and the answer says resumed and keeps the session ID. A failed resume answers invalidResume, and the client registers again without the token. The relay session record behind the token lives 24 hours, is touched on every RPC, and is invalidated on logout. iOS keeps the token in the keychain.
Offline, reconnecting, and timeout errors stay distinct, and only errors marked retryable permit a transport retry. A timeout describes the reply the caller did not get. It says nothing about whether the mutation ran, so the client follows the operation’s recovery path before it issues new work.
| State | Meaning and next step |
|---|---|
| Reconnecting | The phone is restoring its server connection and then re-verifying the desktop. Give it a moment. |
| Offline or disconnected | Check the desktop app, the computer’s sleep state, the account, the region, and the network. |
| Request timed out | The reply did not arrive in time. The request may still have run, so read the session state before you repeat a mutation. |
| Connected, but history unavailable | The relay works and the history read failed. Use the reload action in the timeline. |
Timeline events reach only interested phones
- Interest leasechat:timeline-interest { appSessionId, active }
Sent by the phone for the session on its screen and renewed every 20 seconds. The relay stores it per phone and never forwards it. It carries no timeline content.
- Lease endTIMELINE_INTEREST_TTL = 60 s
A lease ends 60 seconds after its last renewal, or at once when the phone sends active: false, opens another session, or its socket closes or is replaced. Queued updates for that phone go with it. The relay drops a silent socket only after 180 seconds, so the lease ends first.
- Desktop deadlinechat:timeline-interest-updated { expiresAt }
On every lease change the relay tells the desktop per session whether a lease exists and when it ends, never which phone holds it. The desktop keeps that deadline itself and stops serializing the session’s timeline once it passes.
- Relay filterauthorizationGeneration
Issued when a session goes from no lease to one. Every frame must carry the current value, and the relay strips it and sends the frame only to phones whose lease is still live at the final send. Nothing is kept for the others, so a returning phone loads a snapshot.
The lease is keyed by user, phone route, target desktop, and base session. appSessionId is trimmed, capped at 1,024 bytes, and stripped of a trailing skills-agent alias. Updates to the desktop also carry a revision and the relayInstanceId, so the desktop ignores stale ones and clears its state when they come from another instance.
Outbound timeline events wait in a per-connection coalescing buffer with one pending overlay per timeline key, a 512 KiB standard lane with an oversized lane above it, and a 250 millisecond flush. A saturated mailbox retires the connection generation, so the phone reconnects and reconciles instead of receiving a partial stream. The desktop mirrors this with 4 MiB of pending bytes, 64 regular events, and 64 coalesced timeline keys per connection.
Push registration and alerts
Both phones register a push token with PUT api/devices/push-token while signed in. The server sends exactly two kinds of push: the silent desktop_online data message when your desktop registers, which both apps use to restore their connection before you open them, and the visible alert written by the agent’s notification tool, which travels desktop, device link, server, then APNs or FCM.
Why the relay runs as one process
- Process memoryDeviceConnectionManager
Holds every connection route, pending request record, reconnect presence, and interest lease. RelaySessionStore holds the resume sessions. A second process would see none of it, which is why the relay runs as one.
- DrainrelayDraining
One-way. From its start the process refuses registrations. It waits at most 60 seconds for routing work in progress, then clears its memory without answering open requests and closes every device socket.
- Instance IDrelayInstanceId
A new UUID per process, sent with every registration answer. Phones ignore presence events from another instance, and the desktop clears its interest state when updates come from a different one.
With two processes behind a load balancer, a phone whose socket landed on the process without its desktop’s socket would see that desktop as offline.
Production therefore has exactly one routing authority per region. A blue-green deploy starts the new process, drains the old one, and then routes traffic to the new one. Per connection the relay caps frames at 32 MiB, rate-limits with a token bucket of 300 burst and 150 per second, pings every 30 seconds, and drops a client after 180 seconds of silence.