Summary
On Windows+WSL, the desktop backend hangs forever at "Connecting to WSL…" when the WSL login shell resolves a node that is present but below the engine floor (< 22.16). The server process starts, loads, and exits 0 silently without binding — so the desktop just respawns it in a loop with no diagnostic.
Two independent issues combine to make this a silent, hard-to-diagnose failure.
Environment
- Windows 11 + WSL2 (Ubuntu), backend launched via
wsl.exe -- node … --bootstrap-fd 0
nvm default node was v22.12.0; a satisfying v22.21.1 was also installed but not the default
- Server
engines.node: ^22.16 || ^23.11 || >=24.10
Root cause 1 — import.meta.main guard fails silently on Node < 22.16
apps/server/src/bin.ts gates all startup on import.meta.main:
if (import.meta.main) {
Command.run(cli, { version: packageJson.version }).pipe(/* … */ NodeRuntime.runMain);
}
import.meta.main was added in Node 22.16.0. On 22.12.0 it is undefined, so the guard is falsy: the module loads its entire import graph and the process exits 0 with no output and no error. Verified:
$ node -v # v22.12.0
$ node -e 'x' --input-type=module ... # import.meta.main === undefined
# server run under 22.12.0 -> loads, exits 0, never creates a socket (strace: zero bind/listen)
# server run under 22.21.1 -> binds and serves /.well-known/t3/environment correctly
Because it exits 0 cleanly, the desktop's restart loop keeps respawning it and the UI shows "Connecting to WSL…" indefinitely. Suggestion: detect an unsupported/undefined import.meta.main Node and fail loudly (clear stderr message + non-zero exit) so the desktop can surface it, e.g. a robust entrypoint check plus an explicit engine assertion:
import { pathToFileURL } from "node:url";
const isEntrypoint =
import.meta.main ??
(process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href);
if (isEntrypoint) { /* … */ }
Root cause 2 — ensure_remote_node_path won't upgrade away from an unsatisfying node
packages/ssh/src/tunnel.ts REMOTE_NODE_ENV_SCRIPT: the early-return correctly checks node present and satisfies engine, but every version-manager fallback (mise/fnm/nodenv/nvm, and the nvm version scan) is gated on ! command -v node — i.e. "no node at all", not "no satisfying node". So when a login shell already exposes a node that fails the engine range (e.g. nvm default 22.12.0, or a system /usr/bin/node), the fallbacks are skipped and ensure_remote_node_path execs the unsatisfying node anyway (… || true).
Minimal fix — introduce a remote_node_ready helper and use it for the gates:
remote_node_ready() {
command -v node >/dev/null 2>&1 && remote_node_satisfies_engine >/dev/null 2>&1
}
ensure_remote_node_path() {
if remote_node_ready; then
return 0
fi
# …prepend_path_if_dir calls unchanged…
if ! remote_node_ready && command -v mise >/dev/null 2>&1; then …; fi
if ! remote_node_ready && command -v fnm >/dev/null 2>&1; then …; fi
if ! remote_node_ready && command -v nodenv >/dev/null 2>&1; then …; fi
if [ -s "$NVM_DIR/nvm.sh" ]; then
. "$NVM_DIR/nvm.sh"
if ! remote_node_ready && command -v nvm >/dev/null 2>&1; then
nvm use --silent default >/dev/null 2>&1 || nvm use --silent node >/dev/null 2>&1 || nvm use --silent --lts >/dev/null 2>&1 || true
fi
fi
if ! remote_node_ready && [ -d "$NVM_DIR/versions/node" ]; then
for T3_NODE_BIN in "$NVM_DIR"/versions/node/*/bin; do
[ -x "$T3_NODE_BIN/node" ] && PATH="$T3_NODE_BIN:$PATH" && export PATH
done
fi
remote_node_ready
}
(The nvm version scan could additionally prefer a satisfying version rather than the last glob match, but the gate change alone lets it fall through to nvm's default and the other managers.)
Impact / workaround
Workaround: make the login-shell default node satisfy the range (nvm alias default 22.21.1) — confirmed fixes it. But the failure mode is invisible: no log line, clean exit, infinite "Connecting to WSL…".
Summary
On Windows+WSL, the desktop backend hangs forever at "Connecting to WSL…" when the WSL login shell resolves a
nodethat is present but below the engine floor (< 22.16). The server process starts, loads, and exits0silently without binding — so the desktop just respawns it in a loop with no diagnostic.Two independent issues combine to make this a silent, hard-to-diagnose failure.
Environment
wsl.exe -- node … --bootstrap-fd 0nvmdefault node was v22.12.0; a satisfying v22.21.1 was also installed but not the defaultengines.node:^22.16 || ^23.11 || >=24.10Root cause 1 —
import.meta.mainguard fails silently on Node < 22.16apps/server/src/bin.tsgates all startup onimport.meta.main:import.meta.mainwas added in Node 22.16.0. On 22.12.0 it isundefined, so the guard is falsy: the module loads its entire import graph and the process exits0with no output and no error. Verified:Because it exits
0cleanly, the desktop's restart loop keeps respawning it and the UI shows "Connecting to WSL…" indefinitely. Suggestion: detect an unsupported/undefined import.meta.mainNode and fail loudly (clear stderr message + non-zero exit) so the desktop can surface it, e.g. a robust entrypoint check plus an explicit engine assertion:Root cause 2 —
ensure_remote_node_pathwon't upgrade away from an unsatisfying nodepackages/ssh/src/tunnel.tsREMOTE_NODE_ENV_SCRIPT: the early-return correctly checks node present and satisfies engine, but every version-manager fallback (mise/fnm/nodenv/nvm, and the nvm version scan) is gated on! command -v node— i.e. "no node at all", not "no satisfying node". So when a login shell already exposes a node that fails the engine range (e.g. nvm default 22.12.0, or a system/usr/bin/node), the fallbacks are skipped andensure_remote_node_pathexecs the unsatisfying node anyway (… || true).Minimal fix — introduce a
remote_node_readyhelper and use it for the gates:(The nvm version scan could additionally prefer a satisfying version rather than the last glob match, but the gate change alone lets it fall through to nvm's default and the other managers.)
Impact / workaround
Workaround: make the login-shell default node satisfy the range (
nvm alias default 22.21.1) — confirmed fixes it. But the failure mode is invisible: no log line, clean exit, infinite "Connecting to WSL…".