Skip to content

fix(mobile): probe on network path changes, restore server leases on reconnect - #5154

Open
t3dotgg wants to merge 1 commit into
mainfrom
t3code/mobile-reconnect-hardening
Open

fix(mobile): probe on network path changes, restore server leases on reconnect#5154
t3dotgg wants to merge 1 commit into
mainfrom
t3code/mobile-reconnect-hardening

Conversation

@t3dotgg

@t3dotgg t3dotgg commented Jul 31, 2026

Copy link
Copy Markdown
Member

Mobile clients coming back from iOS backgrounding had two gaps left after #5561 and #5404 landed: a WiFi-to-cellular switch left the app hanging 5-10s until the ping timeout noticed the dead socket, and the server's activity lease lagged a resume by up to 25s because the AppState-triggered report races the reconnect and gets dropped — leaving provider/VCS work paused server-side.

This PR is the mobile-specific delta on top of main's reconnect work:

  • New advisory network-path-changed wakeup: the mobile adapter watches the interface type and the supervisor probes the session (3s bound) on WiFi/cellular flips instead of waiting for the ping timeout. A failed probe rides the existing wakeProbeFailed fast path from fix: prevent reconnect loops during server stalls #5561; flapping interfaces cannot cut backoff delays short.
  • Client activity is re-reported on every newly connected session generation, with per-supervisor generation dedup (replacement supervisors restart their counter).
  • The RPC onPingTimeout hook is wired to logging so zombie-socket disconnects are distinguishable from ordinary closes in telemetry.
  • Server websocket idleTimeout drops to 30s from Bun's 120s default — clients ping every 5s so live connections never idle, and a suspended phone's half-dead socket and its server-side buffers are released four times sooner.

Earlier revisions of this branch also carried backoff jitter, a probe-failure fast path, and a shell warm-cache resume; those were superseded by #5561's ladder rework, wakeProbeFailed, and authoritative-session guard, and have been dropped rather than re-fought.

No native changes; everything ships over Metro. 587 client-runtime tests pass including three new network-path cases, repo typecheck clean.


Written by Claude Fable 5 via Claude Code, directed by Theo.

🤖 Generated with Claude Code


Note

Medium Risk
Touches connection lifecycle (supervisor wakeups, probes, backoff) and server WebSocket idle behavior; well-covered by new tests but affects all mobile reconnect paths.

Overview
Improves mobile reconnect and server-side lease freshness when the app stays “online” but the transport is stale.

Network path changes: Mobile emits an advisory network-path-changed wakeup when the interface type flips (e.g. Wi‑Fi ↔ cellular) while active and still connected. EnvironmentSupervisor treats it like other foreground health checks: probe the live session with the shorter mobile probe timeout; on failure, reconnect without the first backoff rung. Path-change wakeups are ignored during backoff so flapping interfaces do not shorten retry delays.

Activity leases: The background activity reporter now triggers a client-activity report on each new connected session generation, so a resume report that races a reconnect and fails no longer leaves provider/VCS work paused until the next 25s tick.

Server / RPC: Bun WebSocket idleTimeout drops from 120s to 30s to drop suspended clients’ half-dead sockets sooner. RPC sessions log when a ping timeout kills a zombie socket.

New supervisor tests cover path-change probe, immediate reconnect after a failed probe, and no backoff shortcut on path flaps during backoff.

Reviewed by Cursor Bugbot for commit 8ab3326. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Reconnect faster and reduce unnecessary resyncs after mobile backgrounding

  • Adds a network-path-changed wakeup type that fires when the device's network interface changes (e.g. WiFi → cellular) while online and active, via platform.ts
  • When connected, EnvironmentSupervisor now probes the current session on network-path-changed; if the probe fails, the subsequent reconnect skips the initial backoff rung for faster recovery
  • During backoff, network-path-changed wakeups are ignored so they cannot shorten existing retry delays
  • The mobile background activity reporter now immediately re-reports on every new connected session generation, reducing unnecessary full resyncs after the app resumes
  • Sets websocket.idleTimeout: 30s on the Bun HTTP server, down from the default 120s, to reap stale WebSocket connections faster

Macroscope summarized 8ab3326.

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Title check ✅ Passed The title clearly summarizes the primary mobile network-path probing and reconnect lease changes.
Description check ✅ Passed The description explains the problem, changes, scope, testing, and risk; the omitted checklist is non-critical.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Jul 31, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 3 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 4abafa4. Configure here.

Comment thread packages/client-runtime/src/state/shell.ts
Comment thread packages/client-runtime/src/connection/supervisor.ts
Comment thread apps/mobile/src/connection/platform.ts
@macroscopeapp

macroscopeapp Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR adds new mobile network path change detection, modifies server idle timeout from 120s to 30s, and introduces connection probing/lease restoration logic - significant runtime behavior changes affecting mobile connectivity that warrant human review despite being well-tested.

You can customize Macroscope's approvability policy. Learn more.

Comment thread apps/mobile/src/connection/background-activity.ts Outdated
Comment thread apps/mobile/src/connection/background-activity.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
patches/@effect__platform-bun@4.0.0-beta.102.patch (1)

5-22: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Add focused tests for the Bun WebSocket patch behavior.

The current perMessageDeflate test runs with NodeHttpServer.layerTest, so it does not cover the Bun runtime. Add a companion test for both patched Bun server variants that exercises compression negotiation plus a compressed round trip, and add a separate idleTimeout: 30 close-path test with deterministic close receipts or configuration.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@patches/`@effect__platform-bun@4.0.0-beta.102.patch around lines 5 - 22, Add
focused Bun runtime tests for both patched Bun server variants, alongside the
existing perMessageDeflate coverage, verifying negotiation and a compressed
client/server round trip. Add a separate Bun test for idleTimeout: 30 that
deterministically verifies the connection-close receipt or configured timeout
behavior, using the relevant Bun server test symbols and avoiding
NodeHttpServer.layerTest.

Source: Coding guidelines

🧹 Nitpick comments (2)
packages/client-runtime/src/state/shell.ts (1)

190-196: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reduce the implementation narrative.

Replace this branch-by-branch comment with a short caller-contract comment, or extract the branch into a named helper. The current comment describes local behavior in detail instead of how makeSubscribeInput is used.

As per coding guidelines, “Use comments mainly to describe how a function is used; avoid annotating every line of behavior, and move comments when code moves.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/client-runtime/src/state/shell.ts` around lines 190 - 196, Replace
the detailed branch-by-branch comment near makeSubscribeInput with a concise
caller-contract comment describing when this subscription input is used and its
expected completion behavior. Do not document internal replay, snapshot, or
HTTP-path mechanics there; preserve the existing implementation unchanged.

Source: Coding guidelines

packages/client-runtime/src/state/shell-sync.test.ts (1)

50-58: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Infer the session helper return type.

Remove : RpcSession.RpcSession. If validation is required, apply satisfies RpcSession.RpcSession to the returned object. The helper is local to this test file, so TypeScript can infer its return type.

As per coding guidelines, “Prefer inferred types over explicit annotations and do not use any.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/client-runtime/src/state/shell-sync.test.ts` around lines 50 - 58,
Update the local session helper to remove the explicit RpcSession.RpcSession
return annotation and let TypeScript infer the returned object type. If
structural validation is needed, apply satisfies RpcSession.RpcSession to the
returned object without changing its behavior.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/client-runtime/src/connection/supervisor.test.ts`:
- Around line 912-937: Replace the fixed Effect.yieldNow loop in the “does not
cut backoff short when the network path flaps” test with a deterministic typed
wait. Extend the visible harness or use TestClock so the test awaits
confirmation that both network-path-changed signals were consumed before
asserting prepareCount and supervisor.state.phase, while preserving the backoff
and no-early-retry assertions.

In `@packages/client-runtime/src/state/shell-sync.test.ts`:
- Around line 397-419: Replace the polling loops around the
“application-active”, “application-active-probe”, and
“application-active-reconnect” wakeups with typed resubscription receipts
emitted by the subscription handler, including the attempt number and
afterSequence. Await the corresponding receipt after each Queue.offer, then
retain the loaderCalls and subscriptionCount assertions; remove the arbitrary
scheduler-turn polling.

---

Outside diff comments:
In `@patches/`@effect__platform-bun@4.0.0-beta.102.patch:
- Around line 5-22: Add focused Bun runtime tests for both patched Bun server
variants, alongside the existing perMessageDeflate coverage, verifying
negotiation and a compressed client/server round trip. Add a separate Bun test
for idleTimeout: 30 that deterministically verifies the connection-close receipt
or configured timeout behavior, using the relevant Bun server test symbols and
avoiding NodeHttpServer.layerTest.

---

Nitpick comments:
In `@packages/client-runtime/src/state/shell-sync.test.ts`:
- Around line 50-58: Update the local session helper to remove the explicit
RpcSession.RpcSession return annotation and let TypeScript infer the returned
object type. If structural validation is needed, apply satisfies
RpcSession.RpcSession to the returned object without changing its behavior.

In `@packages/client-runtime/src/state/shell.ts`:
- Around line 190-196: Replace the detailed branch-by-branch comment near
makeSubscribeInput with a concise caller-contract comment describing when this
subscription input is used and its expected completion behavior. Do not document
internal replay, snapshot, or HTTP-path mechanics there; preserve the existing
implementation unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 697a6784-5144-44fc-9bb0-7f90021f9812

📥 Commits

Reviewing files that changed from the base of the PR and between ca72e38 and b889ec4.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (9)
  • apps/mobile/src/connection/background-activity.ts
  • apps/mobile/src/connection/platform.ts
  • packages/client-runtime/src/connection/supervisor.test.ts
  • packages/client-runtime/src/connection/supervisor.ts
  • packages/client-runtime/src/connection/wakeups.ts
  • packages/client-runtime/src/rpc/session.ts
  • packages/client-runtime/src/state/shell-sync.test.ts
  • packages/client-runtime/src/state/shell.ts
  • patches/@effect__platform-bun@4.0.0-beta.102.patch

Comment on lines +912 to +937
it.effect("does not cut backoff short when the network path flaps", () =>
Effect.gen(function* () {
const harness = yield* makeHarness({
prepare: () => Effect.fail(transient()),
});
const supervisor = yield* EnvironmentSupervisor.make(TARGET_ENTRY, {
initiallyDesired: true,
}).pipe(Effect.provide(harness.dependencies));

yield* awaitState(
supervisor.state,
(state) => state.phase === "backoff" && state.attempt === 1,
);
expect(yield* Ref.get(harness.prepareCount)).toBe(1);

// Advisory path-change wakeups have no session to probe during backoff
// and must not trigger an early retry.
yield* harness.wake("network-path-changed");
yield* harness.wake("network-path-changed");
for (let attempt = 0; attempt < 20; attempt += 1) {
yield* Effect.yieldNow;
}
expect(yield* Ref.get(harness.prepareCount)).toBe(1);
expect((yield* SubscriptionRef.get(supervisor.state)).phase).toBe("backoff");
}),
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Replace the fixed Effect.yieldNow loop with a typed wait.

This test waits for two queued "network-path-changed" wakeups to be processed by looping Effect.yieldNow a fixed 20 times before asserting prepareCount and phase. This does not wait for a typed receipt that the supervisor consumed both signals; it relies on an arbitrary iteration count matching the current fiber-scheduling depth. If the internal signal-processing chain gets deeper (e.g., another yield* is added upstream), this test can pass or fail non-deterministically without a related regression in production code.

Use a typed receipt instead, for example by extending the harness to expose an effect that completes once the signals queue is drained, or by using TestClock.adjust to a known point (since waitForRetrySignal races against Effect.sleep(delayMs), advancing time deterministically resolves the race without a magic iteration count).

As per coding guidelines, "Tests must wait for typed receipts and worker drains in event-sourced async flows; do not use sleeps, polling, or arbitrary timeouts to make tests pass."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/client-runtime/src/connection/supervisor.test.ts` around lines 912 -
937, Replace the fixed Effect.yieldNow loop in the “does not cut backoff short
when the network path flaps” test with a deterministic typed wait. Extend the
visible harness or use TestClock so the test awaits confirmation that both
network-path-changed signals were consumed before asserting prepareCount and
supervisor.state.phase, while preserving the backoff and no-early-retry
assertions.

Source: Coding guidelines

Comment on lines +397 to +419
// Foreground wakeups resubscribe from the cached sequence and still
// skip the HTTP loader; catch-up is the server's sequence replay.
yield* Queue.offer(wakeups, "application-active");
yield* SubscriptionRef.changes(shellState).pipe(
Stream.filter(
(value) =>
value.status === "synchronizing" &&
Option.isSome(value.snapshot) &&
value.snapshot.value.snapshotSequence === 20,
),
Stream.runHead,
);

for (let attempt = 0; attempt < 100; attempt += 1) {
if ((yield* Ref.get(subscriptionCount)) >= 2) break;
yield* Effect.yieldNow;
}

expect(yield* Ref.get(loaderCalls)).toBe(2);
expect(yield* Ref.get(loaderCalls)).toBe(0);
expect(yield* Ref.get(subscriptionCount)).toBe(2);

yield* Queue.offer(wakeups, "application-active-probe");
for (let attempt = 0; attempt < 100; attempt += 1) {
if ((yield* Ref.get(subscriptionCount)) >= 3) break;
yield* Effect.yieldNow;
}
expect(yield* Ref.get(loaderCalls)).toBe(3);
expect(yield* Ref.get(loaderCalls)).toBe(0);
expect(yield* Ref.get(subscriptionCount)).toBe(3);

yield* Queue.offer(wakeups, "application-active-reconnect");
for (let attempt = 0; attempt < 10; attempt += 1) {
yield* Effect.yieldNow;
}
expect(yield* Ref.get(loaderCalls)).toBe(3);
expect(yield* Ref.get(loaderCalls)).toBe(0);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Wait for typed resubscription receipts.

The loops poll subscriptionCount for an arbitrary number of scheduler turns. A slow scheduler can run the subscription after the loop and cause a false test failure. Emit a typed receipt from the subscription handler, such as its attempt number and afterSequence, then await that receipt after each wakeup.

As per coding guidelines, “Tests must wait for typed receipts and worker drains in event-sourced async flows; do not use sleeps, polling, or arbitrary timeouts to make tests pass.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/client-runtime/src/state/shell-sync.test.ts` around lines 397 - 419,
Replace the polling loops around the “application-active”,
“application-active-probe”, and “application-active-reconnect” wakeups with
typed resubscription receipts emitted by the subscription handler, including the
attempt number and afterSequence. Await the corresponding receipt after each
Queue.offer, then retain the loaderCalls and subscriptionCount assertions;
remove the arbitrary scheduler-turn polling.

Source: Coding guidelines

Rebuilt on main after #5561 and #5404 landed overlapping reconnect
work. What main already covers is dropped from this branch: the backoff
ladder rework replaces our jitter, wakeProbeFailed replaces our
probe-failure fast path, and the authoritative-session guard replaces
our shell warm-cache short-circuit.

What remains is the mobile-specific delta:

- New advisory `network-path-changed` wakeup: WiFi<->cellular keeps
  isConnected true while invalidating the socket's path, so the mobile
  adapter emits a wakeup on interface-type changes while active and the
  supervisor probes the session (3s bound) instead of waiting for the
  ping timeout. A failed probe rides main's wakeProbeFailed fast path;
  flapping interfaces cannot cut backoff delays short.
- Client activity is re-reported on every newly connected session
  generation; the AppState-triggered report races the reconnect and is
  dropped, leaving provider/VCS work paused server-side for up to 25s
  after a resume. Generation dedup is per-supervisor since replacements
  restart the counter.
- The RPC onPingTimeout hook is wired to logging so zombie-socket
  disconnects are distinguishable from ordinary closes.
- Server websocket idleTimeout drops to 30s from Bun's 120s default
  (clients ping every 5s, so live connections never idle), releasing a
  suspended phone's half-dead socket and buffers four times sooner.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@t3dotgg
t3dotgg force-pushed the t3code/mobile-reconnect-hardening branch from 9193ab0 to 8ab3326 Compare August 7, 2026 09:45
@t3dotgg t3dotgg changed the title fix(mobile): reconnect faster and stop resyncing more than needed after backgrounding fix(mobile): probe on network path changes, restore server leases on reconnect Aug 7, 2026
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

Provider Metric Main baseline This PR Impact PR ceiling
Codex Total thread wire 11.3 KiB 11.3 KiB +15 B (+0.1%) 15.1 KiB
Codex Thread snapshot wire 5.4 KiB 5.4 KiB +2 B (+0.0%) 7.3 KiB
Codex Live turn WebSocket wire 5.9 KiB 5.9 KiB +13 B (+0.2%) 7.8 KiB
Codex Live turn WebSocket decoded 49.7 KiB 49.7 KiB 0 B (0.0%) 66.4 KiB
Codex Live turn messages 16 16 0 (0.0%) 21
Claude Total thread wire 11.3 KiB 11.3 KiB −21 B (−0.2%) 15.1 KiB
Claude Thread snapshot wire 5.5 KiB 5.5 KiB +3 B (+0.1%) 7.3 KiB
Claude Live turn WebSocket wire 5.9 KiB 5.8 KiB −24 B (−0.4%) 7.8 KiB
Claude Live turn WebSocket decoded 50.6 KiB 50.6 KiB 0 B (0.0%) 66.4 KiB
Claude Live turn messages 16 16 0 (0.0%) 21

Baseline: 8100062 · PR result: 8ab3326 · 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: 94.6 KiB
  • Claude decoded thread snapshot: 95.3 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

size:L 100-499 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant