fix(#501): stop stranding forked children of js attach targets - #514
Merged
Conversation
js-debug's pwa-node attach defaults autoAttachChildProcesses to true, so
attaching to a Node process that fork()s children injected the auto-attach
bootloader into it; every fork then parked under waitForDebugger and sent a
startDebugging reverse request that the single-child ChildSessionManager
silently dropped — the child never ran (its main module never executed).
Two complementary fixes:
- Default autoAttachChildProcesses to false on the js attach path (mirroring
launch mode), caller-overridable via adapterConfig and now listed in
supportedAttachKeys. Forks of an attach target run untouched by default.
- When a startDebugging target cannot be adopted (adoption in progress or a
child already active), release it instead of dropping it: a throwaway DAP
connection attaches with its __pendingTargetId (which makes js-debug run
the parked target) and detaches without terminating. The child runs
undebugged and a loud warning is logged. js-debug delivers fork auto-attach
requests on the adopted child's connection, so the child-safe policy now
forwards unadoptable targets back to the manager instead of squashing them.
createChildSession now reports an outcome ('adopted' | 'duplicate' |
'released' | 'release-failed'); MinimalDapClient rolls its adoptedTargets
back on 'release-failed' so a re-sent startDebugging can retry (parity with
the #249 rollback), and keeps released targets recorded — their server-side
pending deferred has settled and can never be adopted.
The dynamic MinimalDapClient import in ChildSessionManager is now cached in
a module-level promise: an adoption racing a release issued two concurrent
import() calls for the same module, which vitest's module mocker can resolve
inconsistently (one caller got the real module, the other the mock).
New fork_attach_target.js fixture (fork + IPC-ack pattern, mirroring
ProxyManager's worker handshake) and e2e coverage: forks keep completing
their IPC handshake while attached with the default config, and with
autoAttachChildProcesses:true the release path frees every unadoptable fork.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NgA5kHzkhFZLqbWNZRTsVG
debugmcpdev
enabled auto-merge (squash)
August 27, 2026 14:11
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
This was referenced Aug 27, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #501.
What
Attaching a javascript debug session to a Node process that
fork()s children left those children permanently wedged: js-debug's pwa-node attach defaults inheritautoAttachChildProcesses: true, so the auto-attach bootloader was injected into the target; every fork parked underwaitForDebuggerand sent astartDebuggingreverse request that the single-childChildSessionManagersilently dropped. The child's main module never ran.Two complementary fixes:
autoAttachChildProcesses: falseon the js attach path (transformAttachConfig, mirroring launch mode; defensive normalization inperformHandshakefor embedders). Caller-overridable viaadapterConfig— a supplied boolean is respected, never silently overwritten (cf. ruby attach across a container boundary can bind no breakpoints: localfs is silently overridden, localfsMap is inert #499) — and the key is now insupportedAttachKeysso opting in doesn't trip the unrecognized-key warning (adapterConfig: unknown keys are accepted silently — a typo of a supported key (pathMapping vs pathMappings) gets no warning #466 mechanism).startDebuggingtarget is now released to run undebugged: a throwawayMinimalDapClientperforms the minimal unpark sequence verified against the vendored bundle (initialize→configurationDone→attach {__pendingTargetId, continueOnAttach: true}→ ready-signal grace →disconnect {terminateDebuggee: false}), bounded at ~20s, never touching adoption state, with a loud warning.createChildSessionnow reports'adopted' | 'duplicate' | 'released' | 'release-failed';MinimalDapClientrolls back itsadoptedTargetsonly on'release-failed'(parity with the js-debug: failed child adoption is unretryable (pendingTargetId never removed from adoptedTargets) #249 rollback) so a re-sent request can retry — released targets stay recorded, since their server-side deferred has settled and can never be adopted.Two findings from validating against the real stack:
startDebuggingreceived, 1 adoption, 0 releases). The child-safe policy now forwards unadoptable targets back to the owning manager for release instead of squashing them.import('./minimal-dap.js')is now cached in a module-level promise: an adoption racing a release issued two concurrentimport()calls, which vitest's module mocker can resolve inconsistently (one caller received the real module, the other the mock).Testing
examples/javascript/fork_attach_target.js: forks a child every 2s; each child announces itself over IPC and the parent logschild-handshake N— the ProxyManager fork + init-ACK shape.mcp-server-smoke-javascript-attach.test.ts): forks keep completing their IPC handshake while attached with default config (direct js attach to a Node process that forks children strands those children (second startDebugging target ignored, child waits forever) #501 regression), and withautoAttachChildProcesses: truethe release path frees every unadoptable fork while parent debugging stays intact. Full suite: 6/6.adoptedTargetsretention/rollback.node --inspect … http) no longer breaks the subject's ability to fork proxy workers — 6/6 clean attach→drive→detach stress cycles (0/6 before). Case study PR to follow.🤖 Generated with Claude Code
https://claude.ai/code/session_01NgA5kHzkhFZLqbWNZRTsVG