Skip to content

fix(client): slow networks no longer bounce a connection between routes - #875

Merged
incognitojam merged 1 commit into
mainfrom
styal/connection-route-flapping
Oct 16, 2026
Merged

incognitojam merged 1 commit into
mainfrom
styal/connection-route-flapping

Conversation

@incognitojam

Copy link
Copy Markdown
Owner

On a slow, lossy network (train Wi-Fi, then a phone hotspot), a desktop client connected to a styal Link environment kept showing "reconnecting". Client traces showed two problems in the connection supervisor:

  • Route bouncing. The environment also had learned LAN and tailnet routes. The tailnet route went through a distant DERP relay. It answered the 60-second better-route check in a few hundred milliseconds, so the client dropped its working styal Link session and switched. That session dropped about three minutes later. After reconnecting over styal Link, the next check switched straight back. The existing cooldown only covered a switch that failed to connect, not one that connected and then dropped.
  • Minute-long reconnects. When styal Link failed during a reconnect, each route that had not answered its 2.5 s check still got a full attempt. An attempt can take 15 s per saved route, so failed reconnects took 40–60 s.

Dropped sessions also said "Your DNS or firewall may be blocking styal Link" even though the socket had reached the server.

Change

  • If a better-route check switches to a route and that session drops within five minutes, the check skips that route for five minutes. The wait doubles on each repeat, up to 30 minutes, and while it lasts the route is tried last when reconnecting. If a switched-to session holds for five minutes, the history is cleared, so a later drop such as a server restart does not keep the client off the LAN.
  • After an attempted route fails transiently, routes that did not answer their check no longer get full attempts. The supervisor backs off and checks every route again. They are still tried when every attempted route was blocked, for example when signed out of styal Link, or when no route was attempted.
  • A styal Link socket that opened and later closed or stopped responding no longer carries the DNS or firewall hint. A socket that never opened still does, and so does the overall setup timeout, which cannot tell a blocked network from a slow one.

The route paragraphs in docs/internals/remote.md and docs/user/remote-access.md describe the new behavior, and the fork ledger has a slow-network-route-stability entry. Upstream has no related work in progress.

Validation

New supervisor tests drive the real supervisor and route walk on a test clock:

  • A switched-to LAN session drops a minute after each switch. The client stays on styal Link for five minutes after the first drop and ten after the second, then switches back.
  • A LAN session that held for ten minutes and then dropped reconnects straight to the LAN.
  • A timed-out styal Link attempt backs off without preparing a LAN route that did not answer its check.

A new session test checks that a relay socket that opened and then closed reports "disconnected." without the hint.

Three of these four fail without the change. The ten-minute case passes either way: it guards against penalizing a route that worked. The full client-runtime suite (2,147 tests), typecheck, lint, and the ledger check pass.

Not verified: behavior on a real slow network. The diagnosis came from client traces of the original trip, but this change has not been exercised on a throttled link.


Written by an agent (Claude Code, claude-opus-5-5).

On a slow or lossy network a tailnet route can answer one quick check and
then fail to hold a session. The better-route check switched to it, the
session dropped a few minutes later, and the next check switched straight
back. A route whose session drops within five minutes of such a switch is
now skipped by the check for five minutes, doubling on each repeat up to
30 minutes, and is tried last when reconnecting while it cools down. A
switched-to route that held for five minutes clears that history.

Once an attempted route fails transiently, routes that did not answer
their check no longer get a full attempt each, which made a failed
reconnect take up to a minute. The supervisor backs off and checks every
route again. They are still tried when the attempted routes were blocked.

A relay socket that opened and later closed or stopped responding no
longer blames DNS or a firewall, since it reached the server.
@github-actions

Copy link
Copy Markdown

Upstream divergence

Both sides are compared with upstream at the in-order tip 54b6b667f3. Excludes .repos/ and pnpm-lock.yaml.

Measure Base This PR Change
Upstream files the fork changes 1,411 1,412 +1
Lines changed in them 67,502 67,777 +275
Upstream files the fork removes 41 41 0
Fork-only files 473 473 0
Fork-only lines 83,008 83,034 +26

Upstream files that start to differ (1)

  • packages/client-runtime/src/connection/driver.ts: +4 −2 from upstream
Upstream files whose difference changes (6)
File Change Now differs by
packages/client-runtime/src/connection/supervisor.test.ts +123 798 lines
packages/client-runtime/src/connection/supervisor.ts +81 158 lines
packages/client-runtime/src/rpc/session.test.ts +26 63 lines
docs/internals/remote.md +18 27 lines
packages/client-runtime/src/rpc/session.ts +14 33 lines
docs/user/remote-access.md +7 113 lines

@incognitojam
incognitojam enabled auto-merge (squash) October 16, 2026 15:54
@incognitojam
incognitojam merged commit 0b80358 into main Oct 16, 2026
28 checks passed
@incognitojam
incognitojam deleted the styal/connection-route-flapping branch October 16, 2026 15:56
@github-actions

Copy link
Copy Markdown

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

Provider Metric Main baseline This PR Impact PR ceiling
Codex Total thread wire 4.9 KiB 5.0 KiB +23 B (+0.5%) 6.8 KiB ✅
Codex Thread snapshot wire 3.8 KiB 3.8 KiB 0 B (0.0%) 4.9 KiB ✅
Codex Live turn WebSocket wire 1.2 KiB 1.2 KiB +23 B (+1.9%) 2.0 KiB ✅
Codex Live turn WebSocket decoded 20.8 KiB 20.9 KiB +41 B (+0.2%) 29.3 KiB ✅
Codex Live turn messages 1 2 +1 (+100.0%) 8 ✅
Claude Total thread wire 5.0 KiB 5.0 KiB 0 B (0.0%) 6.8 KiB ✅
Claude Thread snapshot wire 3.8 KiB 3.8 KiB 0 B (0.0%) 4.9 KiB ✅
Claude Live turn WebSocket wire 1.2 KiB 1.2 KiB 0 B (0.0%) 2.0 KiB ✅
Claude Live turn WebSocket decoded 21.2 KiB 21.2 KiB 0 B (0.0%) 29.3 KiB ✅
Claude Live turn messages 2 2 0 (0.0%) 8 ✅

Baseline: 4b3dc4b · PR result: 216ea28 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 108.5 KiB
  • Claude decoded thread snapshot: 108.8 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant