Skip to content

fix(watch): track and cleanup orphaned MCP/child processes on exit #921

Closed

Description

@tamirdresher

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

  1. No process group management on Windows. On Unix, setsid creates process groups that can be killed together. On Windows, there's no equivalent unless you explicitly use Job Objects.
  2. 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).
  3. Watch spawns fresh copilot sessions per round. The Execute capability spawns copilot --message which starts new MCP servers. Each round accumulates more if previous ones didn't clean up.
  4. 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-napi or 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 PIDs
  • process.on('SIGINT') / process.on('SIGTERM') — graceful shutdown
  • process.on('uncaughtException') — best-effort cleanup before crash

Cleanup logic:

  1. Read .squad/.watch-pids
  2. For each PID: check if still alive → kill with tree (taskkill /T /F /PID on Windows, kill -9 -pgid on Unix)
  3. 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-mcp
  • node.*workiq
  • node.*playwright.*mcp
  • copilot.exe with 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions