Skip to content

Windows: terminal-agent from browse connect dies with the CLI console (missing detached) and runs with its owner watchdog disabled (missing ownerPid) #2637

Description

@elderfo

Summary

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:

  1. 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.

  2. 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:

$ tail -3 .gstack/browse-daemon.log
[browse] State file: C:\dev\avalanche\.gstack\browse.json
[browse] Idle timeout: 1800s
[browse] terminal-agent respawned by watchdog (PID: 27056)

$ tasklist /FI "IMAGENAME eq bun.exe"
bun.exe                      27056 Console                    1     65,508 K

$ cat .gstack/terminal-port
23095

$ curl -s http://127.0.0.1:34567/health
{"status":"healthy","mode":"headed","uptime":133,"tabs":1,"terminalPort":23095}

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.

Related

Happy to send a PR.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions