Skip to content

fix(launch): a noDebug launch says what it did to the debugger instead of blaming the breakpoints (#710) - #747

Merged
debugmcpdev merged 7 commits into
mainfrom
fix/710-nodebug-launch-warning
Sep 17, 2026
Merged

debugmcpdev merged 7 commits into
mainfrom
fix/710-nodebug-launch-warning

Conversation

@debugmcpdev

@debugmcpdev debugmcpdev commented Sep 16, 2026

Copy link
Copy Markdown
Collaborator

Closes #710.

What

dapLaunchArgs.noDebug: true is DAP's "launch without enabling debugging". Where the adapter honours it, no breakpoint binds, no exception filter arms, and no stop ever arrives — and the session read as a mystery: a short script ended stopped carrying the #467 warning ("check the file path and line") for breakpoints that were never going to bind; a server stayed running with no warning while the caller waited for a stop that could not come.

start_debugging now says what the flag did. Where the debugger is off and the caller asked for a stop:

dapLaunchArgs.noDebug is true, so the debugger is disabled for this launch and no stop can arrive:
2 breakpoint(s), 1 function breakpoint(s), breakOnExceptions='all' and stopOnEntry will not fire.
Drop noDebug to debug, or ignore this if you only meant to run the program

Where the adapter ignores the flag:

dapLaunchArgs.noDebug has no effect with the ruby adapter: the debugger stays on, and breakpoints,
breakOnExceptions and stopOnEntry work as usual

The premise was wrong, so it is now measured

The issue (and the first version of this PR) assumed every adapter honours the flag. The /code-review on the first commit checked the sources, and a live probe of every adapter (set a breakpoint, launch with { stopOnEntry: false, noDebug: true }) gave the full picture:

adapter flag reaches the adapter honours it today
javascript (js-debug) yes (#706) yes runs to completion, stopped, exit 0
python (debugpy) yes yes launch fails at init — #746
go (Delve) yes yes program ran, no initialized, init failure — #746
cpp (CodeLLDB) yes yes, by refusing "Not supported in noDebug mode" → launch error — #746
rust (CodeLLDB) no — the launch transform builds from a fixed key set paused at the breakpoint
ruby (rdbg) yes no (server_dap.rb never reads it) paused at the breakpoint
dotnet (netcoredbg) yes no paused at the breakpoint
java (JDI bridge) yes no (JdiDapServer.java never reads it) debugger on
mock yes no paused at the breakpoint

So the first version's warning was false for half the languages — and worse, it suppressed the accurate #467/#308/#469 diagnostics for them. The redesign:

  • AdapterPolicy.honoursNoDebug (beside the functionBreakpointsBindLate precedent), true for javascript/python/go/cpp and unset elsewhere, pinned per language in tests/unit/shared/adapter-policy-contract.test.ts so a policy cannot start or stop claiming it silently. The four trues carry a comment saying what was observed.
  • The launcher resolves noDebug the way the adapter will see itadapterLaunchConfig over dapLaunchArgs over the server defaults, the proxy launcher's merge order — and decides debuggerOff = noDebug && honoursNoDebug. Only that gates the suppression of the breakpoint-shaped warnings.
  • Where the flag is ignored, the caller is told it had no effect and the usual diagnostics stay.

Design decisions kept from the first round

Also from the reviews (two rounds)

  • With the debugger off, stopOnEntry is neutralized once, for everything that reads it. stopOnEntry: true + noDebug: true used to sit on the 30 s readiness ceiling. The first fix (a readiness-only view plus a post-hoc RUNNING projection) was shown to be inert for python/go/cpp — their isSessionReady is state === PAUSED, they always request an entry stop and auto-continue — and racy for js-debug, whose adapter-configured fires before the readiness listener exists while the core still withheld RUNNING under the caller's value. Now the launch args handed to the proxy, the handshake and the readiness wait carry stopOnEntry: false, the readiness policy drops its pause-only predicate (running is ready), and the core's own projection does the rest; session.lastLaunch and the warning keep the caller's value. Both band-aids are gone.
  • noDebug and stopOnEntry are resolved the way the adapter sees them (adapterLaunchConfig over dapLaunchArgs, over the server defaults for noDebug) and by truthiness — the proxy forwards a string 'true' verbatim and js-debug gates on truthiness.
  • A launch that fails under the flag (debugpy/Delve/CodeLLDB today — noDebug launches never complete proxy init on python and go (no initialized event) and error on cpp (CodeLLDB refuses configuration in noDebug mode) #746) carries a noDebug note in its data, so the caller sees the flag before retrying paths and ports. The note states the fact and the remedy ("the debugger was off; if the failure is unexpected, drop noDebug") without asserting the flag caused a failure it cannot attribute — a bad scriptPath under noDebug is still a bad path.
  • A stop that lands before the launch reports outranks the static policy pin (an adapter build that ignores the flag after all): the ordinary diagnostics stay and the note says the flag had no effect. A stop that lands after the launch answered is noDebug sessions: set_breakpoint, list_breakpoints, pause and inspection still explain themselves in debugger terms after a debugger-off launch #749's territory.
  • Logpoints are counted apart from breakpoints (the run-to-completion summary already distinguishes them); the builder takes ExceptionBreakMode; tests build real Breakpoint maps; the two dry-run payloads collapse into one helper; setMockProxyRunning moves to session-manager-test-utils.ts; the redundant second policy lookup reads the hoisted one.
  • Third round: a pause that came anyway counts as ready (it had fallen to RUNNING-only); an entry stop the core already resumed counts as evidence of a wrong pin (firstStopHandled); stopOnEntry is neutralized in adapterLaunchConfig too (it wins the adapter merge — the worker and the js child adoption would still have waited on it); the worker's js-debug function breakpoints via the CDP proxy (revives #282 by another route) #295 stopOnEntry force for pending CDP function breakpoints yields to an honoured noDebug; attach-shaped start_debugging is left alone (noDebug is a launch-request property); a bare noDebug launch that fails carries a note even with nothing armed, and every failure return carries it; 'true'/'false' strings are read the way the proxy parser coerces them; the server-defaults source is dropped (they are typed stopOnEntry/justMyCode only); the texts name the flag alone since either source may have carried it.
  • Declined: recording the decision on the session for downstream surfaces (set_breakpoint, list_breakpoints, pause_execution on a noDebug session) is the right next step but a separate change — noDebug sessions: set_breakpoint, list_breakpoints, pause and inspection still explain themselves in debugger terms after a debugger-off launch #749. breakpointsReapplied on restart keeps its documented meaning (breakpoints re-sent); the warning beside it says they will not fire.

Changes

  • packages/shared/src/interfaces/adapter-policy.ts + the js/python/go/cpp policies — honoursNoDebug.
  • src/session/breakpoints/launch-warnings.tsbuildNoDebugLaunchWarning, pure, two texts.
  • src/session/launch/debug-launcher.tsresolveLaunchFlag, the policy-gated decision, the neutralized launchArgs/readinessPolicy when the debugger is off, failureData, dryRunResult; operations-context.ts gives the launch slice defaultDapLaunchArgs.
  • src/server/tool-schemas.ts (+ the 24 tool-list snapshots), docs/tool-reference.md, changelog.d/710.fixed.md — say which adapters do what.
  • Tests: builder (10), launcher wiring on the mock-proxy harness (honouring via a policy override on the facade seam, ignoring via the mock's real policy, adapterLaunchConfig as the source and as the override, readiness with the real event order and with a pause-only policy, string-typed flag, failed launch, a stop that arrived anyway, dry run, restart, string forms, attach-shaped, bare failed launch, resumed entry stop — 18), the policy contract pin, and two e2e cases in tests/e2e/mcp-server-break-on-exceptions.test.ts: js-debug (honours: success, warning, no Breakpoint-binding failures are under-surfaced: start_debugging hides unbound breakpoints, and known-never-bind function breakpoints are still accepted #467 text, stopped/exit 0 — fails on main's build with exactly the issue's symptom) and the mock adapter (ignores: "no effect", still paused at the breakpoint).

Verification

lint, typecheck:all, changelog:check, check:docs clean; tests/core/unit/session + tests/core/unit/server + tests/unit/shared: 102 files / 1840 tests (session, server, shared, proxy). CI does not run the e2e project, and the worker change sits on the js function-breakpoint path only e2e drives, so tests/e2e/mcp-server-smoke-js-function-bp.test.ts + the full tests/e2e/mcp-server-break-on-exceptions.test.ts (26 tests) were run locally against the rebuilt dist — green; the readiness and merged-source wiring tests each fail when their fix is disabled; pre-push gate passed.

Follow-ups filed: #746 (python/go/cpp launches fail under noDebug — matrix attached), #749 (downstream surfaces).

🤖 Generated with Claude Code

#710)

dapLaunchArgs.noDebug is a standard DAP flag every adapter honours by not
enabling the debugger (JavaScript included since #706 forwards unknown
launch keys): no breakpoint binds and no stop ever arrives. The session
then read as a mystery — a short script ended `stopped` with the #467
"check the file path and line" warning for breakpoints that were never
going to bind; a server stayed `running` with no warning at all.

- launch-warnings.ts: buildNoDebugLaunchWarning, a pure builder over the
  breakpoint stores, the launch args and the caller's explicit
  breakOnExceptions. Fires only when the caller asked for a stop (line or
  function breakpoints, explicit breakOnExceptions other than 'none',
  stopOnEntry); the policy default does not count, so a deliberate plain
  run stays silent.
- debug-launcher.ts: decided once per launch, before the dry-run branch,
  so a dryRunSpawn configuration check and a restart_debugging replay
  carry it too (restart bypasses handler intake, which is why this lives
  in the launcher). When it fires, the #308/#467/#469 breakpoint-shaped
  warnings are withheld — each diagnoses a symptom of this one cause.
- tool-schemas.ts + docs/tool-reference.md: document the flag; the
  tool-list snapshot fence re-recorded for the new property.
- Tests: builder + launcher/dry-run/restart wiring (mock proxy), and an
  e2e against real js-debug asserting the warning replaces the #467 text.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Sep 16, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

… at #746 for debugpy (#710)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…lity; say "no effect" where the flag is ignored (#710)

Review of #747: the issue's premise that every adapter honours noDebug was
wrong, and the first version of this PR carried it into a warning that was
false for half the languages while suppressing the accurate diagnostics.
Measured per adapter (set a breakpoint, launch with noDebug):

  honours it: js-debug (runs, no stop); debugpy, Delve, CodeLLDB (they
  honour it too, but the launch currently fails under it — #746)
  ignores it: rdbg, netcoredbg, the JDI bridge, the mock adapter
  never sees it: rust (transformLaunchConfig builds from a fixed key set)

- AdapterPolicy.honoursNoDebug (pinned per language in the policy
  contract test): true for javascript/python/go/cpp, unset elsewhere.
- The launcher resolves noDebug the way the adapter will see it
  (adapterLaunchConfig over dapLaunchArgs over the server defaults — the
  proxy launcher's merge order) and decides debuggerOff = noDebug &&
  honoursNoDebug. Only that gates the #308/#467/#469 suppression.
- Where the adapter ignores the flag the warning says so ("noDebug has no
  effect with the ruby adapter: the debugger stays on") and the usual
  diagnostics stay.
- With the debugger off, readiness no longer waits for an entry stop that
  cannot come (stopOnEntry + noDebug used to sit on the 30 s ceiling), and
  the session is projected RUNNING instead of staying INITIALIZING.
- Builder takes ExceptionBreakMode; tests build real Breakpoint maps; the
  two dry-run payloads collapse into one helper; setMockProxyRunning moves
  to session-manager-test-utils (was copied in two suites).
- Docs, schema and fragment state which adapters do what; e2e adds the
  mock (ignoring) case beside the js (honouring) one. Follow-ups: #746
  (go/cpp findings added), #749 (downstream surfaces).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@debugmcpdev debugmcpdev changed the title fix(launch): warn when noDebug disables the breakpoints the caller set (#710) fix(launch): a noDebug launch says what it did to the debugger instead of blaming the breakpoints (#710) Sep 16, 2026
debugmcpdev and others added 4 commits September 16, 2026 19:09
…verything that reads it (#710)

Second review of #747, on the readiness handling added last round:

- The readiness-only stopOnEntry view was inert for python/go/cpp, whose
  isSessionReady is `state === PAUSED` (they always request an entry stop
  and auto-continue), and racy for js-debug, whose adapter-configured fires
  before the readiness listener exists while the core still withheld
  RUNNING under the caller's stopOnEntry; the post-hoc RUNNING projection
  then stamped a state nothing had observed. Replaced at the root: when
  debuggerOff, the launch args handed to the proxy, the handshake and the
  readiness wait carry stopOnEntry:false, so the core's projection, the
  readiness predicate and the adapter agree; the readiness policy drops
  its pause-only predicate (running is ready). session.lastLaunch and the
  warning keep the caller's value. Both band-aids are gone.
- noDebug and stopOnEntry are resolved the way the adapter sees them
  (adapterLaunchConfig over dapLaunchArgs [over defaults for noDebug]) and
  by truthiness — the proxy forwards a string 'true' verbatim and js-debug
  gates on truthiness.
- A failed launch under the flag (debugpy/Delve/CodeLLDB today, #746)
  carries the noDebug note in its data instead of an init failure with no
  pointer to the flag behind it.
- A stop that lands before the launch reports outranks the policy pin: the
  ordinary diagnostics stay and the note says the flag had no effect.
- Logpoints are counted apart from breakpoints, as the run-to-completion
  summary does; the redundant second policy lookup reads the hoisted one;
  overridePolicy's doc comment is back on overridePolicy and says which
  seam it targets.
- Tests: adapter-configured is emitted synchronously inside start() (the
  real ordering); a pause-only readiness policy; stopOnEntry via
  adapterLaunchConfig; string-typed noDebug; the failed-launch note; the
  stop-that-arrived-anyway guard; logpoint counting. The two readiness
  tests fail with the root fix disabled.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- A pause that came anyway counts as ready under debuggerOff (it fell to
  RUNNING-only and sat on the 30 s ceiling); an entry stop the core
  already resumed counts as evidence too (firstStopHandled is set on
  every stop of the launch), so a wrong policy pin plus stopOnEntry no
  longer reports "no stop can come" after one fired.
- stopOnEntry is neutralized in adapterLaunchConfig as well as
  dapLaunchArgs — adapterLaunchConfig wins the adapter merge, so the
  worker and the js child adoption would still have waited on it.
- The #295 worker force (stopOnEntry=true for pending CDP function
  breakpoints) yields to an honoured noDebug: the breakpoints cannot bind
  and the entry stop cannot come.
- Attach-shaped start_debugging is left alone: noDebug is a launch
  request property.
- A bare noDebug launch that fails under the flag carries a note too
  (the warning had nothing armed to say), and every failure return uses
  failureData — the dry-run timeout and the teardown branch dropped it.
- resolveLaunchFlag reads 'true'/'false' the way the proxy parser
  coerces them and drops the defaults source (the server defaults are
  typed stopOnEntry/justMyCode only); LaunchContext no longer needs
  defaultDapLaunchArgs. The texts name the flag alone — either source
  may have carried it. Readiness is one explicit predicate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… cannot know (#710)

"the adapter accepted the flag but the launch did not complete under it"
asserted causation on any failure under an honoured noDebug, including a
bad scriptPath. The note now says the debugger was off and offers the
remedy: drop the flag if the failure is unexpected, since some adapters
cannot complete a launch under it (#746).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@debugmcpdev
debugmcpdev merged commit 651237a into main Sep 17, 2026
10 checks passed
@debugmcpdev
debugmcpdev deleted the fix/710-nodebug-launch-warning branch September 17, 2026 00:04
debugmcpdev pushed a commit that referenced this pull request Sep 17, 2026
…t reason, a hit, an exception or an entry — and the launch warning stops claiming no stop can arrive (#749 review 3)

Third pass on #751 (twelve findings; eleven taken, one declined below):

- js-debug relabels a `debugger;` statement stop ('pause', "Paused on
  debugger statement") to 'breakpoint', and under noDebug that pause
  lands (measured) — so the clear must key on what the adapter itself
  reported: stopProvesDebuggerOn() = hitBreakpointIds present, or an
  exception or entry stop, or a breakpoint reason the adapter called one
  (raw and normalized). An uncaught throw under js noDebug was measured
  not to stop, so 'exception' stays as proof.
- The restored PAUSED clause in stoppedAnyway made the launch response say
  "no effect … breakpoints work as usual" while the record said they cannot
  bind. The root was #747's wording — "the debugger is disabled … and no
  stop can arrive" — refuted for js by a pause, a step and a `debugger;`
  statement. The warning now says the debugger is off for this launch and
  names what will not fire, so the record and the response agree, and the
  PAUSED clause goes again.
- The raw record is now `launchDebuggerOff` on ManagedSession, distinct
  from the projected `debuggerDisabled` on DebugSessionInfo, so no handler
  can read the ungated value by accident.
- isDebuggerOff() includes INITIALIZING: the proxy is up and a breakpoint
  set in that window still goes to the adapter, whose "Unbound breakpoint"
  needs the why.
- A paused session gets only the clause still true of it — breakpoints
  cannot bind — not "no stop is expected".
- The debugger-off pending-pause message keeps the policy's own
  explanation: on js-debug the pause can land under the flag, and #678's
  smart-stepper advice is what makes it.
- withDebuggerOffWhy no longer mutates the adapter's error: a new Error
  with the why, the original untouched as the cause.
- The composed "not paused" texts live in error-messages.ts (notPaused,
  cannotEvaluateNotPaused, stackTraceNotPaused, noStackFramesNotPaused,
  withDebuggerOffWhy, pausePendingDebuggerOff); one debuggerOffWhyFor(ctx,
  id) in handlers/shared.ts replaces three ctx→session→why copies; the
  USER_BREAK_REASONS re-export shim is gone.
- Declined: treating an auto-continued 'pause'-reason entry stop as proof
  the flag was ignored (a stale-pin js-debug build stopping at entry as
  'pause' while the launcher neutralised stopOnEntry) — the record
  self-corrects on the first breakpoint the adapter reports hit, and
  reading a pause as proof is the bug the round fixed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
debugmcpdev added a commit that referenced this pull request Sep 17, 2026
…so on every later surface (#749) (#751)

* fix(session): a session whose launch runs with the debugger off says so on every later surface (#749)

#747 decided "this noDebug launch runs with the debugger off" once, for the
start_debugging response, and forgot it. Nothing on the session recorded
it, so set_breakpoint on the running session came back verified:false with
no reason, list_breakpoints showed everything unbound with none,
pause_execution ended in the #678 "may be blocked in native code" guess,
and stepping, get_stack_trace, get_local_variables and evaluate_expression
answered "not paused" in debugger terms.

Record: `DebugSessionInfo.debuggerDisabled` (inherited by ManagedSession),
written in the launcher where debuggerOff is decided (real launches, not
dry runs), reset in the launcher's and the attach controller's per-attempt
blocks — not in the core's setupProxyEventHandlers, which runs inside
proxyLauncher.start after the launcher wrote it — and cleared in
handleStopped: a real stop is stronger evidence than the policy's pin.
Projected by SessionStore.getAll() and list_debug_sessions.

Consult, never pre-empting the adapter: every request still goes to the
adapter and its own answer is kept; one sentence
(ErrorMessages.debuggerOffForLaunch) is added beside it — set_breakpoint
(unverified only), list_breakpoints (top-level warning), pause pending
(instead of the policy's guess) and pause refused (appended), "Not paused:"
on step/continue, and the stack-trace note, evaluate error and locals
message. Measured: js-debug still lands a pause under noDebug, so the
sentence says "no stop is expected" rather than "cannot come", and that
stop clears the flag.

Tests: field lifecycle in session-manager-nodebug-warning.test.ts, both
projections, every consumer, and a js e2e over pause_test.js (run locally
— CI does not run the e2e project).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(session): a pause does not clear the debugger-off decision; one gated why for every surface (#749 review)

Review of #751 (nine findings, all taken):

- handleStopped cleared debuggerDisabled on ANY stop, but the PR's own
  measurement says js-debug lands a user pause under noDebug with its
  breakpoints still off — so after the first pause every surface lost its
  why again. Only a stop a live debugger produces clears it now
  (DEBUGGER_ON_STOP_REASONS: the breakpoint family, exception, entry,
  step); the launcher's stoppedAnyway reads the same record instead of its
  own firstStopHandled/PAUSED discriminator, so the launch response and
  the record cannot disagree.
- The record was never cleared on STOPPED/ERROR, so a breakpoint queued
  after the run said "cannot bind" and list_debug_sessions kept reporting
  a stopped session as debugger-off. isDebuggerOff() gates the decision on
  the session being running or paused; the projection and every consumer
  read through it.
- One home for the concern: src/session/debugger-off.ts (the reason set,
  isDebuggerOff, debuggerOffWhy) replaces five hand-spliced reads.
- The pending-pause message appended "no stop is expected" to a base text
  that promises one; ErrorMessages.pausePendingDebuggerOff is one message.
- A refused pause threw a fresh Error, dropping the adapter's own error
  object (stack, props); the original is rethrown with the why appended,
  and the no-debug-target return carries the why too.
- list_breakpoints warned even when every record was verified, contradicting
  set_breakpoint's own gate; now only with an unverified record.
- The e2e's refusal branch was unreachable (callTool throws on an MCP
  error); callToolSafely, and the landed-pause branch asserts the decision
  is kept, as measured.
- docs: list_breakpoints documents the top-level warning; the noDebug
  paragraph, the list_debug_sessions field note and the fragment describe
  which stops clear the decision and that it is not reported once the
  session is over.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(session): a step from a noDebug pause proves nothing either; the decision is consulted only while running or paused (#749 review 2)

Re-review of #751 (eight findings, all taken):

- 'step' was in the set of stops that clear the decision, but the js
  pause the PR measured lands because the inspector is attached, and so
  does a step taken from it (measured: reason 'step', breakpoints still
  unbound) — one step_over after the pause put every surface back in the
  pre-#749 state. The set is now the user-asked stops (breakpoint family,
  exception) plus 'entry'; the e2e steps after the pause and checks the
  record survives.
- A launch refused before the proxy existed (the MSVC-toolchain branch)
  moves the session back to CREATED with the record intact, and the gate
  only excluded terminal states. isDebuggerOff() now means running or
  paused, exactly as the docs said.
- stoppedAnyway had dropped the PAUSED clause, so a launch ending paused on
  a 'pause' stop could say no stop can arrive. Restored: a paused launch
  never claims that, whatever the record says about breakpoints.
- withDebuggerOffWhy appends to the adapter's own error; when the message
  will not take the append (getter-only), it wraps with the original as
  the cause instead of throwing from inside the pause path.
- The tool-reference bullet said "no stop ever arrives" and, four sentences
  on, that js-debug lands a pause: "no breakpoint, exception or entry stop
  ever arrives".
- The e2e comment said the pause makes the session forget the decision
  while the assertion checked the opposite; the comment matches now.
- handleListDebugSessions re-gated a field SessionStore.getAll() had
  already gated; it mirrors the field.
- USER_BREAK_REASONS moved next to BREAKPOINT_STOP_REASONS in
  @debugmcp/shared (the core re-exports it); DEBUGGER_ON_STOP_REASONS is
  that set plus 'entry' rather than a third hand-built union.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(session): proof the debugger is on is the adapter's own breakpoint reason, a hit, an exception or an entry — and the launch warning stops claiming no stop can arrive (#749 review 3)

Third pass on #751 (twelve findings; eleven taken, one declined below):

- js-debug relabels a `debugger;` statement stop ('pause', "Paused on
  debugger statement") to 'breakpoint', and under noDebug that pause
  lands (measured) — so the clear must key on what the adapter itself
  reported: stopProvesDebuggerOn() = hitBreakpointIds present, or an
  exception or entry stop, or a breakpoint reason the adapter called one
  (raw and normalized). An uncaught throw under js noDebug was measured
  not to stop, so 'exception' stays as proof.
- The restored PAUSED clause in stoppedAnyway made the launch response say
  "no effect … breakpoints work as usual" while the record said they cannot
  bind. The root was #747's wording — "the debugger is disabled … and no
  stop can arrive" — refuted for js by a pause, a step and a `debugger;`
  statement. The warning now says the debugger is off for this launch and
  names what will not fire, so the record and the response agree, and the
  PAUSED clause goes again.
- The raw record is now `launchDebuggerOff` on ManagedSession, distinct
  from the projected `debuggerDisabled` on DebugSessionInfo, so no handler
  can read the ungated value by accident.
- isDebuggerOff() includes INITIALIZING: the proxy is up and a breakpoint
  set in that window still goes to the adapter, whose "Unbound breakpoint"
  needs the why.
- A paused session gets only the clause still true of it — breakpoints
  cannot bind — not "no stop is expected".
- The debugger-off pending-pause message keeps the policy's own
  explanation: on js-debug the pause can land under the flag, and #678's
  smart-stepper advice is what makes it.
- withDebuggerOffWhy no longer mutates the adapter's error: a new Error
  with the why, the original untouched as the cause.
- The composed "not paused" texts live in error-messages.ts (notPaused,
  cannotEvaluateNotPaused, stackTraceNotPaused, noStackFramesNotPaused,
  withDebuggerOffWhy, pausePendingDebuggerOff); one debuggerOffWhyFor(ctx,
  id) in handlers/shared.ts replaces three ctx→session→why copies; the
  USER_BREAK_REASONS re-export shim is gone.
- Declined: treating an auto-continued 'pause'-reason entry stop as proof
  the flag was ignored (a stale-pin js-debug build stopping at entry as
  'pause' while the launcher neutralised stopOnEntry) — the record
  self-corrects on the first breakpoint the adapter reports hit, and
  reading a pause as proof is the bug the round fixed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(schema): the dapLaunchArgs.noDebug description stops claiming no stop ever arrives (#749)

The schema is the one noDebug text the model reads before choosing the
flag, and it still said "no stop ever arrives" after #751 retired that
claim in the warning, the fragment and the docs: js-debug lands a user
pause under the flag. It now says what is true of every honouring
adapter — no breakpoint, exception or entry stop — and that the session
reports debuggerDisabled while such a launch runs. Fences re-recorded.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(server): get_stack_trace on a running session with no thread answers not-paused with the why, not "no active proxy" (#749)

Measured on the merged tree (#750 + #751) with debugpy under noDebug:
nothing ever stops, so no thread is current, and debugpy refuses the
`threads` discovery ("Server is not available") — the facade then threw
ProxyNotRunningError, "Cannot get stack trace: no active proxy", for a
session whose proxy was alive. js-debug hid this: its inspector answers
`threads`, so the js e2e reached the resolver's not-paused note.

On a session that is not paused, no thread is expected; the session
layer's not-paused answer (with the debugger-off why) needs none, so the
facade hands it there. A paused session with no thread to name keeps
the error — that is the anomaly it describes. The two existing throw
tests now say "paused".

The python surfaces e2e (the clean case: debugpy attaches no debugger
at all) covers set_breakpoint, list_breakpoints, get_stack_trace,
get_local_variables, evaluate_expression, step_over and pause_execution;
watched it fail on exactly this answer against a dist without the fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(session): a verified breakpoint is proof the debugger is on; get_stack_trace never claims "no active proxy"; pause on an initializing launch says why (#749 review 4)

Fourth pass, on the merged tree (#750 + #751), seven findings, all taken:

- get_stack_trace asked the adapter for `threads` before the state check
  that made the answer irrelevant: a session that is not paused gets the
  session layer's not-paused note (with the why) with no round trip — on
  a wedged adapter that request blocked for the DAP timeout first.
- The PAUSED + no-thread branch still threw "no active proxy" for a proxy
  that was alive; the resolver's "No stopped thread is known for this
  session." is the truthful answer, so the throw is gone (the error stays
  for a session with no proxy at all).
- A breakpoint the adapter verified is proof this build debugs after all,
  and it arrives before any hit. Measured first: every honouring adapter
  refuses or unbinds a live breakpoint under the flag (js-debug "Unbound
  breakpoint", debugpy "Server is not available", Delve "noDebug mode:
  unable to process 'setBreakpoints'", CodeLLDB "Not supported in noDebug
  mode"), so a verified record can only come from a build that ignores
  it. Consulted on read (adapterVerifiedABreakpoint, inside isDebuggerOff)
  since bindings are per-launch state; the launch response reads the same
  evidence, so "will not fire" is never said of a breakpoint
  list_breakpoints shows bound.
- pause_execution on an INITIALIZING debugger-off session was the one
  surface without the why.
- withDebuggerOffWhy now passes the rejected value itself as the cause,
  not a synthesized Error, when the bridge threw a non-Error.
- The "Used in" map on debuggerOffForLaunch named five files that never
  reference it; it names the one gate that does.
- The list_breakpoints doc note quotes the paused variant too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: JF <john.franklin@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

start_debugging: dapLaunchArgs.noDebug silently disables every breakpoint; warn at intake when breakpoints are set

2 participants