microsoft / microsoft/vscode-js-debug

Explicit DAP pause is swallowed by smart stepping on an idle Node/Express server

Open
#2,412 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
2k
Forks
373
Avg merge
1d 9m
Merged PRs (30d)
6

Description

An explicit DAP pause can be acknowledged, then consumed by automatic stepping without producing a stopped event while the debuggee continues handling requests.

Environment

  • js-debug DAP server v1.112.0 (unmodified release bundle)
  • Node.js v24.14.1; Windows x64
  • Reproduced with a standalone raw TCP/DAP client, without mcp-debugger in the protocol path.

Reproduction

Use this small Express server (Express 5.2.1):

import express from 'express';
const app = express();
app.get('/ping', (_req, res) => res.json({ ok: true }));
const server = app.listen(0, '127.0.0.1', () => {
  console.log(`listening ${server.address().port}`);
});
  1. Launch it through the standalone DAP server with type: 'pwa-node', stopOnEntry: false, smartStep: true, skipFiles: ['<node_internals>/**', '**/node_modules/**'], sourceMaps: true, pauseForSourceMap: false, and autoAttachChildProcesses: false.
  2. Complete initialize/configurationDone and the reverse startDebugging child adoption. Wait until the server is listening, make one successful /ping request, then wait one second.
  3. Send pause to the adopted child's actual thread ID returned by threads.
  4. Send GET /ping every 250 ms for ten seconds while recording DAP events.

Observed

  • The pause response succeeds.
  • With smartStep: true: 1,517 continued events, 40 successful HTTP responses, and zero stopped events during the 10.2-second observation window.
  • With smartStep: false and the same skip list: a stopped event with reason pause, no continued events, and an inspectable emitHook frame in <node_internals>/internal/async_hooks. The subsequent HTTP request remains pending while paused, as expected.
  • These counts describe one run, not required thresholds for reproduction.

Expected

Once JavaScript is executing and the requested pause is reached, expose a usable stopped state. An internal or skipped frame is preferable to silently continuing to execute indefinitely. Choosing to search for a user frame first could be reasonable, but that search needs a fallback that actually stops.

Suspected mechanism and possible fix

Thread.pause() records _expectedPauseReason = { reason: 'pause' }. SmartStepper.getSmartStepDirection() exempts breakpoint/exception/entry stops, but does not exempt that explicit pause intent; it chooses another step for a skipped frame and eventually switches to step-out after its threshold.

A focused candidate fix would bypass synthetic stepping for an explicitly requested pause, preserving normal smart stepping and skipFiles behavior for actual step requests. Regression coverage should include manual pause in skipped frames and unchanged stepping through dependencies and generated helpers. This candidate is now implemented and has been tested against upstream main at 41bd9bb831a46c2668cab14d8455e8e3bff58477.

Current-main confirmation

A minimal integration fixture (setInterval(() => {}, 10) plus a readiness message), with both its script and Node internals skipped, also reproduces this on current main. Before the fix, the explicit pause succeeds but the test times out at ten seconds; the DAP send trace contains 1,437 continued events and no stopped event. After the fix, it receives a stopped event with reason pause, reads the stack, evaluates an expression, and resumes successfully. The standalone Express/DAP probe also pauses successfully with smartStep: true after rebuilding the adapter with the fix.

Related: #1085 introduced synthetic blackboxing; #429 exempts other deliberate stops. This report is separate from debugmcp/mcp-debugger#687, which deliberately retained smartStep's launch default after measuring the costs of disabling it. The mcp-debugger-side launch/auto-continue fixes in #708 are present in the downstream reproduction.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reading src/adapter/threads.ts and src/adapter/smartStepping.ts, then run the minimal integration fixture described in the issue with smart stepping and skipped frames enabled. Confirm that an explicit pause produces a stopped event, permits stack inspection and evaluation, and resumes successfully, while ordinary stepping through dependencies and generated helpers remains unchanged.

Written by the indexing model from the issue text.

Assessment

Tech stack
express, node.js, typescript
Domain
devtools
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
30/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.