garrytan / garrytan/gstack

test:free leaves orphaned browse/src/server.ts processes after shards finish

Open
#2,816 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

`bun run test:free` leaves `browse/src/server.ts` processes running with
`ppid=1` after every run. They are not reaped when the shard that spawned them
ends, and they survive indefinitely.

**Observed** — macOS 26.5.2 (arm64), bun 1.3.13, gstack `v1.80.0.0`.
Two `bun run test:free` runs, started 20:44:17 and 20:52:43. Process table at
21:06, roughly thirteen minutes after the first run finished:

```
PID PPID STARTED COMMAND
98417 1 20:44:20 bun run .../gstack/browse/src/server.ts
2065 1 20:44:33 bun run .../gstack/browse/src/server.ts
5820 1 20:44:46 bun run .../gstack/browse/src/server.ts
20882 1 20:45:18 bun run .../gstack/browse/src/server.ts
25602 1 20:52:58 bun run .../gstack/browse/src/server.ts
29406 1 20:53:12 bun run .../gstack/browse/src/server.ts
44323 1 20:53:42 bun run .../gstack/browse/src/server.ts
```

Seven servers — four from the first run, three from the second — each started
within the first ~60s of its run. Three `browse/src/terminal-agent.ts` children
were still attached to them.

The first run also hit `shard 2/6 exceeded the 435s wall-clock deadline —
killed the process group`, so at least some of these outlived an explicit
group kill. The second run completed normally — every shard reported — and it
still left three.

**Why the existing cleanup does not catch them.** `runShardChild` calls
`killProcessGroup(child, 'SIGKILL')` on both the timeout path and the clean
path ("Reap survivors of this shard even on the clean path",
`scripts/test-free-shards.ts:1293`). That kills the *shard's* process group.
A child spawned with `detached: true` becomes the leader of its own group, so
a group kill aimed at the shard does not reach it — which is the same property
`session-runner.ts` relies on deliberately when it calls `killProcessGroup` on
its own child. Anything that starts a browse server and does not itself group-kill
it on teardown therefore leaks, and a shard killed from outside never runs its
in-process cleanup at all.

**Impact.** Each leaked server holds a port and memory for the lifetime of the
machine session. Seven accumulated from two runs; a day of local test iterations
would leave dozens. It also makes later runs noisier — a stale server on a
port a fresh test expects is a plausible source of the kind of intermittent
failure that gets triaged as flake.

**Not reported here.** I could not confirm the related claim that the
`claude` provider itself is orphaned; no orphaned `claude` process appeared in
the same window, and `session-runner.ts:266-271` group-kills it explicitly.

**Suggested direction** (not a patch — the right owner boundary is not obvious
from outside): have whatever starts a browse server for a test register it for
group-kill on shard teardown, the way `session-runner.ts` does for its own
child, so the reap on `scripts/test-free-shards.ts:1293` covers it.

**Cleanup is harder than it looks** (found while reaping these by hand):

- `browse stop` cannot reach them. It resolves the daemon through `readState()`
(`/.gstack/browse.json`), and shards run with an isolated state dir that
`runShardChild` deletes on teardown (`fs.rmSync(stateDir, …)`). The state file
is gone while the process lives on, so the supported stop path has nothing to
look up.
- They ignore `SIGTERM`. Seven servers survived a group `SIGTERM`; only group
`SIGKILL` cleared them.
- Killing the `terminal-agent` alone is not enough — the server respawns it
(`terminal-agent-control.ts` names a "v1.44 watchdog/respawn loop"). A new
agent appeared ~17 minutes after the run, re-parented to a leaked server.
Kill the server, not the agent.

**Repro**

```bash
bun run test:free
# after it finishes:
ps -eo pid,ppid,lstart,command | grep '[b]rowse/src/server.ts' | awk '$2==1'
```

Contributor guide

Open the contributing guide

Research direction

Start with scripts/test-free-shards.ts:1293 and trace how browse/src/server.ts is started for each shard, comparing its lifecycle with session-runner.ts. Reproduce with bun run test:free and inspect for orphaned server.ts processes after clean completion and timeout; done means the shard teardown reaps those servers without leaving them behind.

Written by the indexing model from the issue text.

Assessment

Tech stack
bun, typescript
Domain
testing-qa, tooling
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.