Coder Link gives every Coder client one connection owner per host. The owner decides when to connect, when to wait, and when to stop trying, so screens never run competing reconnect loops. It reports transport health and data freshness as separate fields, so a screen can say "offline" and "out of date" as different things.
The crate is a deterministic state machine. It has no dependencies and
performs no network, storage, or UI work. A host application injects the
transport through the Connector trait and time through the Clock trait.
Issue #9707 defines
the scope; the remote access program
composes it with host enrollment, reachability, and terminals.
A Supervisor owns one host. Each call takes the current moment, updates the
state, and returns Command values: open a connection, probe one, cancel an
attempt, or close a connection. A Registry keeps one supervisor per
HostKey, reads the clock, and passes each command to the Connector. The
connector starts the work and returns at once. Outcomes come back later
through Registry::report.
Every attempt has a fresh AttemptId. A report for a cancelled or superseded
attempt, or for a closed connection, returns RegistryError::Stale and
changes nothing. A slow transport can never revive state that the supervisor
has moved past.
| Phase | Meaning | Leaves when |
|---|---|---|
Available |
Known and able to connect; no connection is wanted. | Connect or RetryNow. |
Offline |
A connection is wanted, and the device has no network. | The network returns, or RetryNow. Time alone never leaves it. |
Connecting(Establishing) |
Opening a connection where none exists. | Success, failure, or the establishment timeout. |
Connecting(Probing) |
Checking that an existing connection still answers. | Success, failure, or the probe timeout. |
Connecting(Replacing) |
Opening a new connection beside a presumed-stale one. | Success closes the old connection; failure closes both. |
Backoff { until } |
Waiting after a transient failure. | The deadline passes, or a signal says to try now. |
Connected |
A connection is up. | Loss, a probe or replacement, or a disconnect. |
Blocked(reason) |
Retrying cannot help. | RetryNow, Connect, or CredentialsChanged only. |
A blocked reason is Authentication, Revoked, Incompatible, or
Configuration. Time, network changes, and application lifecycle changes
never leave a blocked state.
Freshness is independent of the phase:
Unknown: the client has never caught up with the host.Current { as_of }: the client caught up on the current connection.Stale { as_of, cause }: the data may be behind. The cause isSyncing(connected, not yet caught up),TransportDown, orSubscriptionFailed.
Report::SubscriptionFailed marks data stale and leaves the phase at
Connected. A failed subscription never shows as reconnecting. A lost
connection keeps its last as_of, so a screen can show when its cached data
was last current.
| Signal | Effect when the host is wanted and switched on |
|---|---|
Connect |
Starts an attempt from Available, Backoff, or Blocked. |
Disconnect |
Cancels and closes everything, then returns to Available. |
RetryNow |
Starts an attempt at once, even when offline or blocked. Probes a connected host. |
NetworkChanged |
Loss moves to Offline and closes the connection. Return connects from Offline or Backoff and probes a connected host. |
ApplicationBackground |
Records when the background began. |
ApplicationActive |
Probes a connection after a short background. Replaces it after a background of at least Policy::long_background. Ends a backoff. |
CredentialsChanged |
Restarts an attempt in flight, replaces a connection, and leaves Blocked or Backoff with a reset ladder. |
Registry::signal_all sends device-wide signals, such as network and
lifecycle changes, to every host.
Policy::default() uses a 10-second establishment timeout, a 3-second probe
timeout, a ladder of 1, 2, 4, 8, 16, and 30 seconds, a 30-second stable
period, and a 5-minute long background.
- Each transient failure (
Timeout,Unreachable, orClosed) waits the next ladder step. The last step repeats as the cap. - A connection that stays up for the stable period resets the ladder.
- A probe failure reconnects at once, skipping the first wait. If that attempt fails, the supervisor waits the second step.
NetworkUnavailablemoves toOffline, which waits for a network signal instead of climbing the ladder.Registry::next_deadlinereturns when the host application next needs to callRegistry::tick. Nothing needs a periodic timer.
registeradds an idle host with aRoutethat names the credentials it depends on. Credential IDs name credentials; they never hold secrets.switch_offstops all transport work and keeps the host, its route, its credentials, and its cached data.switch_onresumes if the user still wants the connection.removestops all transport work, forgets the host, and then calls a callback so the application clears the host's cached projections and credentials. Later reports for the host returnRegistryError::Unknown.credentials_changedsendsCredentialsChangedto each host whose route depends on that credential and returns those hosts. Other hosts are untouched.
This issue does not change coder-mobile. Its App currently keeps a single
retry_after time, an active flag, an 8-second observation timeout, and a
15-second or 60-second retry, and it combines connection and data status in
one status string. The following mapping replaces that logic:
- Hold a
Registry<SystemClock, ObserverConnector>inApp. Key each paired computer by its host public key. Name its read grant as the route credential. - Implement
ObserverConnectorovercoder-connect.openstarts an authenticated session on the existing Tokio runtime and reportsEstablishedorFailed.proberuns a bounded observation on the current session.cancelandclosedrop the task or session. - Map
coder-connecterror codes to reports:TransportbecomesUnreachableorTimeout;RevokedandExpiredbecomeBlocked(Revoked);ForbiddenbecomesBlocked(Authentication).SourceChangedandConflictbecomeSubscriptionFailed, because the transport still works and only the data needs a reload. - Translate requests into signals:
Request::Foreground { active }sendsApplicationActiveorApplicationBackgroundthroughsignal_all;Request::RefreshNowandIntent::RefreshsendRetryNow;Request::Connectregisters the computer and sendsConnect;Request::Disconnectcallsremovewith a callback that erases the cache and the stored connection code. - Let the platform's foreground timer call
tickatnext_deadline. Run catalog and transcript refreshes only while the phase isConnected, and reportDataCurrentafter each successful page. - Project
Statusinto two view fields: connection health fromphaseandlast_failure, and data age fromfreshness. - Add a platform network signal. The iOS and Android hosts report network
path changes as
NetworkChanged.
coder-hostimplements a real connector that tries proven direct routes and falls back to the relay.coder-computers' live service (coder_computers::live::Live) holds a registry with it, and the phone app (openagents-mobile) opens that service for its Computers screens.coder-mobile'sAppstill keeps its own retry timer; the adoption steps above remain for it.- A server-supplied retry time, such as a rate-limit hint, has no failure variant. An adapter maps it to a transient failure, which follows the ladder instead of the server's time.
- The ladder has no random jitter, so many clients that fail together retry together. Jitter would need an injected random source to stay testable.
- The supervisor trusts the connector's reports. It does not verify host identity, grants, or routes; those checks belong to the transport and the host.
cargo test -p coder-link
cargo clippy -p coder-link --all-targets -- -D warnings
cargo fmt -p coder-link -- --checkThe verification record lists what these checks cover.