fix(client-runtime): stop waiting 3 s on a socket iOS killed in the background#23056

closed1
main
GitHub fork: fix/mobile-fast-resume
opened9d agobyOomar7550
View on GitHubTwo-way sync enabledPR last imported 4d ago

⚡ Reconnect after a brief background: 3.18 s → 0.73 s (77% faster, 2.45 s saved)

Same Wi‑Fi, socket killed by iOS (median of 3)BeforeAfterChange
Return to app → connected3.18 s0.73 s−2.45 s (−77%, 4.4× faster)
Return to app → thread list live (small host)3.37 s0.92 s−2.45 s (−73%)
Return to app → thread list live (large 17 GB host)7.32 s4.87 s−2.45 s (−34%)
Away from home (Tailscale), → connected6.46 s4.10 s−2.37 s (−37%)
Healthy / busy / stale-frame sockets—no slower0 s

On iPhone, switching away from T3 Code for a second or two and coming back often shows "Reconnecting to …" followed by "Syncing threads…". iOS frequently kills a suspended WebSocket without a close event. On resume the supervisor sends an application-active-probe and can only detect the dead socket when QUICK_CONNECTION_PROBE_TIMEOUT (3 s) expires. Until then the app looks connected but shows stale data, and only after that does the reconnect begin.

This focused reliability fix shortens that wait. If a resume probe is still unanswered after 500 ms, the supervisor sends the existing public reachability check (driver.checkRoute, the environment descriptor, no credential) to the same route. If the host answers that request and the socket has delivered no frame since the probe started, the socket is treated as dead and the reconnect starts immediately.

Everything else keeps upstream behaviour:

  • Probes still complete only on their own reply.
  • Any inbound frame vetoes the shortcut, including one buffered before the path died or a resubscribe replay on a busy socket, and the 3 s timeout stays in charge.
  • The check only runs when the host reports capabilities.connectionProbe, which makes the probe cheap.
  • Relay and SSH routes (unchecked), network-change and retry probes, and desktop are untouched.
  • No parallel session or websocket ticket is created.

RpcSession gains an optional inbound-frame counter for the veto. The fast failure has its own detail message, so it can be told apart from a real timeout in traces.

Problem (observed on iPhone, same home Wi‑Fi as the Mac)

~1 s after returning to the appthen
External imageExternal image

Before / after

Resume timeline before and after

Measured with a harness that runs the real client-runtime reconnect path: supervisor, driver, resolver, bearer authorization, and RpcSession over a fake protocol-speaking WebSocket. The network is simulated. Medians of 3 runs, in ms from wake-up to connected:

Scenarioupstream/mainthis PR
Same Wi‑Fi, socket silently killed (the reported case)3179725
Away (saved LAN route silent, Tailscale 150 ms), socket killed64644097
Socket killed, stale frames drained on resume31583167 (falls back to 3 s, as upstream)
Healthy socketkept, 0 HTTP requestskept, 0 HTTP requests
Busy healthy socket (replay frames before a slow probe reply)keptkept (1 descriptor request, vetoed)

Harness, per-run results and method notes · Evidence README

Validation

  • vp test run packages/client-runtime/src/connection/ packages/client-runtime/src/rpc/: 164 passed. With shell-sync/threads-sync added, 194 passed. Mobile workspace-connection-status.test.ts: 3 passed.
  • New tests cover:
    • the fast path;
    • probe answered before or during the check;
    • a frame vetoing the shortcut (session kept until the reply or the 3 s timeout);
    • silent, unchecked and older hosts all waiting the full 3 s;
    • network-changed and retry probes never running the check;
    • the session counting frames without answering a probe.
  • Against the upstream supervisor.ts/session.ts, the 4 tests that depend on the new behaviour fail.
  • The @t3tools/client-runtime typecheck passes, and vp run check:changed upstream/main (format, lint, knip, typecheck, tests) passes.
  • Three independent review rounds were done before submission. Round 1 caught the busy-socket false positive and the older-host probe cost. Round 2 caught that letting any frame end the probe could regress stale-socket detection to the 15 s ping. Both are fixed by the design above, and round 3 approved.

Not checked / limitations

  • The native app was not run with this change, because this machine has no Xcode or iOS Simulator. The timings come from the harness, not a device.
  • If iOS also reuses a dead pooled keep-alive connection for the descriptor request, the check stays silent and the app falls back to today's 3 s.
  • A healthy socket that stays completely silent for more than 500 ms while a fresh request to the same host answers would be replaced (one reconnect plus thread-list reload).
  • Out of scope: the full thread-list snapshot reload after every reconnect (intentional per state/shell.ts). On a large state database this is the remaining multi-second "Syncing threads…" cost.

Model: Claude Opus 5.5 (implementation, with separate Claude Opus 5.5 reviewers). Harness: Claude Code in T3 Code.

🤖 Generated with Claude Code

Checks

23 successful · 12 skipped · 1 neutral
All checks passed
208fda9

Added 1 commit

  • fix(client-runtime): stop waiting 3 s on a socket iOS killed in the background208fda9
Synced from GitHub

Approvability

Verdict: Needs Human Approval

Macroscope's review found this PR requires human approval — This focused reliability fix changes the existing resume path by performing an early route check and potentially replacing a live WebSocket 2.5 seconds sooner than before. The new asynchronous gating and documented possibility of replacing a completely silent healthy socket make human review appropriate.

You can add or adjust custom eligibility rules. Learn more.

Synced from GitHub

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Path: .coderabbit.config.ts
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 51cc1d59-d2ca-4e86-9393-0215376fd785
📥 Commits

Reviewing files that changed from the base of the PR and between d13fc846700e5bb7d63e1bfaabb1e55733912b21 and 208fda9cd5ad8849a669c07c3f47a9a038d7b565.

📒 Files selected for processing: 4
  • packages/client-runtime/src/connection/supervisor.test.ts
  • packages/client-runtime/src/connection/supervisor.ts
  • packages/client-runtime/src/rpc/session.test.ts
  • packages/client-runtime/src/rpc/session.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 8 remain after this review.

📝 Walkthrough

RpcSession now exposes an optional inbound-frame count. During an application-active resume probe, EnvironmentSupervisor can run a delayed route check and compare the count. A successful check with no new frames classifies the socket as silent; the supervisor stops both monitoring fibers and reports a distinct timeout error.

Changes: Resume probe route checks

Layer / File(s)Summary
Track inbound WebSocket frames
packages/client-runtime/src/rpc/session.ts, packages/client-runtime/src/rpc/session.test.ts
RpcSession counts incoming WebSocket messages and exposes the count through inboundFrames. A test verifies that a Pong increments the count without completing the probe.
Detect silent sockets during resume probes
packages/client-runtime/src/connection/supervisor.ts, packages/client-runtime/src/connection/supervisor.test.ts
EnvironmentSupervisor performs a delayed route check during eligible resume probes. A successful check with no new inbound frames triggers a distinct silent-socket timeout. Tests cover route responses, buffered frames, timeout cases, legacy configuration, and retry triggers.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Bug fix

Sequence Diagram(s)

mermaid
1sequenceDiagram
2 participant EnvironmentSupervisor
3 participant RpcSession
4 participant WebSocket
5 participant FreshRouteCheck
6 EnvironmentSupervisor->>RpcSession: Read inboundFrames before the check
7 EnvironmentSupervisor->>FreshRouteCheck: Check the active route after 500 ms
8 WebSocket->>RpcSession: Deliver an incoming message
9 RpcSession->>RpcSession: Increment the inbound-frame count
10 FreshRouteCheck-->>EnvironmentSupervisor: Return fresh route response
11 EnvironmentSupervisor->>RpcSession: Read inboundFrames after the check
12 EnvironmentSupervisor->>EnvironmentSupervisor: Classify socket as silent if count is unchanged

Suggested reviewers: t3dotgg, juliusmarminge

Merge Risk: ⚪ Minimal · up to 208fd

No identified issue blocks merging after normal checks. The resume shortcut has not been validated on a native iOS device.

Pre-merge checks: Passed 3 · Failed 1

Failed Failed checks: 1 warning

Check nameStatusExplanationResolution
Description checkWarningThe description clearly explains the problem, implementation, scope, verification results, limitations, and agent details. It does not include the required Scope and approval section or a triaged issu…Add a Scope and approval section with the triaged bug or discussion link and the explicit maintainer approval comment. If the change qualifies as a focused obvious-bug fix without prior approval, explain why. Rename Validation to Verificati…
Passed Passed checks: 3 passed
Check nameStatusExplanation
Title checkPassedThe title clearly and concisely describes the main fix: reducing the wait after an iOS backgrounded socket is killed.
Linked Issues checkPassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes checkPassedCheck skipped because no linked issues were found for this pull request.
Full details: Description check

Explanation

The description clearly explains the problem, implementation, scope, verification results, limitations, and agent details. It does not include the required Scope and approval section or a triaged issue, maintainer approval, or documented exemption rationale.

Resolution

Add a Scope and approval section with the triaged bug or discussion link and the explicit maintainer approval comment. If the change qualifies as a focused obvious-bug fix without prior approval, explain why. Rename Validation to Verification or otherwise align it with the required template heading.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

Note

🤖 Claude Opus 5.5 responding on behalf of Theo

Thanks @omar7550. Your fresh-request check landed in #25824, with Co-authored-by credit. It now runs on the session's own origin, so it covers T3 Connect too. It retries until the host answers and also covers long resumes. On the iOS simulator, a silently killed socket after a short resume now reconnects in about 0.8 s instead of 3.2 s. Closing as superseded.