You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Windows: terminal-agent from browse connect dies with the CLI console (missing detached) and runs with its owner watchdog disabled (missing ownerPid) #2637
On Windows, the terminal-agent that browse connect starts dies as soon as the CLI process exits. .gstack/terminal-port is never written, so the sidebar Terminal pane fails with Cannot start: 503 {"error":"terminal-agent not ready"} for the first ~20-60s of every session, until the daemon's 60s watchdog respawns the agent under the long-lived server. The watchdog respawn is the only reason the terminal ever works; the CLI spawn never survives.
Two separate defects sit behind this, both reproducible on current main:
spawnTerminalAgent (browse/src/terminal-agent-control.ts:73-87) never passes detached to the spawn call, so the child is torn down with the CLI's console/job object on Windows. The function's own doc comment at line 44 describes it as spawning "a fresh terminal-agent as a detached child", and the daemon spawn in browse/src/cli.ts (lines 379 and 499) does pass detached: true — which is why the server survives the same CLI exit and the agent does not. proc.unref() alone does not cover this on Windows.
Both CLI call sites — browse/src/cli.ts:1406 (connect) and browse/src/cli.ts:1498 (supervisor respawn) — omit ownerPid, which the helper's signature declares as required. It reaches the child as the literal string "undefined", parseInt yields NaN, and the BROWSE_OWNER_PID > 0 guard at browse/src/terminal-agent.ts:1018 is false, so the owner watchdog is silently disabled for every CLI-spawned agent. browse/src/server.ts:1599 passes it correctly, so only the CLI path is affected. git log -S ownerPid puts the parameter's introduction in c86e647 (v1.67.1.0, v1.67.1.0 fix: external-contributor security sweep — 6 findings hardened, regression-pinned #2605); the two CLI call sites were not updated with it.
Defect 2 is currently masked by defect 1 — the CLI-spawned agent dies before an orphan can outlive anything. Fixing only the detach would leave every CLI-spawned agent with no owner watchdog, which is the orphan class #2530 set out to close.
Environment
Windows 11 Pro, build 26200
gstack v1.67.2.0 (60e5134)
node v24.19.0, bun 1.3.14
Host: Claude Code; daemon running as node.exe dist/server-node.mjs with bun-polyfill.cjs
Reproduction
$ browse connect
Launching headed Chromium with extension + terminal agent...
Connected to real Chrome
Status: healthy
Mode: headed
URL: http://127.0.0.1:34567/welcome
Tabs: 1
PID: 11156
[browse] Terminal agent started (PID: 29976)
# seconds later, from a new shell:
$ tasklist /FI "PID eq 29976"
INFO: No tasks are running which match the specified criteria.
$ tasklist /FI "IMAGENAME eq bun.exe"
INFO: No tasks are running which match the specified criteria.
$ ls .gstack/terminal-port
ls: cannot access '.gstack/terminal-port': No such file or directory
The sidebar Terminal pane at this point shows:
Cannot start: 503 {"error":"terminal-agent not ready"}
which is /pty-session failing its readTerminalPort() gate (browse/src/server.ts:1950, and the same gate at :1999 and :2048).
Roughly 20-60s later the daemon watchdog picks it up and the pane starts working:
That agent — the watchdog's, parented by the detached server — survives indefinitely. Only the CLI-spawned one dies.
Isolating the cause
Spawning the identical argv and env through cross-spawn (the polyfill's Windows path) from a Node process that stays alive produces a healthy agent:
pid: 36384
OUT: [terminal-agent] listening on 127.0.0.1:16190 pid=36384 gen=MTu56F6PjVR8P0UMqhBCqQ
still alive after 8s
The agent itself is fine. What kills it is the parent CLI exiting immediately after the spawn, with no detached.
Impact
Every browse connect on Windows leaves the sidebar Terminal pane broken for the first ~20-60s, with an error that reads like a permanent failure rather than a wait.
The recovery path depends entirely on the 60s watchdog, so the agent gets spawned at least twice per session and the reported PID in connect output is always dead.
Every CLI-spawned agent runs with its owner watchdog disabled.
Acceptance criteria
Given a Windows host with no browse daemon running, when the user runs browse connect and opens the sidebar Terminal pane, then the pane attaches without a 503 and .gstack/terminal-port exists by the time connect returns.
Given connect has printed Terminal agent started (PID: N) and the CLI process has since exited, when process N is checked, then it is still running and matches the pid recorded in .gstack/terminal-agent-pid.
Given an agent started by the CLI path, when the browse server that owns it exits or is killed, then the agent exits on its own and removes its port and pid records, rather than being orphaned.
Given a connect on macOS or Linux, when the agent is spawned, then its lifecycle is unchanged and no console window or stray process appears on any platform.
Summary
On Windows, the terminal-agent that
browse connectstarts dies as soon as the CLI process exits..gstack/terminal-portis never written, so the sidebar Terminal pane fails withCannot start: 503 {"error":"terminal-agent not ready"}for the first ~20-60s of every session, until the daemon's 60s watchdog respawns the agent under the long-lived server. The watchdog respawn is the only reason the terminal ever works; the CLI spawn never survives.Two separate defects sit behind this, both reproducible on current
main:spawnTerminalAgent(browse/src/terminal-agent-control.ts:73-87) never passesdetachedto the spawn call, so the child is torn down with the CLI's console/job object on Windows. The function's own doc comment at line 44 describes it as spawning "a fresh terminal-agent as a detached child", and the daemon spawn inbrowse/src/cli.ts(lines 379 and 499) does passdetached: true— which is why the server survives the same CLI exit and the agent does not.proc.unref()alone does not cover this on Windows.Both CLI call sites —
browse/src/cli.ts:1406(connect) andbrowse/src/cli.ts:1498(supervisor respawn) — omitownerPid, which the helper's signature declares as required. It reaches the child as the literal string"undefined",parseIntyieldsNaN, and theBROWSE_OWNER_PID > 0guard atbrowse/src/terminal-agent.ts:1018is false, so the owner watchdog is silently disabled for every CLI-spawned agent.browse/src/server.ts:1599passes it correctly, so only the CLI path is affected.git log -S ownerPidputs the parameter's introduction inc86e647(v1.67.1.0, v1.67.1.0 fix: external-contributor security sweep — 6 findings hardened, regression-pinned #2605); the two CLI call sites were not updated with it.Defect 2 is currently masked by defect 1 — the CLI-spawned agent dies before an orphan can outlive anything. Fixing only the detach would leave every CLI-spawned agent with no owner watchdog, which is the orphan class #2530 set out to close.
Environment
60e5134)node.exe dist/server-node.mjswithbun-polyfill.cjsReproduction
The sidebar Terminal pane at this point shows:
which is
/pty-sessionfailing itsreadTerminalPort()gate (browse/src/server.ts:1950, and the same gate at:1999and:2048).Roughly 20-60s later the daemon watchdog picks it up and the pane starts working:
That agent — the watchdog's, parented by the detached server — survives indefinitely. Only the CLI-spawned one dies.
Isolating the cause
Spawning the identical argv and env through
cross-spawn(the polyfill's Windows path) from a Node process that stays alive produces a healthy agent:The agent itself is fine. What kills it is the parent CLI exiting immediately after the spawn, with no
detached.Impact
browse connecton Windows leaves the sidebar Terminal pane broken for the first ~20-60s, with an error that reads like a permanent failure rather than a wait.connectoutput is always dead.Acceptance criteria
browse connectand opens the sidebar Terminal pane, then the pane attaches without a 503 and.gstack/terminal-portexists by the timeconnectreturns.connecthas printedTerminal agent started (PID: N)and the CLI process has since exited, when process N is checked, then it is still running and matches the pid recorded in.gstack/terminal-agent-pid.Related
Happy to send a PR.