Skip to content

fix(#513): make pause_execution land on attached idle Node servers - #522

Merged
debugmcpdev merged 2 commits into
mainfrom
fix/513-js-attach-pause
Aug 27, 2026
Merged

fix(#513): make pause_execution land on attached idle Node servers#522
debugmcpdev merged 2 commits into
mainfrom
fix/513-js-attach-pause

Conversation

@debugmcpdev

Copy link
Copy Markdown
Collaborator

Fixes #513: on a js attach session against an idle Node server, pause_execution returned pending:true and 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)

  1. js-debug's smart-stepper eats user pauses on idle servers. A pause that lands on a blackboxed/unmapped frame is converted into an auto-step; on a mostly-idle server every frame the pause can land on is one (node internals / no-URL frames), so it steps forever at ~43ms per CDP round-trip — observable as a flood of continued events with zero stopped. Its >256-step failsafe switches to step-out, which never escapes an idle event loop. js-debug exempts breakpoint/exception/entry stops but not pause — which is exactly why the set-a-breakpoint workaround in the issue always worked. launchConfig.smartStep defaults true; our launch transform pins it, our attach transform never mentioned it.
  2. A pause dispatched mid-adoption is swallowed without a response. The child DAP connection exists (and activeChild is set) before its attach {__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.
  3. A pause with no child to run against fell through to the js-debug root session, whose handler is async () => ({}) — success, no CDP Debugger.pause, no stop possible. The tool then reported pending:true forever, which is the contract promising something that cannot happen.

Fix

  • js attach defaults smartStep: false (caller value wins via adapterConfig, same pattern as fix(#501): stop stranding forked children of js attach targets #514's autoAttachChildProcesses; 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.
  • New policy vocabulary DapClientBehavior.childRequiredCommands (js: {'pause'}) — commands the parent is known to no-op. MinimalDapClient.sendRequest gates them on ChildSessionManager.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. threads deliberately keeps its parent fallback (the attach verify loop depends on it).
  • pause() surfaces the no-target error as a structured {success: false, error} (matching the Cannot pause in state: shape), and the deliberate retention of pausePending across the pending-timeout settle is documented (the late-landing stop must normalize to 'pause'). Tool description now names the failure mode.

Verification

  • Live before/after against node --inspect dist/index.js http: pre-fix pending forever (512-continued-event step chase in the trace); post-fix immediate paused with stopReason: 'pause', and on a truly idle target pending:true that lands the moment JS runs — with a working stack trace / evaluate_expression / continue cycle.
  • First-ever test coverage for pause on js attach: unit coverage of the child-vs-parent dispatch gate in MinimalDapClient.sendRequest (7 cases incl. the netcoredbg/Java regression guard: policies without childRequiredCommands keep 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 /work request itself hangs because the pause fires mid-request, which is the point) and a busy-target pause-within-grace smoke.
  • Full unit suite (4227), full js attach e2e (8/8), js launch e2e smoke, lint — all green.

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 (ensureInitialStop burns 12s of threads polling on every js attach).

🤖 Generated with Claude Code

https://claude.ai/code/session_01XtvNu3aNB1nw1URRL5UTgR

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

codecov Bot commented Aug 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.29730% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/proxy/minimal-dap.ts 95.83% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@debugmcpdev
debugmcpdev merged commit 0a2b78d into main Aug 27, 2026
10 checks passed
@debugmcpdev
debugmcpdev deleted the fix/513-js-attach-pause branch August 27, 2026 16:59
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>
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.

js attach: pause_execution stays pending forever on an idle Node server — the stop never lands even once JS runs

1 participant