Skip to content

session tree M1 (2/3): tree-aware wait_for_stop with sessionId and children, and the cascade answers (#1081) - #1090

Merged
debugmcpdev merged 11 commits into
mainfrom
feat/1081-m1-tree-wait
Nov 5, 2026
Merged

debugmcpdev merged 11 commits into
mainfrom
feat/1081-m1-tree-wait

Conversation

@debugmcpdev

Copy link
Copy Markdown
Collaborator

PR 2 of 3 for #1081 (the session tree, design; PR 1 was #1087). The designed flow — set a breakpoint in the child's code, start the parent, wait — and the answer fields the cascades owe. Still behind an explicit dapLaunchArgs: { subProcess: true }; the default flips in PR 3.

What changes for a caller

  • wait_for_stop is tree-aware. A wait on a session observes it and every descendant, present or adopted while it waits, and answers for the session a fixed priority names: the session itself if paused or in error; else the first paused or errored descendant, depth first in adoption order; else the session itself if ended; else it keeps waiting — an ended child alone never completes the parent's wait. Level-triggered as before (nothing consumed, the auto-continued entry stop still never reported). Every answer carries sessionId (the selected session; the one asked for on a plain session or a pending answer) and the ordinary stop answer for that session (lastStop, location, context, snapshot, exitCode, programEnd); a session with descendants also carries children: [{ sessionId, name, state, parentSessionId }], the pending answer included. A session with no children answers byte-for-byte as before plus sessionId.
  • start_debugging / restart_debugging carry children (adopted during the hold), childrenReleased (children that run undebugged this run: beyond DEBUG_MCP_MAX_CHILD_SESSIONS, or an adoption that failed — a per-run running total) and childrenClosed (a relaunch closes the previous run's children first). A hot restart keeps its children.
  • close_debug_session answers childrenClosed when it took descendants with it, processEnded: true for a child whose process it ended (debugpy), released: true for an attach root (its descendants detached with their targets kept running, like its own). Every closed descendant's output resource is forgotten too (it was leaked before).
  • detach_from_process on an attach root releases the children first, deepest first (disconnect with terminateDebuggee: false), then the parent; the child rows stay, stopped, with programEnd.data.released: true; the answer says childrenReleased. terminateProcess: true closes the subtree (childrenClosed).
  • The wait_for_stop tool description gains one sentence on the tree (24 tool-list snapshots follow); the Python subProcess: true diagnostic says the parent's wait reports the child.

How

  • src/session/execution/tree-settle.ts — selectSettledInTree, the pure four-rule read; describeTreeAnswer builds children / childrenReleased.
  • src/session/execution/session-state-wait.ts — one waitForSettle primitive (the wait_for_stop: block until a session's next stop or end instead of polling list_debug_sessions #849 microtask re-read, abort, timeout, gone) behind an unchanged waitForSessionState and the new waitForSubtreeSettle.
  • SessionManagerCore — subtree listeners (onSubtreeStateChange: a session's transition or removal tells its own listeners and each ancestor's; a child's first transition is its adoption), descendantsDepthFirst, releasedChildReports (bumped in adoptReportedChild's release, never for a release taken while the subtree is closing; reset per launch), closeSessionReporting (the cascade with its outcome; closeSession wraps it), closeSingleSession's release mode, releaseDescendants (the detach cascade), stopProxyPreservingSession returns the count.
  • ExecutionController.waitForStop composes the answer for the selected session; the handler copies sessionId and reads the landing (line context, stale build) for it; hoistTreeFields lifts the tree fields beside pending on start, restart and detach; the close handler answers from the outcome.
  • ChildProxyView.stop({ terminateDebuggee }) for the release mode; isChildProxyView guard.

Proven

  • Unit: tree-settle.test.ts (the rules), session-state-wait.test.ts (+ the subtree wait), session-manager-wait-for-stop-tree.test.ts (13 cases: rules 1–4, pre-order, adoption during the wait, a descendant closed during the wait, a wait on a child, pending with children, an errored child), session-manager-child-sessions.test.ts (+ the counters, the launch's children, the close outcomes, the detach cascade order), the handler tests, the view test.
  • Real debugpy, tests/adapters/python/integration/python-subprocess-tree.test.ts (6 cases): the designed flow — a breakpoint in child.py only, the parent's wait_for_stop answers the child's stop three times with the child's sessionId and children, then the parent's end with exit 0; a restart answers childrenClosed: 1; closing a child answers processEnded: true; closing the parent childrenClosed: 1.
gate result
typecheck:all 0 errors, ratchet 0
lint, check-docs (145 files), changelog:check clean
unit (pre-push hook) 407 files, 8,074 passed, 5 skipped
integration (pre-push hook, full build) 26 files passed, 1 skipped; 126 tests passed, 1 skipped

The first push was blocked by the two bridge-tree-kill cases, a Windows timing flake in untouched code (the bridges' 300 ms tree-kill grace; passes on main and standalone): filed as #1089; the second hook run was clean.

Calls made here (overturnable)

  1. children lists every descendant (pre-order), each with parentSessionId; the design's { sessionId, name, state } is a subset.
  2. childrenReleased is a per-run running total (nothing consumed, concurrent waiters agree); it counts every release that let a child run undebugged, never one taken while the subtree was closing.
  3. childrenClosed also rides on close_debug_session and on a start_debugging that tore down a live run.
  4. A hot restart keeps the children (no cascade, no childrenClosed).
  5. An errored descendant is selected on every later wait until it is closed (level-triggered); the reference says to close it to wait past it.
  6. Only the wait_for_stop description changed; the other four tools' text is unchanged.

Not in this PR

PR 3: the subProcess default flip and the #1008 warning, the attach fixture (the attach-root close and the detach cascade ship here with unit tests and are measured there — no attach root has children until the attach default flips), the xdist and grandchild suites, the KNOWN_ISSUES entry, the skill rewrite. outputOn is js-debug's (M2, #1082). The idle policy is SDK step 2 (#1025).

🤖 Generated with Claude Code

cynarlab and others added 8 commits November 4, 2026 17:39
…#1081 step 1)

selectSettledInTree is the four-rule read the tree-aware wait_for_stop answers
by: the session itself when paused or in error, else the first paused or
errored descendant in adoption order depth first, else the session itself when
ended, else keep waiting. session-state-wait.ts now has one waitForSettle
primitive (the #849 microtask re-read, abort, timeout, gone) behind an
unchanged waitForSessionState and a new waitForSubtreeSettle, fed by the core's
subtree listeners: a session's transition or removal tells its own subtree
listeners and each ancestor's, so a root's waiter learns of a child's adoption
(its first transition), its stops and its removal. The context gains
onSubtreeChange and descendantsOf (pre-order).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ession, children lists the descendants (#1081 step 2)

waitForStop observes the session and every descendant, present or adopted
while it waits, through waitForSubtreeSettle, and answers for the session the
four-rule selection names: the ordinary stop answer (landing, snapshot,
lastStop, exitCode, programEnd) read on that session, with `sessionId` on
every branch — pending included — and `children` (every descendant, depth
first in adoption order, each with its state and parent) when the session has
any. An ended descendant alone never completes an ancestor's wait; a
descendant closed meanwhile is one fewer, not the end of the wait. The
TreeAnswerFields type (children, childrenReleased, childrenClosed) joins the
launch answer's type for the next step.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ldren, and the handlers' tree fields (#1081 step 3)

A child released to run undebugged (beyond the cap, no adopter, a failed
adoption — never one taken while its subtree was closing) counts on the
parent as releasedChildReports, reset per launch and carried by the wait and
launch answers as childrenReleased: a running total, never consumed.
stopProxyPreservingSession returns how many descendants the teardown closed,
and a relaunch answers childrenClosed with it; the launch and hot-restart
answers list the children adopted during the hold. The wait handler copies
sessionId and reads the landing (line context, stale build) for the selected
session; start, restart and detach hoist children, childrenClosed and
childrenReleased beside pending. The detach answer is typed for its fields.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…thon diagnostic no longer waits for it (#1081 step 4)

One sentence on the tool: a session whose program spawned child sessions is
waited on as a tree, sessionId names the session the answer is about and
children lists the descendants. The 24 tool-list snapshots follow. The Python
launch diagnostic for an explicit subProcess: true says the parent's
wait_for_stop reports the child's stop, and that the default flips in the
last step of #1081.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…eased, childrenClosed, childrenReleased (#1081 steps 5–6)

closeSessionReporting owns the cascade and says what the close did: the rows
removed (descendants first, deepest first — the handler forgets each output
resource), childrenClosed, processEnded for a child whose process the close
ended (the policy's closeChild pin; debugpy terminates), released for an
attach root, whose descendants are detached with their targets kept running
like its own. closeSession wraps it. A child view's stop takes
{ terminateDebuggee: false } for that release. detach_from_process on a
parent releases every descendant first, deepest first, before the root's
disconnect, keeps their rows as stopped with a program end that says
released, and answers childrenReleased; a terminating detach closes the
subtree and answers childrenClosed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…he guides, the skill and the fragment (#1081 step 7)

wait_for_stop's reference gains the four-rule selection, sessionId, children,
childrenReleased and a worked answer; start_debugging, restart_debugging,
detach_from_process and close_debug_session name their fields; the
parentSessionId paragraph, the Python guide, KNOWN_ISSUES, the skill and the
server instructions say the parent's wait reports a child's stop; the design
doc's status names the merged PR 1 and this PR; CLAUDE.md names the pieces.
A changelog fragment for this PR; PR 1's no longer waits for the tree-aware
wait.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…rts the child's stops, restart and close answer their counts (#1081 step 8)

A breakpoint in the child's code only, the parent started, a wait on the
parent: three times the child's stop with the child's sessionId and the
children list, then the parent's own end with exit 0 once the child is done.
A restart closes the previous run's child (childrenClosed: 1) and pauses the
new run; closing a child answers processEnded, closing the parent
childrenClosed: 1. The tree's first wait names the parent when the parent is
the one paused.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…he parent's own end (#1081)

The wait_for_stop description and the fragment said "the session itself when
it is paused or ended, else the first paused descendant", which inverts rules
2 and 3 for the one case the design singles out — a parent that ended while
a child is paused reports the child. Now: the session itself when paused,
else the first paused descendant, else the session itself when ended. The 24
snapshots follow.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@codecov

codecov Bot commented Nov 4, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.96774% with 10 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/session/session-manager-core.ts 93.04% 8 Missing ⚠️
src/session/execution/execution-controller.ts 94.59% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

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.

2 participants