Skip to content

feat(adapters): NodeAdapter + DAP reverse-request / child-session support#4

Closed
niradler wants to merge 2 commits into
feat/go-adapterfrom
feat/node-adapter
Closed

feat(adapters): NodeAdapter + DAP reverse-request / child-session support#4
niradler wants to merge 2 commits into
feat/go-adapterfrom
feat/node-adapter

Conversation

@niradler
Copy link
Copy Markdown
Owner

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.

NodeAdapter scaffolds 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 ✅

  • Discoveryfind_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.
  • Adapter spawn + DAP handshakenode dapDebugServer.js <port> 127.0.0.1 accepts our initialize; test_node_dap_initialize passes against the real adapter.
  • V8 stack-trace parser — 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. Validated against TypeError + ReferenceError fixtures.
  • Launch-target resolution — peels node [-flags] script args, ts-node, tsx; correctly consumes -r module / --require module.
  • CLI surface--lang node on session start, localize, diagnose. Auto-detection for .js .mjs .cjs .ts .mts .cts.
dbga localize --lang node --file crash.txt              # works today
dbga session start --break-at main.js:12 -- main.js     # blocked on reverse-request support

Known blocker (intentional alpha scope) ⚠️

vscode-js-debug delegates the launched program to a child DAP session via a reverse startDebugging request. Our DapClient._dispatch silently drops server-to-client requests today, so the child is never created and stopped never 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_process attach as a bonus when it lands.

Test plan

  • 24 new unit tests (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_SERVER discovery override.
  • 2 new integration tests (tests/integration/test_node_session.py):
    • test_node_dap_initializepasses against real vscode-js-debug.
    • test_node_dap_launch_stopsxfail 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, 45 e2e).
  • uv run ruff check . clean
  • uv run ruff format --check . clean
  • uv run mypy src clean (30 files)
  • README updated with language-toolchain install matrix.

Out of scope (follow-up)

  • DapClient reverse-request handling (startDebugging etc.) — the one change that promotes NodeAdapter from "handshake works" to "live session works end-to-end."
  • dbga instrument probe templates for JS (console.log defaults).
  • TypeScript source-map UX polish.

Stack

niradler added 2 commits May 29, 2026 03:21
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.
@niradler niradler changed the title feat(adapters): add NodeAdapter (vscode-js-debug) — alpha feat(adapters): NodeAdapter + DAP reverse-request / child-session support May 29, 2026
@niradler
Copy link
Copy Markdown
Owner Author

Superseded by #5, which consolidates the refactor + Go + Node work into a single PR against main (with post-review fixes applied). Closing in favor of #5.

@niradler niradler closed this May 29, 2026
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.

1 participant