fix(#513): make pause_execution land on attached idle Node servers - #522
Merged
Conversation
Three distinct holes, found by attaching mcp-debugger to a live
mcp-debugger server and reading the DAP/CDP traces:
1. js-debug's smart-stepper converts a user pause landing on a
blackboxed/unmapped frame into an auto-step — and on a mostly-idle
server every frame is one, so it steps forever (~40ms/step; the
>256-step failsafe switches to step-out, which never escapes an idle
event loop) and the DAP 'stopped' never fires. js attach now defaults
smartStep: false (caller value wins, launch unchanged; js-debug
exempts breakpoint/exception/entry stops from smart-stepping itself).
2. A pause dispatched after the child connection existed but before its
attach handshake bound it to the pending target was swallowed by
js-debug without a response. Child-required commands now wait for
adoption to COMPLETE ('active' target state), not merely for an
activeChild reference.
3. A pause with no child session to ever run against fell through to the
js-debug root session, which acks 'pause' as a silent no-op — the tool
then reported pending:true forever. Child-required commands now fail
with an actionable "no debug target" error surfaced as a structured
{success:false} from pause_execution, never a silent parent fallback
(including when the child dies mid-dispatch).
New policy vocabulary: DapClientBehavior.childRequiredCommands (js sets
{'pause'}; 'threads' deliberately excluded — the parent's empty answer is
load-bearing for attach verify), ChildSessionManager.getChildTargetState()
('active'/'adopting'/'ended'/'none'), and a shared no-debug-target error
builder used by both the proxy thrower and the session-manager matcher.
First-ever test coverage for pause on js attach: unit coverage of the
child-vs-parent dispatch gate in MinimalDapClient.sendRequest, child
target state lifecycle, pause() structured failure, plus two e2e cases
against a new truly-idle HTTP fixture (the pending pause must land the
moment the target runs JS).
Fixes #513
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XtvNu3aNB1nw1URRL5UTgR
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
# Conflicts: # CHANGELOG.md
debugmcpdev
added a commit
that referenced
this pull request
Aug 27, 2026
The full RCA narrative behind PR #522: how a js attach pause_execution could return success forever without pausing anything, why the routing hole a code-reading pass produced was real but not the field mechanism (js-debug's smart-stepper converting user pauses into an endless step chase through internal frames was), the adapterConfig {trace: true} technique that made the adapter narrate its own CDP decisions, and the third bug the fix's e2e caught that interactive repro timing could not reach (pre-adoption dispatch swallowed without a response). Side findings link to the issues they became: #518 (trace parent/child discriminator), #519 (routing logs invisible in the per-session proxy log), #520 (ensureInitialStop's 12s threads poll on js attach). Continues the docs/case-studies series (#510, #516); README links it. Claude-Session: https://claude.ai/code/session_01XtvNu3aNB1nw1URRL5UTgR Co-authored-by: CI Bot <ci@example.com> Co-authored-by: Claude Fable 5 <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.
Fixes #513: on a js attach session against an idle Node server,
pause_executionreturnedpending:trueand the stop never landed — even once the target provably executed JavaScript. Root-caused by attaching mcp-debugger to a live mcp-debugger server and reading the adapter's own CDP/DAP trace (adapterConfig: {trace: true}); full narrative coming as a case-study docs PR.Root cause (three distinct holes)
continuedevents with zerostopped. Its >256-step failsafe switches to step-out, which never escapes an idle event loop. js-debug exemptsbreakpoint/exception/entrystops but notpause— which is exactly why the set-a-breakpoint workaround in the issue always worked.launchConfig.smartStepdefaults true; our launch transform pins it, our attach transform never mentioned it.activeChildis set) before itsattach {__pendingTargetId}binds it to the target; a request written in that window gets no response from js-debug, ever. Interactive repro can't hit this (~200ms window vs seconds between tool calls) — the new e2e's back-to-back calls hit it every time.async () => ({})— success, no CDPDebugger.pause, no stop possible. The tool then reportedpending:trueforever, which is the contract promising something that cannot happen.Fix
smartStep: false(caller value wins viaadapterConfig, same pattern as fix(#501): stop stranding forked children of js attach targets #514'sautoAttachChildProcesses; launch unchanged). Pause now lands truthfully — at an internal frame if that's where the VM is, exactly like an IDE pausing an idle server.DapClientBehavior.childRequiredCommands(js:{'pause'}) — commands the parent is known to no-op.MinimalDapClient.sendRequestgates them onChildSessionManager.getChildTargetState()('active' | 'adopting' | 'ended' | 'none'): waits for adoption to complete (not merely for an activeChild reference), grants a short grace for the attach → reverse-startDebugging race, and otherwise throws a shared "no debug target" error instead of falling through to the parent — including when the child dies mid-dispatch.threadsdeliberately keeps its parent fallback (the attach verify loop depends on it).pause()surfaces the no-target error as a structured{success: false, error}(matching theCannot pause in state:shape), and the deliberate retention ofpausePendingacross the pending-timeout settle is documented (the late-landing stop must normalize to'pause'). Tool description now names the failure mode.Verification
node --inspect dist/index.js http: pre-fixpendingforever (512-continued-event step chase in the trace); post-fix immediatepausedwithstopReason: 'pause', and on a truly idle targetpending:truethat lands the moment JS runs — with a working stack trace /evaluate_expression/continuecycle.MinimalDapClient.sendRequest(7 cases incl. the netcoredbg/Java regression guard: policies withoutchildRequiredCommandskeep the old fallback),getChildTargetState()lifecycle,pause()structured failure, adapter transform defaults; two e2e cases against a new zero-timer HTTP fixture (examples/javascript/idle_server_attach_target.js) — the js attach: pause_execution stays pending forever on an idle Node server — the stop never lands even once JS runs #513 regression (pending pause must land when JS runs; the/workrequest itself hangs because the pause fires mid-request, which is the point) and a busy-target pause-within-grace smoke.Side findings from the investigation filed as #518 (DAP trace lacks a parent/child discriminator), #519 (routing-layer logs invisible in the per-session proxy log), #520 (
ensureInitialStopburns 12s of threads polling on every js attach).🤖 Generated with Claude Code
https://claude.ai/code/session_01XtvNu3aNB1nw1URRL5UTgR