Skip to content
Merged
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ The codebase follows a **layered architecture with dependency injection** and **
- Collaborators see the facade through `OperationsContext` (`operations-context.ts`), whose members are late-bound arrows and getters — never captured references — so reassigning a facade method or writing a tunable on a live instance is visible to them; each takes the narrowest `Pick<>` slice it uses. Collaborator-to-collaborator dependencies are constructor arguments (`DebugLauncher(ctx, proxyLauncher, breakpoints, inFlight)`, `AttachController(ctx, proxyLauncher, breakpoints, pauseCoordinator, inFlight)`) held as captured instance references: their methods resolve at call time, so `vi.spyOn(internals(ops).proxyLauncher, 'start')` intercepts, but reassigning a collaborator field after construction is not observed. The facade owns one shared `InFlightGuard` (`in-flight-guard.ts`, issue #711) that the launcher and the attach controller both take: `startDebugging`, `restartDebugging`, `attachToProcess` and `detachFromProcess` each claim the session before their first await and release it in `finally`, so one launch-shaped call per session at a time; `restartDebugging` replays through the launcher's private `launch()` under its own claim, never through the facade's `startDebugging`. Shared per-request DAP helpers live in `dap-request-helpers.ts`
- Coordinates ProxyManager instances (one per session)
- Handles breakpoint management and queuing
- The session tree (issue #1081, `docs/architecture/session-tree.md`): a child the adapter reports (debugpy; on by default, `dapLaunchArgs: { subProcess: false }` opts a launch out) is adopted by `SessionManagerCore.adoptReportedChild` as a session of its own — `parentSessionId`, the parent's language and breakpoints, a child proxy view over the parent's worker wired with the same handlers — under the per-tree cap of `child-session-cap.ts` (`DEBUG_MCP_MAX_CHILD_SESSIONS`, default 32, `0` off; beyond it the child is released); `closeSession` closes a subtree deepest first, a root that ends with live children defers its worker's reap until the last ends (`stopOrDeferWorker`), and the launch-shaped tools and the mirror refuse a child id by name (`handlers/shared.ts` `childSessionRefusal`). The tree-aware wait: `ExecutionController.waitForStop` runs on `waitForSubtreeSettle` (`execution/session-state-wait.ts`, the `waitForSettle` primitive shared with `waitForSessionState`) over the core's subtree listeners (`onSubtreeStateChange`, fired for a session and each ancestor on every transition and removal) and `selectSettledInTree` (`execution/tree-settle.ts`, the four-rule read: the session itself when paused or errored, else the first paused or errored descendant depth first in adoption order, else the session itself when ended, else keep waiting; `describeTreeAnswer` builds `children`/`childrenReleased`). The answer fields: `sessionId` (the selected session; set by the wait handler in `handlers/execution-tools.ts`), `children`/`childrenReleased`/`childrenClosed` (hoisted beside `pending` by `handlers/shared.ts` `hoistTreeFields` on the wait, launch, restart and detach answers) and `processEnded`/`released` (spread by the `close_debug_session` handler from the `closeSessionReporting` outcome — the close outcome it answers from, `childrenClosed` included); `releaseDescendants` is the detach cascade (children detached deepest first with `terminateDebuggee: false`, their rows kept as `stopped` with `programEnd.data.released`, then the parent)
- The session tree (issue #1081, `docs/architecture/session-tree.md`): a child the adapter reports (debugpy and js-debug; on by default for launches and verified-local JavaScript attaches, `dapLaunchArgs: { subProcess: false }` or `{ autoAttachChildProcesses: false }` opts a launch out, `adapterConfig: { autoAttachChildProcesses: false }` a JavaScript attach) is adopted by `SessionManagerCore.adoptReportedChild` as a session of its own — `parentSessionId`, the parent's language and breakpoints, a child proxy view over the parent's worker wired with the same handlers — under the per-tree cap of `child-session-cap.ts` (`DEBUG_MCP_MAX_CHILD_SESSIONS`, default 32, `0` off; beyond it the child is released); `closeSession` closes a subtree deepest first, a root that ends with live children defers its worker's reap until the last ends (`stopOrDeferWorker`), and the launch-shaped tools and the mirror refuse a child id by name (`handlers/shared.ts` `childSessionRefusal`). The tree-aware wait: `ExecutionController.waitForStop` runs on `waitForSubtreeSettle` (`execution/session-state-wait.ts`, the `waitForSettle` primitive shared with `waitForSessionState`) over the core's subtree listeners (`onSubtreeStateChange`, fired for a session and each ancestor on every transition and removal) and `selectSettledInTree` (`execution/tree-settle.ts`, the four-rule read: the session itself when paused or errored, else the first paused or errored descendant depth first in adoption order, else the session itself when ended, else keep waiting; `describeTreeAnswer` builds `children`/`childrenReleased`). The answer fields: `sessionId` (the selected session; set by the wait handler in `handlers/execution-tools.ts`), `children`/`childrenReleased`/`childrenClosed` (hoisted beside `pending` by `handlers/shared.ts` `hoistTreeFields` on the wait, launch, restart and detach answers) and `processEnded`/`released` (spread by the `close_debug_session` handler from the `closeSessionReporting` outcome — the close outcome it answers from, `childrenClosed` included); `releaseDescendants` is the detach cascade (children detached deepest first with `terminateDebuggee: false`, their rows kept as `stopped` with `programEnd.data.released`, then the parent)

4. **ProxyManager** (`src/proxy/proxy-manager.ts`)
- Manages communication with debug proxy process
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,7 @@ Then get the local variables:
- 🔧 [Adapter Development](./docs/architecture/adapter-development-guide.md) – Add new languages
- 🔌 [Dynamic Loading Architecture](./docs/architecture/dynamic-loading-architecture.md) – Runtime discovery, lazy loading, caching
- 🧩 [Adapter API Reference](./docs/architecture/adapter-api-reference.md) – Adapter, factory, loader, and registry contracts
- 🌳 [Session Tree Design](./docs/architecture/session-tree.md) – A debuggee's child processes as debug sessions (design for issue #1005, not yet shipped)
- 🌳 [Session Tree Design](./docs/architecture/session-tree.md) – A debuggee's child processes as debug sessions (issue #1005; shipped for Python, #1081, and JavaScript, #1082)
- 🔄 [CHANGELOG](./CHANGELOG.md) – Release history and upgrade notes
- 🐍 [Python Debugging Guide](./docs/python/README.md) – Python-specific features
- 💎 [Ruby Debugging Guide](./docs/ruby/README.md) – Ruby debugging with `rdbg`, including remote attach
Expand Down
1 change: 1 addition & 0 deletions changelog.d/1082-default.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
**JavaScript child processes are debugged by default on launch and verified-local attach** — a Node launch now sends js-debug `autoAttachChildProcesses: true` unless the caller says otherwise; every attach form, including `processId`, defaults it on only when the inspector target can read both a temporary server-side probe file and the installed js-debug bootloader files. An unverified target defaults it off with a warning, preventing a server-local bootloader path from breaking later forks in a remote or container target; loopback addresses and shared PID namespaces alone do not prove filesystem access. An explicit attach `true` is retained for callers that ensure the target and its children can load the server's js-debug bootloader and reach its debugger endpoint. With child auto-attach enabled, every Node process the program starts (`fork()`, `spawn(process.execPath, …)`, a `cluster` worker, a grandchild) is reported and becomes a child debug session: listed with `parentSessionId`, stopping on the parent's breakpoints, reported by the parent's `wait_for_stop` with `sessionId` naming it. Each child counts against `DEBUG_MCP_MAX_CHILD_SESSIONS` (default 32); beyond the cap a fork is released by a throwaway attach to run undebugged rather than parked (#501). Opt out per launch with `dapLaunchArgs: { autoAttachChildProcesses: false }`, per attach with `adapterConfig: { autoAttachChildProcesses: false }` (the children then run untouched, as before), or server-wide with `DEBUG_MCP_MAX_CHILD_SESSIONS=0`. A launch child's output is on the launch session's buffer (`get_output` answers `outputOn`); an attach root's children keep their own console output (#1005, #1082).
2 changes: 1 addition & 1 deletion changelog.d/1082.added.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@
**JavaScript forks become child debug sessions** — a Node launch with `dapLaunchArgs.autoAttachChildProcesses: true` used to adopt the program itself and release every `fork()`, `spawn(process.execPath, …)` or `cluster` worker to run undebugged (#501); js-debug's report of each is now adopted the way debugpy's is (#1081): every process after the program's own becomes a debug session of its own, listed by `list_debug_sessions` with `parentSessionId`, stopping on the parent's breakpoints (re-sent before it runs), reported by the parent's tree-aware `wait_for_stop` with its `sessionId`, with its own `wait_for_stop` and `close_debug_session`; a grandchild hangs under the fork that started it. What js-debug's measured shapes change: the whole tree's output stays on the launch session's buffer (a child's `get_output` and resource answer `outputOn: <launch session id>` instead of an empty page), `close_debug_session` on a child releases it to run on (`released: true` — js-debug cannot end a target it adopted), and a child the server does not adopt (the cap, `DEBUG_MCP_MAX_CHILD_SESSIONS`) is released by a throwaway attach rather than a refusal, which would park it forever. The key still defaults to `false`; the default flips in the next step of #1082 (#1005, #1082).
**JavaScript forks become child debug sessions** — a Node launch with `dapLaunchArgs.autoAttachChildProcesses: true` used to adopt the program itself and release every `fork()`, `spawn(process.execPath, …)` or `cluster` worker to run undebugged (#501); js-debug's report of each is now adopted the way debugpy's is (#1081): every process after the program's own becomes a debug session of its own, listed by `list_debug_sessions` with `parentSessionId`, stopping on the parent's breakpoints (re-sent before it runs), reported by the parent's tree-aware `wait_for_stop` with its `sessionId`, with its own `wait_for_stop` and `close_debug_session`; a grandchild hangs under the fork that started it. What js-debug's measured shapes change: the whole tree's output stays on the launch session's buffer (a child's `get_output` and resource answer `outputOn: <launch session id>` instead of an empty page), `close_debug_session` on a child releases it to run on (`released: true` — js-debug cannot end a target it adopted), and a child the server does not adopt (the cap, `DEBUG_MCP_MAX_CHILD_SESSIONS`) is released by a throwaway attach rather than a refusal, which would park it forever (#1005, #1082).
1 change: 1 addition & 0 deletions changelog.d/1096-review.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
**Forwarded JavaScript attaches keep child processes runnable** — the default child-auto-attach setting now checks that the inspected target can read the debugger host's temporary storage and js-debug bootloader files before enabling injection. A localhost SSH or Kubernetes port forward can reach a different filesystem; injecting the host-only preload there made later Node children fail with `MODULE_NOT_FOUND`. If locality cannot be verified, attachment still succeeds with child auto-attach off and a warning. Explicit `adapterConfig.autoAttachChildProcesses` values are preserved; embedders bypassing the adapter transform default off without a locality check. The sibling-session lifecycle test also waits for both children's real breakpoints before closing one, avoiding a race with js-debug's initial pause.
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ Start with the [architecture overview](architecture/README.md); it indexes the r
[component design](architecture/component-design.md) the per-module detail, and
[adapter development](architecture/adapter-development-guide.md) the guide to adding a language.
The [session tree](architecture/session-tree.md) is the design for a debuggee's child processes as
sessions of their own (issue #1005 part B; not yet shipped).
sessions of their own (issue #1005 part B: shipped for Python, #1081, and JavaScript, #1082).
Cross-cutting patterns are in [`patterns/`](patterns/), and the
[logging format](logging-format-specification.md) and
[error handling](error-handling-guide.md) specs sit alongside.
Expand Down
57 changes: 37 additions & 20 deletions docs/architecture/session-tree.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@ Status: decisions taken 2026-11-03 (listed with their owners at the end); merged
cascade answers) in [#1090](https://github.com/debugmcp/mcp-debugger/pull/1090), PR 3 (on by
default, the attach measurement, the lifecycle suites) in
[#1091](https://github.com/debugmcp/mcp-debugger/pull/1091). M2
([#1082](https://github.com/debugmcp/mcp-debugger/issues/1082), js-debug) ships in two PRs: the
first lands the mechanism behind an explicit `autoAttachChildProcesses: true` (every report after
the program's own is a child session, the throwaway release, `outputOn`, a child's close as a
release, the fork/grandchild suite and the parent-exit measurement below); the second flips the
launch and attach defaults and adds the attach, cluster and cascade suites.
([#1082](https://github.com/debugmcp/mcp-debugger/issues/1082), js-debug) is complete: PR 1
(the mechanism behind an explicit `autoAttachChildProcesses: true` — every report after the
program's own is a child session, the throwaway release, `outputOn`, a child's close as a release,
the fork/grandchild suite and the parent-exit measurement below) merged in
[#1094](https://github.com/debugmcp/mcp-debugger/pull/1094), PR 2 (the launch and verified-local
attach defaults on, the attach, cascade and cluster suites) follows it.
Scope: debuggee children reported by the adapter (debugpy subprocesses, js-debug forks and, later,
browser targets). The tree is additive to the shipped tool surface: no new tool, no changed
parameter, new fields only.
Expand Down Expand Up @@ -227,12 +228,21 @@ Two measured effects the answers name:

Adoption is on by default. The Python launch transform stops defaulting `subProcess` to `false`
(that default existed only because of the park hang, which adoption removes; the warning that
names the hang goes with it) and the JavaScript launch and attach transforms default
`autoAttachChildProcesses` to `true` (the attach path's `false` was #501's wedge guard, which
adoption replaces; the `attach_to_process` schema text that states it changes with it). An
names the hang goes with it) and the JavaScript launch transform defaults
`autoAttachChildProcesses` to `true`. The JavaScript attach transform defaults it on only for a
verified-local target: every attach form, including `processId`, checks that the inspector
target can read a temporary probe file created by the server and the installed js-debug
`bootloader.js` and `watchdog.js`. An unverified target defaults it off with a warning, because
injecting a server-local bootloader path can break its later forks. A loopback address or shared
PID namespace alone does not establish filesystem access. An explicit attach boolean is retained;
`true` requires the target and its children to be able to load the server's js-debug bootloader
and reach its debugger endpoint. The raw policy fallback stays off when no transformed or
explicit value is available. Adoption replaces #501's wedge guard for local targets; it cannot
make a host-only bootloader available remotely. An
explicit `false` in `dapLaunchArgs` turns adoption off for that launch and children run
undebugged, as they did before the tree (an attach's opt-out is the target's own flag, measured
below). Each flip ships in the milestone that
undebugged, as they did before the tree (JavaScript attach uses
`adapterConfig: { autoAttachChildProcesses: false }`; Python attach uses the target's own flag,
measured below). Each flip ships in the milestone that
makes it safe (M1 for Python, M2 for js-debug), as a documented behaviour change with a `changed`
changelog entry, because it changes what a launch that spawns Python or Node processes shows:
child rows for every xdist worker, autoreloader child or script that shells out.
Expand Down Expand Up @@ -492,17 +502,22 @@ rows, while close removes them. Seams: `src/proxy/minimal-dap.ts`
(rows), `packages/shared/src/interfaces/adapter-policy*.ts` (the contract),
`packages/adapter-python/src/python-debug-adapter.ts` (the default).

**M2 — js-debug N targets ([#1082](https://github.com/debugmcp/mcp-debugger/issues/1082)).** `ChildSessionManager`'s single-child gate becomes the per-tree cap;
**M2 — js-debug N targets ([#1082](https://github.com/debugmcp/mcp-debugger/issues/1082)), done.** `ChildSessionManager`'s single-child gate became the per-tree cap;
every report after the program's own becomes a child session; grandchild reports on child
connections are adopted instead of released; #501's throwaway attach stays the release lever and
the only one; `autoAttachChildProcesses` defaults on for launch and attach with a `changed` entry
(the `attach_to_process` schema text changes with it); the attach smoke that
today expects release ([`tests/e2e/mcp-server-smoke-javascript-attach.test.ts`](../../tests/e2e/mcp-server-smoke-javascript-attach.test.ts))
expects adoption. Reuse M1's subtree-close, idle, attach-target preservation and cleanup-isolation
cases; verify that cap `0` still adopts the program's own target but releases forks (the first PR's
suite does), and measure parent exit with an already adopted, paused child (measured in the first
PR: the child ends with the parent, lifecycle row 2). Unblocks #985 M2 (jest and vitest workers as
sessions).
the only one (PR 1, [#1094](https://github.com/debugmcp/mcp-debugger/pull/1094)).
`autoAttachChildProcesses` defaults on for launch and verified-local attach with a `changed` entry (the
`attach_to_process` schema text changed with it); the attach smoke
([`tests/e2e/mcp-server-smoke-javascript-attach.test.ts`](../../tests/e2e/mcp-server-smoke-javascript-attach.test.ts))
expects local adoption by default and untouched forks on opt-out; the attach suite
(`javascript-subprocess-attach.test.ts`: the designed flow over `node --inspect`, detach and close as
releases, the opt-out, #501's wedge guard) and the cascade suite (`javascript-subprocess-cascade.test.ts`:
row 1, a sibling's close as a release, a cluster primary's four workers) ride beside the fork/grandchild
suite of PR 1, which also verified that cap `0` still adopts the program's own target but releases
forks and measured parent exit with an already adopted, paused child (the child ends with the parent,
lifecycle row 2). Measured in PR 2: an attach root's children keep their own console output (no
`outputOn`; the attach owns no pipes), and `cluster.fork()` workers are reported like any fork.
Unblocks #985 M2 (jest and vitest workers as sessions).

**M3 — browser targets.** After the [#989](https://github.com/debugmcp/mcp-debugger/issues/989)
spike: the same mechanism, one session per page, frame and worker; the attach-addressing form is
Expand Down Expand Up @@ -548,7 +563,9 @@ Taken by the author of this document, marked so they can be overturned:
- Detach cascades as a detach (children released); restart closes and removes the previous run's
children; neither classifies children as `error`.
- The Python attach default flips in M1 only on its own measurement; the js-debug attach default
flips in M2 together with the launch default.
flips in M2 for targets with verified access to the server's temporary and debugger files,
together with the launch default. Unverified targets retain the safe off default unless the
caller explicitly opts in.

## Left to the builder

Expand Down
Loading
Loading