Skip to content
Source
Sign inSign up

connection-runtime.md

163 lines10.0 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 and Home's list has laid out those rows, so the first frame
107holds all cached rows.
108​
109Shell snapshots can leave rows out when the client asks. Servers that advertise
110`shellThreadFamilies` leave out idle subagent children: a child stays while it
111has a live or queued run, a pending request or background work, or a subagent
112child of its own, so badge and working walks never meet a gap. Servers that also
113advertise `shellOlderSettled` keep only the 25 newest settled top-level threads
114(and any tied with the 25th, pinned, snoozed, live, or above a kept child); the
115snapshot's `olderSettled` names that cut and counts the rest per project. Left-out
116rows load on demand and merge behind shell rows
117([thread families](../../packages/client-runtime/src/state/threadFamilies.ts)):
118the family a thread view or a queued message mounts holds every active thread
119sharing the thread's root, so it also brings back an old settled thread opened by
120link; the Settled shelf reads pages below the cut; searches return the rows of
121older matches. A missing shell row therefore does not prove a thread is gone:
122code that deletes data or a worktree on absence must check the family or the
123server first.
124​
125The Settled count is local Settled rows, minus local rows below the cut, plus the
126server's counts
127([older settled threads](../../packages/client-runtime/src/state/olderSettledThreads.ts)).
128The counts are re-read when a removal or an old row's arrival or change could
129move them, so archiving any thread costs one small read. A shelf lists an older
130row only above the highest page frontier of the environments that can still
131load, so the merged list stays a correct prefix while a page loads. An
132environment with no snapshot this session, or a failed page, holds no one back.
133On-demand reads wait for the session's first authoritative snapshot
134(`generation > 0`), and each is keyed by generation. A resume that skips that
135snapshot must still bump `generation` once verified, and may reuse a cached
136snapshot only when its omissions match what the session asks for.
137​
138[Thread detail](../../packages/client-runtime/src/state/threads.ts) separates
139subscription lifetime from cache lifetime. Mounted consumers share one live
140stream, which stops when the last consumer unmounts; hidden mounted routes still
141count. A registry-local cache retains state and its replay cursor for five idle
142minutes so back navigation can resume without another snapshot download.
143​
144The desktop app adds one consumer: a
145[keep-alive](../../apps/web/src/state/threads.ts) mounts every thread whose
146session is starting or running, in each enabled environment. Opening a running
147thread then needs no replay. The shell and detail streams are independent, so
148the shell can report a stop before the detail loads or catches up. A stopped
149thread stays mounted until its own stream is live and shows the stop, and the
150stream then closes and saves the settled state.
151Web and mobile do not keep threads alive.
152​
153Retain state and cursor together only after an update finishes. Cancellation must
154not advance the cached cursor beyond the applied data, and an old scope must not
155overwrite its successor's cache. Preserve pagination data on reuse, but clear
156canceled loading state.
157​
158The [RPC boundary](../../packages/client-runtime/src/rpc/client.ts) resolves
159requests against the current session at execution time. Durable subscriptions
160follow replacement sessions. After a transport failure they wait for the
161supervisor; an expected domain failure may resubscribe on the same healthy
162session. Reconnection does not automatically replay mutations, whose retry and
163idempotency rules belong to the operation.