garrytan / garrytan/gstack

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

Open
#2,637 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
133k
Forks
19.9k
Avg merge
18h 46m
Merged PRs (30d)
26

Description

## 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, #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

- #1650 — same 503 string, closed as a duplicate of #1605 (Windows DACL). Different root cause: there the state dir was unreadable, here the process is dead.
- #2295 — the inverse failure (watchdog respawning agents that are already alive). Independent of this.
- #2530 — introduced the owner-PID tie that defect 2 leaves inert on the CLI path.

Happy to send a PR.

Contributor guide

Open the contributing guide

Research direction

Start with browse/src/terminal-agent-control.ts:73-87 and compare its spawn call with the detached daemon spawns in browse/src/cli.ts:379 and :499. Then inspect the CLI call sites at browse/src/cli.ts:1406 and :1498, the owner check in browse/src/terminal-agent.ts:1018, and server handling in browse/src/server.ts:1599. Done means the Windows agent survives CLI exit, records its port and PID promptly, and still exits with its owning server on Windows, macOS, and Linux.

Written by the indexing model from the issue text.

Assessment

Tech stack
bun, node.js, typescript
Domain
backend, cli
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
72/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.