Repository navigation
feat(javascript): enable child debugging for launches and local attaches - #1096
Merged
Merged
Conversation
… and attach — every Node child is a debug session (#1082) The session tree's M2 second slice: with the mechanism of #1094 in place (every process js-debug reports after the program's own is adopted as a child session, or released by a throwaway attach beyond the cap), the guard that defaulted the key off — #501's park, which could only happen while a report could be dropped — has nothing left to guard. The launch transform, the attach transform and the policy's self-contained attach guard now default `autoAttachChildProcesses` to `true`; an explicit `false` (dapLaunchArgs on launch, adapterConfig on attach) keeps the children undebugged as before. The `attach_to_process` schema text says what the key does now (a child debug session per fork, the opt-out), the `wait_for_stop` text and the skill text name JavaScript beside Python as on by default, and the 24 tool-list snapshots move with the two strings. The unit pins of the old default are inverted, with a launch twin. Measured before this flip, on the self-debug suite (an mcp-debugger attached to another): the nested server's forked proxy worker is adopted as a child session and still acknowledges its init inside the window. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015BDrn6AsHQ4rmTXFHzQkn8
…on tree, and the attach smoke expecting adoption (#1082) `javascript-subprocess-attach.test.ts` (real js-debug over `node --inspect`, runs in CI on both OSes): a fork started after the attach stops on the breakpoint set before it and the root ends when the target does; detach releases the paused fork and keeps both rows; close releases it and removes them; `adapterConfig: { autoAttachChildProcesses: false }` keeps the fork undebugged; and #501's wedge guard on `fork_attach_target.js`, whose forks now become child sessions and keep completing their IPC handshake. Measured here as #1094's review said: under an attach root a child's console output is on its own connection — its `get_output` has `child start` and no `outputOn`. `javascript-subprocess-cascade.test.ts` (no case names the key: the default is what it proves): row 1 — a fork that ends while the parent is paused leaves the parent paused with the fork listed `stopped`; closing one of two forks releases that fork alone (`released: true`, js-debug cannot end a target it adopted) and the survivor's stops keep arriving; a cluster primary's four `cluster.fork()` workers are four child sessions that each stop on the breakpoint in the worker branch. The fixture `attach_parent.js` forks `child.js` after one line on stdin, since js-debug's bootloader reaches only processes started after the attach. The e2e attach smoke's default round now expects child rows and flowing handshakes; its opted-in round became the opt-out round. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015BDrn6AsHQ4rmTXFHzQkn8
… by default, the opt-outs, outputOn, a js child's close as a release (#1082) The design doc's status says M2 shipped in #1094 and this PR, and its M2 paragraph records what PR 2 measured (an attach root's children keep their own output; cluster workers are reported like any fork). The JavaScript README's child-process section states the default and the three opt-outs. The tool reference names JavaScript beside Python wherever the tree is described (`parentSessionId` with js-debug's `<script> [<pid>]` name shape, the launch and restart answers, the attach-root detach), says a JavaScript child's close releases it, and documents `outputOn` on `get_output`. The skill reference, CLAUDE.md and the README's design-doc line no longer call the tree Python-only or unshipped. Changelog: the `changed` entry for the default; the unreleased `added` entry of #1094 no longer says the key defaults to false. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015BDrn6AsHQ4rmTXFHzQkn8
…sentence shapes (#1082) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015BDrn6AsHQ4rmTXFHzQkn8
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
…d inheriting --inspect is scanned as a nameless target (#1082) Windows CI failed two attach cases: the fork was adopted as `Child Process [<pid>]`, its breakpoint in child.js echoed back unbound, and the parent ran to its end. The fixture forked with Node's default `execArgv`, so under `node --inspect=<port>` the child inherited the flag; js-debug's process-tree scan attaches a watchdog to any child whose arguments carry an inspector port under that label (`scriptName: "Child Process"`, `dynamicAttach: true`), and on the slow runner that report won the race against the bootloader's own. `fork_attach_target.js` has avoided this with `execArgv: []` since #501; the fixture does the same, and the JavaScript README names the hazard for anyone attaching to a forking program started with --inspect. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015BDrn6AsHQ4rmTXFHzQkn8
…ause of an adopted child and wait on (#1082) The pre-push hook's full integration run failed the cluster case once: with four workers starting at once, one worker's first wait answered a stop at the module's first statement (line 9) instead of its breakpoint (line 29). The proxy log has js-debug's event — `stopped`, reason `pause`, "Paused on debugger statement", no hitBreakpointIds — its generic description for a CDP pause with no breakpoint hit, surfaced under load for a target attached with continueOnAttach; the js policy's debugger-statement rule then reported it as a breakpoint. The other three workers and every earlier run stopped on the breakpoint only. That stop is js-debug's entry pause, not the tree's doing: the shared child-stop helper of the attach, cascade and tree suites now resumes a child stop that is not at the expected line and waits again (at most twice), with the measurement in its comment. The mislabel is filed as a dogfood ticket. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015BDrn6AsHQ4rmTXFHzQkn8
… is in its environment — the injection lands after the attach answers (#1082) Windows CI, second run: three attach cases ended without a child row and with the child's breakpoint unbound. The proxy log has the attach answered at 14.472 s, the program target adopted by 14.573 s, and no `startDebugging` ever after: the fork ran unreported. js-debug injects its bootloader into an attached process by evaluating `process.env. NODE_OPTIONS` and `process.env.VSCODE_INSPECTOR_OPTIONS` in it over the inspector, with up to 200 retries, after the attach request is answered; the fixture forked on the harness's `go` written right after that answer, inside the window on a slow runner. The fixture's `go` now waits (at most 5 s) for `VSCODE_INSPECTOR_OPTIONS` to appear before it forks and says which way it went; `now` forks at once for the opt-out case, where nothing is injected. The designed-flow and opt-out cases pin those two lines. The JavaScript README names the window for anyone attaching to a program that forks on a trigger. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015BDrn6AsHQ4rmTXFHzQkn8
This was referenced Nov 6, 2026
Collaborator
Author
|
CI is green on |
| // resumed and its environment is unchanged. Sharing /tmp alone is not | ||
| // enough: a sidecar may share it without sharing the adapter installation. | ||
| const requiredFiles = ['bootloader.js', 'watchdog.js'].map(name => join(options.adapterDirectory!, name)); | ||
| const expression = `(async()=>{try{const fs=typeof process.getBuiltinModule==='function'?process.getBuiltinModule('node:fs'):typeof require==='function'?require('node:fs'):await import('node:fs');for(const p of ${JSON.stringify(requiredFiles)})fs.accessSync(p,fs.constants.R_OK);return fs.readFileSync(${JSON.stringify(file)},'utf8')===${JSON.stringify(nonce)}}catch{return false}})()`; |
| // resumed and its environment is unchanged. Sharing /tmp alone is not | ||
| // enough: a sidecar may share it without sharing the adapter installation. | ||
| const requiredFiles = ['bootloader.js', 'watchdog.js'].map(name => join(options.adapterDirectory!, name)); | ||
| const expression = `(async()=>{try{const fs=typeof process.getBuiltinModule==='function'?process.getBuiltinModule('node:fs'):typeof require==='function'?require('node:fs'):await import('node:fs');for(const p of ${JSON.stringify(requiredFiles)})fs.accessSync(p,fs.constants.R_OK);return fs.readFileSync(${JSON.stringify(file)},'utf8')===${JSON.stringify(nonce)}}catch{return false}})()`; |
| // resumed and its environment is unchanged. Sharing /tmp alone is not | ||
| // enough: a sidecar may share it without sharing the adapter installation. | ||
| const requiredFiles = ['bootloader.js', 'watchdog.js'].map(name => join(options.adapterDirectory!, name)); | ||
| const expression = `(async()=>{try{const fs=typeof process.getBuiltinModule==='function'?process.getBuiltinModule('node:fs'):typeof require==='function'?require('node:fs'):await import('node:fs');for(const p of ${JSON.stringify(requiredFiles)})fs.accessSync(p,fs.constants.R_OK);return fs.readFileSync(${JSON.stringify(file)},'utf8')===${JSON.stringify(nonce)}}catch{return false}})()`; |
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.
JavaScript launches now enable child-process debugging by default. Each Node fork, spawn, cluster worker or grandchild reported by js-debug becomes a child session with the parent's breakpoints, a
parentSessionId, and stops reported through the parent'swait_for_stop. JavaScript attaches enable the same default after verifying that the target can access the debugger's temporary storage and installed bootloader files.This completes M2 of the session-tree design (#1082, part of #1005), following #1094.
Behavior
dapLaunchArgs.autoAttachChildProcesseson launch andadapterConfig.autoAttachChildProcesseson attach remain authoritative.falseopts out. Explicit attachtruerequires the target to load the debugger's files and reach its local endpoint.DEBUG_MCP_MAX_CHILD_SESSIONS(default 32; 0 disables child adoption). Reports beyond the cap are released with a throwaway attach.get_outputat the launch buffer while preserving child diagnostics; attach children retain their own console output.Defects fixed during review
A localhost port forward can reach a different filesystem. With unconditional auto-attach, a Windows debugger injected its Windows bootloader path into a Linux Docker target; the target's next fork exited 1 with
MODULE_NOT_FOUND. The locality check prevents that injection. The same real-container reproduction now attaches successfully, warns that child debugging is off, leaves the target's debugging environment untouched, and the next fork exits 0.The sibling-close test now waits for both children to reach their actual breakpoints before closing one. This prevents the second child's initial js-debug pause from causing a false failure. Documentation also distinguishes attach-child output from launch-child output and preserves diagnostic entries.
Validation
The known js-debug first-statement pause/mislabel remains tracked in #1097; the subprocess tests handle that pause before asserting the intended breakpoint.