microsoft / microsoft/vscode-js-debug
Explicit DAP pause is swallowed by smart stepping on an idle Node/Express server
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}`);
});
- Launch it through the standalone DAP server with
type: 'pwa-node',stopOnEntry: false,smartStep: true,skipFiles: ['<node_internals>/**', '**/node_modules/**'],sourceMaps: true,pauseForSourceMap: false, andautoAttachChildProcesses: false. - Complete initialize/configurationDone and the reverse startDebugging child adoption. Wait until the server is listening, make one successful /ping request, then wait one second.
- Send
pauseto the adopted child's actual thread ID returned bythreads. - Send GET /ping every 250 ms for ten seconds while recording DAP events.
Observed
- The pause response succeeds.
- With
smartStep: true: 1,517continuedevents, 40 successful HTTP responses, and zerostoppedevents during the 10.2-second observation window. - With
smartStep: falseand the same skip list: astoppedevent with reasonpause, no continued events, and an inspectableemitHookframe 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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