fix(session): a session whose launch runs with the debugger off says so on every later surface (#749) - #751
Conversation
…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>
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
…ated 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>
Review round 1 (
|
| # | Finding | Resolution |
|---|---|---|
| 1 | handleStopped cleared the decision on any stop, but js-debug lands a user pause under noDebug with breakpoints still off — after the first pause the surfaces lost their why |
Only a stop a live debugger produces clears it: DEBUGGER_ON_STOP_REASONS = breakpoint family ∪ exception, entry, step. The launcher's stoppedAnyway now reads the same record (finalSession.debuggerDisabled !== true) instead of its own discriminator. Tests: a pause stop keeps the flag; each of the five reasons clears it; the e2e asserts the flag survives the landed pause (measured live: "debuggerDisabled":true after paused) |
| 2 | Pending-pause message appended "no stop is expected" to a base that promises one | ErrorMessages.pausePendingDebuggerOff(n) — one message, no native-code guess, no promise |
| 3 | Never cleared on STOPPED/ERROR — a queued breakpoint after the run said "cannot bind"; the listing kept the flag on a stopped session | isDebuggerOff(session) gates on running/paused; projection (store + handler) and every consumer go through it; doc comment + tool-reference say so |
| 4 | e2e refusal branch unreachable (MCP error throws out of callTool) |
callToolSafely; the landed-pause branch asserts the decision is kept |
| 5 | list_breakpoints warned beside a verified record, contradicting set_breakpoint's gate |
Warning only when at least one listed record is unverified |
| 6 | Refused pause threw a fresh Error, dropping the adapter's own object |
The original error is rethrown with the why appended to its message (test asserts identity + a preserved prop) |
| 7 | NO_DEBUG_TARGET_MARKER return carried no why |
Carries it |
| 8 | Five hand-spliced reads of the sentence/gate | src/session/debugger-off.ts (DEBUGGER_ON_STOP_REASONS, isDebuggerOff, debuggerOffWhy) is the one home; joiners stay per site for grammar |
| 9 | list_breakpoints reference lacked the top-level warning |
Documented |
Re-verified: session + server suites (1213), unit + integration, typecheck:all, lint, check:docs; js e2e green against the rebuilt dist.
…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>
Review round 2 (
|
| # | Finding | Resolution |
|---|---|---|
| 1 | step was in the clearing set, but the measured js pause lands because the inspector is attached — so a step taken from it lands too (reason step) and wiped the decision, putting every surface back in the pre-#749 state |
Measured live: pause → step_over → lastStop.reason: 'step', breakpoints still unbound. DEBUGGER_ON_STOP_REASONS = USER_BREAK_REASONS ∪ entry; a unit test pins pause-then-step keeping the record; the e2e now steps after the pause and checks it survives |
| 2 | The MSVC-toolchain refusal returns the session to CREATED with the record intact, and the gate only excluded terminal states | isDebuggerOff() = recorded ∧ (RUNNING ∨ PAUSED), as the docs said; projection test covers CREATED/INITIALIZING/STOPPED/ERROR vs PAUSED |
| 3 | stoppedAnyway lost the finalState === PAUSED clause |
Restored — a launch that ends paused never claims no stop can come; test with a pause stop during launch (record kept, warning not "no stop can arrive") |
| 4 | Mutating the adapter's error assumes a writable message |
Guarded: append in place (keeps identity/props, as round 1 asked); a getter-only message is wrapped with the original as cause. Test with a frozen message |
| 5 | The noDebug bullet contradicted itself ("no stop ever arrives" … "js-debug lands a user pause") | "no breakpoint, exception or entry stop ever arrives" |
| 6 | e2e comment rot | Comment matches the assertion (pause and step both keep the decision) |
| 7 | handleListDebugSessions re-gated a field getAll() already gated |
Mirrors the field; the handler test no longer feeds a shape the store cannot produce |
| 8 | Third hand-built reason union | USER_BREAK_REASONS moved beside BREAKPOINT_STOP_REASONS in @debugmcp/shared (core re-exports); the set here is that ∪ entry |
Re-verified: session + server suites (1216), unit + integration, typecheck:all, lint, check:docs; js e2e green against the rebuilt dist (pause, then step, record intact).
…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>
Review round 3 (
|
| # | Finding | Resolution |
|---|---|---|
| 1 | Pending-pause under the flag dropped #678's smart-stepper hint, which on js-debug is the advice that makes the pause land | pausePendingDebuggerOff(n, policyHint) keeps the policy's explanation |
| 2 | The restored PAUSED clause made the launch say "no effect … breakpoints work as usual" while the record said they cannot bind | Root cause was #747's "no stop can arrive" wording — refuted for js by a pause, a step and a debugger;. The warning now says the debugger is off for this launch: N breakpoint(s) will not fire; the PAUSED clause goes; response and record agree |
| 3 | js-debug's relabel of a debugger; stop to breakpoint cleared the record |
stopProvesDebuggerOn(reason, rawReason, body): hitBreakpointIds, or exception/entry, or a breakpoint reason the adapter itself gave (raw and normalized). Tests: the relabelled stop keeps the record; a pause with hitBreakpointIds clears it |
| 4 | exception unmeasured |
Measured: no stop under the flag → stays |
| 5 | PAUSED got "no stop is expected" | debuggerOffForLaunchPaused: the binding clause only |
| 6 | Auto-continued pause-reason entry stop under a stale pin |
Declined: reading a pause as proof is the bug this round fixed; the record self-corrects on the first breakpoint the adapter reports hit |
| 7 | In-place mutation of the adapter's error | Always new Error(composed, { cause }); the original untouched (tests assert cause identity and that its message is unchanged) |
| 8 | USER_BREAK_REASONS re-export shim |
Removed; the controller imports from @debugmcp/shared |
| 9 | Three ctx→session→why copies | debuggerOffWhyFor(ctx, id) in handlers/shared.ts |
| 10 | Raw record shares its name with the projection | ManagedSession.launchDebuggerOff (raw) vs DebugSessionInfo.debuggerDisabled (projected by getAll() only) |
| 11 | INITIALIZING window unexplained | isDebuggerOff includes INITIALIZING (a breakpoint set then still reaches the adapter) |
| 12 | Composed "not paused" literals scattered | Centralised in error-messages.ts (notPaused, cannotEvaluateNotPaused, stackTraceNotPaused, noStackFramesNotPaused, withDebuggerOffWhy) |
Re-verified: session + server suites (1219), unit + integration, typecheck:all, lint, check:docs; the #710 and #749 js e2e cases green against the rebuilt dist.
… 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>
…ers 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>
…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>
Review round 4 (
|
| # | Finding | Resolution |
|---|---|---|
| 1 | get_stack_trace asked the adapter for threads before the state check that made the answer irrelevant (a wasted round trip; the DAP timeout on a wedged adapter) |
A session that is not paused goes straight to the session layer's not-paused note. Test: sendDapRequest not called on a running debugger-off session |
| 2 | The PAUSED + no-thread branch still threw "no active proxy" for a live proxy | The resolver's "No stopped thread is known for this session." is the answer in both branches; the error stays only for a session with no proxy at all. The two throw tests and the handler test now pin that |
| 3 | A breakpoint the adapter verified is as strong a proof as a stop that the build ignores the flag, but nothing consumed it — contradictory answers until the first hit | Measured first on the merged build: 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' request", CodeLLDB "Not supported in noDebug mode"), so a verified record can only come from a build that ignores it. adapterVerifiedABreakpoint inside isDebuggerOff — consulted on read, since bindings are per-launch state — and the launcher reads the same evidence, so the launch response never says "will not fire" of a breakpoint list_breakpoints shows bound. Tests: projection drops (line and function breakpoints), the why drops, the launch says "no effect" on a verified echo with no stop |
| 4 | pause_execution on an INITIALIZING debugger-off session was the one surface without the why |
Composed with debuggerOffWhy; test |
| 5 | "Used in:" on debuggerOffForLaunch named five files that never reference it |
Names the one gate (debugger-off.ts) |
| 6 | The list_breakpoints doc note quoted only the running sentence |
Quotes the paused variant too, and the verified-breakpoint drop |
| 7 | withDebuggerOffWhy attached a synthesized Error as cause for a non-Error rejection |
The rejected value itself is the cause; test |
Re-verified on the merged tree: typecheck:all, lint, check:docs; unit + integration (5435); python/js/cpp/go noDebug e2e (34) against the rebuilt dist.
Closes #749.
What was wrong
#747 decides "this
noDebuglaunch runs with the debugger off" once, for thestart_debuggingresponse, and then forgets it. Nothing on the session recorded it, so every later surface explained a debugger-off session in debugger terms. Measured on this branch's base with js-debug andexamples/javascript/pause_test.jslaunched{ stopOnEntry: false, noDebug: true }:set_breakpointon the running session →verified: false,warning: "Unbound breakpoint"— the adapter's answer with no reason;list_breakpoints→ every breakpoint unbound, no reason;get_stack_trace→ "Session is not paused (state: running); stack traces are only available while paused." — true, but not why;step_over/continue_execution→Not paused;pause_execution→ and here the measurement corrected the issue: js-debug lands the pause (state: paused,lastStop.reason: 'pause'; Node printsDebugger attached.). UndernoDebugjs-debug disables its debug domains — breakpoints — but the inspector is attached and a user pause works.What changes
Record. One raw record,
ManagedSession.launchDebuggerOff— "the current launch runs with the debugger off" — projected asDebugSessionInfo.debuggerDisabledbySessionStore.getAll()alone, so no handler can read the ungated value by accident. Written in the launcher right wheredebuggerOffis decided (real launches only, not dry runs), reset in the launcher's and the attach controller's per-attempt blocks — deliberately not in the core'ssetupProxyEventHandlers, which runs insideproxyLauncher.startafter the launcher wrote it — and cleared inhandleStoppedby a stop only a live debugger produces (stopProvesDebuggerOn:hitBreakpointIdspresent, an exception or entry stop, or a breakpoint reason the adapter itself gave — raw and normalized): that proves the adapter build debugs after all. A breakpoint the adapter verified is proof of the same strength and arrives before any hit — measured on the merged build: 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; it is consulted on read (adapterVerifiedABreakpoint, insideisDebuggerOff) since bindings are per-launch state, and the launcher reads the same evidence so "will not fire" is never said of a breakpointlist_breakpointsshows bound. A pause, a step taken from one, or adebugger;statement does not clear it — all measured on js-debug under the flag: the inspector is attached, so they land (js-debug relabels thedebugger;stopbreakpoint), while line breakpoints still cannot bind and an uncaught throw does not stop. The launcher'sstoppedAnywayreads the same record, so the launch response and the record cannot disagree — which also meant retiring #747's "no stop can arrive" wording: thestart_debuggingwarning now says the debugger is off for this launch: N breakpoint(s) will not fire.restart_debuggingreplays throughlaunch()and recomputes.One home,
src/session/debugger-off.ts:stopProvesDebuggerOn,isDebuggerOff(session)— recorded and the launch is live: INITIALIZING (the proxy is up; a breakpoint set then still reaches the adapter), RUNNING or PAUSED (over, or never launched — a launch refused before the proxy existed leaves the record on a CREATED session — it describes nothing running) — anddebuggerOffWhy(session), the gated sentence: on a paused session only the clause still true of it, breakpoints cannot bind.SessionStore.getAll()and every consumer below (viadebuggerOffWhyFor(ctx, id)in the handlers) read through them; the composed "not paused" texts live inerror-messages.ts.Consult — never pre-empt the adapter. Every request still goes to the adapter and its own answer is kept; the recorded fact adds one sentence beside it (
ErrorMessages.debuggerOffForLaunch: "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" — "no stop is expected" rather than "cannot come", because js-debug lands a pause):set_breakpoint(line + function)warning: "Unbound breakpoint""Unbound breakpoint; the debugger is off for this launch (noDebug is true): …"— no note on a breakpoint the adapter verified anywaylist_breakpointswarningwith the sentence when at least one listed record is unverified (per-bpmessageis wiped every launch and re-send, not a durable slot)pause_execution, accepted but no stop in the grace windowErrorMessages.pausePendingDebuggerOff: one message that promises no stop, keeping the policy's own explanation (on js-debug, #678's advice is what makes the pause land)pause_execution, refused by the adapter (or no debug target yet)(the sentence), the adapter's own rejection untouched ascausepause_executionwhile the launch is stillinitializingCannot pause in state: initializing… (the sentence)step_*/continue_executionNot pausedNot paused: the sentenceget_stack_tracenote,evaluate_expressionerror,get_local_variablesmessagelist_debug_sessionsdebuggerDisabled: truewhile it holdsLive probe after the change (js-debug,
pause_test.js):debuggerDisabled: truein the listing; the live breakpoint answers"Unbound breakpoint; the debugger is off for this launch …";list_breakpointsand the stack-trace note carry the sentence;pause_executionlands →paused,step_overfrom it lands →lastStop.reason: 'step', and the listing still carriesdebuggerDisabled: trueafter both — neither proves anything about breakpoints, so the surfaces keep explaining.After #750 landed (merged into this branch), the clean case — python. debugpy attaches no debugger at all under the flag, and its own answer to every later request is
Server is not available. Probe onexamples/python/pause_test.py:debuggerDisabled: true; the liveset_breakpoint→"live sync failed: Server is not available; the debugger is off for this launch …";list_breakpointswarning;get_local_variablesmessage andevaluate_expressionerror carry the sentence;step_over/continue_execution→Not paused: …;pause_execution→ debugpy's refusal with the sentence beside it (Failed to pause execution: Server is not available (the debugger is off …)). And one surface the js measurement had hidden:get_stack_traceansweredCannot get stack trace: no active proxy— nothing ever stops, so no thread is current, and debugpy refuses thethreadsdiscovery too; the facade then threwProxyNotRunningErrorfor a proxy that was alive (js-debug's inspector answersthreads, so the js case reached the resolver's note). On a session that is not paused no thread is expected, so the facade now hands the answer to the session layer — the not-paused note with the why — without asking the adapter forthreadsat all (a wasted round trip; the DAP timeout on a wedged adapter); a paused session with no thread to name gets the resolver's "No stopped thread is known for this session." — the "no active proxy" error is kept only for a session with no proxy at all.Tests
session-manager-nodebug-warning.test.ts(fix(launch): a noDebug launch says what it did to the debugger instead of blaming the breakpoints (#710) #747 harness): set for an honoured launch; unset where ignored / without the flag; cleared on the next launch; not set for a dry run; kept by apausestop and by astepfrom it, cleared by each of breakpoint / function breakpoint / exception / entry; not projected once the run is over or on a CREATED session; a launch that ends paused on apausestop never says "no stop can arrive"; recomputed on restart.session-store-projection.test.ts+handlers/session-tools.test.ts: both projections (watched them fail with the projection hunks stashed).session-manager-operations-coverage.test.ts: pause pending is the one no-stop-promised message and does not consult the policy; a refused pause rethrows the adapter's own error object with the why appended (identity and a custom prop asserted); the no-debug-target return carries it; step/continueNot paused: …(and plainNot pausedwhen the debugger is on); evaluate and stack trace carry it; nothing carries it once the session is stopped.server-control-tools.test.ts(set_breakpoint: the DAP send still happens, adapter message + sentence; verified-anyway gets no note),server-breakpoint-management-tools.test.ts(list warning),handlers/inspection-tools.test.ts(locals message).server-coverage.test.ts: a running debugger-off session with no thread whosethreadsdiscovery is refused is handed to the session layer (getStackTraceDetailed(id, undefined, …)) instead of throwing; the two existing "no active proxy" throw tests (andserver-inspection-tools' missing-thread case) now say paused, which is the case the throw is for.dist; watched each fail against the pre-fix build): jspause_test.jsundernoDebug— the listing flag, the live breakpoint,list_breakpoints,get_stack_trace,step_over, and the pause (viacallToolSafely, so a refusal is an answer rather than a thrown MCP error), which asserts the measured outcome (pause lands → decision kept) while accepting a build that refuses or never lands it (the sentence rides on that answer); pythonpause_test.pyundernoDebug(after fix(proxy): a noDebug launch completes without waiting on a configuration phase the adapter never opens (#746) #750) — the same surfaces plusget_local_variablesandevaluate_expression, debugpy'sServer is not availablekept beside the sentence, andget_stack_traceanswering the note rather than "no active proxy" (watched exactly that assertion fail with the facade fix stashed).typecheck:all,lint,check:docs,changelog:check, unit + integration (5430) and the break-on-exceptions e2e file (23) pass on the merged tree.Docs
docs/tool-reference.md: the noDebug paragraph gains the later-surfaces sentence;list_debug_sessionsfield notes gaindebuggerDisabled.changelog.d/749.fixed.md. ThedapLaunchArgs.noDebugschema text — the one noDebug text the model reads before choosing the flag — stops claiming "no stop ever arrives" too (24 tool-list fences re-recorded).Notes for the review
debugger;land) and was the root of a launch-response/record contradiction, so this PR aligns it: "the debugger is off for this launch: N breakpoint(s) will not fire". The start_debugging: dapLaunchArgs.noDebug silently disables every breakpoint; warn at intake when breakpoints are set #710 e2e and unit assertions (/noDebug is true/,will not fire) hold; the710.fixed.mdfragment is reworded to match.get_variables/get_scopesstay silent-empty when not paused (frame-driven; the stack-trace note already explains).initializedevent) and error on cpp (CodeLLDB refuses configuration in noDebug mode) #746: the launch itself completes on python/go/cpp) is merged into this branch; the three text conflicts (710.fixed.md, the docs paragraph, an e2e const) are resolved in the merge commit.🤖 Generated with Claude Code