feat(adapters): NodeAdapter + DAP reverse-request / child-session support#4
Closed
niradler wants to merge 2 commits into
Closed
feat(adapters): NodeAdapter + DAP reverse-request / child-session support#4niradler wants to merge 2 commits into
niradler wants to merge 2 commits into
Conversation
Third PR in the multi-language stack. Stacks on top of #3 (GoAdapter); merge that first. NodeAdapter scaffolds Node.js / TypeScript debugging via Microsoft's vscode-js-debug (the same DAP server VS Code itself uses). Status: alpha. Status — what works: * Discovery: `find_dap_server()` locates `dapDebugServer.js` from $DBGA_JS_DEBUG_SERVER, then VS Code / Cursor / Insiders extension dirs, then a manual `~/.local/share/js-debug/` install. Errors with a clear install hint (GitHub releases URL) when nothing is found. Verified end-to-end against vscode-js-debug v1.117.0 on Windows. * Spawn + handshake: `node dapDebugServer.js <port> 127.0.0.1` accepts our DAP `initialize` — `test_node_dap_initialize` passes against the real adapter. * V8 stack-trace parser: handles named + anonymous frames, node:internal + node_modules library detection, oldest-first frame ordering so the shared `deepest_user_frame` heuristic lands on the failure site. 13 fixture-driven cases pass against real Node 20 / 26 traces. * `resolve_launch_target` peels `node [-flags] script args`, `ts-node`, `tsx`; correctly consumes the `-r module` / `--require module` pair. * `--lang node` flag plumbed through `session start`, `localize`, `diagnose`. Auto-detection covers `.js .mjs .cjs .ts .mts .cts`. Status — known blocker (intentional alpha scope): vscode-js-debug delegates the actual launched program to a *child* DAP session via a reverse `startDebugging` request. Our DapClient currently drops all server-to-client requests (see `dap_client.py::_dispatch`), so the child is never created and `stopped` never arrives. The full launch flow test (`test_node_dap_launch_stops`) is marked `xfail strict` and will start passing automatically once DapClient gains reverse- request + child-session handling. This is the documented follow-up scope — not in this PR. Test plan: * 24 new unit tests in `tests/unit/test_node_adapter.py` covering registry, extension detection, launch-payload shape, listen-mode flag, V8 parser fixtures (TypeError + ReferenceError + node_modules + anonymous frames), missing-node hint, and the env-var discovery override path. * 1 new integration test pair: - test_node_dap_initialize — PASSES against real js-debug. - test_node_dap_launch_stops — xfail strict; tracks the reverse- request blocker. Both auto-skip when node + js-debug aren't both discoverable. * Full suite: 152 passed + 1 xfailed locally (61 Python unit + 14 Go unit + 24 Node unit + 8 misc unit, 9 integration including Go + Node initialize, 45 e2e). * ruff check + ruff format + mypy --strict all clean (30 src files). README updated with the language-toolchain install matrix. Out of scope (deferred to a follow-up PR): * DapClient reverse-request handling (`startDebugging` etc.) — the one change that promotes NodeAdapter from "handshake works" to "full live session". Unblocks worker-thread + child_process attach as a bonus. * `dbga instrument` probe templates for JS (`console.log` defaults).
Promotes the Node adapter from "alpha (handshake only)" to a full,
live-debugger experience by teaching DapClient and DapSession to handle
DAP server-to-client requests — specifically vscode-js-debug's
`startDebugging`, which delegates every launched program to a fresh
child DAP session.
What this PR adds on top of the previous NodeAdapter scaffold:
DapClient — server-to-client requests
-------------------------------------
* `register_reverse_handler(command, handler)`: register a callable that
runs when the DAP server sends `type: "request"`. The handler returns
the response body (or `None` for empty); raising surfaces as a DAP
`success: false` response with the error message.
* `_dispatch` now routes `type == "request"` through the handler map.
Unknown commands respond with `"not supported"` so the server isn't
left waiting.
* `_send_response` emits a properly-framed DAP response with a fresh
client-side `seq` and the server's `request_seq` echoed back.
DapSession — child-session orchestration
----------------------------------------
* Tracks adapter host/port and a `_child_clients` list. The `start()`
path registers `startDebugging` so every adapter that delegates
(currently only vscode-js-debug) gets transparent child-session
support — Python/Go never send the request so the handler stays
dormant for them.
* `_on_start_debugging` opens a fresh TCP connection to the same DAP
server, runs the full DAP handshake on it (initialize / launch (or
attach) / configurationDone) using whatever configuration the parent
passed in, registers the handler recursively (workers / child_process
nest deeper), and appends the new client to `_child_clients`. The
handler runs on the parent's reader thread; it MUST only do I/O on
the child connection it just opened to avoid reader-thread deadlock.
* `_active_client` is the client that owns the live debuggee. It starts
as the parent and gets promoted to the child that just emitted
`stopped`, so `continue_` / `step` / `evaluate` / `set_breakpoints`
all route to the right place via `_require_client`.
* `wait_for_stop` now round-robin-polls the parent and every child
client via `_poll_any_client`. Terminal events drain across all live
clients.
* `release` disconnects child clients before the parent so they don't
leak; the parent tree-kill remains the unconditional fallback.
Node.js: alpha → fully live
---------------------------
* NodeAdapter docstring updated — drops the "handshake-only" caveat.
TypeScript via ts-node/tsx, plus worker threads and `child_process`
children, all flow through the nested-session mechanism (handler is
registered recursively on child clients).
* `tests/integration/test_node_session.py::test_node_dap_launch_stops`
drops `@xfail`. Now goes through `DapSession.start()` (not raw
DapClient) so the handler fires. Verified end-to-end against real
vscode-js-debug v1.117.0 on Windows: launch → stopOnEntry → continue
→ terminated, ~6 seconds.
Tests
-----
* 4 new unit tests in `tests/unit/test_dap_reverse_requests.py` cover
the DapClient routing in isolation (no debugger spawn): unknown
reverse request → `success: false`; handler return value → response
body; handler exception → `success: false` + message; response seq
is distinct from request_seq.
* Full suite: 158 passed locally (previously 152 + 1 xfailed). 0
failures, 0 xfails. ruff + ruff format + mypy --strict all clean.
CLAUDE.md updated to describe the reverse-request mechanism alongside
the rest of the DAP plumbing.
Sources / spec references this implementation followed:
* DAP spec `startDebugging` reverse request:
https://microsoft.github.io/debug-adapter-protocol/specification#Reverse_Requests_StartDebugging
* vscode-js-debug's child-session model is the same one VS Code's
debug-adapter client implements; this PR mirrors that contract.
5 tasks
Owner
Author
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.
Summary
Third PR in the multi-language stack. Stacks on top of #3 (GoAdapter) — merge that first. Once both #2 and #3 are in, this PR's base becomes
main.NodeAdapterscaffolds Node.js / TypeScript debugging via Microsoft's vscode-js-debug (the same DAP server VS Code itself uses). Status: alpha — handshake works against real js-debug, parser is fixture-validated, but the full live-launch flow is blocked on DAP reverse-request support (intentional follow-up scope; see below).What works ✅
find_dap_server()walks$DBGA_JS_DEBUG_SERVER→ VS Code / Cursor / VS Code Insiders / VS Code Server / Cursor / Windsurf extensions dirs → manual~/.local/share/js-debug/. Clear install hint pointing at the GitHub release URL when nothing is found. Verified end-to-end against vscode-js-debug v1.117.0 on Windows.node dapDebugServer.js <port> 127.0.0.1accepts ourinitialize;test_node_dap_initializepasses against the real adapter.node:internal/node_moduleslibrary detection; oldest-first frame ordering so the shareddeepest_user_frameheuristic lands on the failure site. Validated against TypeError + ReferenceError fixtures.node [-flags] script args,ts-node,tsx; correctly consumes-r module/--require module.--lang nodeonsession start,localize,diagnose. Auto-detection for.js .mjs .cjs .ts .mts .cts.Known blocker (intentional alpha scope)⚠️
vscode-js-debug delegates the launched program to a child DAP session via a reverse
startDebuggingrequest. OurDapClient._dispatchsilently drops server-to-client requests today, so the child is never created andstoppednever arrives. The full-launch test (test_node_dap_launch_stops) is marked@pytest.mark.xfail(strict=True)and will start passing automatically once DapClient gains reverse-request + child-session handling.This is the documented follow-up scope — out of this PR. Unblocks worker-thread +
child_processattach as a bonus when it lands.Test plan
tests/unit/test_node_adapter.py) — registry, extension detection (.js/.mjs/.cjs/.ts/etc.), launch-payload shape (type: "pwa-node",skipFiles: <node_internals>), V8 parser fixtures (TypeError + ReferenceError + node_modules + anonymous), missing-node hint,$DBGA_JS_DEBUG_SERVERdiscovery override.tests/integration/test_node_session.py):test_node_dap_initialize— passes against real vscode-js-debug.test_node_dap_launch_stops— xfail strict (tracks the reverse-request blocker).node+ js-debug aren't both discoverable.uv run ruff check .cleanuv run ruff format --check .cleanuv run mypy srcclean (30 files)Out of scope (follow-up)
startDebuggingetc.) — the one change that promotes NodeAdapter from "handshake works" to "live session works end-to-end."dbga instrumentprobe templates for JS (console.logdefaults).Stack
dlv dap#3 — GoAdapter.