Skip to content

fix(js-debug): tag vsDebugServer argv and reap stranded instances (#431) - #432

Merged
debugmcpdev merged 1 commit into
mainfrom
fix/431-jsdebug-orphan-reap
Aug 22, 2026
Merged

fix(js-debug): tag vsDebugServer argv and reap stranded instances (#431)#432
debugmcpdev merged 1 commit into
mainfrom
fix/431-jsdebug-orphan-reap

Conversation

@debugmcpdev

Copy link
Copy Markdown
Collaborator

Problem

Fixes #431. js-debug DAP servers (vsDebugServer.cjs) are spawned detached:true + unref() by the proxy worker, and their PID never leaves the worker's memory. When the worker dies hard (win32 TerminateProcess — force-kill escalations, interrupted test runs), nothing can reach them: the startup orphan reaper's matcher requires the proxy-bootstrap marker, and win32 has no process-group kill. On the reporting dev box 14 of them had accumulated, holding file locks into vendor/js-debug/ that broke pnpm run clean:vendor with EPERM.

Fix (two prongs)

Cleanup — make stranded instances findable and reapable:

  • JavascriptDebugAdapter.buildAdapterCommand now appends --mcp-owner-pid=<pid> / --mcp-session-id=<id> after the host argument. vsDebugServer.cjs reads only argv[2] (port) and argv[3] (host) and ignores trailing tokens, so the markers are inert at runtime (same trick as the [FEATURE] Startup reaper for orphaned proxy chains (owner-PID tag), modeled on the JVM orphan reaper #343 proxy-worker tagging). Tokens are whitespace-free (the win32 scan splits CommandLine on whitespace) and never contain --help (which would make vsDebugServer print usage instead of serving).
  • The startup janitor feeds its existing single process scan through a third matcher (parseJsDebugAdapterArgs): two-factor — a vsDebugServer token AND a valid owner marker — so VS Code's own js-debug instances can never match. Reaping reuses the proxy reaper's kill paths; on win32 that's taskkill /PID <pid> /T /F, which also sweeps the debuggee/watchdog children js-debug spawned beneath the stranded server.
  • Marker constants moved to @debugmcp/shared (process-markers.ts) so the adapter package can import them; re-exported from proxy-orphan-reaper.ts so existing importers are untouched.

Prevention — stop creating the orphans in the first place:

  • On win32, ProxyProcessAdapter.kill() now fires taskkill /PID <workerPid> /T /F before the plain kill, while the worker is still alive (taskkill /T can only discover children through a live parent). This covers all three ProxyManager hard-kill sites with one seam. Guarded by the adapter's tracked exit state so an already-exited (possibly recycled) PID is never tree-killed. On win32 every childProcess.kill() signal was already a hard TerminateProcess, so tree semantics is strictly what was intended; POSIX behavior is unchanged.

Notes / accepted scope

  • The tree-kill also applies to the dap-core killProcess command path and the IPC-test path — both mean "kill the worker", so this is intended.
  • Double-reap race (worker reaper's /T killing the vsDebugServer before the adapter reaper gets to it) is benign: killWindows treats taskkill exit 128/1 as already-gone.
  • POSIX reap of a stranded vsDebugServer is single-PID (SIGTERMSIGKILL); a process-group sweep for its children is a possible follow-up, but the observed issue is win32-scoped.
  • Dry-run command snapshots now include the marker tokens (display-only).

Verification

  • TDD throughout; new tests: parseJsDebugAdapterArgs matcher (5), janitor third-matcher wiring (3 extended), adapter argv markers (3), win32 tree-kill in kill() (5).
  • Full unit suite: 214 files / 3857 tests green; coverage 93.01% stmts / 83.0% branch (4114 tests across projects).
  • Live repro on the reporting Windows box: started a JS launch session (markers visible in the process table), force-killed the proxy worker single-PID → vsDebugServer stranded with a dead PPID (bug reproduced); restarted the server → the janitor reaped the marked orphan within seconds, including the debuggee grandchild. No vsDebugServer processes remained.

🤖 Generated with Claude Code

js-debug DAP servers are spawned detached+unref'd by the proxy worker, so
a hard-killed worker (win32 TerminateProcess) stranded them outside every
tree-kill path; they accumulated as orphans holding file locks on the
vendor tree.

Two-prong fix:
- Tag the vsDebugServer argv with the same inert --mcp-owner-pid /
  --mcp-session-id markers the proxy worker carries (vsDebugServer reads
  only argv[2]/argv[3]; trailing tokens are ignored), and teach the
  startup janitor a third matcher over its existing process scan that
  reaps marked instances whose owner is dead - taskkill /T /F also
  sweeps the debuggee/watchdog children while the vsDebugServer parent
  is still alive. Marker constants move to @debugmcp/shared so the
  adapter package can import them.
- On win32, ProxyProcessAdapter.kill() now tree-kills the worker first
  (taskkill /PID <pid> /T /F) while it is still alive, so ProxyManager's
  force-kill escalations no longer strand the detached adapter subtree.
  Guarded by the adapter's tracked exit state to avoid PID-reuse kills.

Closes #431

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@codecov

codecov Bot commented Aug 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.00000% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/utils/startup-janitor.ts 77.77% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@debugmcpdev
debugmcpdev merged commit 6e31957 into main Aug 22, 2026
10 checks passed
@debugmcpdev
debugmcpdev deleted the fix/431-jsdebug-orphan-reap branch August 22, 2026 20:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

js-debug vsDebugServer.cjs children accumulate as orphans and hold locks on the vendor tree

2 participants