feat: surface js debuggee exit code (exitCode) via preload shim synthesis - #252
Merged
Conversation
…247) vscode-js-debug never emits a DAP 'exited' event (verified against the vendored bundle), so JavaScript sessions could not distinguish a crash from a clean exit - the exact contract #220 added for other adapters. The forwarding path was already complete; the event source was missing. The debuggee now records its own exit code and the worker replays it: - New assets/exitcode-shim.cjs preload: writes process exit code to a per-session temp file, root-process-only via an inherited env claim so spawned children / worker_threads / cluster workers skip while tsx-style wrappers still record the propagated code. Fully try/caught, never breaks the debuggee. - transformLaunchConfig injects the shim (NODE_OPTIONS --require, quoted + forward slashes for Windows paths, idempotent) plus the MCP_DEBUGGER_EXITCODE_FILE env marker. Launch mode only; missing shim asset degrades gracefully to today's behavior. - The worker's onTerminated synthesizes 'exited' from the recorded file BEFORE forwarding 'terminated' (ordering matters: whichever event arrives first strips the other's session handler). Real exited events stay authoritative; parent+child double-terminated synthesizes once. Self-gating via the env marker, so non-js sessions skip instantly. - IFileSystem gains readFile/remove (fs-extra wired, worker stays unit-testable). Signal-killed debuggees and attach mode still report no exitCode (exit handler never runs / env not ours) - documented; never a guessed value. The #242 e2e regression guard now asserts exitCode like its python twin, plus a new clean-exit exitCode=0 case. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
3 tasks
debugmcpdev
added a commit
that referenced
this pull request
Aug 4, 2026
… fixtures (issue #247 e2e) (#256) The 'reports exit code 0 for a clean run' e2e test added in PR #252 referenced tests/fixtures/debug-scripts/js-clean-exit.js, but the fixture never made it into the commit: the blanket tests/**/*.js ignore (meant for compiled TS artifacts) made 'git add' silently skip it, so it lived on as an untracked local file until branch cleanup removed it. Any fresh checkout then failed the test with 'Script file not found'. CI never caught it because only the container e2e subset runs there. The committed sibling js-throws.js only exists because it was force-added, which is why the trap was invisible. Fix both layers: recreate the fixture (timer-callback shape like js-throws.js, natural exit 0) and add a narrow gitignore negation for tests/fixtures/debug-scripts/*.js — hand-written debuggee fixtures, not TS output — so future fixtures can't be silently dropped. Compiled artifacts elsewhere under tests/ stay ignored. Verified: all 9 tests in mcp-server-break-on-exceptions.test.ts pass; every debug-scripts fixture referenced from tests/ exists. Co-authored-by: JF <john.franklin@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
debugmcpdev
added a commit
that referenced
this pull request
Aug 5, 2026
…h sessions end 'stopped', not 'error' (fixes #258) (#263) Two defects, both exposed by the #254 stdio drain barrier: 1. Ordering: onExited/onTerminated/onClose all await the same drain barrier, and onTerminated's extra await (#252 synthesis) let onClose win by a microtask - the codeless dap_connection_closed status reached the parent before the terminated DAP event, stripping the handlers that would have marked the session stopped. A FIFO terminal-signal queue in the proxy worker now preserves arrival order, and the adapter process 'exit' status rides the same queue. 2. Fabricated exit code: ProxyManager emitted 'exit' with `message.code ?? 1` (and dap-core with `message.code || 1`, mangling a real 0), so every codeless closure became exit code 1 and SessionManager mapped it to ERROR - even for a clean run. Terminal statuses now carry an explicit `expected` flag (terminal DAP event already forwarded, or shutdown underway) and pass the code through untouched. SessionManager maps expected teardowns to STOPPED (recording the debuggee exit code), unexpected closures to ERROR, and keeps the legacy rule for real proxy-process exits. The duplicate unlatched 'exit' emit from the functional core is suppressed. Ruby also gains adapterExitCodeIsDebuggeeExitCode: rdbg -c propagates the debuggee's exit status but never sends a DAP exited event, so the worker now synthesizes one - clean run records exitCode 0, unhandled raise records exitCode 1, matching Python/js. Deferred (cosmetic): Ruby's isSessionReady still resolves the start_debugging ready-wait via the exit path for quick scripts, logging 'proxy exited during startup'; the reported state is now accurate. Co-authored-by: JF <john.franklin@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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.
Fixes #247
Second of two PRs for the
cluster:js-child-sessionbacklog chunk (#248/#249/#250 are in #251).Root cause
Not a forwarding bug: vscode-js-debug never emits a DAP
exitedevent at all (verified against the vendored bundle — the onlyexitedoccurrence is athreadevent reason; the only exit-code text is a stderr line gated behindoutputCapture: 'console', which we can't use without regressing #218 stdout capture). The child-event forwarding path is complete and unfiltered; the event source was missing. So JavaScript sessions could never distinguish a crash (exit 1) from a clean exit (exit 0) — the contract #220 added for Python and mock.Mechanism: the debuggee records, the worker replays
assets/exitcode-shim.cjs— a tiny CJS preload registered viaNODE_OPTIONS --require. Writesprocess.on('exit')'s code to a per-session temp file. Root-process-only via an inherited env claim (MCP_DEBUGGER_EXITCODE_CLAIMED), so spawned children /worker_threads/ cluster workers skip while tsx-style wrappers still record the propagated code at the outermost process. Fully try/caught — can never break the debuggee.transformLaunchConfiginjects the shim (double-quoted, forward-slashed for Windows paths with spaces; idempotent; multi-location resolution mirroring the vendor lookup) andMCP_DEBUGGER_EXITCODE_FILE(per-session UUID temp path). Launch mode only; a missing asset degrades gracefully to today's behavior with a warning.onTerminatedreads the file and synthesizesexited {exitCode}before forwardingterminated— ordering is load-bearing: whichever of exited/terminated reaches the SessionManager first strips the other's handler, and the worker shuts down inside onTerminated. Realexitedevents stay authoritative (flag), parent+child double-terminatedsynthesizes once, and non-js sessions skip instantly (no env marker). No new config plumbing: the worker already holds the launch config env.IFileSystemgainsreadFile/remove(fs-extra-wired) so the worker stays unit-testable.Documented gaps (never a guessed value): signal-killed debuggees (exit handler never runs) and attach mode (target env not ours) still report no
exitCode.Testing
exitCodedefined and non-zero exactly like its python twin (closing the loop [BUG] js-debug sessions never surface the debuggee exit code (exitCode missing) #247 requested), plus a new clean-exitexitCode === 0case — the first such assertion for any js session.🤖 Generated with Claude Code