Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 under `subProcess: true`) 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 session tree (issue #1081, `docs/architecture/session-tree.md`): a child the adapter reports (debugpy under `subProcess: true`) 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
1 change: 1 addition & 0 deletions changelog.d/1081-tree-wait.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
**`wait_for_stop` is tree-aware — set a breakpoint in the child's code, start the parent, wait** — a wait on a session now observes it and every child session its program spawned (Python `subProcess: true`) and answers for the session a fixed priority names: the session itself when it is paused, else the first paused descendant, else the session itself when it has ended; `sessionId` names the session the answer is about (inspect, step and continue with it), `children` lists every descendant with its state, and an ended child alone never completes the parent's wait. The `start_debugging` and `restart_debugging` answers carry the same `children`, plus `childrenReleased` (children run undebugged beyond `DEBUG_MCP_MAX_CHILD_SESSIONS`) and `childrenClosed` (a relaunch closes the previous run's children; a Flutter hot restart keeps them); `detach_from_process` on an attach root detaches the children first and leaves them running (`childrenReleased`, or `childrenClosed` with `terminateProcess: true`); `close_debug_session` says `childrenClosed` when it took descendants with it, `processEnded: true` for a debugpy child it ended, or `released: true` for an attach root whose targets run on. The Python `subProcess` default still flips in the last step (#1005, #1081).
2 changes: 1 addition & 1 deletion changelog.d/1081.added.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@
**Python child processes become child debug sessions under `subProcess: true`** — a Python launch with `dapLaunchArgs.subProcess: true` used to park every child the program spawned until a debugger attached, which the server never did; debugpy's report is now adopted: each child 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), with its own `get_output`, `wait_for_stop` and `close_debug_session`; closing the parent closes its children first; `DEBUG_MCP_MAX_CHILD_SESSIONS` caps a tree's live children (default 32, `0` turns adoption off; a child beyond it runs undebugged). The default stays off until the tree-aware `wait_for_stop` lands (#1005, #1081).
**Python child processes become child debug sessions under `subProcess: true`** — a Python launch with `dapLaunchArgs.subProcess: true` used to park every child the program spawned until a debugger attached, which the server never did; debugpy's report is now adopted: each child 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), with its own `get_output`, `wait_for_stop` and `close_debug_session`; closing the parent closes its children first; `DEBUG_MCP_MAX_CHILD_SESSIONS` caps a tree's live children (default 32, `0` turns adoption off; a child beyond it runs undebugged). The default stays off until the last step of #1081 flips it (#1005, #1081).
1 change: 1 addition & 0 deletions changelog.d/1090-review.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Session-tree teardown now ends child processes before the parent when `detach_from_process` requests termination, prevents new child adoption throughout detach, and clears output subscriptions whenever a session is removed, including during restart. Tree-aware waits reselect when a chosen child resumes or is closed while its answer is being read, preserving the original timeout. Closing a previously released child no longer claims its process was terminated (#1081, #1090).
10 changes: 5 additions & 5 deletions docs/KNOWN_ISSUES.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,11 @@ undebugged: a breakpoint in code that only a child executes does not hit. With a
`dapLaunchArgs: { subProcess: true }` on a launch, each Python child the program starts
(`subprocess`, `multiprocessing`, pytest-xdist workers) becomes a child debug session:
`list_debug_sessions` lists it with `parentSessionId`, it stops on the parent's breakpoints (re-sent
to it before it runs), and it has its own `get_output`, `wait_for_stop` and `close_debug_session`;
closing the parent closes its children first. Today a child is discovered through
`list_debug_sessions` (its row carries `parentSessionId`) and waited on with its own id. The default
stays `false` until the tree-aware `wait_for_stop` lands (#1081); the design is
[the session tree](architecture/session-tree.md).
to it before it runs), and it has its own `get_output` and `close_debug_session`; closing the parent
closes its children first. A `wait_for_stop` on the parent reports a child's stop with `sessionId`
naming the child and `children` listing the tree with each session's state; a child can still be
waited on by its own id. The default stays `false` until the last step of #1081 flips it; the design
is [the session tree](architecture/session-tree.md).

## Dart/Flutter: what the SDK debug adapter does differently

Expand Down
11 changes: 6 additions & 5 deletions docs/architecture/session-tree.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ in what order). It is built from the measured facts in
below cites one of those or a source named in the survey.

Status: decisions taken 2026-11-03 (listed with their owners at the end); merged in
[#1083](https://github.com/debugmcp/mcp-debugger/pull/1083); implementation in progress: M1 PR 1
(adoption behind an explicit `subProcess: true`) under [#1081](https://github.com/debugmcp/mcp-debugger/issues/1081),
then the tree-aware wait, then the default flip; M2 is [#1082](https://github.com/debugmcp/mcp-debugger/issues/1082).
Nothing here is shipped yet.
[#1083](https://github.com/debugmcp/mcp-debugger/pull/1083). Implementation under
[#1081](https://github.com/debugmcp/mcp-debugger/issues/1081): M1 PR 1 (adoption behind an
explicit `subProcess: true`) merged in [#1087](https://github.com/debugmcp/mcp-debugger/pull/1087);
PR 2 (the tree-aware wait and the cascade answers) in progress; then the default flip. M2 is
[#1082](https://github.com/debugmcp/mcp-debugger/issues/1082). Nothing is on by default yet.
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 @@ -112,7 +113,7 @@ exists). The agent then uses `sessionId` for inspection, stepping and continuing
paused reports the child (rule 2 before rule 3), because nothing can be done on the parent. The
microtask re-check that hides an auto-continued entry stop
([#849](https://github.com/debugmcp/mcp-debugger/issues/849)) is unchanged.
- Every answer on a session that has children carries `children: [{ sessionId, name, state }]`,
- Every answer on a session that has children carries `children: [{ sessionId, name, state, parentSessionId }]`,
the paused and the `pending: true` answers included, so a caller always sees what else is live or
paused and what to wait on next. The `start_debugging` answer carries the same list when a child
was adopted during the launch hold.
Expand Down
2 changes: 1 addition & 1 deletion docs/python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,4 +270,4 @@ Variable values, captured output, and evaluate results are scanned for credentia
3. Use source context to see code around your current position
4. The stack trace shows the call hierarchy that led to the current position
5. Expressions are evaluated against the current paused debug context (current frame when available), with the evaluation context defaulting to `'variables'` — reads and arithmetic are reliable, but verify a mutation such as `x = 5` with a follow-up evaluate before relying on it
6. Child processes run undebugged by default: debugpy's sub-process debugging is sent as `subProcess: false` until the tree-aware `wait_for_stop` lands ([#1081](https://github.com/debugmcp/mcp-debugger/issues/1081)). With `dapLaunchArgs: { subProcess: true }`, every Python child the program starts (`subprocess`, `multiprocessing`, pytest-xdist workers) becomes a child debug session of its own: `list_debug_sessions` lists it with `parentSessionId` naming this session, it stops on the breakpoints set on this session before launch (re-sent to it before it runs), and it has its own `get_output`, `wait_for_stop` and `close_debug_session`; closing the parent closes its children first. Today a child is discovered through `list_debug_sessions` (its row carries `parentSessionId`) and waited on with its own id. The design is [the session tree](../architecture/session-tree.md), built on the handshake measured in [spike-notes-subprocess.md](spike-notes-subprocess.md)
6. Child processes run undebugged by default: debugpy's sub-process debugging is sent as `subProcess: false` until the default flips in the last step of [#1081](https://github.com/debugmcp/mcp-debugger/issues/1081). With `dapLaunchArgs: { subProcess: true }`, every Python child the program starts (`subprocess`, `multiprocessing`, pytest-xdist workers) becomes a child debug session of its own: `list_debug_sessions` lists it with `parentSessionId` naming this session, it stops on the breakpoints set on this session before launch (re-sent to it before it runs), and it has its own `get_output` and `close_debug_session`; closing the parent closes its children first. A `wait_for_stop` on this session is a wait on the tree: it reports a child's stop with `sessionId` naming the child (inspect, step and continue with that id) and `children` listing every child session with its state; a child can still be waited on by its own id. The design is [the session tree](../architecture/session-tree.md), built on the handshake measured in [spike-notes-subprocess.md](spike-notes-subprocess.md)
Loading
Loading