Skip to content
Merged
2 changes: 1 addition & 1 deletion changelog.d/710.fixed.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@
**A `noDebug` launch says what it did to the debugger instead of blaming your breakpoints** — `dapLaunchArgs.noDebug: true` is DAP's "launch without enabling debugging", and where the adapter honours it 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` while the caller waited for a stop that could not come. Whether the flag turns the debugger off is now the adapter policy's word (`honoursNoDebug`), measured rather than assumed: js-debug, debugpy, Delve and CodeLLDB honour it; rdbg, netcoredbg and the Java bridge ignore it, and the rust launch transform never forwards it. Where the debugger is off, `start_debugging` warns when the flag is set alongside something the caller asked to stop on — line or function breakpoints, an *explicit* `breakOnExceptions` other than `none` (the policy default does not count, so a deliberate plain run stays silent), or `stopOnEntry` — naming each thing that will not fire, withholds the breakpoint-shaped launch warnings (#308, #467, #469) that presuppose a debugger, and no longer waits for an entry stop that cannot come. Where the adapter ignores the flag, the warning says it had no effect and the usual diagnostics stay. The flag is read the way the adapter will see it (`adapterLaunchConfig` over `dapLaunchArgs` over the server defaults), and the decision lives in the launcher, so `restart_debugging` and a `dryRunSpawn` configuration check carry the same warning; the `dapLaunchArgs` schema documents the flag. Found by the review of PR #706 (#710)
**A `noDebug` launch says what it did to the debugger instead of blaming your breakpoints** — `dapLaunchArgs.noDebug: true` is DAP's "launch without enabling debugging", and where the adapter honours it no breakpoint binds and no breakpoint, exception or entry 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` while the caller waited for a stop that could not come. Whether the flag turns the debugger off is now the adapter policy's word (`honoursNoDebug`), measured rather than assumed: js-debug, debugpy, Delve and CodeLLDB honour it; rdbg, netcoredbg and the Java bridge ignore it, and the rust launch transform never forwards it. Where the debugger is off, `start_debugging` warns when the flag is set alongside something the caller asked to stop on — line or function breakpoints, an *explicit* `breakOnExceptions` other than `none` (the policy default does not count, so a deliberate plain run stays silent), or `stopOnEntry` — naming each thing that will not fire, withholds the breakpoint-shaped launch warnings (#308, #467, #469) that presuppose a debugger, and no longer waits for an entry stop that cannot come. Where the adapter ignores the flag, the warning says it had no effect and the usual diagnostics stay. The flag is read the way the adapter will see it (`adapterLaunchConfig` over `dapLaunchArgs` over the server defaults), and the decision lives in the launcher, so `restart_debugging` and a `dryRunSpawn` configuration check carry the same warning; the `dapLaunchArgs` schema documents the flag. Found by the review of PR #706 (#710)
1 change: 1 addition & 0 deletions changelog.d/749.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
**A session whose launch runs with the debugger off says so on every later surface** — `start_debugging` decided "this `noDebug` launch runs with the debugger off" (#710) and then forgot it: `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 "may be blocked in native code" guess, and stepping, `get_stack_trace`, `get_local_variables` and `evaluate_expression` answered "not paused" in debugger terms — or worse: with no thread ever current and debugpy refusing the `threads` discovery, `get_stack_trace` claimed "no active proxy" for a session whose proxy was alive. The decision is now recorded on the session — `list_debug_sessions` reports `debuggerDisabled: true` while the launch is live — reset per launch and attach, and cleared by a `stopped` event only a live debugger produces (a breakpoint the adapter itself reports or names in `hitBreakpointIds`, an exception, an entry stop): that proves the debugger on for the adapter build — as does a breakpoint the adapter verified, since every honouring adapter refuses or unbinds one under the flag. A pause, a step taken from one, or a `debugger;` statement does not clear it — js-debug lands all three under the flag with its breakpoints still unbound — and a paused session is told only that breakpoints cannot bind. The `start_debugging` warning itself no longer claims "no stop can arrive": it says the debugger is off for this launch and names what will not fire. Every request still goes to the adapter and its own answer is kept ("Unbound breakpoint", a refusal in the adapter's words); the recorded fact adds the why beside it — "the debugger is off for this launch (noDebug is true): breakpoints cannot bind and no stop is expected; drop noDebug and launch again to debug" — and a pause that is accepted but never lands gets a message that promises no stop instead of the native-code guess (#749)
5 changes: 4 additions & 1 deletion docs/tool-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ Lists all active debugging sessions.

**`lastStop`:** present while the session is `paused` (the stop it is at: `reason`, `threadId`, `timestamp`, the adapter's `description`/`text`, and `exceptionInfo` for exception stops) and after it reaches `stopped`/`error` (the last stop before it ended). A `running` session never carries one, so a poller that calls this after `continue_execution` or a step sees `state: "running"` with no stop record until the next stop lands — the record of the stop it just left is not repeated as if the program were still paused.

**`debuggerDisabled`:** present (`true`) while the current launch runs with the debugger off — `dapLaunchArgs.noDebug: true` on an adapter that honours it (see `start_debugging`) — and the launch is live (`initializing`, `running` or `paused`). Cleared by the next launch or attach, and by a `stopped` event only a live debugger produces: a breakpoint the adapter itself reports (or names in `hitBreakpointIds`), an exception stop, or an entry stop — not a pause, a step, or a `debugger;` statement js-debug relabels as a breakpoint. Omitted otherwise (issue #749).

Errored sessions include optional `diagnostics` with the current launch attempt's server-host `proxyLogPath` and remote-safe `proxyLogResource`. The record is retained for proxy initialization failures and for proxy/adapter deaths after initialization, and is cleared when a new launch or attach attempt begins.

---
Expand Down Expand Up @@ -300,6 +302,7 @@ Lists all breakpoints in a session with their current verified state and adapter
- `functionBreakpoints`/`functionCount` are always present in the unfiltered response (empty arrays when none exist). When filtering by `file` they are omitted — function breakpoints are session-global, not file-scoped.
- `adapterId` is the debug adapter's own numeric id for the breakpoint, captured from setBreakpoints responses and breakpoint events. It is absent until the adapter has seen the breakpoint.
- Verification is eventually consistent: some adapters (js-debug, JDI, netcoredbg) bind breakpoints asynchronously and confirm via DAP breakpoint events shortly after launch or class load.
- `warning` (top level) appears only while the session's current launch runs with the debugger off (`noDebug: true` on an adapter that honours it, see `start_debugging`) and at least one listed breakpoint is unverified: "the debugger is off for this launch (noDebug is true): breakpoints cannot bind and no stop is expected; drop noDebug and launch again to debug" — on a paused session (js-debug lands a pause under the flag) only the clause still true of it, "…: breakpoints cannot bind; drop noDebug and launch again to debug". The per-breakpoint records keep the adapter's own answers; a breakpoint the adapter *did* verify is proof this build debugs after all, and the warning (with `debuggerDisabled` and every other debugger-off note) drops (issue #749).
- A breakpoint the program has stopped on is reported `verified: true` from that stop onward, even if the adapter never confirmed it (issue #673), for adapters whose `stopped` event names the breakpoints it hit (`hitBreakpointIds`: js-debug, debugpy, Delve, CodeLLDB — netcoredbg, the JDI bridge and rdbg omit the field). Such a record carries `verifiedBy: "hit"` until the adapter itself confirms it (`"adapter"`); an adapter answer of "unbound" does not downgrade it. A provisional "Unbound breakpoint" `message` is dropped by the hit; any other note is kept.
- On entries of the `breakpoints` array, `boundFile`/`boundLine` appear when the adapter answers under a *different* file from the request. For a source-mapped `.ts` request on a JavaScript launch with maps on (the default) js-debug verifies the request under the `.ts` path, `get_stack_trace` frames show `.ts`, and the pair is absent; it appears when js-debug answers under the generated `dist/*.js` instead — measured with `adapterLaunchConfig: { sourceMaps: false }` (the second entry in the example above was captured that way, and the frames then show the generated file too), and possible whenever the `.ts` source cannot be resolved through the map (issues #673, #700). `file` and `line` keep describing the request; the bound pair is where it landed. (Entries of `functionBreakpoints` use the same names for the bound location of the symbol, present whenever it is bound.)

Expand Down Expand Up @@ -366,7 +369,7 @@ Starts debugging a script.
- `dapLaunchArgs` (object, optional): Standard DAP launch arguments:
- `stopOnEntry` (boolean): Stop at first line (default `false` — the opposite of attach, which pauses unless `stopOnEntry` is `false`)
- `justMyCode` (boolean): Debug only user code (default `true`). JavaScript launch: `true` blackboxes `node_modules` through js-debug's `skipFiles` and keeps js-debug's smart-stepper on, so a pause or step that lands in skipped code (Node internals, `node_modules`) is stepped through and may never land (`pending: true`, with an explanation); `false` drops `node_modules` from the skip list and turns the stepper off, so steps land inside dependencies and `pause_execution` lands as soon as any JavaScript runs (issue #678). A caller `skipFiles` replaces the default list. Source maps are on for every JavaScript launch, `.js` programs included — stops report `src/*.ts` when maps and sources are present; `adapterLaunchConfig: { sourceMaps: false }` opts out (issue #684)
- `noDebug` (boolean): DAP's "launch without enabling debugging" flag (default `false`). What it does depends on the adapter, and the response says which case you are in. **Honoured** — the debugger is off: no breakpoint binds, no exception filter arms, no entry stop lands, and no stop ever arrives, so a short script ends `stopped` and a server stays `running`. That is a legitimate plain run and says nothing by itself; when the session has breakpoints (line or function), an explicit `breakOnExceptions` other than `"none"`, or `stopOnEntry`, the `warning` says the debugger is off and names what will not fire, in place of the unbound-breakpoint diagnostics that would otherwise send you to check paths that are fine (issue #710). js-debug, debugpy, Delve and CodeLLDB honour it (debugpy and Delve open no configuration phase at all, so the launch is complete on the launch response; CodeLLDB refuses the configuration requests, and the refusal reaches the `warning` and `get_output` — issue #746). The run ends `stopped` with the program's output and, where the adapter reports one, the exit code (Delve prints the status to the console only). Under the flag Delve runs the program through Go's `exec`, so on Windows the binary needs its `.exe` extension. **Ignored** — rdbg, netcoredbg and the Java bridge run the debugger regardless, and the rust launch transform never forwards the flag: the `warning` says the flag had no effect and everything works as usual. The same warning comes back from `restart_debugging` and from a `dryRunSpawn` check.
- `noDebug` (boolean): DAP's "launch without enabling debugging" flag (default `false`). What it does depends on the adapter, and the response says which case you are in. **Honoured** — the debugger is off: no breakpoint binds, no exception filter arms, no entry stop lands, and no breakpoint, exception or entry stop ever arrives, so a short script ends `stopped` and a server stays `running`. That is a legitimate plain run and says nothing by itself; when the session has breakpoints (line or function), an explicit `breakOnExceptions` other than `"none"`, or `stopOnEntry`, the `warning` says the debugger is off for this launch and names what will not fire — not that no stop of any kind can come — in place of the unbound-breakpoint diagnostics that would otherwise send you to check paths that are fine (issue #710). js-debug, debugpy, Delve and CodeLLDB honour it (debugpy and Delve open no configuration phase at all, so the launch is complete on the launch response; CodeLLDB refuses the configuration requests, and the refusal reaches the `warning` and `get_output` — issue #746). The run ends `stopped` with the program's output and, where the adapter reports one, the exit code (Delve prints the status to the console only). Under the flag Delve runs the program through Go's `exec`, so on Windows the binary needs its `.exe` extension. **Ignored** — rdbg, netcoredbg and the Java bridge run the debugger regardless, and the rust launch transform never forwards the flag: the `warning` says the flag had no effect and everything works as usual. The same warning comes back from `restart_debugging` and from a `dryRunSpawn` check. While such a launch runs, the session remembers the decision (`list_debug_sessions` reports `debuggerDisabled: true`) and every later surface says why: `set_breakpoint` and `list_breakpoints` keep the adapter's own answer (still sent — e.g. js-debug's "Unbound breakpoint", or CodeLLDB's refusal echoed onto each pre-launch breakpoint) and add "the debugger is off for this launch (noDebug is true)…", `pause_execution` is still sent and the same sentence explains a pause that never lands or one the adapter refused, and `step_*`/`continue_execution`/`get_stack_trace`/`get_local_variables`/`evaluate_expression` append it to their "not paused" answers. A `stopped` event only a live debugger produces — a breakpoint the adapter itself reports (or names in `hitBreakpointIds`), an exception stop, an entry stop — proves the debugger on for that adapter build and clears the decision, and so does a breakpoint the adapter verified (every honouring adapter refuses or unbinds one under the flag, so a bound one can only come from a build that ignores it); a pause, a step taken from one, or a `debugger;` statement do not (js-debug lands all three under the flag while its breakpoints stay unbound; an uncaught throw does not stop). On a paused session the sentence keeps only the clause still true of it — breakpoints cannot bind — and outside a live launch (`initializing`/`running`/`paused`) the decision is neither reported nor consulted (issue #749).
- Additional DAP launch keys (`program`, `cwd`, `env`, language-specific options) pass through to the adapter. Top-level parameters do **not** belong here: a nested `breakOnExceptions` is honored as an alias (the top-level value wins if both are given) and reported via a `warning` in the response; other misplaced top-level keys (`dryRunSpawn`, `sessionId`, `scriptPath`, `adapterLaunchConfig`) are stripped with a warning instead of silently riding into the launch config.
- `adapterLaunchConfig` (object, optional): Adapter-specific launch configuration overrides. Use this for language-specific settings that go beyond standard DAP arguments (e.g., `mainClass` and `classpath` for Java, `buildCommand` for Rust). For Rust, `_adapterSettings` passes through to CodeLLDB (issue #441) — e.g. `{"_adapterSettings": {"scriptConfig": {"lang": {"rust": {"sysroot": "/path"}}}}}` points the Rust formatter lookup at an explicit sysroot; the `CODELLDB_RUST_SYSROOT` env var does the same without per-launch config (a user-supplied `_adapterSettings` value wins over the env var).
- `dryRunSpawn` (boolean, optional): Test spawn without actually starting
Expand Down
2 changes: 1 addition & 1 deletion packages/shared/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ export type {
QueuedDapCommand,
PendingStopContext
} from './interfaces/adapter-policy.js';
export { BREAKPOINT_STOP_REASONS } from './interfaces/adapter-policy.js';
export { BREAKPOINT_STOP_REASONS, USER_BREAK_REASONS } from './interfaces/adapter-policy.js';
export {
DefaultAdapterPolicy,
resolveExceptionFilters,
Expand Down
7 changes: 7 additions & 0 deletions packages/shared/src/interfaces/adapter-policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@ export const BREAKPOINT_STOP_REASONS: ReadonlySet<string> = new Set([
'instruction breakpoint'
]);

/**
* Stop reasons the user asked for: the breakpoint family plus an exception
* the user asked to break on. The first-stop auto-continue must never
* swallow one, and one is proof a debugger is live (issues #749, #746).
*/
export const USER_BREAK_REASONS: ReadonlySet<string> = new Set([...BREAKPOINT_STOP_REASONS, 'exception']);

/**
* Context passed to AdapterPolicy.normalizeStopReason (issues #260/#302).
* See that method's doc comment for the completeness rules.
Expand Down
9 changes: 9 additions & 0 deletions packages/shared/src/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,15 @@ export interface DebugSessionInfo {
exitCode?: number;
/** Present when the session is in ERROR because its proxy failed. */
diagnostics?: SessionFailureDiagnostics;
/**
* Present (`true`) while the current launch runs with the debugger off:
* the launch carried `noDebug: true` and the adapter honours it (issue
* #710), so no breakpoint can bind. A projection of the session's
* `launchDebuggerOff` record, made only while the launch is live
* (initializing, running or paused) — see `isDebuggerOff` in
* `src/session/debugger-off.ts` (issue #749).
*/
debuggerDisabled?: boolean;
/**
* Live DAP mirror endpoint from expose_session (issue #217), host/port
* only — the attach token is returned solely by the expose_session tool.
Expand Down
Loading
Loading