fix(mobile): probe network changes and refresh activity leases - #5154

Closed
t3dotgg wants to merge 4 commits into
mainfrom
t3code/mobile-reconnect-hardening
Closed

fix(mobile): probe network changes and refresh activity leases#5154
t3dotgg wants to merge 4 commits into
mainfrom
t3code/mobile-reconnect-hardening

Conversation

@t3dotgg

@t3dotggt3dotgg commented Jul 31, 2026

Copy link
Copy Markdown
Member

Mobile can miss Wi-Fi/cellular changes and leave activity leases stale after reconnecting.

Probe the current session when the active network interface changes. Keep retry delays unchanged, and report current activity after each newly connected session generation. Keep the current session owner and credential handling. The earlier Bun-wide idle-timeout change is removed.

Verified on current main with 77 focused tests, mobile and client-runtime typechecks, targeted lint, and formatting. The new activity test waits for typed debounce and report receipts.

Device background/resume, real network switching, and live relay/tunnel behavior were not tested. No UI changes.

Original work by Theo Browne and Julius Marminge with Claude Code and Codex. Original commit authors and co-authors are preserved. Updated by GPT-5.6 Sol in Codex.


Note

Medium Risk
Touches connection supervisor wakeup and probe/reconnect logic plus mobile lifecycle networking; mistakes could cause reconnect storms or stale leases, but behavior is heavily tested and path-change wakeups are explicitly non-disruptive during backoff.

Overview
Mobile now detects Wi‑Fi/cellular interface changes (even when isConnected stays true) and emits a network-path-changed wakeup while the app is active and online. The shared supervisor probes the current session with the 3s mobile probe timeout; a failed or timed-out probe reconnects immediately without shell resubscription. During backoff, path-change wakeups are ignored so flapping interfaces do not shorten retry delays.

The mobile background activity reporter triggers another debounced serverReportClientActivity when an environment reaches a new connected session generation, so activity leases are restored after reconnect if an earlier report had no RPC session.

Supporting changes: pure network-path-change helpers and unit tests, ping timeout info logging on RPC sessions, supervisor/wakeup tests (including a queue-based test harness), shell-sync test tightening, and user/internal docs for mobile network behavior.

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

Note

Probe connected sessions on mobile network changes and refresh activity leases on reconnect

  • Adds a network-path-changed advisory wakeup that fires when the mobile app's known network interface changes while online and active, prompting a liveness probe of the current session with a mobile timeout
  • The supervisor consumes network-path-changed without interrupting connection establishment or shortening retry backoff; a failed probe triggers an immediate reconnect through the existing wake-probe path
  • Adds a NetworkPathBaseline model in network-path-change.ts that records the first known interface asynchronously and requests probes only on genuine interface transitions, not repeated or unknown observations
  • Updates the mobile activity reporter in background-activity.ts to queue a debounced activity report when an environment reaches a newly connected supervisor generation, deduplicating per generation
  • Behavioral Change: ConnectionWakeup gains a new network-path-changed variant; any out-of-tree consumer pattern-matching on that type without a catch-all will fail to compile
📊 Macroscope summarized 1d5093b. 8 files reviewed, 1 issue evaluated, 1 issue filtered, 0 comments posted

🗂️ Filtered Issues

packages/client-runtime/src/connection/supervisor.ts — 0 comments posted, 1 evaluated, 1 filtered
  • line 424: Adding network-path-changed to the probe branch also routes a failed path-change probe through wakeProbeFailed. After the failed probe tears down the lease, the run loop resets the retry ladder and starts the next connection immediately, so a network-change failure skips the normal first backoff delay despite waitForRetrySignal explicitly treating these wakeups as advisory. This can cause reconnect churn on a flapping interface. [ Out of scope (triage) ]

@coderabbitai

coderabbitaiBot commented Jul 31, 2026

Copy link
Copy Markdown
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring 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.
Description check⚠️ WarningThe description explains the main mobile issue, the intended fix, testing, limitations, and lack of UI changes. However, it omits several changes in the pull request, including warm shell resume, jitt…Update the description using the repository template. Add explicit What Changed, Why, UI Changes, and Checklist sections. Document all current changes, including warm shell resume, jittered reconnect backoff, network-path wakeups, activity …
✅ Passed checks (3 passed)
Check nameStatusExplanation
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
Title check✅ PassedThe title clearly summarizes the mobile network probing and activity lease refresh changes. It is concise and relevant, even though it does not describe every runtime and server change.
Full details: Description check

Explanation

The description explains the main mobile issue, the intended fix, testing, limitations, and lack of UI changes. However, it omits several changes in the pull request, including warm shell resume, jittered backoff, RPC ping-timeout logging, and the 30-second Bun WebSocket idle timeout. It also conflicts with the changeset by stating that the Bun idle-timeout change was removed.

Resolution

Update the description using the repository template. Add explicit What Changed, Why, UI Changes, and Checklist sections. Document all current changes, including warm shell resume, jittered reconnect backoff, network-path wakeups, activity reporting, RPC ping-timeout logging, and the 30-second Bun WebSocket idle timeout. Resolve the conflicting statements about retry delays and the Bun idle timeout.

  • Fix all pre-merge checks with AI

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

@github-actionsgithub-actionsBot 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

@cursorcursorBot 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 threadpackages/client-runtime/src/state/shell.ts
Comment threadpackages/client-runtime/src/connection/supervisor.ts
Comment threadapps/mobile/src/connection/platform.ts
@macroscopeapp

macroscopeappBot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This change alters production connection behavior across mobile lifecycle handling, the shared supervisor, and activity leases: network transitions can trigger probes, reconnects, and additional reports. Although the behavior is covered by focused tests, the cross-component runtime impact merits human review.

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

Comment threadapps/mobile/src/connection/background-activity.ts Outdated
Comment threadapps/mobile/src/connection/background-activity.ts Outdated

@coderabbitaicoderabbitaiBot 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

@t3dotgg
t3dotggforce-pushed the t3code/mobile-reconnect-hardening branch 2 times, most recently from 9193ab0 to 8ab3326CompareAugust 7, 2026 09:45
@t3dotggt3dotgg changed the title fix(mobile): reconnect faster and stop resyncing more than needed after backgroundingfix(mobile): probe on network path changes, restore server leases on reconnectAug 7, 2026
@github-actions

github-actionsBot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.4 KiB13.2 KiB−223 B (−1.6%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB−14 B (−0.2%)7.3 KiB
CodexLive turn WebSocket wire6.5 KiB6.3 KiB−209 B (−3.1%)7.8 KiB
CodexLive turn WebSocket decoded57.0 KiB55.5 KiB−1.5 KiB (−2.6%)66.4 KiB
CodexLive turn messages108−2 (−20.0%)21
ClaudeTotal thread wire13.5 KiB13.3 KiB−182 B (−1.3%)15.1 KiB
ClaudeThread snapshot wire6.9 KiB6.9 KiB−9 B (−0.1%)7.3 KiB
ClaudeLive turn WebSocket wire6.6 KiB6.4 KiB−173 B (−2.6%)7.8 KiB
ClaudeLive turn WebSocket decoded57.8 KiB56.4 KiB−1.4 KiB (−2.5%)66.4 KiB
ClaudeLive turn messages10100 (0.0%)21

Baseline: ea71a19 · PR result: 1d5093b · 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: 109.4 KiB
  • Claude decoded thread snapshot: 110.1 KiB

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

@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-reconnect-hardening branch from 8ab3326 to 68b85b9CompareAugust 10, 2026 10:58
Comment threadapps/mobile/src/connection/platform.ts Outdated

@macroscopeappmacroscopeappBot 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.

One change-discipline finding: documentation lost while moving the WebSocket options out of server.ts. Everything else in the changed Effect service code (supervisor wakeup handling, wakeups.ts union, mobile platform layer, rpc/session.ts hooks) follows the conventions.

Posted via Macroscope — Effect Service Conventions

Comment threadapps/server/src/httpServerOptions.ts Outdated
t3dotggand others added 4 commits September 1, 2026 17:46
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>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Keep advisory interface probes and activity reports after reconnect. Leave server socket idle timeouts unchanged. Test report recovery and the short probe timeout.
@t3dotgg
t3dotggforce-pushed the t3code/mobile-reconnect-hardening branch from eff1bf7 to 1d5093bCompareSeptember 2, 2026 00:47
@t3dotggt3dotgg changed the title fix(mobile): probe on network path changes, restore server leases on reconnectfix(mobile): probe network changes and refresh activity leasesSep 2, 2026
@t3dotggt3dotgg closed this Sep 2, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@t3dotgg@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(mobile): probe network changes and refresh activity leases - #5154

Closed
t3dotgg wants to merge 4 commits into
mainfrom
t3code/mobile-reconnect-hardening
Closed

fix(mobile): probe network changes and refresh activity leases#5154
t3dotgg wants to merge 4 commits into
mainfrom
t3code/mobile-reconnect-hardening

Conversation

@t3dotgg

@t3dotggt3dotgg commented Jul 31, 2026

Copy link
Copy Markdown
Member

Mobile can miss Wi-Fi/cellular changes and leave activity leases stale after reconnecting.

Probe the current session when the active network interface changes. Keep retry delays unchanged, and report current activity after each newly connected session generation. Keep the current session owner and credential handling. The earlier Bun-wide idle-timeout change is removed.

Verified on current main with 77 focused tests, mobile and client-runtime typechecks, targeted lint, and formatting. The new activity test waits for typed debounce and report receipts.

Device background/resume, real network switching, and live relay/tunnel behavior were not tested. No UI changes.

Original work by Theo Browne and Julius Marminge with Claude Code and Codex. Original commit authors and co-authors are preserved. Updated by GPT-5.6 Sol in Codex.


Note

Medium Risk
Touches connection supervisor wakeup and probe/reconnect logic plus mobile lifecycle networking; mistakes could cause reconnect storms or stale leases, but behavior is heavily tested and path-change wakeups are explicitly non-disruptive during backoff.

Overview
Mobile now detects Wi‑Fi/cellular interface changes (even when isConnected stays true) and emits a network-path-changed wakeup while the app is active and online. The shared supervisor probes the current session with the 3s mobile probe timeout; a failed or timed-out probe reconnects immediately without shell resubscription. During backoff, path-change wakeups are ignored so flapping interfaces do not shorten retry delays.

The mobile background activity reporter triggers another debounced serverReportClientActivity when an environment reaches a new connected session generation, so activity leases are restored after reconnect if an earlier report had no RPC session.

Supporting changes: pure network-path-change helpers and unit tests, ping timeout info logging on RPC sessions, supervisor/wakeup tests (including a queue-based test harness), shell-sync test tightening, and user/internal docs for mobile network behavior.

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

Note

Probe connected sessions on mobile network changes and refresh activity leases on reconnect

  • Adds a network-path-changed advisory wakeup that fires when the mobile app's known network interface changes while online and active, prompting a liveness probe of the current session with a mobile timeout
  • The supervisor consumes network-path-changed without interrupting connection establishment or shortening retry backoff; a failed probe triggers an immediate reconnect through the existing wake-probe path
  • Adds a NetworkPathBaseline model in network-path-change.ts that records the first known interface asynchronously and requests probes only on genuine interface transitions, not repeated or unknown observations
  • Updates the mobile activity reporter in background-activity.ts to queue a debounced activity report when an environment reaches a newly connected supervisor generation, deduplicating per generation
  • Behavioral Change: ConnectionWakeup gains a new network-path-changed variant; any out-of-tree consumer pattern-matching on that type without a catch-all will fail to compile
📊 Macroscope summarized 1d5093b. 8 files reviewed, 1 issue evaluated, 1 issue filtered, 0 comments posted

🗂️ Filtered Issues

packages/client-runtime/src/connection/supervisor.ts — 0 comments posted, 1 evaluated, 1 filtered
  • line 424: Adding network-path-changed to the probe branch also routes a failed path-change probe through wakeProbeFailed. After the failed probe tears down the lease, the run loop resets the retry ladder and starts the next connection immediately, so a network-change failure skips the normal first backoff delay despite waitForRetrySignal explicitly treating these wakeups as advisory. This can cause reconnect churn on a flapping interface. [ Out of scope (triage) ]

@coderabbitai

coderabbitaiBot commented Jul 31, 2026

Copy link
Copy Markdown
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring 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.
Description check⚠️ WarningThe description explains the main mobile issue, the intended fix, testing, limitations, and lack of UI changes. However, it omits several changes in the pull request, including warm shell resume, jitt…Update the description using the repository template. Add explicit What Changed, Why, UI Changes, and Checklist sections. Document all current changes, including warm shell resume, jittered reconnect backoff, network-path wakeups, activity …
✅ Passed checks (3 passed)
Check nameStatusExplanation
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
Title check✅ PassedThe title clearly summarizes the mobile network probing and activity lease refresh changes. It is concise and relevant, even though it does not describe every runtime and server change.
Full details: Description check

Explanation

The description explains the main mobile issue, the intended fix, testing, limitations, and lack of UI changes. However, it omits several changes in the pull request, including warm shell resume, jittered backoff, RPC ping-timeout logging, and the 30-second Bun WebSocket idle timeout. It also conflicts with the changeset by stating that the Bun idle-timeout change was removed.

Resolution

Update the description using the repository template. Add explicit What Changed, Why, UI Changes, and Checklist sections. Document all current changes, including warm shell resume, jittered reconnect backoff, network-path wakeups, activity reporting, RPC ping-timeout logging, and the 30-second Bun WebSocket idle timeout. Resolve the conflicting statements about retry delays and the Bun idle timeout.

  • Fix all pre-merge checks with AI

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

@github-actionsgithub-actionsBot 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

@cursorcursorBot 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 threadpackages/client-runtime/src/state/shell.ts
Comment threadpackages/client-runtime/src/connection/supervisor.ts
Comment threadapps/mobile/src/connection/platform.ts
@macroscopeapp

macroscopeappBot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This change alters production connection behavior across mobile lifecycle handling, the shared supervisor, and activity leases: network transitions can trigger probes, reconnects, and additional reports. Although the behavior is covered by focused tests, the cross-component runtime impact merits human review.

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

Comment threadapps/mobile/src/connection/background-activity.ts Outdated
Comment threadapps/mobile/src/connection/background-activity.ts Outdated

@coderabbitaicoderabbitaiBot 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

@t3dotgg
t3dotggforce-pushed the t3code/mobile-reconnect-hardening branch 2 times, most recently from 9193ab0 to 8ab3326CompareAugust 7, 2026 09:45
@t3dotggt3dotgg changed the title fix(mobile): reconnect faster and stop resyncing more than needed after backgroundingfix(mobile): probe on network path changes, restore server leases on reconnectAug 7, 2026
@github-actions

github-actionsBot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.4 KiB13.2 KiB−223 B (−1.6%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB−14 B (−0.2%)7.3 KiB
CodexLive turn WebSocket wire6.5 KiB6.3 KiB−209 B (−3.1%)7.8 KiB
CodexLive turn WebSocket decoded57.0 KiB55.5 KiB−1.5 KiB (−2.6%)66.4 KiB
CodexLive turn messages108−2 (−20.0%)21
ClaudeTotal thread wire13.5 KiB13.3 KiB−182 B (−1.3%)15.1 KiB
ClaudeThread snapshot wire6.9 KiB6.9 KiB−9 B (−0.1%)7.3 KiB
ClaudeLive turn WebSocket wire6.6 KiB6.4 KiB−173 B (−2.6%)7.8 KiB
ClaudeLive turn WebSocket decoded57.8 KiB56.4 KiB−1.4 KiB (−2.5%)66.4 KiB
ClaudeLive turn messages10100 (0.0%)21

Baseline: ea71a19 · PR result: 1d5093b · 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: 109.4 KiB
  • Claude decoded thread snapshot: 110.1 KiB

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

@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-reconnect-hardening branch from 8ab3326 to 68b85b9CompareAugust 10, 2026 10:58
Comment threadapps/mobile/src/connection/platform.ts Outdated

@macroscopeappmacroscopeappBot 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.

One change-discipline finding: documentation lost while moving the WebSocket options out of server.ts. Everything else in the changed Effect service code (supervisor wakeup handling, wakeups.ts union, mobile platform layer, rpc/session.ts hooks) follows the conventions.

Posted via Macroscope — Effect Service Conventions

Comment threadapps/server/src/httpServerOptions.ts Outdated
t3dotggand others added 4 commits September 1, 2026 17:46
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>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Keep advisory interface probes and activity reports after reconnect. Leave server socket idle timeouts unchanged. Test report recovery and the short probe timeout.
@t3dotgg
t3dotggforce-pushed the t3code/mobile-reconnect-hardening branch from eff1bf7 to 1d5093bCompareSeptember 2, 2026 00:47
@t3dotggt3dotgg changed the title fix(mobile): probe on network path changes, restore server leases on reconnectfix(mobile): probe network changes and refresh activity leasesSep 2, 2026
@t3dotggt3dotgg closed this Sep 2, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@t3dotgg@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(mobile): probe network changes and refresh activity leases - #5154

Closed
t3dotgg wants to merge 4 commits into
mainfrom
t3code/mobile-reconnect-hardening
Closed

fix(mobile): probe network changes and refresh activity leases#5154
t3dotgg wants to merge 4 commits into
mainfrom
t3code/mobile-reconnect-hardening

Conversation

@t3dotgg

@t3dotggt3dotgg commented Jul 31, 2026

Copy link
Copy Markdown
Member

Mobile can miss Wi-Fi/cellular changes and leave activity leases stale after reconnecting.

Probe the current session when the active network interface changes. Keep retry delays unchanged, and report current activity after each newly connected session generation. Keep the current session owner and credential handling. The earlier Bun-wide idle-timeout change is removed.

Verified on current main with 77 focused tests, mobile and client-runtime typechecks, targeted lint, and formatting. The new activity test waits for typed debounce and report receipts.

Device background/resume, real network switching, and live relay/tunnel behavior were not tested. No UI changes.

Original work by Theo Browne and Julius Marminge with Claude Code and Codex. Original commit authors and co-authors are preserved. Updated by GPT-5.6 Sol in Codex.


Note

Medium Risk
Touches connection supervisor wakeup and probe/reconnect logic plus mobile lifecycle networking; mistakes could cause reconnect storms or stale leases, but behavior is heavily tested and path-change wakeups are explicitly non-disruptive during backoff.

Overview
Mobile now detects Wi‑Fi/cellular interface changes (even when isConnected stays true) and emits a network-path-changed wakeup while the app is active and online. The shared supervisor probes the current session with the 3s mobile probe timeout; a failed or timed-out probe reconnects immediately without shell resubscription. During backoff, path-change wakeups are ignored so flapping interfaces do not shorten retry delays.

The mobile background activity reporter triggers another debounced serverReportClientActivity when an environment reaches a new connected session generation, so activity leases are restored after reconnect if an earlier report had no RPC session.

Supporting changes: pure network-path-change helpers and unit tests, ping timeout info logging on RPC sessions, supervisor/wakeup tests (including a queue-based test harness), shell-sync test tightening, and user/internal docs for mobile network behavior.

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

Note

Probe connected sessions on mobile network changes and refresh activity leases on reconnect

  • Adds a network-path-changed advisory wakeup that fires when the mobile app's known network interface changes while online and active, prompting a liveness probe of the current session with a mobile timeout
  • The supervisor consumes network-path-changed without interrupting connection establishment or shortening retry backoff; a failed probe triggers an immediate reconnect through the existing wake-probe path
  • Adds a NetworkPathBaseline model in network-path-change.ts that records the first known interface asynchronously and requests probes only on genuine interface transitions, not repeated or unknown observations
  • Updates the mobile activity reporter in background-activity.ts to queue a debounced activity report when an environment reaches a newly connected supervisor generation, deduplicating per generation
  • Behavioral Change: ConnectionWakeup gains a new network-path-changed variant; any out-of-tree consumer pattern-matching on that type without a catch-all will fail to compile
📊 Macroscope summarized 1d5093b. 8 files reviewed, 1 issue evaluated, 1 issue filtered, 0 comments posted

🗂️ Filtered Issues

packages/client-runtime/src/connection/supervisor.ts — 0 comments posted, 1 evaluated, 1 filtered
  • line 424: Adding network-path-changed to the probe branch also routes a failed path-change probe through wakeProbeFailed. After the failed probe tears down the lease, the run loop resets the retry ladder and starts the next connection immediately, so a network-change failure skips the normal first backoff delay despite waitForRetrySignal explicitly treating these wakeups as advisory. This can cause reconnect churn on a flapping interface. [ Out of scope (triage) ]

@coderabbitai

coderabbitaiBot commented Jul 31, 2026

Copy link
Copy Markdown
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring 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.
Description check⚠️ WarningThe description explains the main mobile issue, the intended fix, testing, limitations, and lack of UI changes. However, it omits several changes in the pull request, including warm shell resume, jitt…Update the description using the repository template. Add explicit What Changed, Why, UI Changes, and Checklist sections. Document all current changes, including warm shell resume, jittered reconnect backoff, network-path wakeups, activity …
✅ Passed checks (3 passed)
Check nameStatusExplanation
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
Title check✅ PassedThe title clearly summarizes the mobile network probing and activity lease refresh changes. It is concise and relevant, even though it does not describe every runtime and server change.
Full details: Description check

Explanation

The description explains the main mobile issue, the intended fix, testing, limitations, and lack of UI changes. However, it omits several changes in the pull request, including warm shell resume, jittered backoff, RPC ping-timeout logging, and the 30-second Bun WebSocket idle timeout. It also conflicts with the changeset by stating that the Bun idle-timeout change was removed.

Resolution

Update the description using the repository template. Add explicit What Changed, Why, UI Changes, and Checklist sections. Document all current changes, including warm shell resume, jittered reconnect backoff, network-path wakeups, activity reporting, RPC ping-timeout logging, and the 30-second Bun WebSocket idle timeout. Resolve the conflicting statements about retry delays and the Bun idle timeout.

  • Fix all pre-merge checks with AI

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

@github-actionsgithub-actionsBot 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

@cursorcursorBot 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 threadpackages/client-runtime/src/state/shell.ts
Comment threadpackages/client-runtime/src/connection/supervisor.ts
Comment threadapps/mobile/src/connection/platform.ts
@macroscopeapp

macroscopeappBot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This change alters production connection behavior across mobile lifecycle handling, the shared supervisor, and activity leases: network transitions can trigger probes, reconnects, and additional reports. Although the behavior is covered by focused tests, the cross-component runtime impact merits human review.

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

Comment threadapps/mobile/src/connection/background-activity.ts Outdated
Comment threadapps/mobile/src/connection/background-activity.ts Outdated

@coderabbitaicoderabbitaiBot 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

@t3dotgg
t3dotggforce-pushed the t3code/mobile-reconnect-hardening branch 2 times, most recently from 9193ab0 to 8ab3326CompareAugust 7, 2026 09:45
@t3dotggt3dotgg changed the title fix(mobile): reconnect faster and stop resyncing more than needed after backgroundingfix(mobile): probe on network path changes, restore server leases on reconnectAug 7, 2026
@github-actions

github-actionsBot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.4 KiB13.2 KiB−223 B (−1.6%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB−14 B (−0.2%)7.3 KiB
CodexLive turn WebSocket wire6.5 KiB6.3 KiB−209 B (−3.1%)7.8 KiB
CodexLive turn WebSocket decoded57.0 KiB55.5 KiB−1.5 KiB (−2.6%)66.4 KiB
CodexLive turn messages108−2 (−20.0%)21
ClaudeTotal thread wire13.5 KiB13.3 KiB−182 B (−1.3%)15.1 KiB
ClaudeThread snapshot wire6.9 KiB6.9 KiB−9 B (−0.1%)7.3 KiB
ClaudeLive turn WebSocket wire6.6 KiB6.4 KiB−173 B (−2.6%)7.8 KiB
ClaudeLive turn WebSocket decoded57.8 KiB56.4 KiB−1.4 KiB (−2.5%)66.4 KiB
ClaudeLive turn messages10100 (0.0%)21

Baseline: ea71a19 · PR result: 1d5093b · 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: 109.4 KiB
  • Claude decoded thread snapshot: 110.1 KiB

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

@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-reconnect-hardening branch from 8ab3326 to 68b85b9CompareAugust 10, 2026 10:58
Comment threadapps/mobile/src/connection/platform.ts Outdated

@macroscopeappmacroscopeappBot 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.

One change-discipline finding: documentation lost while moving the WebSocket options out of server.ts. Everything else in the changed Effect service code (supervisor wakeup handling, wakeups.ts union, mobile platform layer, rpc/session.ts hooks) follows the conventions.

Posted via Macroscope — Effect Service Conventions

Comment threadapps/server/src/httpServerOptions.ts Outdated
t3dotggand others added 4 commits September 1, 2026 17:46
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>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Keep advisory interface probes and activity reports after reconnect. Leave server socket idle timeouts unchanged. Test report recovery and the short probe timeout.
@t3dotgg
t3dotggforce-pushed the t3code/mobile-reconnect-hardening branch from eff1bf7 to 1d5093bCompareSeptember 2, 2026 00:47
@t3dotggt3dotgg changed the title fix(mobile): probe on network path changes, restore server leases on reconnectfix(mobile): probe network changes and refresh activity leasesSep 2, 2026
@t3dotggt3dotgg closed this Sep 2, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@t3dotgg@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(mobile): probe network changes and refresh activity leases - #5154

Closed
t3dotgg wants to merge 4 commits into
mainfrom
t3code/mobile-reconnect-hardening
Closed

fix(mobile): probe network changes and refresh activity leases#5154
t3dotgg wants to merge 4 commits into
mainfrom
t3code/mobile-reconnect-hardening

Conversation

@t3dotgg

@t3dotggt3dotgg commented Jul 31, 2026

Copy link
Copy Markdown
Member

Mobile can miss Wi-Fi/cellular changes and leave activity leases stale after reconnecting.

Probe the current session when the active network interface changes. Keep retry delays unchanged, and report current activity after each newly connected session generation. Keep the current session owner and credential handling. The earlier Bun-wide idle-timeout change is removed.

Verified on current main with 77 focused tests, mobile and client-runtime typechecks, targeted lint, and formatting. The new activity test waits for typed debounce and report receipts.

Device background/resume, real network switching, and live relay/tunnel behavior were not tested. No UI changes.

Original work by Theo Browne and Julius Marminge with Claude Code and Codex. Original commit authors and co-authors are preserved. Updated by GPT-5.6 Sol in Codex.


Note

Medium Risk
Touches connection supervisor wakeup and probe/reconnect logic plus mobile lifecycle networking; mistakes could cause reconnect storms or stale leases, but behavior is heavily tested and path-change wakeups are explicitly non-disruptive during backoff.

Overview
Mobile now detects Wi‑Fi/cellular interface changes (even when isConnected stays true) and emits a network-path-changed wakeup while the app is active and online. The shared supervisor probes the current session with the 3s mobile probe timeout; a failed or timed-out probe reconnects immediately without shell resubscription. During backoff, path-change wakeups are ignored so flapping interfaces do not shorten retry delays.

The mobile background activity reporter triggers another debounced serverReportClientActivity when an environment reaches a new connected session generation, so activity leases are restored after reconnect if an earlier report had no RPC session.

Supporting changes: pure network-path-change helpers and unit tests, ping timeout info logging on RPC sessions, supervisor/wakeup tests (including a queue-based test harness), shell-sync test tightening, and user/internal docs for mobile network behavior.

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

Note

Probe connected sessions on mobile network changes and refresh activity leases on reconnect

  • Adds a network-path-changed advisory wakeup that fires when the mobile app's known network interface changes while online and active, prompting a liveness probe of the current session with a mobile timeout
  • The supervisor consumes network-path-changed without interrupting connection establishment or shortening retry backoff; a failed probe triggers an immediate reconnect through the existing wake-probe path
  • Adds a NetworkPathBaseline model in network-path-change.ts that records the first known interface asynchronously and requests probes only on genuine interface transitions, not repeated or unknown observations
  • Updates the mobile activity reporter in background-activity.ts to queue a debounced activity report when an environment reaches a newly connected supervisor generation, deduplicating per generation
  • Behavioral Change: ConnectionWakeup gains a new network-path-changed variant; any out-of-tree consumer pattern-matching on that type without a catch-all will fail to compile
📊 Macroscope summarized 1d5093b. 8 files reviewed, 1 issue evaluated, 1 issue filtered, 0 comments posted

🗂️ Filtered Issues

packages/client-runtime/src/connection/supervisor.ts — 0 comments posted, 1 evaluated, 1 filtered
  • line 424: Adding network-path-changed to the probe branch also routes a failed path-change probe through wakeProbeFailed. After the failed probe tears down the lease, the run loop resets the retry ladder and starts the next connection immediately, so a network-change failure skips the normal first backoff delay despite waitForRetrySignal explicitly treating these wakeups as advisory. This can cause reconnect churn on a flapping interface. [ Out of scope (triage) ]

@coderabbitai

coderabbitaiBot commented Jul 31, 2026

Copy link
Copy Markdown
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring 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.
Description check⚠️ WarningThe description explains the main mobile issue, the intended fix, testing, limitations, and lack of UI changes. However, it omits several changes in the pull request, including warm shell resume, jitt…Update the description using the repository template. Add explicit What Changed, Why, UI Changes, and Checklist sections. Document all current changes, including warm shell resume, jittered reconnect backoff, network-path wakeups, activity …
✅ Passed checks (3 passed)
Check nameStatusExplanation
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
Title check✅ PassedThe title clearly summarizes the mobile network probing and activity lease refresh changes. It is concise and relevant, even though it does not describe every runtime and server change.
Full details: Description check

Explanation

The description explains the main mobile issue, the intended fix, testing, limitations, and lack of UI changes. However, it omits several changes in the pull request, including warm shell resume, jittered backoff, RPC ping-timeout logging, and the 30-second Bun WebSocket idle timeout. It also conflicts with the changeset by stating that the Bun idle-timeout change was removed.

Resolution

Update the description using the repository template. Add explicit What Changed, Why, UI Changes, and Checklist sections. Document all current changes, including warm shell resume, jittered reconnect backoff, network-path wakeups, activity reporting, RPC ping-timeout logging, and the 30-second Bun WebSocket idle timeout. Resolve the conflicting statements about retry delays and the Bun idle timeout.

  • Fix all pre-merge checks with AI

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

@github-actionsgithub-actionsBot 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

@cursorcursorBot 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 threadpackages/client-runtime/src/state/shell.ts
Comment threadpackages/client-runtime/src/connection/supervisor.ts
Comment threadapps/mobile/src/connection/platform.ts
@macroscopeapp

macroscopeappBot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This change alters production connection behavior across mobile lifecycle handling, the shared supervisor, and activity leases: network transitions can trigger probes, reconnects, and additional reports. Although the behavior is covered by focused tests, the cross-component runtime impact merits human review.

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

Comment threadapps/mobile/src/connection/background-activity.ts Outdated
Comment threadapps/mobile/src/connection/background-activity.ts Outdated

@coderabbitaicoderabbitaiBot 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

@t3dotgg
t3dotggforce-pushed the t3code/mobile-reconnect-hardening branch 2 times, most recently from 9193ab0 to 8ab3326CompareAugust 7, 2026 09:45
@t3dotggt3dotgg changed the title fix(mobile): reconnect faster and stop resyncing more than needed after backgroundingfix(mobile): probe on network path changes, restore server leases on reconnectAug 7, 2026
@github-actions

github-actionsBot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.4 KiB13.2 KiB−223 B (−1.6%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB−14 B (−0.2%)7.3 KiB
CodexLive turn WebSocket wire6.5 KiB6.3 KiB−209 B (−3.1%)7.8 KiB
CodexLive turn WebSocket decoded57.0 KiB55.5 KiB−1.5 KiB (−2.6%)66.4 KiB
CodexLive turn messages108−2 (−20.0%)21
ClaudeTotal thread wire13.5 KiB13.3 KiB−182 B (−1.3%)15.1 KiB
ClaudeThread snapshot wire6.9 KiB6.9 KiB−9 B (−0.1%)7.3 KiB
ClaudeLive turn WebSocket wire6.6 KiB6.4 KiB−173 B (−2.6%)7.8 KiB
ClaudeLive turn WebSocket decoded57.8 KiB56.4 KiB−1.4 KiB (−2.5%)66.4 KiB
ClaudeLive turn messages10100 (0.0%)21

Baseline: ea71a19 · PR result: 1d5093b · 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: 109.4 KiB
  • Claude decoded thread snapshot: 110.1 KiB

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

@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-reconnect-hardening branch from 8ab3326 to 68b85b9CompareAugust 10, 2026 10:58
Comment threadapps/mobile/src/connection/platform.ts Outdated

@macroscopeappmacroscopeappBot 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.

One change-discipline finding: documentation lost while moving the WebSocket options out of server.ts. Everything else in the changed Effect service code (supervisor wakeup handling, wakeups.ts union, mobile platform layer, rpc/session.ts hooks) follows the conventions.

Posted via Macroscope — Effect Service Conventions

Comment threadapps/server/src/httpServerOptions.ts Outdated
t3dotggand others added 4 commits September 1, 2026 17:46
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>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Keep advisory interface probes and activity reports after reconnect. Leave server socket idle timeouts unchanged. Test report recovery and the short probe timeout.
@t3dotgg
t3dotggforce-pushed the t3code/mobile-reconnect-hardening branch from eff1bf7 to 1d5093bCompareSeptember 2, 2026 00:47
@t3dotggt3dotgg changed the title fix(mobile): probe on network path changes, restore server leases on reconnectfix(mobile): probe network changes and refresh activity leasesSep 2, 2026
@t3dotggt3dotgg closed this Sep 2, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@t3dotgg@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(mobile): probe network changes and refresh activity leases - #5154

Closed
t3dotgg wants to merge 4 commits into
mainfrom
t3code/mobile-reconnect-hardening
Closed

fix(mobile): probe network changes and refresh activity leases#5154
t3dotgg wants to merge 4 commits into
mainfrom
t3code/mobile-reconnect-hardening

Conversation

@t3dotgg

@t3dotggt3dotgg commented Jul 31, 2026

Copy link
Copy Markdown
Member

Mobile can miss Wi-Fi/cellular changes and leave activity leases stale after reconnecting.

Probe the current session when the active network interface changes. Keep retry delays unchanged, and report current activity after each newly connected session generation. Keep the current session owner and credential handling. The earlier Bun-wide idle-timeout change is removed.

Verified on current main with 77 focused tests, mobile and client-runtime typechecks, targeted lint, and formatting. The new activity test waits for typed debounce and report receipts.

Device background/resume, real network switching, and live relay/tunnel behavior were not tested. No UI changes.

Original work by Theo Browne and Julius Marminge with Claude Code and Codex. Original commit authors and co-authors are preserved. Updated by GPT-5.6 Sol in Codex.


Note

Medium Risk
Touches connection supervisor wakeup and probe/reconnect logic plus mobile lifecycle networking; mistakes could cause reconnect storms or stale leases, but behavior is heavily tested and path-change wakeups are explicitly non-disruptive during backoff.

Overview
Mobile now detects Wi‑Fi/cellular interface changes (even when isConnected stays true) and emits a network-path-changed wakeup while the app is active and online. The shared supervisor probes the current session with the 3s mobile probe timeout; a failed or timed-out probe reconnects immediately without shell resubscription. During backoff, path-change wakeups are ignored so flapping interfaces do not shorten retry delays.

The mobile background activity reporter triggers another debounced serverReportClientActivity when an environment reaches a new connected session generation, so activity leases are restored after reconnect if an earlier report had no RPC session.

Supporting changes: pure network-path-change helpers and unit tests, ping timeout info logging on RPC sessions, supervisor/wakeup tests (including a queue-based test harness), shell-sync test tightening, and user/internal docs for mobile network behavior.

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

Note

Probe connected sessions on mobile network changes and refresh activity leases on reconnect

  • Adds a network-path-changed advisory wakeup that fires when the mobile app's known network interface changes while online and active, prompting a liveness probe of the current session with a mobile timeout
  • The supervisor consumes network-path-changed without interrupting connection establishment or shortening retry backoff; a failed probe triggers an immediate reconnect through the existing wake-probe path
  • Adds a NetworkPathBaseline model in network-path-change.ts that records the first known interface asynchronously and requests probes only on genuine interface transitions, not repeated or unknown observations
  • Updates the mobile activity reporter in background-activity.ts to queue a debounced activity report when an environment reaches a newly connected supervisor generation, deduplicating per generation
  • Behavioral Change: ConnectionWakeup gains a new network-path-changed variant; any out-of-tree consumer pattern-matching on that type without a catch-all will fail to compile
📊 Macroscope summarized 1d5093b. 8 files reviewed, 1 issue evaluated, 1 issue filtered, 0 comments posted

🗂️ Filtered Issues

packages/client-runtime/src/connection/supervisor.ts — 0 comments posted, 1 evaluated, 1 filtered
  • line 424: Adding network-path-changed to the probe branch also routes a failed path-change probe through wakeProbeFailed. After the failed probe tears down the lease, the run loop resets the retry ladder and starts the next connection immediately, so a network-change failure skips the normal first backoff delay despite waitForRetrySignal explicitly treating these wakeups as advisory. This can cause reconnect churn on a flapping interface. [ Out of scope (triage) ]

@coderabbitai

coderabbitaiBot commented Jul 31, 2026

Copy link
Copy Markdown
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring 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.
Description check⚠️ WarningThe description explains the main mobile issue, the intended fix, testing, limitations, and lack of UI changes. However, it omits several changes in the pull request, including warm shell resume, jitt…Update the description using the repository template. Add explicit What Changed, Why, UI Changes, and Checklist sections. Document all current changes, including warm shell resume, jittered reconnect backoff, network-path wakeups, activity …
✅ Passed checks (3 passed)
Check nameStatusExplanation
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
Title check✅ PassedThe title clearly summarizes the mobile network probing and activity lease refresh changes. It is concise and relevant, even though it does not describe every runtime and server change.
Full details: Description check

Explanation

The description explains the main mobile issue, the intended fix, testing, limitations, and lack of UI changes. However, it omits several changes in the pull request, including warm shell resume, jittered backoff, RPC ping-timeout logging, and the 30-second Bun WebSocket idle timeout. It also conflicts with the changeset by stating that the Bun idle-timeout change was removed.

Resolution

Update the description using the repository template. Add explicit What Changed, Why, UI Changes, and Checklist sections. Document all current changes, including warm shell resume, jittered reconnect backoff, network-path wakeups, activity reporting, RPC ping-timeout logging, and the 30-second Bun WebSocket idle timeout. Resolve the conflicting statements about retry delays and the Bun idle timeout.

  • Fix all pre-merge checks with AI

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

@github-actionsgithub-actionsBot 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

@cursorcursorBot 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 threadpackages/client-runtime/src/state/shell.ts
Comment threadpackages/client-runtime/src/connection/supervisor.ts
Comment threadapps/mobile/src/connection/platform.ts
@macroscopeapp

macroscopeappBot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This change alters production connection behavior across mobile lifecycle handling, the shared supervisor, and activity leases: network transitions can trigger probes, reconnects, and additional reports. Although the behavior is covered by focused tests, the cross-component runtime impact merits human review.

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

Comment threadapps/mobile/src/connection/background-activity.ts Outdated
Comment threadapps/mobile/src/connection/background-activity.ts Outdated

@coderabbitaicoderabbitaiBot 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

@t3dotgg
t3dotggforce-pushed the t3code/mobile-reconnect-hardening branch 2 times, most recently from 9193ab0 to 8ab3326CompareAugust 7, 2026 09:45
@t3dotggt3dotgg changed the title fix(mobile): reconnect faster and stop resyncing more than needed after backgroundingfix(mobile): probe on network path changes, restore server leases on reconnectAug 7, 2026
@github-actions

github-actionsBot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.4 KiB13.2 KiB−223 B (−1.6%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB−14 B (−0.2%)7.3 KiB
CodexLive turn WebSocket wire6.5 KiB6.3 KiB−209 B (−3.1%)7.8 KiB
CodexLive turn WebSocket decoded57.0 KiB55.5 KiB−1.5 KiB (−2.6%)66.4 KiB
CodexLive turn messages108−2 (−20.0%)21
ClaudeTotal thread wire13.5 KiB13.3 KiB−182 B (−1.3%)15.1 KiB
ClaudeThread snapshot wire6.9 KiB6.9 KiB−9 B (−0.1%)7.3 KiB
ClaudeLive turn WebSocket wire6.6 KiB6.4 KiB−173 B (−2.6%)7.8 KiB
ClaudeLive turn WebSocket decoded57.8 KiB56.4 KiB−1.4 KiB (−2.5%)66.4 KiB
ClaudeLive turn messages10100 (0.0%)21

Baseline: ea71a19 · PR result: 1d5093b · 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: 109.4 KiB
  • Claude decoded thread snapshot: 110.1 KiB

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

@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-reconnect-hardening branch from 8ab3326 to 68b85b9CompareAugust 10, 2026 10:58
Comment threadapps/mobile/src/connection/platform.ts Outdated

@macroscopeappmacroscopeappBot 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.

One change-discipline finding: documentation lost while moving the WebSocket options out of server.ts. Everything else in the changed Effect service code (supervisor wakeup handling, wakeups.ts union, mobile platform layer, rpc/session.ts hooks) follows the conventions.

Posted via Macroscope — Effect Service Conventions

Comment threadapps/server/src/httpServerOptions.ts Outdated
t3dotggand others added 4 commits September 1, 2026 17:46
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>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Keep advisory interface probes and activity reports after reconnect. Leave server socket idle timeouts unchanged. Test report recovery and the short probe timeout.
@t3dotgg
t3dotggforce-pushed the t3code/mobile-reconnect-hardening branch from eff1bf7 to 1d5093bCompareSeptember 2, 2026 00:47
@t3dotggt3dotgg changed the title fix(mobile): probe on network path changes, restore server leases on reconnectfix(mobile): probe network changes and refresh activity leasesSep 2, 2026
@t3dotggt3dotgg closed this Sep 2, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@t3dotgg@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(mobile): probe network changes and refresh activity leases - #5154

Closed
t3dotgg wants to merge 4 commits into
mainfrom
t3code/mobile-reconnect-hardening
Closed

fix(mobile): probe network changes and refresh activity leases#5154
t3dotgg wants to merge 4 commits into
mainfrom
t3code/mobile-reconnect-hardening

Conversation

@t3dotgg

@t3dotggt3dotgg commented Jul 31, 2026

Copy link
Copy Markdown
Member

Mobile can miss Wi-Fi/cellular changes and leave activity leases stale after reconnecting.

Probe the current session when the active network interface changes. Keep retry delays unchanged, and report current activity after each newly connected session generation. Keep the current session owner and credential handling. The earlier Bun-wide idle-timeout change is removed.

Verified on current main with 77 focused tests, mobile and client-runtime typechecks, targeted lint, and formatting. The new activity test waits for typed debounce and report receipts.

Device background/resume, real network switching, and live relay/tunnel behavior were not tested. No UI changes.

Original work by Theo Browne and Julius Marminge with Claude Code and Codex. Original commit authors and co-authors are preserved. Updated by GPT-5.6 Sol in Codex.


Note

Medium Risk
Touches connection supervisor wakeup and probe/reconnect logic plus mobile lifecycle networking; mistakes could cause reconnect storms or stale leases, but behavior is heavily tested and path-change wakeups are explicitly non-disruptive during backoff.

Overview
Mobile now detects Wi‑Fi/cellular interface changes (even when isConnected stays true) and emits a network-path-changed wakeup while the app is active and online. The shared supervisor probes the current session with the 3s mobile probe timeout; a failed or timed-out probe reconnects immediately without shell resubscription. During backoff, path-change wakeups are ignored so flapping interfaces do not shorten retry delays.

The mobile background activity reporter triggers another debounced serverReportClientActivity when an environment reaches a new connected session generation, so activity leases are restored after reconnect if an earlier report had no RPC session.

Supporting changes: pure network-path-change helpers and unit tests, ping timeout info logging on RPC sessions, supervisor/wakeup tests (including a queue-based test harness), shell-sync test tightening, and user/internal docs for mobile network behavior.

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

Note

Probe connected sessions on mobile network changes and refresh activity leases on reconnect

  • Adds a network-path-changed advisory wakeup that fires when the mobile app's known network interface changes while online and active, prompting a liveness probe of the current session with a mobile timeout
  • The supervisor consumes network-path-changed without interrupting connection establishment or shortening retry backoff; a failed probe triggers an immediate reconnect through the existing wake-probe path
  • Adds a NetworkPathBaseline model in network-path-change.ts that records the first known interface asynchronously and requests probes only on genuine interface transitions, not repeated or unknown observations
  • Updates the mobile activity reporter in background-activity.ts to queue a debounced activity report when an environment reaches a newly connected supervisor generation, deduplicating per generation
  • Behavioral Change: ConnectionWakeup gains a new network-path-changed variant; any out-of-tree consumer pattern-matching on that type without a catch-all will fail to compile
📊 Macroscope summarized 1d5093b. 8 files reviewed, 1 issue evaluated, 1 issue filtered, 0 comments posted

🗂️ Filtered Issues

packages/client-runtime/src/connection/supervisor.ts — 0 comments posted, 1 evaluated, 1 filtered
  • line 424: Adding network-path-changed to the probe branch also routes a failed path-change probe through wakeProbeFailed. After the failed probe tears down the lease, the run loop resets the retry ladder and starts the next connection immediately, so a network-change failure skips the normal first backoff delay despite waitForRetrySignal explicitly treating these wakeups as advisory. This can cause reconnect churn on a flapping interface. [ Out of scope (triage) ]

@coderabbitai

coderabbitaiBot commented Jul 31, 2026

Copy link
Copy Markdown
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring 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.
Description check⚠️ WarningThe description explains the main mobile issue, the intended fix, testing, limitations, and lack of UI changes. However, it omits several changes in the pull request, including warm shell resume, jitt…Update the description using the repository template. Add explicit What Changed, Why, UI Changes, and Checklist sections. Document all current changes, including warm shell resume, jittered reconnect backoff, network-path wakeups, activity …
✅ Passed checks (3 passed)
Check nameStatusExplanation
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
Title check✅ PassedThe title clearly summarizes the mobile network probing and activity lease refresh changes. It is concise and relevant, even though it does not describe every runtime and server change.
Full details: Description check

Explanation

The description explains the main mobile issue, the intended fix, testing, limitations, and lack of UI changes. However, it omits several changes in the pull request, including warm shell resume, jittered backoff, RPC ping-timeout logging, and the 30-second Bun WebSocket idle timeout. It also conflicts with the changeset by stating that the Bun idle-timeout change was removed.

Resolution

Update the description using the repository template. Add explicit What Changed, Why, UI Changes, and Checklist sections. Document all current changes, including warm shell resume, jittered reconnect backoff, network-path wakeups, activity reporting, RPC ping-timeout logging, and the 30-second Bun WebSocket idle timeout. Resolve the conflicting statements about retry delays and the Bun idle timeout.

  • Fix all pre-merge checks with AI

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

@github-actionsgithub-actionsBot 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

@cursorcursorBot 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 threadpackages/client-runtime/src/state/shell.ts
Comment threadpackages/client-runtime/src/connection/supervisor.ts
Comment threadapps/mobile/src/connection/platform.ts
@macroscopeapp

macroscopeappBot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This change alters production connection behavior across mobile lifecycle handling, the shared supervisor, and activity leases: network transitions can trigger probes, reconnects, and additional reports. Although the behavior is covered by focused tests, the cross-component runtime impact merits human review.

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

Comment threadapps/mobile/src/connection/background-activity.ts Outdated
Comment threadapps/mobile/src/connection/background-activity.ts Outdated

@coderabbitaicoderabbitaiBot 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

@t3dotgg
t3dotggforce-pushed the t3code/mobile-reconnect-hardening branch 2 times, most recently from 9193ab0 to 8ab3326CompareAugust 7, 2026 09:45
@t3dotggt3dotgg changed the title fix(mobile): reconnect faster and stop resyncing more than needed after backgroundingfix(mobile): probe on network path changes, restore server leases on reconnectAug 7, 2026
@github-actions

github-actionsBot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.4 KiB13.2 KiB−223 B (−1.6%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB−14 B (−0.2%)7.3 KiB
CodexLive turn WebSocket wire6.5 KiB6.3 KiB−209 B (−3.1%)7.8 KiB
CodexLive turn WebSocket decoded57.0 KiB55.5 KiB−1.5 KiB (−2.6%)66.4 KiB
CodexLive turn messages108−2 (−20.0%)21
ClaudeTotal thread wire13.5 KiB13.3 KiB−182 B (−1.3%)15.1 KiB
ClaudeThread snapshot wire6.9 KiB6.9 KiB−9 B (−0.1%)7.3 KiB
ClaudeLive turn WebSocket wire6.6 KiB6.4 KiB−173 B (−2.6%)7.8 KiB
ClaudeLive turn WebSocket decoded57.8 KiB56.4 KiB−1.4 KiB (−2.5%)66.4 KiB
ClaudeLive turn messages10100 (0.0%)21

Baseline: ea71a19 · PR result: 1d5093b · 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: 109.4 KiB
  • Claude decoded thread snapshot: 110.1 KiB

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

@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-reconnect-hardening branch from 8ab3326 to 68b85b9CompareAugust 10, 2026 10:58
Comment threadapps/mobile/src/connection/platform.ts Outdated

@macroscopeappmacroscopeappBot 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.

One change-discipline finding: documentation lost while moving the WebSocket options out of server.ts. Everything else in the changed Effect service code (supervisor wakeup handling, wakeups.ts union, mobile platform layer, rpc/session.ts hooks) follows the conventions.

Posted via Macroscope — Effect Service Conventions

Comment threadapps/server/src/httpServerOptions.ts Outdated
t3dotggand others added 4 commits September 1, 2026 17:46
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>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Keep advisory interface probes and activity reports after reconnect. Leave server socket idle timeouts unchanged. Test report recovery and the short probe timeout.
@t3dotgg
t3dotggforce-pushed the t3code/mobile-reconnect-hardening branch from eff1bf7 to 1d5093bCompareSeptember 2, 2026 00:47
@t3dotggt3dotgg changed the title fix(mobile): probe on network path changes, restore server leases on reconnectfix(mobile): probe network changes and refresh activity leasesSep 2, 2026
@t3dotggt3dotgg closed this Sep 2, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@t3dotgg@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(mobile): probe network changes and refresh activity leases - #5154

Closed
t3dotgg wants to merge 4 commits into
mainfrom
t3code/mobile-reconnect-hardening
Closed

fix(mobile): probe network changes and refresh activity leases#5154
t3dotgg wants to merge 4 commits into
mainfrom
t3code/mobile-reconnect-hardening

Conversation

@t3dotgg

@t3dotggt3dotgg commented Jul 31, 2026

Copy link
Copy Markdown
Member

Mobile can miss Wi-Fi/cellular changes and leave activity leases stale after reconnecting.

Probe the current session when the active network interface changes. Keep retry delays unchanged, and report current activity after each newly connected session generation. Keep the current session owner and credential handling. The earlier Bun-wide idle-timeout change is removed.

Verified on current main with 77 focused tests, mobile and client-runtime typechecks, targeted lint, and formatting. The new activity test waits for typed debounce and report receipts.

Device background/resume, real network switching, and live relay/tunnel behavior were not tested. No UI changes.

Original work by Theo Browne and Julius Marminge with Claude Code and Codex. Original commit authors and co-authors are preserved. Updated by GPT-5.6 Sol in Codex.


Note

Medium Risk
Touches connection supervisor wakeup and probe/reconnect logic plus mobile lifecycle networking; mistakes could cause reconnect storms or stale leases, but behavior is heavily tested and path-change wakeups are explicitly non-disruptive during backoff.

Overview
Mobile now detects Wi‑Fi/cellular interface changes (even when isConnected stays true) and emits a network-path-changed wakeup while the app is active and online. The shared supervisor probes the current session with the 3s mobile probe timeout; a failed or timed-out probe reconnects immediately without shell resubscription. During backoff, path-change wakeups are ignored so flapping interfaces do not shorten retry delays.

The mobile background activity reporter triggers another debounced serverReportClientActivity when an environment reaches a new connected session generation, so activity leases are restored after reconnect if an earlier report had no RPC session.

Supporting changes: pure network-path-change helpers and unit tests, ping timeout info logging on RPC sessions, supervisor/wakeup tests (including a queue-based test harness), shell-sync test tightening, and user/internal docs for mobile network behavior.

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

Note

Probe connected sessions on mobile network changes and refresh activity leases on reconnect

  • Adds a network-path-changed advisory wakeup that fires when the mobile app's known network interface changes while online and active, prompting a liveness probe of the current session with a mobile timeout
  • The supervisor consumes network-path-changed without interrupting connection establishment or shortening retry backoff; a failed probe triggers an immediate reconnect through the existing wake-probe path
  • Adds a NetworkPathBaseline model in network-path-change.ts that records the first known interface asynchronously and requests probes only on genuine interface transitions, not repeated or unknown observations
  • Updates the mobile activity reporter in background-activity.ts to queue a debounced activity report when an environment reaches a newly connected supervisor generation, deduplicating per generation
  • Behavioral Change: ConnectionWakeup gains a new network-path-changed variant; any out-of-tree consumer pattern-matching on that type without a catch-all will fail to compile
📊 Macroscope summarized 1d5093b. 8 files reviewed, 1 issue evaluated, 1 issue filtered, 0 comments posted

🗂️ Filtered Issues

packages/client-runtime/src/connection/supervisor.ts — 0 comments posted, 1 evaluated, 1 filtered
  • line 424: Adding network-path-changed to the probe branch also routes a failed path-change probe through wakeProbeFailed. After the failed probe tears down the lease, the run loop resets the retry ladder and starts the next connection immediately, so a network-change failure skips the normal first backoff delay despite waitForRetrySignal explicitly treating these wakeups as advisory. This can cause reconnect churn on a flapping interface. [ Out of scope (triage) ]

@coderabbitai

coderabbitaiBot commented Jul 31, 2026

Copy link
Copy Markdown
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring 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.
Description check⚠️ WarningThe description explains the main mobile issue, the intended fix, testing, limitations, and lack of UI changes. However, it omits several changes in the pull request, including warm shell resume, jitt…Update the description using the repository template. Add explicit What Changed, Why, UI Changes, and Checklist sections. Document all current changes, including warm shell resume, jittered reconnect backoff, network-path wakeups, activity …
✅ Passed checks (3 passed)
Check nameStatusExplanation
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
Title check✅ PassedThe title clearly summarizes the mobile network probing and activity lease refresh changes. It is concise and relevant, even though it does not describe every runtime and server change.
Full details: Description check

Explanation

The description explains the main mobile issue, the intended fix, testing, limitations, and lack of UI changes. However, it omits several changes in the pull request, including warm shell resume, jittered backoff, RPC ping-timeout logging, and the 30-second Bun WebSocket idle timeout. It also conflicts with the changeset by stating that the Bun idle-timeout change was removed.

Resolution

Update the description using the repository template. Add explicit What Changed, Why, UI Changes, and Checklist sections. Document all current changes, including warm shell resume, jittered reconnect backoff, network-path wakeups, activity reporting, RPC ping-timeout logging, and the 30-second Bun WebSocket idle timeout. Resolve the conflicting statements about retry delays and the Bun idle timeout.

  • Fix all pre-merge checks with AI

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

@github-actionsgithub-actionsBot 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

@cursorcursorBot 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 threadpackages/client-runtime/src/state/shell.ts
Comment threadpackages/client-runtime/src/connection/supervisor.ts
Comment threadapps/mobile/src/connection/platform.ts
@macroscopeapp

macroscopeappBot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This change alters production connection behavior across mobile lifecycle handling, the shared supervisor, and activity leases: network transitions can trigger probes, reconnects, and additional reports. Although the behavior is covered by focused tests, the cross-component runtime impact merits human review.

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

Comment threadapps/mobile/src/connection/background-activity.ts Outdated
Comment threadapps/mobile/src/connection/background-activity.ts Outdated

@coderabbitaicoderabbitaiBot 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

@t3dotgg
t3dotggforce-pushed the t3code/mobile-reconnect-hardening branch 2 times, most recently from 9193ab0 to 8ab3326CompareAugust 7, 2026 09:45
@t3dotggt3dotgg changed the title fix(mobile): reconnect faster and stop resyncing more than needed after backgroundingfix(mobile): probe on network path changes, restore server leases on reconnectAug 7, 2026
@github-actions

github-actionsBot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.4 KiB13.2 KiB−223 B (−1.6%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB−14 B (−0.2%)7.3 KiB
CodexLive turn WebSocket wire6.5 KiB6.3 KiB−209 B (−3.1%)7.8 KiB
CodexLive turn WebSocket decoded57.0 KiB55.5 KiB−1.5 KiB (−2.6%)66.4 KiB
CodexLive turn messages108−2 (−20.0%)21
ClaudeTotal thread wire13.5 KiB13.3 KiB−182 B (−1.3%)15.1 KiB
ClaudeThread snapshot wire6.9 KiB6.9 KiB−9 B (−0.1%)7.3 KiB
ClaudeLive turn WebSocket wire6.6 KiB6.4 KiB−173 B (−2.6%)7.8 KiB
ClaudeLive turn WebSocket decoded57.8 KiB56.4 KiB−1.4 KiB (−2.5%)66.4 KiB
ClaudeLive turn messages10100 (0.0%)21

Baseline: ea71a19 · PR result: 1d5093b · 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: 109.4 KiB
  • Claude decoded thread snapshot: 110.1 KiB

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

@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-reconnect-hardening branch from 8ab3326 to 68b85b9CompareAugust 10, 2026 10:58
Comment threadapps/mobile/src/connection/platform.ts Outdated

@macroscopeappmacroscopeappBot 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.

One change-discipline finding: documentation lost while moving the WebSocket options out of server.ts. Everything else in the changed Effect service code (supervisor wakeup handling, wakeups.ts union, mobile platform layer, rpc/session.ts hooks) follows the conventions.

Posted via Macroscope — Effect Service Conventions

Comment threadapps/server/src/httpServerOptions.ts Outdated
t3dotggand others added 4 commits September 1, 2026 17:46
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>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Keep advisory interface probes and activity reports after reconnect. Leave server socket idle timeouts unchanged. Test report recovery and the short probe timeout.
@t3dotgg
t3dotggforce-pushed the t3code/mobile-reconnect-hardening branch from eff1bf7 to 1d5093bCompareSeptember 2, 2026 00:47
@t3dotggt3dotgg changed the title fix(mobile): probe on network path changes, restore server leases on reconnectfix(mobile): probe network changes and refresh activity leasesSep 2, 2026
@t3dotggt3dotgg closed this Sep 2, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@t3dotgg@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(mobile): probe network changes and refresh activity leases - #5154

Closed
t3dotgg wants to merge 4 commits into
mainfrom
t3code/mobile-reconnect-hardening
Closed

fix(mobile): probe network changes and refresh activity leases#5154
t3dotgg wants to merge 4 commits into
mainfrom
t3code/mobile-reconnect-hardening

Conversation

@t3dotgg

@t3dotggt3dotgg commented Jul 31, 2026

Copy link
Copy Markdown
Member

Mobile can miss Wi-Fi/cellular changes and leave activity leases stale after reconnecting.

Probe the current session when the active network interface changes. Keep retry delays unchanged, and report current activity after each newly connected session generation. Keep the current session owner and credential handling. The earlier Bun-wide idle-timeout change is removed.

Verified on current main with 77 focused tests, mobile and client-runtime typechecks, targeted lint, and formatting. The new activity test waits for typed debounce and report receipts.

Device background/resume, real network switching, and live relay/tunnel behavior were not tested. No UI changes.

Original work by Theo Browne and Julius Marminge with Claude Code and Codex. Original commit authors and co-authors are preserved. Updated by GPT-5.6 Sol in Codex.


Note

Medium Risk
Touches connection supervisor wakeup and probe/reconnect logic plus mobile lifecycle networking; mistakes could cause reconnect storms or stale leases, but behavior is heavily tested and path-change wakeups are explicitly non-disruptive during backoff.

Overview
Mobile now detects Wi‑Fi/cellular interface changes (even when isConnected stays true) and emits a network-path-changed wakeup while the app is active and online. The shared supervisor probes the current session with the 3s mobile probe timeout; a failed or timed-out probe reconnects immediately without shell resubscription. During backoff, path-change wakeups are ignored so flapping interfaces do not shorten retry delays.

The mobile background activity reporter triggers another debounced serverReportClientActivity when an environment reaches a new connected session generation, so activity leases are restored after reconnect if an earlier report had no RPC session.

Supporting changes: pure network-path-change helpers and unit tests, ping timeout info logging on RPC sessions, supervisor/wakeup tests (including a queue-based test harness), shell-sync test tightening, and user/internal docs for mobile network behavior.

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

Note

Probe connected sessions on mobile network changes and refresh activity leases on reconnect

  • Adds a network-path-changed advisory wakeup that fires when the mobile app's known network interface changes while online and active, prompting a liveness probe of the current session with a mobile timeout
  • The supervisor consumes network-path-changed without interrupting connection establishment or shortening retry backoff; a failed probe triggers an immediate reconnect through the existing wake-probe path
  • Adds a NetworkPathBaseline model in network-path-change.ts that records the first known interface asynchronously and requests probes only on genuine interface transitions, not repeated or unknown observations
  • Updates the mobile activity reporter in background-activity.ts to queue a debounced activity report when an environment reaches a newly connected supervisor generation, deduplicating per generation
  • Behavioral Change: ConnectionWakeup gains a new network-path-changed variant; any out-of-tree consumer pattern-matching on that type without a catch-all will fail to compile
📊 Macroscope summarized 1d5093b. 8 files reviewed, 1 issue evaluated, 1 issue filtered, 0 comments posted

🗂️ Filtered Issues

packages/client-runtime/src/connection/supervisor.ts — 0 comments posted, 1 evaluated, 1 filtered
  • line 424: Adding network-path-changed to the probe branch also routes a failed path-change probe through wakeProbeFailed. After the failed probe tears down the lease, the run loop resets the retry ladder and starts the next connection immediately, so a network-change failure skips the normal first backoff delay despite waitForRetrySignal explicitly treating these wakeups as advisory. This can cause reconnect churn on a flapping interface. [ Out of scope (triage) ]

@coderabbitai

coderabbitaiBot commented Jul 31, 2026

Copy link
Copy Markdown
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring 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.
Description check⚠️ WarningThe description explains the main mobile issue, the intended fix, testing, limitations, and lack of UI changes. However, it omits several changes in the pull request, including warm shell resume, jitt…Update the description using the repository template. Add explicit What Changed, Why, UI Changes, and Checklist sections. Document all current changes, including warm shell resume, jittered reconnect backoff, network-path wakeups, activity …
✅ Passed checks (3 passed)
Check nameStatusExplanation
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
Title check✅ PassedThe title clearly summarizes the mobile network probing and activity lease refresh changes. It is concise and relevant, even though it does not describe every runtime and server change.
Full details: Description check

Explanation

The description explains the main mobile issue, the intended fix, testing, limitations, and lack of UI changes. However, it omits several changes in the pull request, including warm shell resume, jittered backoff, RPC ping-timeout logging, and the 30-second Bun WebSocket idle timeout. It also conflicts with the changeset by stating that the Bun idle-timeout change was removed.

Resolution

Update the description using the repository template. Add explicit What Changed, Why, UI Changes, and Checklist sections. Document all current changes, including warm shell resume, jittered reconnect backoff, network-path wakeups, activity reporting, RPC ping-timeout logging, and the 30-second Bun WebSocket idle timeout. Resolve the conflicting statements about retry delays and the Bun idle timeout.

  • Fix all pre-merge checks with AI

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

@github-actionsgithub-actionsBot 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

@cursorcursorBot 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 threadpackages/client-runtime/src/state/shell.ts
Comment threadpackages/client-runtime/src/connection/supervisor.ts
Comment threadapps/mobile/src/connection/platform.ts
@macroscopeapp

macroscopeappBot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This change alters production connection behavior across mobile lifecycle handling, the shared supervisor, and activity leases: network transitions can trigger probes, reconnects, and additional reports. Although the behavior is covered by focused tests, the cross-component runtime impact merits human review.

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

Comment threadapps/mobile/src/connection/background-activity.ts Outdated
Comment threadapps/mobile/src/connection/background-activity.ts Outdated

@coderabbitaicoderabbitaiBot 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

@t3dotgg
t3dotggforce-pushed the t3code/mobile-reconnect-hardening branch 2 times, most recently from 9193ab0 to 8ab3326CompareAugust 7, 2026 09:45
@t3dotggt3dotgg changed the title fix(mobile): reconnect faster and stop resyncing more than needed after backgroundingfix(mobile): probe on network path changes, restore server leases on reconnectAug 7, 2026
@github-actions

github-actionsBot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.4 KiB13.2 KiB−223 B (−1.6%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB−14 B (−0.2%)7.3 KiB
CodexLive turn WebSocket wire6.5 KiB6.3 KiB−209 B (−3.1%)7.8 KiB
CodexLive turn WebSocket decoded57.0 KiB55.5 KiB−1.5 KiB (−2.6%)66.4 KiB
CodexLive turn messages108−2 (−20.0%)21
ClaudeTotal thread wire13.5 KiB13.3 KiB−182 B (−1.3%)15.1 KiB
ClaudeThread snapshot wire6.9 KiB6.9 KiB−9 B (−0.1%)7.3 KiB
ClaudeLive turn WebSocket wire6.6 KiB6.4 KiB−173 B (−2.6%)7.8 KiB
ClaudeLive turn WebSocket decoded57.8 KiB56.4 KiB−1.4 KiB (−2.5%)66.4 KiB
ClaudeLive turn messages10100 (0.0%)21

Baseline: ea71a19 · PR result: 1d5093b · 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: 109.4 KiB
  • Claude decoded thread snapshot: 110.1 KiB

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

@juliusmarminge
juliusmarmingeforce-pushed the t3code/mobile-reconnect-hardening branch from 8ab3326 to 68b85b9CompareAugust 10, 2026 10:58
Comment threadapps/mobile/src/connection/platform.ts Outdated

@macroscopeappmacroscopeappBot 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.

One change-discipline finding: documentation lost while moving the WebSocket options out of server.ts. Everything else in the changed Effect service code (supervisor wakeup handling, wakeups.ts union, mobile platform layer, rpc/session.ts hooks) follows the conventions.

Posted via Macroscope — Effect Service Conventions

Comment threadapps/server/src/httpServerOptions.ts Outdated
t3dotggand others added 4 commits September 1, 2026 17:46
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>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Keep advisory interface probes and activity reports after reconnect. Leave server socket idle timeouts unchanged. Test report recovery and the short probe timeout.
@t3dotgg
t3dotggforce-pushed the t3code/mobile-reconnect-hardening branch from eff1bf7 to 1d5093bCompareSeptember 2, 2026 00:47
@t3dotggt3dotgg changed the title fix(mobile): probe on network path changes, restore server leases on reconnectfix(mobile): probe network changes and refresh activity leasesSep 2, 2026
@t3dotggt3dotgg closed this Sep 2, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@t3dotgg@juliusmarminge