Skip to content

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

Closed
omar7550 wants to merge 1 commit into
pingdotgg:mainfrom
omar7550:fix/mobile-fast-resume
Closed

omar7550 wants to merge 1 commit into
pingdotgg:mainfrom
omar7550:fix/mobile-fast-resume

Conversation

@omar7550

@omar7550 omar7550 commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

⚡ 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) Before After Change
Return to app → connected 3.18 s 0.73 s −2.45 s (−77%, 4.4× faster)
Return to app → thread list live (small host) 3.37 s 0.92 s −2.45 s (−73%)
Return to app → thread list live (large 17 GB host) 7.32 s 4.87 s −2.45 s (−34%)
Away from home (Tailscale), → connected 6.46 s 4.10 s −2.37 s (−37%)
Healthy / busy / stale-frame sockets — no slower 0 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 app then

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:

Scenario upstream/main this PR
Same Wi‑Fi, socket silently killed (the reported case) 3179 725
Away (saved LAN route silent, Tailscale 150 ms), socket killed 6464 4097
Socket killed, stale frames drained on resume 3158 3167 (falls back to 3 s, as upstream)
Healthy socket kept, 0 HTTP requests kept, 0 HTTP requests
Busy healthy socket (replay frames before a slow probe reply) kept kept (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

…ackground

After a brief background, mobile probes the live socket and waits up to
3 s for the reply. iOS often kills a suspended socket without a close
event, so nothing ever answers: the app looked connected but stale for
3 s before "Reconnecting..." even started.

When a resume probe is still unanswered after 500 ms, the supervisor now
sends the existing public reachability check (the environment
descriptor, no credential) to the same route. If the host answers it and
the socket has delivered no frame since the probe started, the socket is
dead and the reconnect starts at once, with its own failure detail.

Anything else keeps upstream behaviour: probes still complete only on
their own reply, any inbound frame (including one buffered before the
path died) vetoes the shortcut and leaves the 3 s timeout in charge, and
the check is skipped for hosts without the cheap probe capability, for
routes it cannot reach (relay, SSH), and for network-change, retry and
desktop probes. The session counts inbound frames for this. No parallel
session or websocket ticket is created.

A timing harness over the real reconnect path, with phone and Mac on the
same Wi-Fi, measured time to connected dropping from 3.18 s to 0.73 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Oct 21, 2026
@macroscopeapp

macroscopeapp Bot commented Oct 21, 2026

Copy link
Copy Markdown
Contributor

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.

@coderabbitai

coderabbitai Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

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 d13fc84 and 208fda9.

📒 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)

sequenceDiagram
  participant EnvironmentSupervisor
  participant RpcSession
  participant WebSocket
  participant FreshRouteCheck
  EnvironmentSupervisor->>RpcSession: Read inboundFrames before the check
  EnvironmentSupervisor->>FreshRouteCheck: Check the active route after 500 ms
  WebSocket->>RpcSession: Deliver an incoming message
  RpcSession->>RpcSession: Increment the inbound-frame count
  FreshRouteCheck-->>EnvironmentSupervisor: Return fresh route response
  EnvironmentSupervisor->>RpcSession: Read inboundFrames after the check
  EnvironmentSupervisor->>EnvironmentSupervisor: Classify socket as silent if count is unchanged
Loading

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 name Status Explanation Resolution
Description check Warning 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 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 name Status Explanation
Title check Passed The title clearly and concisely describes the main fix: reducing the wait after an iOS backgrounded socket is killed.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check 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.

@t3dotgg

t3dotgg commented Oct 27, 2026

Copy link
Copy Markdown
Member

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.

@t3dotgg t3dotgg closed this Oct 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M 30-99 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants