Skip to content

fix(desktop): prevent WSL backend exiting with code 0 by keeping stdi… - #3613

Closed
jibin7jose wants to merge 26 commits into
pingdotgg:mainfrom
jibin7jose:fix-wsl-code-0-exit
Closed

fix(desktop): prevent WSL backend exiting with code 0 by keeping stdi…#3613
jibin7jose wants to merge 26 commits into
pingdotgg:mainfrom
jibin7jose:fix-wsl-code-0-exit

Conversation

@jibin7jose

@jibin7jose jibin7jose commented Jun 30, 2026

Copy link
Copy Markdown

What Changed

  • Keeps WSL stdin open: For stdin-delivery backends, concatenates Stream.never to the bootstrap stdin so it stays open indefinitely after sending the bootstrap JSON, preventing premature exit with code 0.
  • Caps never-ready retries: Tracks consecutive pre-readiness exits via a neverReadyAttempt counter. After 5 consecutive failures (MAX_PREFLIGHT_FAILURE_ATTEMPTS), invokes onPreflightFailed with a non-fatal reason to fall back (e.g., to Windows) instead of looping forever. The counter correctly resets upon reaching readiness.
  • Robust bootstrap error handling: Fixes readBootstrapEnvelope in bootstrap.ts to handle stream errors by registering the error listener on the readline.Interface. This avoids unhandled errors by properly mapping EBADF/ENOENT to no envelope and generic errors to BootstrapEnvelopeReadError, with consistent cleanup.
  • WSL node-pty validation: Validates the WSL node-pty probe against the required Node engine range, failing fast with a clear fatal error when the Node.js version is missing or incompatible.
  • Tests added: Includes focused tests in DesktopBackendManager.test.ts and bootstrap.test.ts to verify the new capped restart behavior, stream error handling paths, and proper resolution of TestClock deadlocks in the backend restart loops.

Why

Fixes #3611 where the WSL backend gets stuck in an infinite "Connecting to WSL" loop. Previously, the Windows-side pipe would close before the backend process completed reading it, causing the process to exit with code 0. Additionally, capping the retry attempts ensures that if the WSL backend genuinely fails to reach HTTP readiness, it will cleanly fall back rather than indefinitely attempting to restart, improving the overall reliability of the connection lifecycle.

UI Changes

This PR fixes backend connection reliability and infinite loading states under the hood. No direct UI changes.

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

Note

Medium Risk
Changes desktop backend lifecycle and WSL spawn/bootstrap paths that affect connection reliability and fallback behavior, though behavior is covered by new tests.

Overview
Fixes WSL backends exiting with code 0 and spinning forever on "Connecting to WSL" by keeping stdin open after the bootstrap JSON (Stream.never for stdin delivery) and tracking consecutive exits before HTTP readiness via neverReadyAttempt. After five failures, onPreflightFailed runs and the instance stops when the callback returns false; the counter resets on readiness, stop, or a fresh start.

onReady now ignores stale runs that already exited or were stopped, and readBootstrapEnvelope listens for errors on the readline interface with cleanup on all paths so EBADF/ENOENT map to no envelope without unhandled errors.

The WSL node probe only prints nodeVersion when Node exists and fails fast with a fatal message when the version is missing or outside nodeEngineRange. Tests cover the never-ready cap, readiness reset, bootstrap stream errors, and TestClock races in restart tests.

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

Note

Prevent WSL backend from exiting early by keeping stdin open and capping preflight failures

  • Fixes the WSL backend exiting with code 0 by appending Stream.never to the bootstrap input stream for stdin-delivery backends, keeping stdin open after the JSON is sent.
  • Adds a neverReadyAttempt counter in DesktopBackendManager.ts that tracks consecutive exits before HTTP readiness; after 5 failures, onPreflightFailed is invoked and the backend either stops or restarts based on the callback result.
  • Fixes readBootstrapEnvelope in bootstrap.ts to register the error listener on the readline.Interface so stream errors before the first line are caught, returning none for unavailable-fd errors and failing with BootstrapEnvelopeReadError for others.
  • Adds validation in DesktopWslEnvironment.ts to fail fast with a fatal error when the WSL Node.js version is missing or does not satisfy the required engine range.
  • Risk: stdin-delivery backends now have a persistent open stdin stream; backends that rely on stdin closing as a signal will be affected.

Macroscope summarized 4721a2a.

…n open

Fixes pingdotgg#3611 by appending Stream.never to the bootstrap stream so the Windows-side pipe does not close before the backend process completes reading it.
@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown

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: 1debe434-2012-4eec-b99d-e94b265aaf60

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
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

❤️ Share

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

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XS 0-9 changed lines (additions + deletions). labels Jun 30, 2026
Comment thread apps/desktop/src/backend/DesktopBackendManager.ts
@macroscopeapp

macroscopeapp Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR introduces new WSL backend restart/fallback behavior with significant lifecycle changes. Multiple blocking review comments identify bugs including Effect.yieldNow usage errors, race conditions with readiness after exit, and test timing issues that need resolution before merging.

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

Tests using fd3 delivery were hanging because the stream never terminated. Only stdin delivery (WSL) needs the stream kept open.
macroscopeapp[bot]
macroscopeapp Bot previously approved these changes Jun 30, 2026
…gg#3611)

Consolidates fixes from PRs pingdotgg#3613, pingdotgg#3621, and pingdotgg#3623 to address both root causes and add a fail-safe:

1. DesktopWslEnvironment: Parse the Node version in the WSL node-pty probe and validate it against options.nodeEngineRange. Preflight now fails cleanly with an actionable error if the distro's default Node is incompatible.

2. DesktopBackendManager: Track neverReadyAttempt to cap consecutive post-spawn exits on stdin delivery. Prevents the desktop from permanently getting stuck restarting if the WSL backend consistently fails before readiness.

3. bootstrap: Clean up the readline interface on all event paths to prevent leaks when stdin stays open, and register the error listener on the readline interface to safely catch early stream errors.
@jibin7jose
jibin7jose force-pushed the fix-wsl-code-0-exit branch from 340fb76 to 215bd10 Compare July 2, 2026 12:52
@github-actions github-actions Bot added size:L 100-499 changed lines (additions + deletions). and removed size:XS 0-9 changed lines (additions + deletions). labels Jul 2, 2026
@macroscopeapp
macroscopeapp Bot dismissed their stale review July 2, 2026 12:52

Dismissing prior approval to re-evaluate 215bd10

Comment thread apps/desktop/src/backend/DesktopBackendManager.ts
Comment thread apps/desktop/src/backend/DesktopBackendManager.ts Outdated
Comment thread apps/desktop/src/backend/DesktopBackendManager.ts
@jibin7jose

Copy link
Copy Markdown
Author

@juliusmarminge Could you please approve the workflows for this PR so the checks can run? Thank you!

Comment thread apps/desktop/src/backend/DesktopBackendManager.ts Outdated
Comment thread apps/desktop/src/backend/DesktopBackendManager.ts Outdated
Comment thread .github/CODEOWNERS Outdated
Comment thread .github/CODEOWNERS Outdated
@jibin7jose

Copy link
Copy Markdown
Author

Hi @juliusmarminge! When you have time, could you please take a look at this PR? It addresses issue #3611 by fixing the WSL backend exiting early when using stdin bootstrap delivery. I’ve incorporated the follow-up fixes and kept the PR up to date with main. If you have any feedback or would like any changes, I’d be happy to update the PR. Thank you!

@martinoturrina

Copy link
Copy Markdown

I still have the WSL stuck at connection issue on the latest alpha T3 Code build, any updates for this PR?

@jibin7jose
jibin7jose force-pushed the fix-wsl-code-0-exit branch from 865db91 to ea01cf3 Compare August 10, 2026 14:40
@github-actions github-actions Bot added size:XXL 1,000+ changed lines (additions + deletions). and removed size:L 100-499 changed lines (additions + deletions). labels Aug 10, 2026
@jibin7jose
jibin7jose force-pushed the fix-wsl-code-0-exit branch from ea01cf3 to 865db91 Compare August 10, 2026 14:42
@github-actions github-actions Bot added size:L 100-499 changed lines (additions + deletions). and removed size:XXL 1,000+ changed lines (additions + deletions). labels Aug 10, 2026

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Effect service conventions: no violations found in imports, service definition, dependency acquisition, or error modeling. Two change-discipline findings: the behavior changes in DesktopBackendManager.ts and apps/server/src/bootstrap.ts land without focused tests, although both modules already have test suites with the needed harnesses.

Posted via Macroscope — Effect Service Conventions

Comment on lines +143 to +149
// Register the error listener on the readline interface, not the raw
// stream. Node's readline.Interface re-emits stream errors onto itself;
// if a stream error arrives before the first line event, listening only
// on the stream leaves the readline interface with an unhandled error —
// registering on `input` ensures every error path is handled regardless
// of when the error occurs in the reader lifecycle.
input.once("error", handleError);

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.

Registering the error listener on the readline interface (plus the added cleanup() calls in each handler) changes bootstrap read behavior, and bootstrap.test.ts currently has no case for a stream error arriving before the first line. Consider adding a focused test that emits an error on the input stream and asserts BootstrapEnvelopeReadError for a generic error and Option.none() for EBADF/ENOENT.

Posted via Macroscope — Effect Service Conventions

Comment on lines +897 to +932
// For stdin-delivery (WSL) backends, track exits that happen
// before HTTP readiness. A run that reached readiness resets
// the counter (in onReady), so this correctly counts only
// *consecutive* never-ready exits. When the cap fires, invoke
// onPreflightFailed so the UI falls back to Windows instead of
// looping forever. fd3 (Windows-native) keeps uncapped restarts.
if (!wasReady && config.value.bootstrapDelivery === "stdin" && Option.isSome(pid)) {
const attempt = yield* Ref.modify(state, (s) => {
const next = s.neverReadyAttempt + 1;
return [next, { ...s, neverReadyAttempt: next }] as const;
});
if (attempt >= MAX_PREFLIGHT_FAILURE_ATTEMPTS) {
yield* logInstanceError(
"WSL backend exited before readiness too many times; surfacing and falling back",
{ reason, attempt },
);
// Reset so a future re-enable gets a fresh allowance.
yield* Ref.update(state, (s) => ({ ...s, neverReadyAttempt: 0 }));
const shouldRestart = yield* (
spec.onPreflightFailed?.({
reason: `WSL backend exited before becoming ready ${attempt} times in a row. ${reason}`,
fatal: false,
}) ?? Effect.succeed(false)
);
if (!shouldRestart) {
yield* Ref.update(state, (s) => ({
...s,
desiredRunning: false,
ready: false,
}));
} else {
yield* scheduleRestart(reason);
}
return;
}
}

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.

This new never-ready cap changes backend restart/fallback behavior (bounded restarts plus an onPreflightFailed fallback for stdin-delivery runs) but no test accompanies it. Consider adding a focused case to DesktopBackendManager.test.ts — the harness already supports bootstrapDelivery and onPreflightFailed — asserting that a stdin-delivery backend which exits before readiness MAX_PREFLIGHT_FAILURE_ATTEMPTS times invokes onPreflightFailed exactly once, stops when it returns false, and that a run reaching readiness resets the counter.

Posted via Macroscope — Effect Service Conventions

@UtkarshUsername

Copy link
Copy Markdown
Contributor

@jibin7jose Can you address the reviews and update your PR? Considering to get this PR merged

@jibin7jose

Copy link
Copy Markdown
Author

Ok 👍

Comment thread apps/desktop/src/backend/DesktopBackendManager.test.ts Outdated
@jibin7jose

Copy link
Copy Markdown
Author

@UtkarshUsername I have addressed all the reviews and updated the PR! I've added the missing focused tests for the error handling paths in DesktopBackendManager.ts and bootstrap.ts (including properly handling the TestClock in the backend restart loops). The Macroscope bot findings should now be fully resolved.

@UtkarshUsername

Copy link
Copy Markdown
Contributor

thanks @jibin7jose, I will get back to your PR after we are done with #5877. Meanwhile, can you fill in the PR template properly in the pr description

@jibin7jose

Copy link
Copy Markdown
Author

Sure, I will update the PR description and fill in the PR template properly. Thanks!

@t3-code t3-code Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

requesting changes on exact head 96907b396fe1562bb35014a436ab84f842cb0cc4.

blocker: Effect.yieldNow is an Effect value in the installed Effect beta, not a function. yield* Effect.yieldNow() therefore fails typecheck and dies when finalizeRun executes, so exited backends do not reach the restart/fallback logic this PR is adding.

local verification:

  • desktop + server typecheck failed at DesktopBackendManager.ts:824, with cascading unknown-context/fiber errors
  • the exact-head DesktopBackendManager.test.ts run timed out in 9/26 tests; the same file on current main passed 24/24
  • changing only the expression to yield* Effect.yieldNow removed the type error, but three lifecycle tests still timed out: the existing restart test plus both new never-ready tests. they advance TestClock before the restart fiber/sleep is observably scheduled. please synchronize on restartScheduled or the next spawn before each clock advance, then run the whole file
  • focused WSL/bootstrap tests passed 37/37, including on the clean synthetic merge with current main
  • formatting currently fails in DesktopBackendManager.test.ts and bootstrap.test.ts; git diff --check also reports three trailing-whitespace lines

please also rebase/reduce the WSL probe portion against current main: #3621 already supplies the confirmed Node-engine fix, and the merged result must retain #5877's node-pty external-package sentinel.

once the compile/runtime failure and deterministic lifecycle tests are fixed, this still needs the repository Test/Check jobs and a real Windows + WSL startup/fallback run. those checks have not run on this head, and my local verification was Linux-hosted.

const finalizeRun = Effect.fn("desktop.backendInstance.finalizeRun")(function* (
reason: string,
) {
yield* Effect.yieldNow();

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.

blocker: Effect.yieldNow is already an Effect<void> in the installed Effect beta, not a function. yield* Effect.yieldNow() fails typecheck and defects whenever finalizeRun runs, so the backend never reaches this PR’s restart/fallback path after an exit. please use the Effect value directly or replace this heuristic with deterministic readiness/exit synchronization, then rerun the full lifecycle suite.

@t3-code t3-code Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

two more lifecycle blockers came out of the independent concurrency pass:

  1. intentional stdin-backend exits during overlapping stop() / start() are counted toward the never-ready fallback because the new condition ignores stopRequested.
  2. a late HTTP 200 can still mark an already-exited run ready and reset the new counter because onReady checks only the run id, not exitObserved / stopRequested.

also, please preserve current main’s first-non-empty nodeVersion: parsing when rebasing. the PR rewrite returns null for nodeVersion:\nnodeVersion:24.13.1, while main correctly returns 24.13.1.

added inline details and suggested focused tests.

// *consecutive* never-ready exits. When the cap fires, invoke
// onPreflightFailed so the UI falls back to Windows instead of
// looping forever. fd3 (Windows-native) keeps uncapped restarts.
if (!wasReady && config.value.bootstrapDelivery === "stdin" && Option.isSome(pid)) {

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.

blocker: this also needs to exclude stopRequested. during an overlapping stop() / fresh start(), desiredRunning becomes true again before the intentionally stopped child exits. this branch then counts that intentional exit toward the stdin never-ready cap and can eventually trigger an incorrect WSL fallback. please require an unexpected observed exit here and add an stdin-focused stop/start overlap test.

restartAttempt: 0,
// This run reached readiness — reset the never-ready counter
// so it only tracks *consecutive* failures from this point on.
neverReadyAttempt: 0,

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.

blocker: a readiness response can complete after onExitObserved but before output draining/finalization clears active. because this transition checks only the run id, it can mark the dead run ready, call spec.onReady, and reset neverReadyAttempt, defeating the new cap. please reject readiness when the active run has exitObserved or stopRequested, with a test that releases HTTP 200 after exit observation.

.find((value) => value.length > 0);
return version ?? null;
.map((l) => l.trim())
.find((l) => l.startsWith("nodeVersion:"));

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.

when rebasing, please keep current main’s first-non-empty parser. this rewrite selects the first marker even when empty, so nodeVersion:\nnodeVersion:24.13.1 returns null instead of 24.13.1. main’s filter/map/find implementation is more robust and already came from #3621.

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Two issues in the new/edited test code for DesktopBackendManager.test.ts: the added waitForNextSpawn helpers reference identifiers that do not exist in scope (spawnCount / startCount) and read state.restartFiber, which is not part of DesktopBackendSnapshot (it exposes restartScheduled). As written the required focused tests for the new never-ready cap will not type-check.

Posted via Macroscope — Effect Service Conventions

Comment thread apps/desktop/src/backend/DesktopBackendManager.test.ts Outdated
Comment thread apps/desktop/src/backend/DesktopBackendManager.test.ts Outdated

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

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 b860e16. Configure here.

Comment thread apps/desktop/src/backend/DesktopBackendManager.test.ts

@t3-code t3-code Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

re-reviewed exact head b300d6ab8f864665e2ef7724c66914287562ee07.

code review: changes requested. the earlier syntax, stop/start accounting, late-readiness, and first-nonempty nodeVersion: issues are fixed, but the lifecycle regression suite still cannot complete. the existing restart test and both new never-ready tests each time out in isolation. the exact base passes the existing test, so this is introduced by the pr. the tests need a real receipt proving each restart sleep is registered before every TestClock.adjust; persistFailure, restartScheduled, and Effect.yieldNow are not that receipt.

formatting also fails in DesktopBackendManager.test.ts, DesktopBackendManager.ts, and bootstrap.test.ts; git diff --check reports three trailing-whitespace lines.

mergeability: github reports mergeable, and a synthetic merge into current main is conflict-free. it preserves the merged node-pty packaging sentinel and materialized WSL server-tree behavior.

ci: no repository jobs ran. this is a cross-repository fork from an author with association NONE, labeled vouch:unvouched; the CI, web preview, and mobile preview workflow runs are all action_required with zero jobs. a maintainer must use approve and run workflows, or vouch the author for future updates. required Test/Check jobs therefore provide no evidence on this head.

local: desktop/server typechecks pass. focused WSL/bootstrap tests pass 37/37 at the exact head and 60/60 on the synthetic current-main merge. the manager suite fails the three timeout cases above.

release readiness: not established. no fresh Windows-native or Windows+WSL end-to-end startup/fallback run was performed.

const finalizeRun = Effect.fn("desktop.backendInstance.finalizeRun")(function* (
reason: string,
) {
yield* Effect.yieldNow;

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.

blocking: this scheduler yield is not a lifecycle receipt. persistFailure completes before scheduleRestart, so tests can advance the clock while this yield still delays timer registration. the existing restart test passes on the exact base but times out on this head. either remove this heuristic if it is not semantically required, or expose an explicit receipt after the restart sleep is installed, then have every clock-driven test await that receipt.

if (!state.desiredRunning) break;
if (spawnCount > sc) break;
if (state.ready) break;
if (state.restartScheduled) break;

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.

blocking: restartScheduled only proves the restart fiber was stored, not that its TestClock.sleep is registered. this helper can return too early, and later backoff advances do not call it at all. both new tests still time out in isolation. wait for an explicit timer-registration receipt before every TestClock.adjust, then wait for the next spawn.

preflightFailedCount++;
lastPreflightFailure = failure;
}).pipe(
Effect.andThen(Deferred.succeed(preflightFailed, void 0)),

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 more test-side race: this deferred is completed inside the callback before the callback returns false and the manager applies desiredRunning: false. an instrumented run that got past the timer race reached the final assertion while desiredRunning was still true. please await an explicit manager-transition receipt or the resulting snapshot here, not a callback-internal signal.

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

Re-ran exact current head b300d6ab files on today's workspace.

bootstrap.test.ts: 9/9.

DesktopBackendManager.test.ts: 23 passed, 3 timed out at 15s — the existing "restarts an unexpectedly exited backend with the Effect clock" plus both new never-ready cases. Same three t3-code[bot] flagged.

The stdin-keep-open idea (Stream.never after the bootstrap JSON) is the right shape for a WSL backend that otherwise exits 0 when stdin closes. I would not merge it while the restart clock tests hang. The CHANGES_REQUESTED still stands.

MERGEABLE, but the lifecycle suite is not.

@t3-code

t3-code Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

closing this pr because basic quality gates have repeatedly failed, including focused lifecycle tests and formatting checks. required ci also was not running because this fork remained unvouched.

please open a fresh, focused pr only if you can follow the repository contribution guidelines and verify the full required baseline first: formatting, typechecks, focused and affected test suites, clean diff checks, and all required github checks green. include real windows + wsl evidence for the affected runtime path.

@t3-code t3-code Bot closed this Aug 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L 100-499 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Windows desktop 0.0.28 gets stuck on connecting to WSL after enabling WSL support

5 participants