Skip to content

feat(javascript): enable child debugging for launches and local attaches - #1096

Merged
debugmcpdev merged 8 commits into
mainfrom
feat/1082-m2-default
Nov 6, 2026
Merged

debugmcpdev merged 8 commits into
mainfrom
feat/1082-m2-default

Conversation

@debugmcpdev

@debugmcpdev debugmcpdev commented Nov 6, 2026 •

Copy link
Copy Markdown
Collaborator

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's wait_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

  • Explicit dapLaunchArgs.autoAttachChildProcesses on launch and adapterConfig.autoAttachChildProcesses on attach remain authoritative. false opts out. Explicit attach true requires the target to load the debugger's files and reach its local endpoint.
  • A default attach checks every addressing form, including PID, before enabling injection. An unavailable or unverified target keeps child debugging off and receives an explanatory warning. Embedders bypassing the adapter transform default off without a locality check.
  • The per-tree cap remains DEBUG_MCP_MAX_CHILD_SESSIONS (default 32; 0 disables child adoption). Reports beyond the cap are released with a throwaway attach.
  • Launch children point get_output at 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

  • Clean build, lint, source/test type checks, documentation, changelog and personal-path checks passed.
  • Full unit suite: 409 files passed (8,177 tests passed, 5 skipped).
  • Final locality-guard suite: 20 tests passed, including five additional transport-failure cases; the test typecheck remains clean.
  • Full integration suite: 33 files passed (157 tests passed; one opt-in stress test skipped). This includes JavaScript attach, cascade, cluster, tree, launch configuration and self-debugging coverage.
  • Real Docker forwarded-attach regression reproduced before the fix and passed afterward.

The known js-debug first-statement pause/mislabel remains tracked in #1097; the subprocess tests handle that pause before asserting the intended breakpoint.

cynarlab and others added 4 commits November 6, 2026 08:57
… 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

codecov Bot commented Nov 6, 2026

Copy link
Copy Markdown

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
cynarlab and others added 2 commits November 6, 2026 09:51
…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
@debugmcpdev

Copy link
Copy Markdown
Collaborator Author

CI is green on f7f18273 (14/14). The Windows build-and-test job went green on its second --failed re-run; the two failures it passed through on the way were unrelated flakes, each in a different suite: #963's rdbg ECONNREFUSED through the connect window (sighting commented there), then the doctor CodeLLDB-version test's bare "Command failed" on a loaded runner (filed as #1098). The JavaScript session-tree suites passed on every Windows run from this head.

@debugmcpdev debugmcpdev changed the title session tree M2 (2/2): JavaScript child processes are debugged by default — the attach, cascade and cluster suites (#1082) feat(javascript): enable child debugging for launches and local attaches Nov 6, 2026
// 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}})()`;
@debugmcpdev
debugmcpdev merged commit 36e574d into main Nov 6, 2026
15 checks passed
@debugmcpdev
debugmcpdev deleted the feat/1082-m2-default branch November 6, 2026 21:00
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.

3 participants