Repository navigation
Description
Problem
When squad watch or squad loop runs, it spawns Copilot CLI sessions which in turn spawn MCP server processes (enghub-mcp, workiq, playwright, ADO, nano-banana, etc.). When the parent process exits — whether cleanly, via Ctrl+C, or via crash — these child processes are NOT cleaned up on Windows.
Observed impact: After ~12 hours of squad watch running on a Windows machine, we found:
- 85 orphaned enghub-mcp node processes (~1 new every 10 minutes, never dying)
- 4 orphaned copilot.exe processes from sessions 33+ hours old
- 8 orphaned MCP servers (workiq, playwright, ADO, nano-banana) from old sessions
- 37 orphaned pwsh.exe processes
- Multiple frozen "Sign in to Microsoft" popup windows from orphaned auth flows
- The machine was noticeably slow from the accumulated process load
Root Cause
- No process group management on Windows. On Unix,
setsidcreates process groups that can be killed together. On Windows, there's no equivalent unless you explicitly use Job Objects. - MCP servers are stdio-based child processes. Each Copilot session spawns its own set of MCP servers. When the session ends, the MCP servers should die — but on Windows, they often don't (especially if the parent crashes or the signal chain is broken).
- Watch spawns fresh copilot sessions per round. The Execute capability spawns
copilot --messagewhich starts new MCP servers. Each round accumulates more if previous ones didn't clean up. - VS Code MCP auto-reconnect compounds it. If VS Code is also running with MCP configs, it spawns additional instances on reconnect without killing old ones.
Proposed Solution
1. PID tracking file
On watch startup and before each copilot session spawn:
- Write child PIDs to
.squad/.watch-pids(or{teamRoot}/.squad/.watch-pids) - Format: one PID per line, with process name and spawn timestamp
- The file is the "known children" registry
2. Windows Job Object (preferred, if available)
On Windows, create a Win32 Job Object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE:
- All child processes added to the job are automatically killed when the job handle closes
- This is the correct Windows equivalent of Unix process groups
- Requires
node-ffi-napior a small native addon — evaluate feasibility - Fallback: if Job Objects aren't available, use PID tracking + cleanup
3. Cleanup on exit
Register cleanup handlers for:
process.on('exit')— sync cleanup of known PIDsprocess.on('SIGINT')/process.on('SIGTERM')— graceful shutdownprocess.on('uncaughtException')— best-effort cleanup before crash
Cleanup logic:
- Read
.squad/.watch-pids - For each PID: check if still alive → kill with tree (
taskkill /T /F /PIDon Windows,kill -9 -pgidon Unix) - Delete the PID file
4. Stale PID cleanup on startup
When watch starts, check for .squad/.watch-pids from a previous run:
- If PIDs are still alive and older than the current watch PID → they're orphans → kill them
- Log:
⚠️ Cleaned up N orphaned processes from previous watch run
5. Periodic reaper (optional, for defense in depth)
Every N rounds (configurable, default: 10), scan for known MCP process patterns that don't belong to the current watch tree:
node.*enghub-mcpnode.*workiqnode.*playwright.*mcpcopilot.exewith no parent watch process- Kill with warning log
Implementation approach
| Component | File | Effort |
|---|---|---|
| PID tracker class | packages/squad-cli/src/cli/commands/watch/pid-tracker.ts |
~80 lines |
| Wire into watch startup/shutdown | watch/index.ts |
~20 lines |
| Wire into Execute capability (before/after copilot spawn) | watch/capabilities/execute.ts |
~15 lines |
| Stale cleanup on startup | watch/index.ts or pid-tracker.ts |
~30 lines |
| Tests | test/cli/watch-pid-tracker.test.ts |
~100 lines |
| Changeset | .changeset/ |
5 lines |
~250 lines total. No new dependencies for the PID tracking approach.
Context
Discovered during a debugging session with @tamirdresher. The enghub-mcp leak was the worst offender — 85 instances accumulating over 15 hours. The machine had to be manually cleaned up. This is a reliability issue for anyone running squad watch for extended periods.
Related: The Dispatch project (bradygaster/Dispatch) also hits this — its start.ps1 spawns squad loop which spawns copilot which spawns MCPs. When killed, 36 child processes were left behind.
Activity
fix(watch): track child PIDs and cleanup orphans on exit (github#921)
feat(watch): external capability loading + PID tracker (github#918, g…
feat(watch): external capability loading + PID tracker (#918, #921) (#…