Repository navigation
session tree M2 (1/2): a js-debug fork is a child session behind an explicit autoAttachChildProcesses (#1082) - #1094
Merged
Merged
Conversation
…hild session (#1082) M2 of the session tree, first slice: with `autoAttachChildProcesses: true` every process js-debug reports after the program's own target — a fork, a spawn of process.execPath, a cluster worker, a grandchild — is adopted through the M1 mechanism as a child debug session instead of being released to run undebugged (#501). The rule: in a js-debug worker the root connection's first report is the program, adopted in-session by ChildSessionManager as before (the #704 launch-ready signal); every other report, on any connection, is a child report held by ChildConnections. Every non-root connection runs the child-safe policy, so no connection but the root ever builds a manager. - The adoption contract gains `readChildReport` (report recognition) and a root config for `buildChildAttachArguments`; js-debug pins `throwaway-attach` / `launch-session` / `release` with the measured attach body (`{ type, request: 'attach', __pendingTargetId, continueOnAttach }`, an attach root's forwardable extras riding along, #466). The js policy's `handleReverseRequest` adopts the first pending target in-session and hands every later one up as a child report, unanswered. - `createChildSafePolicy` moves to its own module: for a contract policy it reads every report through the contract and never adopts in-session; the manager's forward-to-release path goes with it. The program target's initialize advertises `supportsStartDebuggingRequest` too. - A fork is reported on the program target's connection: the manager relays the held request as `childReport`, naming that connection, and MinimalDapClient hands it to the root's holder (`child-report` now carries the origin client, which is where the answer goes). - The throwaway lever in ChildConnections: a js-debug report is acked the moment it is held (measured: the answer decides nothing; refusing parks the child forever) and never answered again; release opens a fresh child-safe connection and runs the #501 sequence (`src/proxy/throwaway-release.ts`: initialize → configurationDone → attach → ready signal → disconnect with the target kept, capped 20 s). A process the released child reports in that window is held on the session the released child was reported on. - `childOutput: 'launch-session'` gets its consumer: a js child's `get_output` and output resource answer `outputOn: <root id>` with a note, since the tree's output arrives on the launch session's connection only. - `closeChild: 'release'` now shapes the close itself: a js child's connection closes with `terminateDebuggee: false` (measured no-op either way; the flag matches the answer `released: true`), in the root's cascade as well. - The contract test pins js-debug's contract and allows the one policy whose program is itself a pending target to combine it with in-session adoption, with per-engine report fixtures. `ChildSessionManager.releaseUndebugged` is unreachable for js-debug after this and stays as the manager's own invariant; its removal is a follow-up. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015BDrn6AsHQ4rmTXFHzQkn8
…ogpoint against real js-debug (#1082) The fixture set under tests/fixtures/javascript/subprocess-tree mirrors the Python one: parent.js forks the child named by argv, child.js ticks three times over a breakpoint line, child_grand.js spawns grandchild.js synchronously, parent_exits.js exits with its fork paused, parent_two.js and cluster_parent.js wait for the second PR's suites. javascript-subprocess-tree.test.ts, seven cases on the shipped server over stdio, each naming `autoAttachChildProcesses: true`: - the designed flow: a breakpoint in child.js only, the root's wait answers the fork's three stops with its sessionId and `children`, the fork's own output answers `outputOn: <root>`, the root then ends with exit 0 and the whole tree's output; - the grandchild chain: three rows, the grandchild under the fork, the root's and the fork's waits both report it; - closing a paused fork: `released: true`, the fork runs on to its end; - restart closes the previous run's fork (`childrenClosed: 1`), closing the root takes the fork with it; - cap 0: the program is still adopted, the fork is released by the throwaway attach and completes, `childrenReleased: 1`; - the measurement M2 owed the design doc: an adopted fork paused on a breakpoint ends the moment its parent exits (js-debug owns the tree); - a logpoint set on the parent prints from the fork into the launch session's output. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015BDrn6AsHQ4rmTXFHzQkn8
…he JavaScript child-process section (#1082) The design doc's status names M1 complete (#1087, #1090, #1091) and M2's two PRs; its lifecycle row 2 and the Exit section state what the suite measured: a js-debug program's exit ends an adopted, paused child at once. The JavaScript README's child-process section describes the tree (what a fork answers, `outputOn`, a child's close as a release, the throwaway release, logpoints yes and function breakpoints no, the parent's exit ending the tree); CLAUDE.md's proxy paragraph covers the js-debug routing; the changelog fragment says what shipped and that the default still flips in the next step. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015BDrn6AsHQ4rmTXFHzQkn8
…is ignored (#1082) The fixtures are hand-written ESM debuggees, not TypeScript output; without the negation the ignore rule for compiled tests makes `git add` skip them silently, the way PR #252 once lost one (the debug-scripts negation above it is the precedent). The suite committed before this would have run on CI against files that were never pushed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015BDrn6AsHQ4rmTXFHzQkn8
…d go, and the fixtures hide their windows (#1082) An adoption failure under the throwaway lever answered nothing (the report was acked when held) and the server's release command that follows found the report already taken, so the fork stayed parked forever. ChildConnections now lets a taken report's child go by the policy's lever in both places: release() and the adoption's failure path share giveUp(), which refuses a debugpy report or runs the throwaway attach for a js-debug one. The fixtures' fork/spawnSync calls pass windowsHide: true (the #843 guard scans every tracked script under tests/), and parent_exits.js waits 12 s before exiting so a slow CI runner can adopt the fork and observe its stop before the parent ends it (the measured row). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015BDrn6AsHQ4rmTXFHzQkn8
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
This was referenced Nov 6, 2026
debugmcpdev
added a commit
that referenced
this pull request
Nov 6, 2026
…hes (#1096) * feat(javascript): autoAttachChildProcesses defaults to true on launch 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 * test(javascript): the attach, cascade and cluster suites of the session 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 * docs(session-tree): M2 complete — JavaScript child processes debugged 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 * docs: the docs index no longer calls the session tree unshipped; two sentence shapes (#1082) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015BDrn6AsHQ4rmTXFHzQkn8 * test(javascript): the attach fixture forks with execArgv: [] — a child 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 * test(javascript): the tree suites resume js-debug's first-statement pause 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 * test(javascript): the attach fixture forks once js-debug's bootloader 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 * fix(javascript): guard child auto-attach across forwarded targets --------- Co-authored-by: JF <john.franklin@gmail.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
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.
M2 of the session tree (design, #1082), first of two PRs: with
autoAttachChildProcesses: true, every Node process js-debug reports after the program's own target becomes a child debug session — afork(), aspawn(process.execPath, …), aclusterworker, a grandchild — through the mechanism M1 built for debugpy (#1081), instead of being released to run undebugged (#501). The second PR flips the launch and attach defaults, adds the attach/cluster/cascade suites and rewrites the attach smoke and the docs sweep; it closes #1082.The rule
In a js-debug worker the root connection's first report is the program, adopted in-session by
ChildSessionManageras before (the #704 launch-ready signal). Every other report, on any connection, is a child report: held byChildConnections, adopted as a child session on a fresh connection to the same js-debug server, or released by a throwaway attach. Every non-root connection runs the child-safe policy, so no connection but the root ever builds a manager.What js-debug's measured shapes change (docs/javascript/spike-notes-forks.md)
childReport, naming the connection it must be answered on;MinimalDapClienthands it to the root's holder (child-reportnow carries the origin client).success: false, which parks a js-debug child forever. A js-debug report is now acked the moment it is held (the ack decides nothing) and never answered again; release opens a fresh child-safe connection and runs js attach to a Node process that forks children strands those children (second startDebugging target ignored, child waits forever) #501's sequence (src/proxy/throwaway-release.ts: initialize → configurationDone → attach{ __pendingTargetId, continueOnAttach: true }→ ready signal → disconnect with the target kept, 20 s cap). A process the released child reports in that window is held on the session the released child was reported on. An adoption that fails lets the child go the same way (the server's release command that follows would find the report already taken).readChildReport(the design's "report recognition" member) and a root config forbuildChildAttachArguments; js-debug's is the measured second-connection body, an attach root's forwardable extras riding along (adapterConfig: unknown keys are accepted silently — a typo of a supported key (pathMapping vs pathMappings) gets no warning #466), a launch root's keys never.ChildConnections.adoptbuilt its clients with the raw policy, which for js-debug would construct aChildSessionManager+ CDP bridge per fork connection;createChildSafePolicy(its own module now) is what every non-root connection runs, and it reads every report through the contract.childOutput: 'launch-session'gets its consumer: a launch child'sget_outputand output resource answeroutputOn: <root id>, preserving any child-local diagnostics. Children under an attach root retain their own console output, measured against js-debug v1.140.0.closeChild: 'release'shapes the close itself: a js child's connection closes withterminateDebuggee: false(either flag leaves the target running, measured) and the answer saysreleased: true, in the root's cascade as well.Measured in this PR (the fact M2 owed the design doc)
An adopted js-debug child, paused on a breakpoint, does not survive its parent's exit: its
terminatedarrives the moment the parent exits (no exit code), the child row goesstopped, the root ends with its own exit code. The launch owns the tree, as the spike found for unreported children. Lifecycle row 2 and the Exit section of the design doc now say so; the JavaScript README tells the agent to keep the parent alive while inspecting a child.Also measured: logpoints set on the parent carry into a fork and print into the launch session's output; function breakpoints do not carry (they are armed over CDP on the program's own target) — documented, a follow-up if wanted.
Release and output hardening
Throwaway releases handle socket errors and closure throughout the handshake, capture ready events before the attach response, and stop sending requests after their deadline. The worker tracks release connections and drains them before root disconnect or shutdown, including reports arriving during release; JavaScript gets 25 seconds of worker-exit grace to cover the 20-second release deadline and cleanup. Regression tests cover these failure and shutdown paths, attach-child output, and output-resource diagnostics. Fork logpoints use a
console.logfallback; replacing it in the child can suppress those messages, now documented.Evidence
tests/adapters/javascript/integration/javascript-subprocess-tree.test.ts(seven cases on the shipped server over stdio, real js-debug v1.140.0, runs inbuild-and-teston both OSes): the designed flow (breakpoint inchild.jsonly; the root's wait answers the fork's three stops with itssessionIdandchildren;outputOn; the root ends 0), the grandchild chain (three rows, the grandchild under the fork, both waits report it), closing a paused fork (released: true, it runs on), restart (childrenClosed: 1) and root close, cap 0 (program adopted, fork released by the throwaway attach,childrenReleased: 1), the parent-exit measurement, the logpoint.ChildConnectionsunder the throwaway lever (hold acks once, adopt never answers again, release and the safety timer run the throwaway on a child-safe client, a report in the release window, a relayed report answered on its origin, failure and shutdown paths); the relay in the manager and the client;releaseByThrowawayAttach; the session layer'sreleasedclose andoutputOnFor; theget_outputpointer.tests/e2e/mcp-server-smoke-javascript-attach.test.tsrun locally (it is not in CI): all ten rounds pass, the two js attach to a Node process that forks children strands those children (second startDebugging target ignored, child waits forever) #501 fork rounds included — with the key opted in, forks are now adopted and still complete their handshake undercontinueOnAttach, anddetach_from_processreleases them.6b21788d: lint,typecheck:all(zero errors), clean build, personal-path audit,check:docs(150 files), andchangelog:checkpassed. The required pre-push run passed 408 unit files (8,159 tests, 5 skipped) and 31 integration files (149 tests, 1 skipped).Not in this PR
autoAttachChildProcesseson launch and attach, the schema text and its 24 snapshots), the attach suite onexamples/javascript/fork_attach_target.js, the cluster and cascade suites, the e2e attach smoke expecting adoption, the docs sweep (docs/tool-reference.md, the skill references,README.md's stale "not yet shipped" line) and thechangedentry: the second PR.ChildSessionManager.releaseUndebuggedis unreachable for js-debug after this and stays as the manager's own invariant; removing it (and its tests) is a small follow-up.Part of #1005. Design:
docs/architecture/session-tree.md.🤖 Generated with Claude Code
https://claude.ai/code/session_015BDrn6AsHQ4rmTXFHzQkn8