What happened
On WSL2, the Device panel never works. device_list reports the local host as
failed, every time:
"local": { "status": "failed",
"detail": "Device host local is unavailable: Device host local failed while waiting for the device hub to answer." }
Android is detected fine (available: true, hubInstalled: true,
agentDeviceInstalled: true) — an emulator is installed and booted, and
npx agent-device drives it without trouble. Only t3's own device integration
is dead. The bundled ~/.t3/userdata/device/bin/agent-device shim is broken the
same way: it prints t3's own --help instead of running agent-device.
Diagnosis
resolveNodeExecutable (packages/shared/src/nodeRuntime.ts:34-41) returns the
host executable path when HostProcessIsExecutable is false:
/** A standalone T3 binary runs its embedded CLI, regardless of script arguments. */
export const resolveNodeExecutable = Effect.fn(...)(function* (feature, environment?) {
const executablePath = yield* HostProcessExecutablePath; // process.execPath
if (!(yield* HostProcessIsExecutable)) return executablePath;
// ...otherwise resolve a real `node` from PATH...
HostProcessIsExecutable defaults to NodeSea.isSea()
(packages/shared/src/hostProcess.ts:74-79). In the WSL runtime build
isSea() evaluates to false, so t3 concludes it is already running under Node
and hands back its own path. LocalDeviceHost.ts:544 takes that as nodePath,
and spawnHub (LocalDeviceHost.ts:336-355) spawns:
<t3 binary> <hub entryPath> --port <port> --host 127.0.0.1 --hide-sidebar --hide-boot-device
The standalone binary cannot run a script argument — which is exactly what the
doc comment above resolveNodeExecutable says. --hide-sidebar is not a t3
flag, so the child prints t3's help and exits 1. Nothing ever binds the port,
and waitForHttpReady polls /readyz for 30s (~290 attempts) before failing.
Verified the hub itself is healthy — run under real Node it works immediately:
node ~/.t3/tools/expo-device-hub/0.9.0/node_modules/expo-device-hub/dist/server/cli.mjs --port 36851
# Expo Device Hub ready -> Local: http://localhost:36851
# /readyz -> {"status":"ready","device":"no-device-id"}
Also confirmed the spawned child never executes the entry file at all: replacing
the hub's dist/server/cli.mjs with a wrapper that appends one line to a log
before importing the real module produced no log output under t3's own spawn.
Same root cause reaches AgentDeviceShim.ts:24 and DeviceService.ts:1025,
which is why the bundled agent-device shim prints t3's help: the shim execs
t3 agent-device-launcher.mjs, and the launcher's own inner spawn goes
through the same binary again.
Both runtime builds present on this machine behave identically, so this is not a
recent regression.
Steps to reproduce
On WSL2, with a standalone/SEA t3 runtime:
echo 'console.log("HELLO")' > /tmp/probe.mjs
~/.t3/wsl-runtime/sha256-<hash>/t3 /tmp/probe.mjs
Expected: prints HELLO.
Actual: the path is parsed as the optional cwd positional and t3 tries to
create it as a directory:
ERROR PlatformError: AlreadyExists: FileSystem.makeDirectory (/tmp/probe.mjs)
[cause]: Error: EEXIST: file already exists, mkdir '/tmp/probe.mjs'
Then, in any thread, call device_list and watch the local host fail after 30s.
Version
v0.0.41-nightly.20260916.1795
Environment
WSL2 (Linux 6.6.87.2-microsoft-standard-WSL2) on Windows, Node v24.18.0 (nvm),
Android SDK in WSL with emulator + platform-tools, expo-device-hub 0.9.0,
agent-device 0.20.10 (bundled).
Evidence
LocalDeviceHost.spawnHub durationMs 30014
Failure: DeviceHostError: Device host local failed while waiting for the device hub to answer.
shared.httpReadiness.waitForHttpReady durationMs 30009
httpReadiness.timedOut { requestUrl: "http://127.0.0.1:36817/readyz",
timeoutMs: 30000, intervalMs: 100, attempts: 290 }
http.client GET http://127.0.0.1:36851/readyz
Failure: HttpClientError: Transport error
# actual spawned child, captured by polling the process table:
<t3 binary> ~/.t3/tools/expo-device-hub/0.9.0/node_modules/expo-device-hub/dist/server/cli.mjs \
--port 38261 --host 127.0.0.1 --hide-sidebar --hide-boot-device
Note: the child's stdout/stderr are piped but do not appear in
userdata/logs/server.trace.ndjson; surfacing them would have made this
diagnosable in minutes rather than by polling /proc.
Suggested fix
Either make HostProcessIsExecutable true for the WSL runtime build (so the
real-node resolution path runs), or drop the isSea() dependency and decide
by testing whether the host executable can actually run a script. Logging the
hub child's stderr on readiness timeout would help regardless.
Filed after investigation with Claude Code (Opus 5).
What happened
On WSL2, the Device panel never works.
device_listreports the local host asfailed, every time:
Android is detected fine (
available: true,hubInstalled: true,agentDeviceInstalled: true) — an emulator is installed and booted, andnpx agent-devicedrives it without trouble. Only t3's own device integrationis dead. The bundled
~/.t3/userdata/device/bin/agent-deviceshim is broken thesame way: it prints t3's own
--helpinstead of running agent-device.Diagnosis
resolveNodeExecutable(packages/shared/src/nodeRuntime.ts:34-41) returns thehost executable path when
HostProcessIsExecutableis false:HostProcessIsExecutabledefaults toNodeSea.isSea()(
packages/shared/src/hostProcess.ts:74-79). In the WSL runtime buildisSea()evaluates to false, so t3 concludes it is already running under Nodeand hands back its own path.
LocalDeviceHost.ts:544takes that asnodePath,and
spawnHub(LocalDeviceHost.ts:336-355) spawns:The standalone binary cannot run a script argument — which is exactly what the
doc comment above
resolveNodeExecutablesays.--hide-sidebaris not a t3flag, so the child prints t3's help and exits 1. Nothing ever binds the port,
and
waitForHttpReadypolls/readyzfor 30s (~290 attempts) before failing.Verified the hub itself is healthy — run under real Node it works immediately:
Also confirmed the spawned child never executes the entry file at all: replacing
the hub's
dist/server/cli.mjswith a wrapper that appends one line to a logbefore importing the real module produced no log output under t3's own spawn.
Same root cause reaches
AgentDeviceShim.ts:24andDeviceService.ts:1025,which is why the bundled
agent-deviceshim prints t3's help: the shim execst3 agent-device-launcher.mjs, and the launcher's own innerspawngoesthrough the same binary again.
Both runtime builds present on this machine behave identically, so this is not a
recent regression.
Steps to reproduce
On WSL2, with a standalone/SEA t3 runtime:
Expected: prints
HELLO.Actual: the path is parsed as the optional
cwdpositional and t3 tries tocreate it as a directory:
Then, in any thread, call
device_listand watch the local host fail after 30s.Version
v0.0.41-nightly.20260916.1795
Environment
WSL2 (Linux 6.6.87.2-microsoft-standard-WSL2) on Windows, Node v24.18.0 (nvm),
Android SDK in WSL with emulator + platform-tools, expo-device-hub 0.9.0,
agent-device 0.20.10 (bundled).
Evidence
Note: the child's stdout/stderr are piped but do not appear in
userdata/logs/server.trace.ndjson; surfacing them would have made thisdiagnosable in minutes rather than by polling
/proc.Suggested fix
Either make
HostProcessIsExecutabletrue for the WSL runtime build (so thereal-
noderesolution path runs), or drop theisSea()dependency and decideby testing whether the host executable can actually run a script. Logging the
hub child's stderr on readiness timeout would help regardless.
Filed after investigation with Claude Code (Opus 5).