Repository navigation
Closed
Labels
enhancementNew feature or requestNew feature or request
Description
Milestone 1 of the session tree (design, merged in #1083; issue #1005 part B): a Python child process reported by debugpy becomes a debug session of its own, linked to the session that spawned it.
Body updated 2026-11-04 to the merged design (the #1083 review tightened the wait, closing, cleanup and idle contracts after this issue was filed); the PR sequence below is how it ships.
What ships
- Adoption. The worker's
initializeadvertisessupportsStartDebuggingRequest: truefor Python, so debugpy reports each child as astartDebuggingreverse request (thedebugpyAttachevent form is the same payload and is recognised too). On a report the server creates a child session (parentSessionId, the parent's language, the report'sname—Subprocess <pid>), opens a second connection to the same adapter port, runs the child handshake in debugpy's order (initialize → attach → initialized → setBreakpoints → configurationDone) with the parent's current breakpoint set re-sent beforeconfigurationDone, and releases the park. Measured ~110 ms from report to first stop (spike notes). - Rows.
list_debug_sessionsrows gainparentSessionIdon child rows;DebugSessionInfogrows the same field. No new tool, no changed parameter. - Tree-aware
wait_for_stop. A wait on a session observes it and all its descendants and returns a pause or error from that subtree, or the session's own end, withsessionIdnaming the session selected (equal to the id asked for when it is the session itself) and the ordinary stop answer for it. Level-triggered as today (nothing is consumed) with a fixed priority when several are settled: the session itself if paused/error, else the first paused/error live descendant in adoption order, else the session itself if ended, else keep waiting — an ended descendant alone never completes an ancestor's wait. Every answer on a session with children carrieschildren: [{ sessionId, name, state }]; thestart_debugginganswer carries the same list when a child was adopted during the launch hold. Sessions without children answer as today plussessionId. - Output. The launch session's buffer is the tree's output (already true). The child's buffer carries its own lines (
redirectOutput: trueon the child's attach). - Exit. Children end with
terminatedand noexitCode(attach sessions). A paused child that outlives its parent stays debuggable and holds the parent'sexited(the python (Windows): the debuggee runs under pythonw.exe, so a subprocess that inherits stderr fails with WinError 6 (pytest-xdist workers) #993 pipe effect); the parent row saysrunningand its wait reports the child. - Closing.
close_debug_sessionon any session closes its subtree, descendants first (deepest first), then the named session; closing a child leaves its ancestors and siblings open. Closing a launch root ends the process tree. Explicitly closing a child uses debugpy's measured semantics: the child is terminated (exit 0 reported to its parent) and the answer saysprocessEnded: true. Closing a non-root subtree never shuts down the shared worker or adapter. Closing or idle-reaping an attach root preserves the external targets: every descendant is detached deepest-first withdisconnect(terminateDebuggee: false), then the root; rows removed;released: true. - Connection-scoped cleanup. Today
handleTerminated/handleExited/handleErrorcallproxyManager.stop(), which tears the worker down. A child's proxy viewstop()closes only that child's connection and listeners; a root's natural end never reaps a worker that still serves live descendants (the reap waits for the last one); explicit root close still cascades. - Default on. The Python launch transform stops defaulting
subProcesstofalse(fix(python): debugpy sub-process debugging off by default — a program that starts a Python child no longer hangs (#1005) #1008's default existed only because of the park hang, which adoption removes); the warning naming the hang goes; an explicitsubProcess: falseopts out per launch. Achangedchangelog entry documents the behaviour change (child rows appear for xdist workers, autoreloaders, scripts that shell out). - Bounds.
DEBUG_MCP_MAX_CHILD_SESSIONS(default 32,0disables adoption): beyond it the release lever runs (answer the requestsuccess: false; debugpy unblocks the child itself) and the parent's next answer sayschildrenReleased: n. Ended children stay listed until closed and leave with their parent. - Detach and restart.
detach_from_processon an attach root with children detaches the children first (deepest first,disconnect(terminateDebuggee: false); a debugpy child resumes undebugged, rows gostoppedwithreleased: true), then the parent.restart_debuggingcloses and removes the previous run's children and answerschildrenClosed: n. Neither marks childrenerror. - Attach root. The Python attach transform's
subProcessdefault flips only once the M1 attach fixture (a by-port attach to a parent that spawns a child) shows the same report on the same connection; otherwise attach keepssubProcess: falseand its warning and a follow-up measures it. - Refusals.
start_debugging,restart_debugging,attach_to_process,detach_from_process,expose_sessionandunexpose_sessionon a child id are refused by name (the mirror follows the root). - Idle. The design's per-debug-session idle policy (
MCP_DEBUG_SESSION_IDLE_MS: activity refreshes a session and its ancestors; an in-flight tree wait protects the subtree it observes) is the 2026-07-28-era replacement for the HTTP stale reaper and ships with the PR that introduces that knob (Move to the v2 TypeScript SDK, then to protocol 2026-07-28 — two steps, v0.27 or later #1025 step 2), together with its tree tests. Until then the existing reaper's cascade (server stop → close all sessions, trees included) is unchanged.
How it ships (three PRs)
- PR 1 — the mechanism, opt-in. The adoption contract on
AdapterPolicywith the Python pins; the worker advertises and keeps N connections; events and responses tagged by connection;ProxyManagerdemultiplexes into a child proxy view with connection-scopedstop();parentSessionIdon rows; adoption with the parent's breakpoints; the cap with the reject lever; the close cascade; the refusals; the child's own output buffer; a real-debugpy integration test. Observable only under an explicitsubProcess: true(today: the child parks forever). ThesubProcessdefault staysfalse. - PR 2 — the designed flow. Tree-aware
wait_for_stopwithsessionIdandchildren;start_debuggingcarryingchildren; the detach and restart cascade answers (released,childrenClosed);processEnded/released/childrenReleased. - PR 3 — on by default. The launch
subProcessdefault flips (changedentry, the fix(python): debugpy sub-process debugging off by default — a program that starts a Python child no longer hangs (#1005) #1008 warning goes); the attach fixture measured and the attach default flipped if it reports the same way; xdist, grandchild and cascade-row suites; the reference docs; theKNOWN_ISSUESentry dropped; the skill content.
Where (seams named in the design)
packages/shared/src/interfaces/adapter-policy*.ts: the adoption contract (report recognition, advertise, attach arguments, release lever, output attribution, end semantics) with the Python pins;'none'stays the default;tests/unit/shared/adapter-policy-contract.test.tspins the new members.src/proxy/dap-proxy-connection-manager.ts(initialize),src/proxy/minimal-dap.ts(reverse requests held instead of acked; events tagged with their connection's session id instead of re-emitted as the parent's),src/proxy/dap-proxy-worker.ts(asessionId → MinimalDapClientmap, request routing, closing every connection on shutdown, no worker shutdown while children live).src/proxy/proxy-manager.ts: demultiplex IPC envelopes bysessionId(the field exists and is ignored today) into a child proxy view implementingIProxyManagerover the parent's channel.packages/shared/src/models/index.tsandsrc/session/session-store.ts(parentSessionId),src/session/session-manager-core.ts(adoption onchild-reported, the deferred worker reap, the cascade on close),src/session/execution/execution-controller.ts(the tree-aware wait; keep the wait_for_stop: block until a session's next stop or end instead of polling list_debug_sessions #849 microtask re-check),src/server/handlers/session-tools.tsandexecution-tools.ts(rows and answer fields),packages/adapter-python/src/python-debug-adapter.ts(the default).- Docs:
docs/tool-reference.md(list_debug_sessions,wait_for_stop,close_debug_session,get_output),docs/python/README.md,docs/KNOWN_ISSUES.md(drop the "children run without the debugger" entry), the skill content.
Done when
tests/fixtures/python/subprocess-inheritand the tree fixtures (subprocess.Popen,multiprocessingspawn, a grandchild) run as a CI suite: a breakpoint set before launch in the child's file stops the child;wait_for_stopon the parent returns with the child'ssessionId;list_debug_sessionsshows the row withparentSessionId; the cascade rows of the design's lifecycle table each have a test, including closing a child with grandchildren without orphaning their sessions and a child's termination without stopping the shared worker or its siblings; the attach fixture shows that root close and root detach release the whole subtree without killing external targets (detach keeps the stopped rows, close removes them);python -m pytest -n 2(pytest-xdist) yields two child sessions and completes.- The conformance lane and
http-spec-contractunchanged; the full suite green;node scripts/check-docs.mjsclean.
Part of #1005. Design: docs/architecture/session-tree.md. Measured facts: docs/python/spike-notes-subprocess.md. Related: #993 (the pipe effect), #985 (pytest preset keeps -p no:xdist until this lands), #987 (a child's logpoint hits are the child's), #1025 (step 2 carries the idle policy).
Metadata
Metadata
Assignees
Labels
enhancementNew feature or requestNew feature or request
Activity
docs: the session-tree design — a debuggee's children as debug sessio…
debugmcpdev commentedon Nov 4, 2026
Body updated to the design as merged in #1083. The review there tightened four contracts after this issue was filed, and they are now in the body: (1)
wait_for_stopreturns a pause or error from the subtree or the session's own end, so an ended descendant alone never completes an ancestor's wait; (2)close_debug_sessionon any session closes its subtree, and closing a non-root subtree never shuts down the shared worker, while closing or idle-reaping an attach root detaches every descendant withterminateDebuggee: false; (3) terminal cleanup is connection-scoped: a child view'sstop()closes only its connection, and a root's natural end never reaps a worker with live descendants; (4) the idle policy's tree rules (activity refreshes ancestors, an in-flight tree wait protects its subtree) ship with the PR that introducesMCP_DEBUG_SESSION_IDLE_MS(#1025 step 2), not here.The body also records how M1 ships: PR 1 is the mechanism behind an explicit
subProcess: true(in progress), PR 2 the tree-aware wait and the cascade answers, PR 3 the default flip and the suites.debugmcpdev commentedon Nov 4, 2026
PR 1 of 3 is open: #1087 — the adoption mechanism behind an explicit
subProcess: true: a debugpy-reported child becomes a session withparentSessionId, stops on the parent's breakpoints re-sent beforeconfigurationDone, has its own output andclose_debug_session; closing the parent closes its children first; the launch-shaped tools and the mirror refuse a child id; the per-tree capDEBUG_MCP_MAX_CHILD_SESSIONS(default 32,0off). Proven against real debugpy intests/adapters/python/integration/python-subprocess-tree.test.ts. ThesubProcessdefault staysfalse. Next: PR 2, the tree-awarewait_for_stopwithsessionIdandchildrenplus the detach and restart answer fields; PR 3, the default flip, the attach fixture and the xdist/grandchild suites.session tree M1 (1/3): a Python child reported by debugpy becomes a c…
debugmcpdev commentedon Nov 4, 2026
PR 2 of 3 is open: #1090 — the designed flow.
wait_for_stopis tree-aware (a wait on a session observes it and every descendant;sessionIdnames the session the answer is about,childrenlists the tree, an ended child alone never completes the parent's wait), the launch and restart answers carrychildren/childrenReleased/childrenClosed,close_debug_sessionanswerschildrenClosed/processEnded/released, anddetach_from_processon an attach root releases the children first (childrenReleased). Proven against real debugpy: a breakpoint inchild.pyonly, the parent's wait answers the child's stop three times with the child'ssessionId, then the parent's end. ThesubProcessdefault still flips in PR 3, with the attach fixture and the xdist/grandchild suites. Found on the way: #1089 (a Windows flake inbridge-tree-kill.test.ts, untouched code).session tree M1 (2/3): tree-aware wait_for_stop with sessionId and ch…
feat(python): enable child debugging and fix session-tree lifecycle h…