fix(desktop): keep probing backend readiness while the process is alive#5526

merged23
main
GitHub fork: fix/desktop-reprobe-readiness-while-alive
openedAug 6, 2026byLlgwacker
View on GitHubTwo-way sync enabledPR last imported 14d ago

What Changed

runBackendProcess (apps/desktop/src/backend/DesktopBackendManager.ts) now probes backend readiness in a loop while the child process is still alive, instead of probing exactly once with a fixed 60s budget and then never retrying.

Each round gets a fresh readiness budget; on timeout, onReadinessFailure fires and the loop immediately starts another round; the loop stops on success or is torn down with the run scope when the child exits. Behavior is unchanged when the backend becomes ready within the first budget.

Why

The readiness probe was one-shot: after the 60s budget expired, the app never probed again even though the backend process kept running and became healthy ~20s later. A slow WSL cold boot (server bundle loading across /mnt/c, or a first launch right after an update — cold Windows file cache + Defender re-scan) routinely exceeds that budget, so wsl-only users were left stuck on the "Connecting to WSL…" splash forever with no retry and no recovery path. This is the failure documented in #4535 and filed as #5522. It makes the app resilient regardless of how long boot takes, without widening the budget or changing the fast-path.

UI Changes

None.

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • I included a video for animation/interaction changes

Fixes #5522


Note

Medium Risk Changes desktop startup and readiness callbacks (onReadinessFailure may fire repeatedly); limited to backend process lifecycle with new test coverage.

Overview runBackendProcess no longer treats HTTP readiness as a single attempt with one timeout budget. It loops waitForHttpReady while the backend child is still running: each round gets a fresh budget; on timeout it invokes onReadinessFailure and starts another round; on success it invokes onReady and stops. The loop runs in a scoped fork so it is cancelled when the process exits.

This fixes cases where a slow WSL cold boot outlasts the first budget but the process later becomes healthy—the app was stuck on “Connecting to WSL…” with no further probes.

Behavioral note: onReadinessFailure may run multiple times before onReady. The fast path (ready within the first budget) is unchanged.

A TestClock test asserts re-probing across expired budgets until HTTP returns 200.

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

Note

Re-probe backend HTTP readiness continuously while the process is alive

Previously, runBackendProcess gave up after the first readiness timeout. Now it loops readiness probing across multiple timeout budgets, calling onReadinessFailure after each expired budget and onReady once a probe succeeds. The loop is forked in the run scope so it stops automatically when the child process exits. Behavioral Change: onReadinessFailure may now be called multiple times before onReady is eventually invoked.

Macroscope summarized 441ed06.

Synced from GitHub

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: be66c833-ab42-437e-a7e3-1952f7688b0b

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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

❤️ Share

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

Synced from GitHub

Approvability

Verdict: Approved bc0c114

This is a focused bug fix that converts a one-shot backend readiness probe into a retry loop, preventing the app from getting stuck on 'Connecting to WSL...' when startup is slow. The change is self-contained with comprehensive test coverage.

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

MmacroscopeappBotreviewed · 1 inline threadAug 9, 2026
Synced from GitHub

One convention finding: the new readiness probe recovers a statically known tagged failure with Effect.result + Result.isSuccess instead of Effect.catchTags. See the inline comment.

Posted via Macroscope — Effect Service Conventions

apps/desktop/src/backend/DesktopBackendManager.tslines 575–590Outdated · 8345a13Open thread
……@@ -563,19 +564,32 @@ export const runBackendProcess = Effect.fn("runBackendProcess")(function* (
587+ }
588+ yield* options.onReadinessFailure?.(outcome.failure) ?? Effect.void;
589+ return false;
590+ });
MmacroscopeappBoton lines 575–590Aug 9, 2026

backend/DesktopBackendManager.ts:590

waitForHttpReady fails only with the known tagged BackendReadinessTimeoutError, so consider recovering it with Effect.catchTags (the convention here, even for a single tag — it is also what the code replaced by this hunk did) rather than converting the whole channel with Effect.result and branching on Result.isSuccess. The boolean the repeat loop needs can come from Effect.as on each branch, and the import * as Result from "effect/Result" added at the top then becomes unnecessary.

suggestion
1 const probeReadiness = Effect.fn("desktop.backendProcess.probeReadiness")(() =>
2 waitForHttpReady({
3 executablePath: options.executablePath,
4 entryPath: options.entryPath,
5 cwd: options.cwd,
6 httpBaseUrl: options.httpBaseUrl,
7 timeout: options.readinessTimeout ?? DEFAULT_BACKEND_READINESS_TIMEOUT,
8 }).pipe(
9 Effect.flatMap(() => options.onReady?.() ?? Effect.void),
10 Effect.as(true),
11 Effect.catchTags({
12 BackendReadinessTimeoutError: (error) =>
13 (options.onReadinessFailure?.(error) ?? Effect.void).pipe(Effect.as(false)),
14 }),
15 ),
16 );

No longer relevant as of bc0c114bcaa48dd75500c0a96d7be3c0d9b71f34

Applied in 3f8340487 (rebased as bc0c114bc): probeReadiness now recovers waitForHttpReady with Effect.catchTags and drives the repeat-loop boolean via Effect.as(true)/Effect.as(false); the Result import is gone. Typecheck, tests, lint, and fmt are green on the updated branch.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

Added 2 commits

  • fix(desktop): keep probing backend readiness while the process is aliveb8b299b
  • fix(desktop): use Effect.catchTags for readiness re-probe per conventionbc0c114

Added 3 commits

  • Merge branch 'main' into fix/desktop-reprobe-readiness-while-alive32c4c2f
  • Merge branch 'main' into fix/desktop-reprobe-readiness-while-alivecd53711
  • Merge branch 'main' into fix/desktop-reprobe-readiness-while-alive038a159
Synced from GitHub

Verified on Windows 11, Node 24: 25/25 pass on this branch. Reverting only DesktopBackendManager.ts to main while keeping the new test fails it with expected 1 to equal 2, so it does cover the regression rather than just the current behavior, and the other 24 still pass.

Confirmed the premise on main separately — waitForHttpReady is called once at DesktopBackendManager.ts:566 with a catchTag on the timeout and no retry.

This is unit-level only. I did not run the packaged app against WSL, so I can't speak to the end-to-end "Connecting to WSL…" case.

Added 1 commit

  • Merge branch 'main' into fix/desktop-reprobe-readiness-while-alive615e3fb
Synced from GitHub

Two of the open Windows issues look like they're already covered here, if it's useful to link them:

#5364 describes exactly this failure — backend alive, desktop headless. On main the probe is forked and its timeout is non-fatal, so the child survives, but onReady never fires, and window creation hangs off it (DesktopBackendPool.ts:292), so no BrowserWindow gets created. Looping while the child is alive is that issue's own suggested direction 3.

#4209 is partly stale against current main. Its headline symptom is the desktop stopping the backend and shutting down, and that no longer happens: onReadinessFailure (DesktopBackendManager.ts:930) only logs a warning and writes a failure snapshot, and nothing wires app.quit to readiness failure. What survives from that report is the headless state this PR fixes.

One thing it doesn't cover, in case it's worth splitting out: discovery still blocks the readiness endpoint. #4209 measured checkCursorProviderStatus at about 22.7s. This makes a slow boot survivable rather than fast, so #5364's suggested direction 1 — serving minimal readiness before expensive initialization — stays open.

I also re-ran the suite on Linux (Ubuntu 26.04 under WSL2, Node 24): 25/25 here against 24/24 on main. Still unit-level, so my earlier caveat stands — I haven't run the packaged app against a WSL backend. Electron's binary isn't installed in that environment, so I stubbed path.txt; the tests mock everything and never launch it.

Added 14 commits

  • Merge branch 'main' into fix/desktop-reprobe-readiness-while-alive405a47b
  • Merge branch 'main' into fix/desktop-reprobe-readiness-while-aliveff8998a
  • Merge branch 'main' into fix/desktop-reprobe-readiness-while-aliveb5111cb
  • Merge branch 'main' into fix/desktop-reprobe-readiness-while-alive0659aec
  • Merge branch 'main' into fix/desktop-reprobe-readiness-while-alivefa5367c
  • Merge branch 'main' into fix/desktop-reprobe-readiness-while-alive877bcd3
  • Merge branch 'main' into fix/desktop-reprobe-readiness-while-aliveaa7fe1f
  • Merge branch 'main' into fix/desktop-reprobe-readiness-while-alive23094b8
  • Merge branch 'main' into fix/desktop-reprobe-readiness-while-alive0c6a92f
  • Merge branch 'main' into fix/desktop-reprobe-readiness-while-alive3331bbb
  • Merge branch 'main' into fix/desktop-reprobe-readiness-while-alive1f4be53
  • Merge branch 'main' into fix/desktop-reprobe-readiness-while-aliveec399ea
  • Merge branch 'main' into fix/desktop-reprobe-readiness-while-alive0cf3131
  • Merge branch 'main' into fix/desktop-reprobe-readiness-while-alive7297860

Added 2 commits

  • Merge branch 'main' into fix/desktop-reprobe-readiness-while-alive0c20d78
  • Merge branch 'main' into fix/desktop-reprobe-readiness-while-alive21e03cd
Synced from GitHub

Independent verification on Linux (WSL2 Ubuntu, Node 24), since this has been sitting a while.

Branch as submitted:

1✓ src/backend/DesktopBackendManager.test.ts (25 tests)
2 Tests 25 passed (25)

Then swapping only DesktopBackendManager.ts for the current main copy and keeping this PR's test file, to check the new test actually pins the fixed behaviour rather than passing either way:

1× re-probes readiness after the first budget expires while the backend is still alive
2 AssertionError: expected 1 to equal 2
3
4 Tests 1 failed | 24 passed (25)

One targeted failure, the other 24 unaffected. That is the useful shape: the new test fails without the source change and passes with it, and it is not incidentally coupled to the rest of the file.

The branch is behind main but still reports MERGEABLE, and the source file applies cleanly, so the staleness looks cosmetic rather than a conflict risk.

No findings against the change itself from my side.

Added 1 commit

  • Merge branch 'main' into fix/desktop-reprobe-readiness-while-alive441ed06