Skip to content
Source
Sign inSign up

connection-runtime.md

162 lines9.9 KB

1# Connection runtime
2​
3Web, the desktop renderer, and mobile share one connection owner per environment
4in `packages/client-runtime`. Platform code supplies storage, credentials, network
5signals, and application lifecycle events. React views consume the runtime.
6Keeping retries and session lifetime here prevents competing reconnect loops when
7several views need the same environment.
8​
9## One transport retry owner
10​
11The [supervisor](../../packages/client-runtime/src/connection/supervisor.ts) owns
12transport retry policy; resolving an endpoint and opening an RPC session are single
13attempts. Transient failures retry with jittered exponential backoff, capped at
14five minutes, that resets only after a connection stays up. Without jitter, every
15client of a restarted server reconnects in the same second; with a short cap, a
16client that can never connect retries all day. Offline states and authentication
17failures wait for a wakeup instead of spending attempts on unchanged conditions.
18​
19Foregrounding, a network change, an explicit retry, and an offline report probe
20the established session, and only a failed probe reconnects. Offline reports are
21often wrong, for example for a loopback server. The OS can kill a suspended
22socket without a close event, so a probe still unanswered after 500 ms asks the
23host for its public descriptor over a fresh request, again each second until it
24answers. If the host answers while the socket delivers no frame, the socket is
25dead and is replaced, even while the network reports offline: at once on
26loopback and LAN, and 2.5 seconds after the check starts elsewhere, where one
27lost packet can stall a live socket. Without that evidence the probe deadline
28decides: 3 seconds on loopback and LAN routes and after a long mobile
29suspension, when the data on screen is stale; 15 seconds elsewhere, because a
30tailnet over DERP or a VPN can be slower than that on a live socket. A long
31suspension's probe that times out also replaces the socket regardless of the
32network report. The web and desktop foreground probe skips the fresh request,
33because a busy host can answer it before a queued probe. Foregrounding also
34wakes a pending retry immediately. A long suspension restarts an in-flight
35attempt; a short one leaves it alone.
36​
37The [registry](../../packages/client-runtime/src/connection/registry.ts) scopes
38connections by environment. An involuntary disconnect retains the registration
39and cached data. Explicit removal closes the scope and clears credentials,
40projections, and platform-owned state such as drafts. Cloud-account changes apply
41to relay registrations; they must not discard directly paired environments.
42​
43## HTTP authorization
44​
45RPC sessions authenticate at socket upgrade, while HTTP requests need current
46credentials from the
47[authorization service](../../packages/client-runtime/src/authorization/service.ts).
48Replacing a healthy socket for HTTP renewal would interrupt conversations and
49change the transport generation without a transport failure. Refresh failure
50belongs to the HTTP operation.
51​
52The server closes a socket when its session is revoked or expires. T3 Connect
53access tokens last one hour, so the driver renews a live socket's token in its
54last minute. The server then moves the socket to the newer session for the same
55DPoP key, subject, and scopes
56([`SessionStore.awaitInvalidation`](../../apps/server/src/auth/SessionStore.ts)).
57If renewal fails, the socket closes at expiry and the supervisor reconnects.
58​
59Session listings must retain unrevoked connected sessions after credential expiry
60so an open connection does not disappear from connection management. This does
61not extend the credential's lifetime. New HTTP requests and socket upgrades still
62require valid credentials.
63​
64## Transport health and data freshness are separate
65​
66A socket opening is insufficient evidence that the environment is usable. The
67[RPC session](../../packages/client-runtime/src/rpc/session.ts) waits for the
68initial server configuration before becoming ready. Shell and thread data then
69have their own synchronization state. A failed shell subscription can coexist
70with a healthy connection; labeling that state "reconnecting" promises a
71transport retry that will never happen.
72​
73Cached projections remain readable offline. They must neither imply a live
74connection nor overwrite newer live data during a reconnect. Loading and
75resuming snapshots belongs to the shared state services, so every view agrees
76on which data is current.
77​
78The shell snapshot loads over HTTP, which needs only the prepared connection, so
79the [shell state](../../packages/client-runtime/src/state/shell.ts) can ask for
80it before the socket and the config are ready. Two constraints follow. The live
81config stays the authority: rows loaded early for a build or with omissions that
82it does not confirm are replaced, never resumed. And an HTTP answer must never
83land on rows the socket has moved on: the subscription waits for the answer, and
84when held rows can resume, it ends the request 3 seconds after the socket is
85ready. A proxy that drops `/api` without an answer then does not hold the
86connect for the HTTP timeout.
87​
88Each environment synchronizes at its own time, so a thread list that follows every
89snapshot moves once per environment on each cold start and resume. Mobile lists read
90[steady shells](../../packages/client-runtime/src/state/steadyShells.ts): an
91environment's rows stay as shown until its connection's server snapshot arrives,
92it is synchronized, or it disconnects, and environments that get there within two
93seconds of each other show together. Only an environment that is connected or on
94its first attempt, and still owes its snapshot, is waited for; a retry can take
95minutes. A move in a list is refused while its rows wait, because its plan would
96use order keys the server may have replaced. Every other row action looks up the
97live row before it reads thread state, so an undo plan or a guard never uses a held
98row. The hold stops only changes the user
99did not cause: a command that moves or removes a row (settle, snooze, pin, archive,
100delete, and their reverses) makes that environment's rows follow the live snapshot
101until it is live. Point reads (a
102thread's own shell, route checks) keep the live snapshot, where a row the server just
103sent must not look missing. The decision reads the snapshot's own sequence: a second
104atom derived from the same shell state can be one update behind the rows. The mobile
105splash screen also waits, for a bounded time, until every enabled environment has
106read its cached shell, so the first frame holds all cached rows.
107​
108Shell snapshots can leave rows out when the client asks. Servers that advertise
109`shellThreadFamilies` leave out idle subagent children: a child stays while it
110has a live or queued run, a pending request or background work, or a subagent
111child of its own, so badge and working walks never meet a gap. Servers that also
112advertise `shellOlderSettled` keep only the 25 newest settled top-level threads
113(and any tied with the 25th, pinned, snoozed, live, or above a kept child); the
114snapshot's `olderSettled` names that cut and counts the rest per project. Left-out
115rows load on demand and merge behind shell rows
116([thread families](../../packages/client-runtime/src/state/threadFamilies.ts)):
117the family a thread view or a queued message mounts holds every active thread
118sharing the thread's root, so it also brings back an old settled thread opened by
119link; the Settled shelf reads pages below the cut; searches return the rows of
120older matches. A missing shell row therefore does not prove a thread is gone:
121code that deletes data or a worktree on absence must check the family or the
122server first.
123​
124The Settled count is local Settled rows, minus local rows below the cut, plus the
125server's counts
126([older settled threads](../../packages/client-runtime/src/state/olderSettledThreads.ts)).
127The counts are re-read when a removal or an old row's arrival or change could
128move them, so archiving any thread costs one small read. A shelf lists an older
129row only above the highest page frontier of the environments that can still
130load, so the merged list stays a correct prefix while a page loads. An
131environment with no snapshot this session, or a failed page, holds no one back.
132On-demand reads wait for the session's first authoritative snapshot
133(`generation > 0`), and each is keyed by generation. A resume that skips that
134snapshot must still bump `generation` once verified, and may reuse a cached
135snapshot only when its omissions match what the session asks for.
136​
137[Thread detail](../../packages/client-runtime/src/state/threads.ts) separates
138subscription lifetime from cache lifetime. Mounted consumers share one live
139stream, which stops when the last consumer unmounts; hidden mounted routes still
140count. A registry-local cache retains state and its replay cursor for five idle
141minutes so back navigation can resume without another snapshot download.
142​
143The desktop app adds one consumer: a
144[keep-alive](../../apps/web/src/state/threads.ts) mounts every thread whose
145session is starting or running, in each enabled environment. Opening a running
146thread then needs no replay. The shell and detail streams are independent, so
147the shell can report a stop before the detail loads or catches up. A stopped
148thread stays mounted until its own stream is live and shows the stop, and the
149stream then closes and saves the settled state.
150Web and mobile do not keep threads alive.
151​
152Retain state and cursor together only after an update finishes. Cancellation must
153not advance the cached cursor beyond the applied data, and an old scope must not
154overwrite its successor's cache. Preserve pagination data on reuse, but clear
155canceled loading state.
156​
157The [RPC boundary](../../packages/client-runtime/src/rpc/client.ts) resolves
158requests against the current session at execution time. Durable subscriptions
159follow replacement sessions. After a transport failure they wait for the
160supervisor; an expected domain failure may resubscribe on the same healthy
161session. Reconnection does not automatically replay mutations, whose retry and
162idempotency rules belong to the operation.