From 2cfc9ca7271943ea3d8f6a6c5fdf5b186697ccd2 Mon Sep 17 00:00:00 2001 From: JF Date: Mon, 19 Oct 2026 11:53:17 -0400 Subject: [PATCH 1/3] feat(launch): start_debugging answers a stop with where it is, and get_stack_trace carries the stale-build note (#951) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A launch (or restart) that is paused when it answers now carries `location`, `context`, and `threadId`/`note` the way wait_for_stop does. DebugLauncher.launch reads the landing through describeStopLocation on the PAUSED arm — only for a stop the core kept, since PAUSED without lastStop is the auto-continue transient — LaunchContext gains getStackTraceDetailed, and LaunchResultData (DebugResultData & StopLandingFields) threads through the five launch/restart signatures. The launch message is unchanged; the structured location is the addition. The three hoist-plus-stale-check blocks in execution-tools.ts (step, wait_for_stop, pause) become one helper, hoistStopLanding, which the start_debugging and restart_debugging handlers now call too. get_stack_trace appends the same sentence when the file of its top frame is newer than the compiled file the debugger binds it through; a `<…>` label or an unresolvedSource top frame is not probed. Measured on the dev proxy with an aged .NET DLL: before, the launch answer and get_stack_trace carried neither location nor note while wait_for_stop carried the sentence; after, both carry it, a rebuild clears both, and a Python stopOnEntry launch answers with location and no note. The start_debugging and get_stack_trace tool descriptions name the new fields (24 tool-list snapshots regenerated); the seven doc pages that said "(#951)" now name the carriers. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Y3ULnoVSoCZ5t2Y8d98Ctv --- changelog.d/951.added.md | 1 + docs/architecture/api-reference.md | 6 +- docs/cobol/README.md | 2 +- docs/cpp/README.md | 2 +- docs/dotnet/README.md | 2 +- docs/go/README.md | 2 +- docs/java/README.md | 2 +- docs/javascript/README.md | 7 +- docs/rust-debugging.md | 2 +- docs/tool-reference.md | 13 +- src/server.ts | 6 +- src/server/handlers/debuggee-tools.ts | 10 +- src/server/handlers/execution-tools.ts | 69 +---------- src/server/handlers/inspection-tools.ts | 14 ++- src/server/handlers/shared.ts | 44 ++++++- src/server/tool-context.ts | 6 +- src/server/tool-schemas.ts | 4 +- src/session/launch/debug-launcher.ts | 24 +++- src/session/operations-context.ts | 6 +- src/session/session-manager-core.ts | 11 ++ src/session/session-manager-operations.ts | 5 +- .../bp-assert.va-explicit.container-true.json | 4 +- ...bp-assert.va-explicit.container-unset.json | 4 +- .../bp-assert.va-open.container-true.json | 4 +- .../bp-assert.va-open.container-unset.json | 4 +- .../bp-assert.va-unset.container-true.json | 4 +- .../bp-assert.va-unset.container-unset.json | 4 +- ...bp-content.va-explicit.container-true.json | 4 +- ...p-content.va-explicit.container-unset.json | 4 +- .../bp-content.va-open.container-true.json | 4 +- .../bp-content.va-open.container-unset.json | 4 +- .../bp-content.va-unset.container-true.json | 4 +- .../bp-content.va-unset.container-unset.json | 4 +- .../bp-line.va-explicit.container-true.json | 4 +- .../bp-line.va-explicit.container-unset.json | 4 +- .../bp-line.va-open.container-true.json | 4 +- .../bp-line.va-open.container-unset.json | 4 +- .../bp-line.va-unset.container-true.json | 4 +- .../bp-line.va-unset.container-unset.json | 4 +- .../bp-unset.va-explicit.container-true.json | 4 +- .../bp-unset.va-explicit.container-unset.json | 4 +- .../bp-unset.va-open.container-true.json | 4 +- .../bp-unset.va-open.container-unset.json | 4 +- .../bp-unset.va-unset.container-true.json | 4 +- .../bp-unset.va-unset.container-unset.json | 4 +- .../server/handlers/debuggee-tools.test.ts | 115 ++++++++++++++++++ .../server/handlers/inspection-tools.test.ts | 104 +++++++++++++++- .../unit/session/debug-result-data.test.ts | 6 +- .../session-manager-launch-contract.test.ts | 87 ++++++++++++- tests/e2e/mcp-server-smoke-cpp.test.ts | 13 ++ tests/e2e/mcp-server-smoke-dotnet.test.ts | 25 +++- tests/e2e/mcp-server-smoke-java.test.ts | 9 ++ tests/e2e/mcp-server-smoke-javascript.test.ts | 9 +- .../javascript-launch-config.test.ts | 6 + ...ession-manager-operations-coverage.test.ts | 4 +- 55 files changed, 559 insertions(+), 153 deletions(-) create mode 100644 changelog.d/951.added.md create mode 100644 tests/core/unit/server/handlers/debuggee-tools.test.ts diff --git a/changelog.d/951.added.md b/changelog.d/951.added.md new file mode 100644 index 00000000..c72049cc --- /dev/null +++ b/changelog.d/951.added.md @@ -0,0 +1 @@ +**`start_debugging` answers a stop with where it is, and `get_stack_trace` carries the stale-build note** — a launch (or `restart_debugging`) that is paused when it answers now carries `location`, the source `context`, and `threadId`/`note` when there is something to say, the way `wait_for_stop` does, read through the same landing read the step and pause answers use. The stale-build sentence (#938, #950) therefore reaches a fast program's first answer instead of waiting for a `wait_for_stop`, and `get_stack_trace` appends it when the file of its top frame is newer than the compiled file the debugger binds it through. One handler helper now hoists and annotates the landing for every stop answer (#951) diff --git a/docs/architecture/api-reference.md b/docs/architecture/api-reference.md index 270044f6..dd0557d7 100644 --- a/docs/architecture/api-reference.md +++ b/docs/architecture/api-reference.md @@ -287,7 +287,7 @@ Creates a new debug session. `SessionManager.createSession` spells the parameter **Returns**: `DebugSessionInfo` — `{ id, language, name, state, createdAt, ... }`. The session id is `id`. -#### `startDebugging(sessionId, scriptPath, scriptArgs?, dapLaunchArgs?, dryRunSpawn?, adapterLaunchConfig?, breakOnExceptions?): Promise` +#### `startDebugging(sessionId, scriptPath, scriptArgs?, dapLaunchArgs?, dryRunSpawn?, adapterLaunchConfig?, breakOnExceptions?): Promise>` Starts (or dry-runs) a launch-mode debug session. The parameters are **positional**, not an options object: ```typescript @@ -304,9 +304,9 @@ startDebugging( Delegates to `DebugLauncher` (`src/session/launch/debug-launcher.ts`), which owns the launch sequence end to end. -**Returns**: `DebugResult` — `{ success, state, error?, data?, canContinue?, errorType?, errorCode? }`. For a dry run, `data` carries `dryRun`, `command` and `script`. A dry run leaves the session lifecycle-TERMINATED but present; `startDebugging` accepts a present session in any lifecycle state — `DebugLauncher.launch` tears down any leftover proxy and resets the per-launch state (issue #793). The result reflects the session when the launched program stops, when it ends, or when the launch hold elapses — the same for every adapter (`waitForLaunchReadiness` in `launch/launch-readiness.ts`, two waits on the session's state: until the program is launched, bounded by `launchReadyCeilingMs` and covering the entry stop of a `stopOnEntry` launch, then `launchHoldMs` for its first stop). When the hold elapses with the program still running, `state` is `RUNNING` and `data.pending` is `true`, with `data.message` saying what is armed at that moment (`describeLaunchArming` in `launch/launch-arming.ts`, which words the answer and no longer decides how long the launch waits; issues #815, #823, #826). Breakpoints changed while the launch was starting are sent before the hold begins (issue #851). +**Returns**: `DebugResult` — `{ success, state, error?, data?, canContinue?, errorType?, errorCode? }`, where `LaunchResultData` is `DebugResultData & StopLandingFields`: for a launch that stopped, `data` carries the landing (`location`, and `threadId`/`note` when there is something to say) read through `describeStopLocation`, as the step, pause and wait_for_stop results do (issue #951). For a dry run, `data` carries `dryRun`, `command` and `script`. A dry run leaves the session lifecycle-TERMINATED but present; `startDebugging` accepts a present session in any lifecycle state — `DebugLauncher.launch` tears down any leftover proxy and resets the per-launch state (issue #793). The result reflects the session when the launched program stops, when it ends, or when the launch hold elapses — the same for every adapter (`waitForLaunchReadiness` in `launch/launch-readiness.ts`, two waits on the session's state: until the program is launched, bounded by `launchReadyCeilingMs` and covering the entry stop of a `stopOnEntry` launch, then `launchHoldMs` for its first stop). When the hold elapses with the program still running, `state` is `RUNNING` and `data.pending` is `true`, with `data.message` saying what is armed at that moment (`describeLaunchArming` in `launch/launch-arming.ts`, which words the answer and no longer decides how long the launch waits; issues #815, #823, #826). Breakpoints changed while the launch was starting are sent before the hold begins (issue #851). -#### `restartDebugging(sessionId: string): Promise` +#### `restartDebugging(sessionId: string): Promise>` Replays the session's last real launch and re-applies breakpoints (content anchors are re-resolved). Launch-mode sessions only. Exposed as the `restart_debugging` MCP tool. Goes through the same two waits as `startDebugging`, so a relaunch still running when the hold elapses answers `RUNNING` with `data.pending: true` (issues #815, #823). #### `setBreakpoint(sessionId: string, bp: { file: string; line: number; condition?: string; logMessage?: string; suspendPolicy?: 'all' | 'thread'; requestedLine?: number; anchor?: { statement: string; nearLine?: number } }): Promise<{ breakpoint: Breakpoint; warning?: string }>` diff --git a/docs/cobol/README.md b/docs/cobol/README.md index baf268da..06f2ac9f 100644 --- a/docs/cobol/README.md +++ b/docs/cobol/README.md @@ -275,7 +275,7 @@ The image installs the `gnucobol3` package of its Ubuntu 26.04 base (GnuCOBOL 3. ## Stale builds -A source launch is never reported stale: cobc rebuilds whenever the sources or copybooks change (a content hash, not mtimes), so the adapter's own build under `.debug-mcp/cobol/` is current by construction, and a source saved without a change — newer by mtime, identical by content — is left alone. A prebuilt executable runs as it stands, and the engine binds a breakpoint through its DWARF while the echo comes from the `.cob` on disk. Once `start_debugging` has named such an executable, mcp-debugger compares the two for every program file or copybook (`.cob`, `.cbl`, `.cobol`, `.cpy`) it is asked about: a `set_breakpoint` in a source newer than the executable carries a `warning` naming both files and their mtimes, and `step_*`, `wait_for_stop` and `pause_execution` carry the same sentence as `note` (issue #950). Not covered: `runner: "cobcrun"` (the launched program is GnuCOBOL's loader, not your module) and attach sessions; a dynamically CALLed module's source is compared against the main program's executable, not against its own module; the `start_debugging` answer and `get_stack_trace` do not carry the sentence yet (#951). Rebuild, or pass `forceRebuild: true`, to clear it. +A source launch is never reported stale: cobc rebuilds whenever the sources or copybooks change (a content hash, not mtimes), so the adapter's own build under `.debug-mcp/cobol/` is current by construction, and a source saved without a change — newer by mtime, identical by content — is left alone. A prebuilt executable runs as it stands, and the engine binds a breakpoint through its DWARF while the echo comes from the `.cob` on disk. Once `start_debugging` has named such an executable, mcp-debugger compares the two for every program file or copybook (`.cob`, `.cbl`, `.cobol`, `.cpy`) it is asked about: a `set_breakpoint` in a source newer than the executable carries a `warning` naming both files and their mtimes, and `step_*`, `wait_for_stop` and `pause_execution` carry the same sentence as `note` (issue #950). Not covered: `runner: "cobcrun"` (the launched program is GnuCOBOL's loader, not your module) and attach sessions; a dynamically CALLed module's source is compared against the main program's executable, not against its own module; the `start_debugging` answer carries it too when the program is already paused when the launch answers, and `get_stack_trace` when its top frame is in such a file (#951). Rebuild, or pass `forceRebuild: true`, to clear it. ## Troubleshooting diff --git a/docs/cpp/README.md b/docs/cpp/README.md index f189ed9e..7dceaf62 100644 --- a/docs/cpp/README.md +++ b/docs/cpp/README.md @@ -119,7 +119,7 @@ Alternatively, compile inside the container (or with `-fdebug-prefix-map=/home/u ## Stale builds -A prebuilt executable runs as it is given; a lone source file is recompiled when that one file is newer than its binary, but its headers are not tracked. Either way CodeLLDB binds a breakpoint through the binary's DWARF while the echo comes from the source on disk. Once `start_debugging` has named the executable, mcp-debugger compares the two for every `.c`, `.cc`, `.cpp`, `.cxx`, `.h`, `.hh`, `.hpp` and `.hxx` it is asked about: a `set_breakpoint` in a source or header newer than the executable carries a `warning` naming both files and their mtimes, and `step_*`, `wait_for_stop` and `pause_execution` carry the same sentence as `note` (issue #950) — so an edited header against an un-rebuilt binary is no longer silent. Not covered: attach sessions (no executable is known), and sources under the toolchain (system headers, GCC's own include trees) — they are newer than every binary after a toolchain update and no rebuild would clear them; the `start_debugging` answer and `get_stack_trace` do not carry the sentence yet (#951). Rebuild, or pass `forceRebuild: true` for a source launch, to clear it. +A prebuilt executable runs as it is given; a lone source file is recompiled when that one file is newer than its binary, but its headers are not tracked. Either way CodeLLDB binds a breakpoint through the binary's DWARF while the echo comes from the source on disk. Once `start_debugging` has named the executable, mcp-debugger compares the two for every `.c`, `.cc`, `.cpp`, `.cxx`, `.h`, `.hh`, `.hpp` and `.hxx` it is asked about: a `set_breakpoint` in a source or header newer than the executable carries a `warning` naming both files and their mtimes, and `step_*`, `wait_for_stop` and `pause_execution` carry the same sentence as `note` (issue #950) — so an edited header against an un-rebuilt binary is no longer silent. Not covered: attach sessions (no executable is known), and sources under the toolchain (system headers, GCC's own include trees) — they are newer than every binary after a toolchain update and no rebuild would clear them; the `start_debugging` answer carries it too when the program is already paused when the launch answers, and `get_stack_trace` when its top frame is in such a file (#951). Rebuild, or pass `forceRebuild: true` for a source launch, to clear it. ## Troubleshooting diff --git a/docs/dotnet/README.md b/docs/dotnet/README.md index 617c9a80..0d9e6e48 100644 --- a/docs/dotnet/README.md +++ b/docs/dotnet/README.md @@ -227,7 +227,7 @@ dotnet build ## Stale builds -The adapter never builds your project: netcoredbg binds a breakpoint through the DLL's Portable PDB, while `set_breakpoint` and the stop answers echo the line from the `.cs` on disk. When the source is newer than the DLL, the echo names a statement that is not the one that runs. Once `start_debugging` has named the DLL — as `program`, as the apphost `.exe`, or as the first argument when `program` is the `dotnet` host — mcp-debugger compares the two: a `set_breakpoint` whose source is newer than the launched DLL carries a `warning` naming both files and their mtimes, and `step_*`, `wait_for_stop` and `pause_execution` carry the same sentence as `note` (issue #950). The DLL's mtime stands for its PDB too (`dotnet build` writes them together). Not covered: attach sessions (no DLL is known); and a `.cs` that is newer but not part of the build reads as stale too — the sentence says "may". The `start_debugging` answer and `get_stack_trace` do not carry the sentence yet (#951): after the launch, look for it on `set_breakpoint`, `wait_for_stop`, `step_*` and `pause_execution`. Run `dotnet build` to clear it. +The adapter never builds your project: netcoredbg binds a breakpoint through the DLL's Portable PDB, while `set_breakpoint` and the stop answers echo the line from the `.cs` on disk. When the source is newer than the DLL, the echo names a statement that is not the one that runs. Once `start_debugging` has named the DLL — as `program`, as the apphost `.exe`, or as the first argument when `program` is the `dotnet` host — mcp-debugger compares the two: a `set_breakpoint` whose source is newer than the launched DLL carries a `warning` naming both files and their mtimes, and `step_*`, `wait_for_stop` and `pause_execution` carry the same sentence as `note` (issue #950). The DLL's mtime stands for its PDB too (`dotnet build` writes them together). Not covered: attach sessions (no DLL is known); and a `.cs` that is newer but not part of the build reads as stale too — the sentence says "may". After the launch, look for it on `set_breakpoint`, on the `start_debugging` answer itself when the program is already paused when the launch answers (a fast program at a breakpoint), on `wait_for_stop`, `step_*` and `pause_execution`, and on `get_stack_trace` when its top frame is in the file (#951). Run `dotnet build` to clear it. ## Troubleshooting diff --git a/docs/go/README.md b/docs/go/README.md index 25680e58..13289f48 100644 --- a/docs/go/README.md +++ b/docs/go/README.md @@ -218,7 +218,7 @@ func add(a, b int) int { ## Stale builds -In `debug` and `test` mode Delve compiles the package for every launch, so nothing can be stale. In `exec` mode (a prebuilt binary as the program) Delve runs it as given and binds breakpoints through its DWARF, while the echo comes from the `.go` on disk. Once `start_debugging` has named the binary, mcp-debugger compares the two: a `set_breakpoint` in a `.go` newer than the binary carries a `warning` naming both files and their mtimes, and `step_*`, `wait_for_stop` and `pause_execution` carry the same sentence as `note` (issue #950). Not covered: attach sessions; the `start_debugging` answer and `get_stack_trace` do not carry the sentence yet (#951). Rebuild with `go build -gcflags="all=-N -l"` to clear it. +In `debug` and `test` mode Delve compiles the package for every launch, so nothing can be stale. In `exec` mode (a prebuilt binary as the program) Delve runs it as given and binds breakpoints through its DWARF, while the echo comes from the `.go` on disk. Once `start_debugging` has named the binary, mcp-debugger compares the two: a `set_breakpoint` in a `.go` newer than the binary carries a `warning` naming both files and their mtimes, and `step_*`, `wait_for_stop` and `pause_execution` carry the same sentence as `note` (issue #950). Not covered: attach sessions. The `start_debugging` answer carries it too when the program is already paused when the launch answers, and `get_stack_trace` when its top frame is in such a file (#951). Rebuild with `go build -gcflags="all=-N -l"` to clear it. ## Troubleshooting diff --git a/docs/java/README.md b/docs/java/README.md index 72140f58..0c1f6bf3 100644 --- a/docs/java/README.md +++ b/docs/java/README.md @@ -233,7 +233,7 @@ java -agentlib:jdwp=transport=dt_socket,server=y,address=5005,suspend=y \ ## Stale builds -The adapter never compiles your code: JDI binds a breakpoint through the `.class` file's `LineNumberTable`, while `set_breakpoint` and the stop answers echo the line from the `.java` on disk. When the source is newer than the class file, the echo names a statement that is not the one that runs. Once a launch has named the classpath, mcp-debugger compares the two: a `set_breakpoint` whose `.java` is newer than its `.class` carries a `warning` naming both files and their mtimes, and `step_*`, `wait_for_stop` and `pause_execution` carry the same sentence as `note` (issue #950). The class file is looked for under each `classpath` directory (resolved against the launch `cwd`; `.` when none is given, as the bridge defaults it) at the package path that mirrors the source's directory chain, then beside the source (a bare `javac Foo.java`). Not covered: classes inside jars, attach sessions (no classpath is known), and stops in a file no breakpoint was set in (the bridge reports those by package-relative path). Before `start_debugging` nothing names the class file, and the `start_debugging` answer and `get_stack_trace` do not carry the sentence yet either (#951): after the launch, look for it on `set_breakpoint`, `wait_for_stop`, `step_*` and `pause_execution`. Recompile with `javac -g` to clear it. +The adapter never compiles your code: JDI binds a breakpoint through the `.class` file's `LineNumberTable`, while `set_breakpoint` and the stop answers echo the line from the `.java` on disk. When the source is newer than the class file, the echo names a statement that is not the one that runs. Once a launch has named the classpath, mcp-debugger compares the two: a `set_breakpoint` whose `.java` is newer than its `.class` carries a `warning` naming both files and their mtimes, and `step_*`, `wait_for_stop` and `pause_execution` carry the same sentence as `note` (issue #950). The class file is looked for under each `classpath` directory (resolved against the launch `cwd`; `.` when none is given, as the bridge defaults it) at the package path that mirrors the source's directory chain, then beside the source (a bare `javac Foo.java`). Not covered: classes inside jars, attach sessions (no classpath is known), and stops in a file no breakpoint was set in (the bridge reports those by package-relative path). Before `start_debugging` nothing names the class file; after the launch, look for it on `set_breakpoint`, on the `start_debugging` answer itself when the program is already paused when the launch answers, on `wait_for_stop`, `step_*` and `pause_execution`, and on `get_stack_trace` when its top frame is in the file (#951). Recompile with `javac -g` to clear it. ## Troubleshooting diff --git a/docs/javascript/README.md b/docs/javascript/README.md index 2ef46b06..b2ebc28f 100644 --- a/docs/javascript/README.md +++ b/docs/javascript/README.md @@ -163,7 +163,7 @@ column 1 of the program, and V8 resolves that to the first breakable position in whose first declaration is a function that the program calls later, that position is inside the function — so the "entry" stop fires at the function's first call, after the top-level code before it has run (output included), and fires again at every later call. mcp-debugger cannot move the stop, but it says so: the -stop's `lastStop.text` (in the `start_debugging` answer, `wait_for_stop`, `list_debug_sessions`) explains +stop's `lastStop.text` (in `wait_for_stop`, `get_stack_trace` and `list_debug_sessions`) explains where it landed and why. To stop at the first statement, put a statement above the function or set a line breakpoint on it. A function breakpoint on that first-declared function is not affected: the stop that binds it *is* the function's first call, and it is reported as the function breakpoint. Reported upstream as @@ -421,8 +421,9 @@ open on its port (Node never closes it by itself) — a later attach by PID or b names an outDir, then a sibling `.js` — keeps the first one whose `.js.map` lists the source, and when that file's mtime is older than the source's (by more than 2 s) adds a `warning` to the `set_breakpoint` answer and a - `note` to the `step_*`/`wait_for_stop`/`pause_execution` answers naming both - files and their mtimes (issue #938). It is mtime-only and hedged ("may be + `note` to the `start_debugging`/`step_*`/`wait_for_stop`/`pause_execution` + answers, and to `get_stack_trace` for its top frame, naming both files and + their mtimes (issues #938, #951). It is mtime-only and hedged ("may be stale"): a source saved without a content change reads as newer too, and an incremental build (`tsc -b`, `--incremental`, `tsc --watch`) does not re-emit for an unchanged content hash, so only a full rebuild or a real diff --git a/docs/rust-debugging.md b/docs/rust-debugging.md index 9880d34a..7672c9ba 100644 --- a/docs/rust-debugging.md +++ b/docs/rust-debugging.md @@ -311,7 +311,7 @@ Alternatively, build inside the container (or with `RUSTFLAGS="--remap-path-pref ## Stale builds -For a `.rs` program the adapter runs `cargo build` when a file under `src/` or `Cargo.toml` is newer than the binary; for an explicit `program` path or a `cargo` configuration it runs the binary as it stands. Either way CodeLLDB binds a breakpoint through the binary's DWARF while the echo comes from the source on disk. Once `start_debugging` has named the binary, mcp-debugger compares the two for every `.rs` it is asked about: a `set_breakpoint` in a source newer than the binary carries a `warning` naming both files and their mtimes, and `step_*`, `wait_for_stop` and `pause_execution` carry the same sentence as `note` (issue #950) — including files the rebuild check does not walk (`build.rs`, `examples/`, `tests/`, path dependencies). Not covered: attach sessions, and sources under the toolchain (std under the sysroot or `~/.rustup`, the remapped `/rustc//` form, cargo registry crates) — a `rustup update` makes them newer than every binary and no rebuild of your program would clear them; the `start_debugging` answer and `get_stack_trace` do not carry the sentence yet (#951). Rebuild to clear it. +For a `.rs` program the adapter runs `cargo build` when a file under `src/` or `Cargo.toml` is newer than the binary; for an explicit `program` path or a `cargo` configuration it runs the binary as it stands. Either way CodeLLDB binds a breakpoint through the binary's DWARF while the echo comes from the source on disk. Once `start_debugging` has named the binary, mcp-debugger compares the two for every `.rs` it is asked about: a `set_breakpoint` in a source newer than the binary carries a `warning` naming both files and their mtimes, and `step_*`, `wait_for_stop` and `pause_execution` carry the same sentence as `note` (issue #950) — including files the rebuild check does not walk (`build.rs`, `examples/`, `tests/`, path dependencies). Not covered: attach sessions, and sources under the toolchain (std under the sysroot or `~/.rustup`, the remapped `/rustc//` form, cargo registry crates) — a `rustup update` makes them newer than every binary and no rebuild of your program would clear them; the `start_debugging` answer carries it too when the program is already paused when the launch answers, and `get_stack_trace` when its top frame is in such a file (#951). Rebuild to clear it. ## Troubleshooting diff --git a/docs/tool-reference.md b/docs/tool-reference.md index 3235e750..ca95aa7c 100644 --- a/docs/tool-reference.md +++ b/docs/tool-reference.md @@ -206,7 +206,7 @@ In the shipped default addressing mode (`content`), `sessionId` is the only para - Setting breakpoints on non-executable lines (comments, blank lines, declarations) may cause unexpected behavior - Executable lines that work well: assignments, function calls, conditionals, returns - The top-level `content` field echoes the bound line's text (same as `context.lineContent`) -- `content`, `context` and `line` come from the file you named. When the adapter binds that file through a compiled one and that compiled file is older than the source, the response adds a `warning`: "The compiled file the debugger binds this location through may be stale: `dist/x.js` (modified …) is older than `src/x.ts` (modified …)…" (issues #938, #950). Which compiled file depends on the language. A TypeScript source under js-debug binds through the generated `.js` and its `.js.map`: found from the nearest `tsconfig.json` that names an `outDir` (with its `rootDir`), or — when no tsconfig names one — the conventional `dist|build|out|lib` under the nearest `package.json`, a sibling `.js` last; only a candidate whose `.js.map` names the source counts, and a launch whose program is itself a `.ts` (tsx, ts-node, type stripping) or a runner under `node_modules` (vitest, jest, mocha) is exempt. A Java source binds through its `.class` under a `classpath` directory of the launch (at the package path mirroring the source's directory, or beside the source); a .NET source through the launched DLL (whose PDB carries the lines); a Go source in `exec` mode, and a Rust, C/C++ (headers included) or COBOL (copybooks included) source, through the executable the launch runs. For those compiled languages the artefact is known only once `start_debugging` has run: before that the echo stands alone; afterwards a `set_breakpoint` carries the `warning`, and `wait_for_stop`, `step_*` and `pause_execution` the `note` — the `start_debugging` answer and `get_stack_trace` carry neither yet (#951), so a fast program that is already paused when the launch answers is first told by `wait_for_stop`. A COBOL `runner: "cobcrun"` launch, a COBOL source launch (cobc's build is keyed on the sources' content, so it is current by construction) and every attach session name no artefact, and a source under the toolchain (a system header, Rust's std, a registry crate) is not compared. When several class files could be the one (a leftover in-place compile beside the source and the build under `out/`), the newest existing one is compared — a stale leftover never outranks the build the engine loads. The check compares mtimes only, so it is a hedge, not a verdict (a file saved without changes reads as newer too, an incremental `tsc` does not re-emit for it, and a source beside the project that was never compiled into the artefact reads as stale). The same sentence appears as `note` on `step_*`, `wait_for_stop` and `pause_execution` answers whose location is such a file +- `content`, `context` and `line` come from the file you named. When the adapter binds that file through a compiled one and that compiled file is older than the source, the response adds a `warning`: "The compiled file the debugger binds this location through may be stale: `dist/x.js` (modified …) is older than `src/x.ts` (modified …)…" (issues #938, #950). Which compiled file depends on the language. A TypeScript source under js-debug binds through the generated `.js` and its `.js.map`: found from the nearest `tsconfig.json` that names an `outDir` (with its `rootDir`), or — when no tsconfig names one — the conventional `dist|build|out|lib` under the nearest `package.json`, a sibling `.js` last; only a candidate whose `.js.map` names the source counts, and a launch whose program is itself a `.ts` (tsx, ts-node, type stripping) or a runner under `node_modules` (vitest, jest, mocha) is exempt. A Java source binds through its `.class` under a `classpath` directory of the launch (at the package path mirroring the source's directory, or beside the source); a .NET source through the launched DLL (whose PDB carries the lines); a Go source in `exec` mode, and a Rust, C/C++ (headers included) or COBOL (copybooks included) source, through the executable the launch runs. For those compiled languages the artefact is known only once `start_debugging` has run: before that the echo stands alone; afterwards a `set_breakpoint` carries the `warning`, and every answer that names a stop carries the `note` — the `start_debugging` (or `restart_debugging`) answer itself when the program is already paused at a breakpoint when the launch answers, `wait_for_stop`, `step_*`, `pause_execution`, and `get_stack_trace` for the file of its top frame (issue #951) — so a fast program that stops as it starts is told in the one call. A COBOL `runner: "cobcrun"` launch, a COBOL source launch (cobc's build is keyed on the sources' content, so it is current by construction) and every attach session name no artefact, and a source under the toolchain (a system header, Rust's std, a registry crate) is not compared. When several class files could be the one (a leftover in-place compile beside the source and the build under `out/`), the newest existing one is compared — a stale leftover never outranks the build the engine loads. The check compares mtimes only, so it is a hedge, not a verdict (a file saved without changes reads as newer too, an incremental `tsc` does not re-emit for it, and a source beside the project that was never compiled into the artefact reads as stale). The same sentence appears as `note` on `start_debugging`, `restart_debugging`, `step_*`, `wait_for_stop` and `pause_execution` answers whose location is such a file, and on `get_stack_trace` when its top frame is in one #### Content assertions and loud snapping @@ -400,16 +400,22 @@ Starts debugging a script. "success": true, "state": "paused", "message": "Debugging started for /abs/path/examples/python_simple_swap/swap_vars.py. Current state: paused", + "location": { "file": "/abs/path/examples/python_simple_swap/swap_vars.py", "line": 12, "column": 1 }, + "context": { + "lineContent": " a, b = b, a", + "surrounding": [ { "line": 10, "content": " print(a, b)" }, { "line": 11, "content": "" }, { "line": 12, "content": " a, b = b, a" } ] + }, "data": { "message": "Debugging started for /abs/path/examples/python_simple_swap/swap_vars.py. Current state: paused", - "reason": "breakpoint" + "reason": "breakpoint", + "location": { "file": "/abs/path/examples/python_simple_swap/swap_vars.py", "line": 12, "column": 1 } } } ``` **When the launch answers:** once the program is launched, `start_debugging` holds its answer for a short window and reports the session as it is when the first of three things happens — the same way in every language (issues #815, #823, #826): -- **The program stops** — `state: "paused"` with `reason` (the example above). A breakpoint reached as the program starts is answered here, in the one call. +- **The program stops** — `state: "paused"` with `reason`, and where, the way `wait_for_stop` answers a stop (issue #951): `location` is the first visible frame, `context` the source line around it, and `threadId` and `note` appear when there is something to say — the answer anchored on a thread other than the reported one, no frame of the stack being program code (as for `pause_execution`), or the compiled file the debugger binds the location through being older than its source (the stale-build sentence `set_breakpoint` warns with, issues #938/#950). `location`, `threadId` and `note` sit at the top level and in `data`; `context` at the top level only. A breakpoint reached as the program starts is answered here, in the one call (the example above). - **The program ends** — `state: "stopped"` with the run-to-completion summary and `exitCode` below. - **The hold elapses** — `state: "running"` with **`pending: true`** (top level and in `data`, like `step_over`) and a `message` that says what is armed: `…has not reached 2 breakpoint(s) yet. Nothing was cancelled: what is armed stays armed…`; or, with nothing armed (the `"uncaught"` default does not count — a crash is not a stop you are waiting for), `…nothing is armed to stop it (no breakpoints, no entry stop, no caught-exception filter)…`; or, for a `noDebug` launch, that the debugger is off. Logpoints the adapter runs on never stop the program, so they are named apart: `…1 logpoint(s) that log without stopping is armed (read get_output for the messages), and nothing is armed to stop it (no pausing breakpoints, …)…`. What is armed is read when the answer is built, so a breakpoint set while the launch was in flight is named. @@ -842,6 +848,7 @@ Gets the current call stack. - `stopReason` and `lastStop` describe the pause the frames were read at, so they are omitted whenever the session is not paused; they are taken from the state at the start of the call, so a `continue_execution` racing the fetch does not strip them from an answer that really was read at a stop. - Frame IDs are used with `get_scopes` - Internal/runtime frames (e.g. Node.js internals and `node_modules` dependencies, Go `/runtime/`, `System.*`) are filtered out by default; pass `includeInternals: true` to see them. When any frames were hidden, the response additionally carries `hiddenFrames` (count) and a `note` explaining how to reveal them. +- When the top frame's file is a source the adapter binds through a compiled file that is older than it — a `.ts` through its generated `.js`, a `.java` through its `.class`, a `.cs` through the launched DLL, a native source through the executable the launch runs — the `note` ends with the stale-build sentence `set_breakpoint` warns with (issues #938, #950, #951): the line shown comes from the newer source and may not be the statement that runs. The top frame only, and not one whose `file` is a `<…>` label or that carries `unresolvedSource: true`; the same hedge applies as for `set_breakpoint` (mtimes only). - A frame with no openable source path on this host carries `unresolvedSource: true`, and the `note` says its `file` is a label rather than a path — do not pass it to `get_source_context` — then adds what such a frame is under the session's adapter (issue #816): js-debug — a source-mapped `.ts` the package did not ship, with the switch that shows the generated `.js` paths instead (`adapterConfig.sourceMaps: false` on attach, `adapterLaunchConfig.sourceMaps: false` on launch); CodeLLDB (C/C++, Rust, COBOL) — a native frame without debug info (system libraries, CRT start-up) that the debugger names by symbol (`@NtWaitForSingleObject`), or a runtime built elsewhere whose relative path is not present here, where the program's own frames are the ones with a full path; Java — a class the JDI bridge names by package path (`java/io/PrintStream.java`) because no breakpoint has taught it the file's location. Adapters with nothing to add leave the neutral sentence alone. - The filtered stack is never empty when the adapter reported frames: if *every* frame is internal (e.g. a goroutine paused inside the Go runtime), the top internal frame is kept so `get_scopes`/`evaluate_expression` still have a valid `frameId`, and the `note` says so. - When the frame the debuggee is paused in is itself internal (a breakpoint or step that landed inside a `node_modules` dependency, say) while user frames survive further down, the response carries `pausedFrame` (`id`, `name`, `file`, `line`, and `kept`). On a breakpoint or step stop (a `debugger;` statement counts), or when an async boundary separates the paused frame from the first visible one (nothing below it can be evaluated), the paused frame is kept as `stackFrames[0]` so locals and evaluation anchor where the program actually stopped; `hiddenFrames` counts only the frames that stayed hidden. After a `pause` or an exception with a synchronous caller visible — stops that routinely land in runtime frames where the first user frame is the useful one — it stays hidden (`kept: false`) and the `note` names it; use `pausedFrame.id` as the `frameId` to inspect it (issue #672). diff --git a/src/server.ts b/src/server.ts index d4d0a909..b6964b91 100644 --- a/src/server.ts +++ b/src/server.ts @@ -19,7 +19,7 @@ import { ProxyNotRunningError } from './errors/debug-errors.js'; import { SessionManager, SessionManagerConfig } from './session/session-manager.js'; -import type { DebugResult, StepResultData, WaitForStopResultData } from './session/session-manager-core.js'; +import type { DebugResult, LaunchResultData, StepResultData, WaitForStopResultData } from './session/session-manager-core.js'; import { FrameSummary, StackTraceResult } from './session/session-manager-data.js'; import { VariableTruncationSummary } from './session/variable-caps.js'; import { createProductionDependencies } from './container/dependencies.js'; @@ -288,7 +288,7 @@ export class DebugMcpServer implements ToolContext { dryRunSpawn?: boolean, adapterLaunchConfig?: Record, breakOnExceptions?: ExceptionBreakMode - ): Promise { + ): Promise> { this.validateSessionExists(sessionId); // Check script file exists for immediate feedback @@ -312,7 +312,7 @@ export class DebugMcpServer implements ToolContext { return result; } - public async restartDebugging(sessionId: string): Promise { + public async restartDebugging(sessionId: string): Promise> { // A finished debuggee is lifecycle-TERMINATED and restarting after exit // is the primary use case (cf. handleGetOutput). this.validateSessionExists(sessionId); diff --git a/src/server/handlers/debuggee-tools.ts b/src/server/handlers/debuggee-tools.ts index 77c62e60..c80bc83b 100644 --- a/src/server/handlers/debuggee-tools.ts +++ b/src/server/handlers/debuggee-tools.ts @@ -3,7 +3,7 @@ * attach_to_process, detach_from_process, redefine_classes. */ import type { ToolHandler } from '../tool-context.js'; -import { attachRequestFromArgs, successWarning } from './shared.js'; +import { attachRequestFromArgs, hoistStopLanding, successWarning } from './shared.js'; import { assertPlainObjectArg, normalizeStartDebuggingArgs, @@ -52,6 +52,12 @@ export const startDebuggingTool: ToolHandler = async (ctx, args) => { if (debugResult.success && startWarnings.length > 0) { responsePayload.warning = startWarnings.join('; '); } + // Where the launch stopped (issue #951), the way wait_for_stop answers a + // stop: location, threadId and note beside state, the source line, and + // the stale-build sentence when the artefact the launch runs is older + // than the source (issue #950). Nothing for a launch that answered + // running or that ended: the session layer gave those no location. + await hoistStopLanding(ctx, responsePayload, debugResult.data, sessionId, 'launch result'); return jsonResult(responsePayload); } catch (error) { return sessionErrorResultOrThrow(error, { state: 'stopped' }); @@ -86,6 +92,8 @@ export const restartDebuggingTool: ToolHandler = async (ctx, args) => { if (debugResult.data.pending) { responsePayload.pending = true; } + // And the same landing when it stopped (issue #951). + await hoistStopLanding(ctx, responsePayload, debugResult.data, args.sessionId, 'restart result'); } return jsonResult(responsePayload); } catch (error) { diff --git a/src/server/handlers/execution-tools.ts b/src/server/handlers/execution-tools.ts index f3448d57..1b67dce2 100644 --- a/src/server/handlers/execution-tools.ts +++ b/src/server/handlers/execution-tools.ts @@ -7,7 +7,7 @@ import { isTerminalSessionState } from '@debugmcp/shared'; import type { ToolContext, ToolHandler } from '../tool-context.js'; import type { DebugResult, StepResultData } from '../../session/session-manager-core.js'; import { requireSessionId } from '../tool-validation.js'; -import { appendNote, describeStaleBuild, readLineContext } from './shared.js'; +import { hoistStopLanding } from './shared.js'; import { failureResult, jsonResult, @@ -78,33 +78,8 @@ export const stepTool: ToolHandler = async (ctx, args, toolName) => { } } - // Where the step landed, beyond the frame (issue #904): the thread the - // answer is anchored on when it is not the reported one, and the note. - if (typeof resultData?.threadId === 'number') { - response.threadId = resultData.threadId; - } - if (resultData?.note) { - response.note = resultData.note; - } - - // Extract location from result data - const location = resultData?.location; - - if (location) { - response.location = location; - - // The line context, and whether the running code may be an older - // build than the source it was read from (issue #938); two reads of - // the same file, overlapped. - const [context, staleBuild] = await Promise.all([ - readLineContext(ctx, location.file, location.line, 'step result'), - describeStaleBuild(ctx, args.sessionId, location.file, 'step result') - ]); - if (context) { - response.context = context; - } - appendNote(response, staleBuild); - } + // Where the step landed (issues #904, #938): the shape every stop answer has. + await hoistStopLanding(ctx, response, resultData, args.sessionId, 'step result'); return jsonResult(response); } catch (error) { @@ -177,24 +152,8 @@ export const waitForStopTool: ToolHandler = async (ctx, args, _toolName, extra) if (typeof data?.exitCode === 'number') { response.exitCode = data.exitCode; } - // The landing beyond the frame (issue #904), as for pause_execution. - if (typeof data?.threadId === 'number') { - response.threadId = data.threadId; - } - if (data?.note) { - response.note = data.note; - } - if (data?.location) { - response.location = data.location; - const [context, staleBuild] = await Promise.all([ - readLineContext(ctx, data.location.file, data.location.line, 'wait_for_stop result'), - describeStaleBuild(ctx, args.sessionId, data.location.file, 'wait_for_stop result') - ]); - if (context) { - response.context = context; - } - appendNote(response, staleBuild); - } + // The landing (issues #904, #938), as for pause_execution and the step tools. + await hoistStopLanding(ctx, response, data, args.sessionId, 'wait_for_stop result'); return jsonResult(response); } catch (error) { // Same contract as the step tools: typed session errors and other @@ -235,23 +194,7 @@ export async function handlePause(ctx: ToolContext, args: { sessionId: string; t response.rawStopReason = data.rawStopReason; } } - if (typeof data?.threadId === 'number') { - response.threadId = data.threadId; - } - if (data?.note) { - response.note = data.note; - } - if (data?.location) { - response.location = data.location; - const [context, staleBuild] = await Promise.all([ - readLineContext(ctx, data.location.file, data.location.line, 'pause result'), - describeStaleBuild(ctx, args.sessionId, data.location.file, 'pause result') - ]); - if (context) { - response.context = context; - } - appendNote(response, staleBuild); - } + await hoistStopLanding(ctx, response, data, args.sessionId, 'pause result'); return jsonResult(response); } catch (error) { ctx.logger.error('Failed to pause execution', { error }); diff --git a/src/server/handlers/inspection-tools.ts b/src/server/handlers/inspection-tools.ts index 4bdea9ad..539d5fca 100644 --- a/src/server/handlers/inspection-tools.ts +++ b/src/server/handlers/inspection-tools.ts @@ -8,7 +8,7 @@ import { SessionTerminatedError } from '../../errors/debug-errors.js'; import { ErrorMessages } from '../../utils/error-messages.js'; import type { ToolContext, ToolHandler } from '../tool-context.js'; import { enforceExplicitNames, requireSessionId } from '../tool-validation.js'; -import { carriesLastStop, debuggerOffWhyFor, variablePayloadExtras } from './shared.js'; +import { carriesLastStop, debuggerOffWhyFor, describeStaleBuild, variablePayloadExtras } from './shared.js'; import { failureResult, jsonResult, @@ -140,6 +140,18 @@ export const getStackTraceTool: ToolHandler = async (ctx, args) => { notes.push(explanation); } } + // Whether the running code may be an older build than the source the top + // frame was read from (issues #938/#950): the sentence the stop answers + // carry, here on the tool the fast-program flow always calls (issue + // #951). The top frame only, and only one with an openable path — a + // `<…>` label or an unresolvedSource frame names no file on this host. + const top = stackTrace.frames[0]; + if (top && !top.unresolvedSource && !top.file.startsWith('<')) { + const staleBuild = await describeStaleBuild(ctx, args.sessionId, top.file, 'stack trace'); + if (staleBuild) { + notes.push(staleBuild); + } + } if (notes.length > 0) { payload.note = notes.join(' '); } diff --git a/src/server/handlers/shared.ts b/src/server/handlers/shared.ts index 0eed2705..43344562 100644 --- a/src/server/handlers/shared.ts +++ b/src/server/handlers/shared.ts @@ -11,7 +11,7 @@ import type { ToolContext } from '../tool-context.js'; import type { ToolArguments } from '../tool-arguments.js'; import { validateBreakOnExceptions } from '../tool-validation.js'; import { debuggerOffWhy } from '../../session/debugger-off.js'; -import type { DebugResult } from '../../session/session-manager-core.js'; +import type { DebugResult, StopLandingFields } from '../../session/session-manager-core.js'; import type { AttachRequest } from '../../session/attach/attach-controller.js'; import type { ManagedSession } from '../../session/session-store.js'; import { ErrorMessages } from '../../utils/error-messages.js'; @@ -136,6 +136,48 @@ export function appendNote(response: Record, note: string | und response.note = typeof existing === 'string' && existing.length > 0 ? `${existing} ${note}` : note; } +/** + * Hoist where a stop landed to the top level of a tool answer and annotate + * it — the one shape every stop answer has: the step tools, pause_execution, + * wait_for_stop, and a launch or relaunch that stopped (issue #951). + * + * Beyond the frame (issue #904): the thread the answer is anchored on when it + * is not the reported one, and the note about it. Then the location, and two + * reads of its file, overlapped: the source line around it, and whether the + * running code may be an older build than the source it was read from + * (issues #938/#950) — that sentence is appended to the note, so it always + * comes after what the session layer had to say. Nothing happens without a + * location: an answer that is pending or that reports a program's end keeps + * exactly the fields it had. + */ +export async function hoistStopLanding( + ctx: ToolContext, + response: Record, + data: StopLandingFields | undefined, + sessionId: string, + logLabel: string +): Promise { + if (typeof data?.threadId === 'number') { + response.threadId = data.threadId; + } + if (data?.note) { + response.note = data.note; + } + const location = data?.location; + if (!location) { + return; + } + response.location = location; + const [context, staleBuild] = await Promise.all([ + readLineContext(ctx, location.file, location.line, logLabel), + describeStaleBuild(ctx, sessionId, location.file, logLabel) + ]); + if (context) { + response.context = context; + } + appendNote(response, staleBuild); +} + /** * Top-level `redaction` notice object for tool results (issue #237): * present when any returned item carries the session layer's `redacted` diff --git a/src/server/tool-context.ts b/src/server/tool-context.ts index bb50f73a..7ffdd28e 100644 --- a/src/server/tool-context.ts +++ b/src/server/tool-context.ts @@ -24,7 +24,7 @@ import type { Variable } from '@debugmcp/shared'; import type { SessionManager } from '../session/session-manager.js'; -import type { DebugResult, StepResultData, WaitForStopResultData } from '../session/session-manager-core.js'; +import type { DebugResult, LaunchResultData, StepResultData, WaitForStopResultData } from '../session/session-manager-core.js'; import type { FrameSummary, StackTraceResult } from '../session/session-manager-data.js'; import type { VariableTruncationSummary } from '../session/variable-caps.js'; import type { SimpleFileChecker, FileExistenceResult } from '../utils/simple-file-checker.js'; @@ -95,8 +95,8 @@ export interface ToolContext { dryRunSpawn?: boolean, adapterLaunchConfig?: Record, breakOnExceptions?: ExceptionBreakMode - ): Promise; - restartDebugging(sessionId: string): Promise; + ): Promise>; + restartDebugging(sessionId: string): Promise>; closeDebugSession(sessionId: string): Promise; setFunctionBreakpoint( sessionId: string, diff --git a/src/server/tool-schemas.ts b/src/server/tool-schemas.ts index 0aa26fe3..6d4640f1 100644 --- a/src/server/tool-schemas.ts +++ b/src/server/tool-schemas.ts @@ -139,7 +139,7 @@ export function buildToolDefinitions(options: BuildToolDefinitionsOptions): Tool { name: 'list_breakpoints', description: 'List all breakpoints in a session with their verified state and adapter-assigned ids. Session-global function breakpoints appear separately as functionBreakpoints (omitted when filtering by file). Works before launch (queued, verified=false), while running or paused, and after the program exits', inputSchema: { type: 'object', properties: { sessionId: { type: 'string' }, file: { type: 'string', description: 'Optional: only list breakpoints in this file' } }, required: ['sessionId'] } }, { name: 'remove_breakpoint', description: 'Remove a breakpoint by breakpointId (returned by set_breakpoint / list_breakpoints), by function name, or by file + line (removes all breakpoints at that location). Takes effect immediately while the program is running or paused; also works after the program exits, before a relaunch', inputSchema: { type: 'object', properties: { sessionId: { type: 'string' }, breakpointId: { type: 'string', description: 'Breakpoint id from set_breakpoint or list_breakpoints. Takes precedence over file + line' }, function: { type: 'string', description: 'Alternative addressing: remove all function breakpoints with this symbol name. Applies the same per-language rewrite as set_breakpoint (e.g. a bare Go "main" matches the stored "main.main"); the response reports the effective name as functionName and, when rewritten, the original as requestedName' }, file: { type: 'string', description: 'Alternative addressing: source file path (use together with line)' }, line: { type: 'number', description: 'Alternative addressing: line number (use together with file)' } }, required: ['sessionId'] } }, { name: 'clear_breakpoints', description: 'Remove all breakpoints in a session, or all breakpoints in one file. Clearing zero breakpoints is success. Takes effect immediately while the program is running or paused', inputSchema: { type: 'object', properties: { sessionId: { type: 'string' }, file: { type: 'string', description: 'Optional: only clear breakpoints in this file' } }, required: ['sessionId'] } }, - { name: 'start_debugging', description: 'Start debugging a script. Answers when the launched program stops, when it ends, or after a short hold — whichever comes first, and the same way in every language: "paused" with a reason (a breakpoint reached as the program starts is answered here), "stopped" with the exit code, or "running" with pending:true when the program has not stopped yet; the message says what is armed. The hold is not a limit on the program — nothing is cancelled when it passes and breakpoints stay armed: call wait_for_stop to block until the program stops or ends (for a server, after sending the request that reaches your breakpoint). A stopOnEntry launch is answered at its entry stop', inputSchema: { + { name: 'start_debugging', description: 'Start debugging a script. Answers when the launched program stops, when it ends, or after a short hold — whichever comes first, and the same way in every language: "paused" with a reason and, as wait_for_stop answers a stop, the location, its source context and a note (a breakpoint reached as the program starts is answered here), "stopped" with the exit code, or "running" with pending:true when the program has not stopped yet; the message says what is armed. The hold is not a limit on the program — nothing is cancelled when it passes and breakpoints stay armed: call wait_for_stop to block until the program stops or ends (for a server, after sending the request that reaches your breakpoint). A stopOnEntry launch is answered at its entry stop', inputSchema: { type: 'object', properties: { sessionId: { type: 'string' }, @@ -205,7 +205,7 @@ export function buildToolDefinitions(options: BuildToolDefinitionsOptions): Tool { name: 'list_threads', description: 'List all threads in the debugged process', inputSchema: { type: 'object', properties: { sessionId: { type: 'string' } }, required: ['sessionId'] } }, { name: 'get_variables', description: 'Get variables (scope is variablesReference: number). Responses are size-guarded: oversized values are cut (truncated:true) and very large scopes return a capped list with a truncation notice — pass names:[...] to fetch specific variables in full', inputSchema: { type: 'object', properties: { sessionId: { type: 'string' }, scope: { type: 'number', description: "The variablesReference number from a StackFrame or Variable" }, names: namesProp }, required: getVariablesRequired } }, { name: 'get_local_variables', description: 'Get local variables for the shared inspection anchor. If the stopped thread is frameless, a sibling with policy-recognized user frames is preferred, adopted, and disclosed in note. At a breakpoint or step inside a frame the stack display hides as internal (a node_modules dependency), the anchor is that frame — the one the program stopped in — and note says so. A JavaScript frame with no Local/block scope returns empty with guidance instead of mislabeling Global bindings. Responses are size-guarded; pass names:[...] to fetch specific variables in full', inputSchema: { type: 'object', properties: { sessionId: { type: 'string' }, includeSpecial: { type: 'boolean', description: 'Include special/internal variables like this, __proto__, __builtins__, etc. Default: false' }, names: namesProp }, required: getLocalVariablesRequired } }, - { name: 'get_stack_trace', description: 'Get stack trace. The response includes stopReason — why the session is paused. For an implicit frameless stopped thread, a sibling with policy-recognized user frames is preferred and adopted, with the switch disclosed in note. Internal/runtime frames are filtered by default; when any are hidden the response carries hiddenFrames and a note. When the frame the program is paused in is itself internal, pausedFrame names it (kept: whether it was retained as stackFrames[0]) and the note says so', inputSchema: { type: 'object', properties: { sessionId: { type: 'string' }, includeInternals: { type: 'boolean', description: 'Include internal/framework frames (e.g., Node.js internals, node_modules dependencies, async separators). Default: false for cleaner output.' }, threadId: { type: 'number', description: 'Inspect this exact thread (ids from list_threads). Explicit selection is authoritative and is never silently switched' } }, required: ['sessionId'] } }, + { name: 'get_stack_trace', description: 'Get stack trace. The response includes stopReason — why the session is paused. For an implicit frameless stopped thread, a sibling with policy-recognized user frames is preferred and adopted, with the switch disclosed in note. Internal/runtime frames are filtered by default; when any are hidden the response carries hiddenFrames and a note. When the frame the program is paused in is itself internal, pausedFrame names it (kept: whether it was retained as stackFrames[0]) and the note says so. The note also says when the top frame\'s file is newer than the compiled file the debugger binds it through (a stale build)', inputSchema: { type: 'object', properties: { sessionId: { type: 'string' }, includeInternals: { type: 'boolean', description: 'Include internal/framework frames (e.g., Node.js internals, node_modules dependencies, async separators). Default: false for cleaner output.' }, threadId: { type: 'number', description: 'Inspect this exact thread (ids from list_threads). Explicit selection is authoritative and is never silently switched' } }, required: ['sessionId'] } }, { name: 'get_scopes', description: 'Get scopes for a stack frame', inputSchema: { type: 'object', properties: { sessionId: { type: 'string' }, frameId: { type: 'number', description: "The ID of the stack frame from a stackTrace response" } }, required: ['sessionId', 'frameId'] } }, { name: 'evaluate_expression', description: 'Evaluate expression in the current debug context. Without frameId, uses the same adopted top frame as stack and locals — at a breakpoint or step that is the frame the program stopped in even when the stack display would hide it — and discloses an automatic thread switch or a hidden paused frame in anchorNote; frame names the frame that was used, on success and on failure. Expressions can read and modify program state. Waits up to 30s by default', inputSchema: { type: 'object', properties: { sessionId: { type: 'string' }, expression: { type: 'string' }, frameId: { type: 'number', description: 'Optional authoritative stack frame ID from get_stack_trace. When provided, no automatic frame/thread selection occurs' }, timeout: { type: 'number', description: 'Max time (ms) to wait for the evaluation to complete (default: 30000, max: 600000). On expiry the request fails but the expression may keep executing in the debuggee. Note: your MCP client may enforce its own overall request timeout' } }, required: ['sessionId', 'expression'] } }, { name: 'get_source_context', description: 'Get source context around a specific line in a file', inputSchema: { type: 'object', properties: { sessionId: { type: 'string' }, file: { type: 'string', description: fileDescription }, line: { type: 'number', description: 'Line number to get context for' }, linesContext: { type: 'number', description: 'Number of lines before and after to include (default: 5)' } }, required: ['sessionId', 'file', 'line'] } }, diff --git a/src/session/launch/debug-launcher.ts b/src/session/launch/debug-launcher.ts index 9d942326..04d3dd13 100644 --- a/src/session/launch/debug-launcher.ts +++ b/src/session/launch/debug-launcher.ts @@ -22,9 +22,10 @@ import { McpError } from '@modelcontextprotocol/sdk/types.js'; import { ErrorMessages } from '../../utils/error-messages.js'; import { checkLaunchToolchain } from '../../utils/language-availability.js'; import { setCuratedMessage } from '../breakpoints/hit-verification.js'; -import type { CustomLaunchRequestArguments, DebugResult } from '../session-manager-core.js'; +import type { CustomLaunchRequestArguments, DebugResult, LaunchResultData } from '../session-manager-core.js'; import type { ManagedSession, ToolchainValidationState } from '../session-store.js'; import type { LaunchContext } from '../operations-context.js'; +import { describeStopLocation, landingFields } from '../inspection/stop-location.js'; import type { BreakpointController, ResyncOutcome } from '../breakpoints/breakpoint-controller.js'; import { reresolveAnchors } from '../breakpoints/anchor-resolution.js'; import { @@ -197,7 +198,7 @@ export class DebugLauncher { dryRunSpawn?: boolean, adapterLaunchConfig?: Record, breakOnExceptions?: ExceptionBreakMode - ): Promise { + ): Promise> { return this.inFlight.run(sessionId, 'launch', 'start_debugging', this.ctx, () => this.launch( sessionId, @@ -220,7 +221,7 @@ export class DebugLauncher { dryRunSpawn?: boolean, adapterLaunchConfig?: Record, breakOnExceptions?: ExceptionBreakMode - ): Promise { + ): Promise> { const session = this.ctx.getSession(sessionId); this.ctx.logger.info( `Attempting to start debugging for session ${sessionId}, script: ${scriptPath}, dryRunSpawn: ${dryRunSpawn}, dapLaunchArgs:`, @@ -636,6 +637,18 @@ export class DebugLauncher { resync = await this.breakpoints.resyncAll(finalSession, { forceFreshEcho: true }); } + // Where the program stopped (issue #951): the same short settle and + // stack read a step, a pause or wait_for_stop makes, so the answer + // carries `location` — and `threadId`/`note` when there is something + // to say — and the handler can read the line and the build behind it. + // Only for a stop the core kept: PAUSED without lastStop is the + // auto-continue transient (below), whose line the program is about to + // leave. Best effort, like every landing read: a stack that cannot be + // read leaves the answer as it was. + const landing = finalState === SessionState.PAUSED && finalSession.lastStop + ? await describeStopLocation(this.ctx, sessionId, 'launch') + : {}; + // The policy's word is a static pin; a stop that arrived anyway is the // stronger evidence (an adapter build that ignores the flag after all): // a stop only a live debugger produces — the core's stopped handler @@ -750,6 +763,7 @@ export class DebugLauncher { (effectiveLaunchArgs?.stopOnEntry ? 'entry' : 'unknown') : undefined, stopOnEntrySuccessful: !!effectiveLaunchArgs?.stopOnEntry && finalState === SessionState.PAUSED, + ...landingFields(landing), }, }; } catch (error) { @@ -837,7 +851,7 @@ export class DebugLauncher { * emulation — so every launch-mode language works with no per-adapter * wiring (issue #238). */ - async restartDebugging(sessionId: string): Promise { + async restartDebugging(sessionId: string): Promise> { // Claimed first (issue #711): a launch or attach still being awaited, or // a restart already replaying, is the most relevant fact about the // session. Held for the whole restart, so the replayed launch runs under @@ -852,7 +866,7 @@ export class DebugLauncher { } /** The restart proper; the caller holds the session's in-flight claim. */ - private async restart(session: ManagedSession): Promise { + private async restart(session: ManagedSession): Promise> { const sessionId = session.id; if (session.attachMode) { diff --git a/src/session/operations-context.ts b/src/session/operations-context.ts index ee685c07..267c5988 100644 --- a/src/session/operations-context.ts +++ b/src/session/operations-context.ts @@ -143,8 +143,9 @@ export type ProxyLaunchContext = Pick< /** * Launch-mode sessions: the toolchain gate, state and store updates (and the * state subscription the readiness waits read), the dry-run window, the - * proxy-failure record (hence the filesystem) and the session-preserving - * teardown of a previous proxy. + * proxy-failure record (hence the filesystem), the session-preserving + * teardown of a previous proxy, and the detailed stack behind the landing a + * launch that stopped reports (issue #951). */ export type LaunchContext = Pick< OperationsContext, @@ -159,6 +160,7 @@ export type LaunchContext = Pick< | 'updateState' | 'onStateChange' | 'selectPolicy' + | 'getStackTraceDetailed' | 'stopProxyPreservingSession' >; diff --git a/src/session/session-manager-core.ts b/src/session/session-manager-core.ts index 2e690224..1dc68f81 100644 --- a/src/session/session-manager-core.ts +++ b/src/session/session-manager-core.ts @@ -172,6 +172,17 @@ export type WaitForStopResultData = DebugResultData & StopLandingFields & { lastStop?: SessionStopInfo; }; +/** + * What a launch (start_debugging, and restart_debugging, which replays one) + * returns: the common fields — `reason`/`stopOnEntrySuccessful` for a stop, + * the run-to-completion fields for a program that ended, `pending` for one + * still running when the hold elapsed — plus, for a stop, where it landed + * (issue #951): the same landing read a step, a pause or wait_for_stop makes, + * so the answer names the frame the program is paused in and the handler can + * read the line and the build behind it. + */ +export type LaunchResultData = DebugResultData & StopLandingFields; + /** * What a successful attach returns on top of the common fields: a `note` * when the attach's stop landed where no frame is program code (an idle diff --git a/src/session/session-manager-operations.ts b/src/session/session-manager-operations.ts index afabdb79..75dc9ce7 100644 --- a/src/session/session-manager-operations.ts +++ b/src/session/session-manager-operations.ts @@ -41,6 +41,7 @@ import { AttachResultData, CustomLaunchRequestArguments, DebugResult, + LaunchResultData, PauseResultData, StepResultData, WaitForStopResultData @@ -235,7 +236,7 @@ export abstract class SessionManagerOperations extends SessionManagerData { dryRunSpawn?: boolean, adapterLaunchConfig?: Record, breakOnExceptions?: ExceptionBreakMode - ): Promise { + ): Promise> { return this.launcher.startDebugging( sessionId, scriptPath, @@ -250,7 +251,7 @@ export abstract class SessionManagerOperations extends SessionManagerData { /** * Restart the debuggee by replaying the last real launch (issue #238). */ - async restartDebugging(sessionId: string): Promise { + async restartDebugging(sessionId: string): Promise> { return this.launcher.restartDebugging(sessionId); } diff --git a/tests/core/unit/server/__snapshots__/tool-list/bp-assert.va-explicit.container-true.json b/tests/core/unit/server/__snapshots__/tool-list/bp-assert.va-explicit.container-true.json index 90f7f10d..f2810eba 100644 --- a/tests/core/unit/server/__snapshots__/tool-list/bp-assert.va-explicit.container-true.json +++ b/tests/core/unit/server/__snapshots__/tool-list/bp-assert.va-explicit.container-true.json @@ -213,7 +213,7 @@ }, { "name": "start_debugging", - "description": "Start debugging a script. Answers when the launched program stops, when it ends, or after a short hold — whichever comes first, and the same way in every language: \"paused\" with a reason (a breakpoint reached as the program starts is answered here), \"stopped\" with the exit code, or \"running\" with pending:true when the program has not stopped yet; the message says what is armed. The hold is not a limit on the program — nothing is cancelled when it passes and breakpoints stay armed: call wait_for_stop to block until the program stops or ends (for a server, after sending the request that reaches your breakpoint). A stopOnEntry launch is answered at its entry stop", + "description": "Start debugging a script. Answers when the launched program stops, when it ends, or after a short hold — whichever comes first, and the same way in every language: \"paused\" with a reason and, as wait_for_stop answers a stop, the location, its source context and a note (a breakpoint reached as the program starts is answered here), \"stopped\" with the exit code, or \"running\" with pending:true when the program has not stopped yet; the message says what is armed. The hold is not a limit on the program — nothing is cancelled when it passes and breakpoints stay armed: call wait_for_stop to block until the program stops or ends (for a server, after sending the request that reaches your breakpoint). A stopOnEntry launch is answered at its entry stop", "inputSchema": { "type": "object", "properties": { @@ -590,7 +590,7 @@ }, { "name": "get_stack_trace", - "description": "Get stack trace. The response includes stopReason — why the session is paused. For an implicit frameless stopped thread, a sibling with policy-recognized user frames is preferred and adopted, with the switch disclosed in note. Internal/runtime frames are filtered by default; when any are hidden the response carries hiddenFrames and a note. When the frame the program is paused in is itself internal, pausedFrame names it (kept: whether it was retained as stackFrames[0]) and the note says so", + "description": "Get stack trace. The response includes stopReason — why the session is paused. For an implicit frameless stopped thread, a sibling with policy-recognized user frames is preferred and adopted, with the switch disclosed in note. Internal/runtime frames are filtered by default; when any are hidden the response carries hiddenFrames and a note. When the frame the program is paused in is itself internal, pausedFrame names it (kept: whether it was retained as stackFrames[0]) and the note says so. The note also says when the top frame's file is newer than the compiled file the debugger binds it through (a stale build)", "inputSchema": { "type": "object", "properties": { diff --git a/tests/core/unit/server/__snapshots__/tool-list/bp-assert.va-explicit.container-unset.json b/tests/core/unit/server/__snapshots__/tool-list/bp-assert.va-explicit.container-unset.json index b5511dbb..b380a7da 100644 --- a/tests/core/unit/server/__snapshots__/tool-list/bp-assert.va-explicit.container-unset.json +++ b/tests/core/unit/server/__snapshots__/tool-list/bp-assert.va-explicit.container-unset.json @@ -213,7 +213,7 @@ }, { "name": "start_debugging", - "description": "Start debugging a script. Answers when the launched program stops, when it ends, or after a short hold — whichever comes first, and the same way in every language: \"paused\" with a reason (a breakpoint reached as the program starts is answered here), \"stopped\" with the exit code, or \"running\" with pending:true when the program has not stopped yet; the message says what is armed. The hold is not a limit on the program — nothing is cancelled when it passes and breakpoints stay armed: call wait_for_stop to block until the program stops or ends (for a server, after sending the request that reaches your breakpoint). A stopOnEntry launch is answered at its entry stop", + "description": "Start debugging a script. Answers when the launched program stops, when it ends, or after a short hold — whichever comes first, and the same way in every language: \"paused\" with a reason and, as wait_for_stop answers a stop, the location, its source context and a note (a breakpoint reached as the program starts is answered here), \"stopped\" with the exit code, or \"running\" with pending:true when the program has not stopped yet; the message says what is armed. The hold is not a limit on the program — nothing is cancelled when it passes and breakpoints stay armed: call wait_for_stop to block until the program stops or ends (for a server, after sending the request that reaches your breakpoint). A stopOnEntry launch is answered at its entry stop", "inputSchema": { "type": "object", "properties": { @@ -590,7 +590,7 @@ }, { "name": "get_stack_trace", - "description": "Get stack trace. The response includes stopReason — why the session is paused. For an implicit frameless stopped thread, a sibling with policy-recognized user frames is preferred and adopted, with the switch disclosed in note. Internal/runtime frames are filtered by default; when any are hidden the response carries hiddenFrames and a note. When the frame the program is paused in is itself internal, pausedFrame names it (kept: whether it was retained as stackFrames[0]) and the note says so", + "description": "Get stack trace. The response includes stopReason — why the session is paused. For an implicit frameless stopped thread, a sibling with policy-recognized user frames is preferred and adopted, with the switch disclosed in note. Internal/runtime frames are filtered by default; when any are hidden the response carries hiddenFrames and a note. When the frame the program is paused in is itself internal, pausedFrame names it (kept: whether it was retained as stackFrames[0]) and the note says so. The note also says when the top frame's file is newer than the compiled file the debugger binds it through (a stale build)", "inputSchema": { "type": "object", "properties": { diff --git a/tests/core/unit/server/__snapshots__/tool-list/bp-assert.va-open.container-true.json b/tests/core/unit/server/__snapshots__/tool-list/bp-assert.va-open.container-true.json index 82ec30e8..1f15dd0e 100644 --- a/tests/core/unit/server/__snapshots__/tool-list/bp-assert.va-open.container-true.json +++ b/tests/core/unit/server/__snapshots__/tool-list/bp-assert.va-open.container-true.json @@ -213,7 +213,7 @@ }, { "name": "start_debugging", - "description": "Start debugging a script. Answers when the launched program stops, when it ends, or after a short hold — whichever comes first, and the same way in every language: \"paused\" with a reason (a breakpoint reached as the program starts is answered here), \"stopped\" with the exit code, or \"running\" with pending:true when the program has not stopped yet; the message says what is armed. The hold is not a limit on the program — nothing is cancelled when it passes and breakpoints stay armed: call wait_for_stop to block until the program stops or ends (for a server, after sending the request that reaches your breakpoint). A stopOnEntry launch is answered at its entry stop", + "description": "Start debugging a script. Answers when the launched program stops, when it ends, or after a short hold — whichever comes first, and the same way in every language: \"paused\" with a reason and, as wait_for_stop answers a stop, the location, its source context and a note (a breakpoint reached as the program starts is answered here), \"stopped\" with the exit code, or \"running\" with pending:true when the program has not stopped yet; the message says what is armed. The hold is not a limit on the program — nothing is cancelled when it passes and breakpoints stay armed: call wait_for_stop to block until the program stops or ends (for a server, after sending the request that reaches your breakpoint). A stopOnEntry launch is answered at its entry stop", "inputSchema": { "type": "object", "properties": { @@ -588,7 +588,7 @@ }, { "name": "get_stack_trace", - "description": "Get stack trace. The response includes stopReason — why the session is paused. For an implicit frameless stopped thread, a sibling with policy-recognized user frames is preferred and adopted, with the switch disclosed in note. Internal/runtime frames are filtered by default; when any are hidden the response carries hiddenFrames and a note. When the frame the program is paused in is itself internal, pausedFrame names it (kept: whether it was retained as stackFrames[0]) and the note says so", + "description": "Get stack trace. The response includes stopReason — why the session is paused. For an implicit frameless stopped thread, a sibling with policy-recognized user frames is preferred and adopted, with the switch disclosed in note. Internal/runtime frames are filtered by default; when any are hidden the response carries hiddenFrames and a note. When the frame the program is paused in is itself internal, pausedFrame names it (kept: whether it was retained as stackFrames[0]) and the note says so. The note also says when the top frame's file is newer than the compiled file the debugger binds it through (a stale build)", "inputSchema": { "type": "object", "properties": { diff --git a/tests/core/unit/server/__snapshots__/tool-list/bp-assert.va-open.container-unset.json b/tests/core/unit/server/__snapshots__/tool-list/bp-assert.va-open.container-unset.json index ef94f34a..2ff0a757 100644 --- a/tests/core/unit/server/__snapshots__/tool-list/bp-assert.va-open.container-unset.json +++ b/tests/core/unit/server/__snapshots__/tool-list/bp-assert.va-open.container-unset.json @@ -213,7 +213,7 @@ }, { "name": "start_debugging", - "description": "Start debugging a script. Answers when the launched program stops, when it ends, or after a short hold — whichever comes first, and the same way in every language: \"paused\" with a reason (a breakpoint reached as the program starts is answered here), \"stopped\" with the exit code, or \"running\" with pending:true when the program has not stopped yet; the message says what is armed. The hold is not a limit on the program — nothing is cancelled when it passes and breakpoints stay armed: call wait_for_stop to block until the program stops or ends (for a server, after sending the request that reaches your breakpoint). A stopOnEntry launch is answered at its entry stop", + "description": "Start debugging a script. Answers when the launched program stops, when it ends, or after a short hold — whichever comes first, and the same way in every language: \"paused\" with a reason and, as wait_for_stop answers a stop, the location, its source context and a note (a breakpoint reached as the program starts is answered here), \"stopped\" with the exit code, or \"running\" with pending:true when the program has not stopped yet; the message says what is armed. The hold is not a limit on the program — nothing is cancelled when it passes and breakpoints stay armed: call wait_for_stop to block until the program stops or ends (for a server, after sending the request that reaches your breakpoint). A stopOnEntry launch is answered at its entry stop", "inputSchema": { "type": "object", "properties": { @@ -588,7 +588,7 @@ }, { "name": "get_stack_trace", - "description": "Get stack trace. The response includes stopReason — why the session is paused. For an implicit frameless stopped thread, a sibling with policy-recognized user frames is preferred and adopted, with the switch disclosed in note. Internal/runtime frames are filtered by default; when any are hidden the response carries hiddenFrames and a note. When the frame the program is paused in is itself internal, pausedFrame names it (kept: whether it was retained as stackFrames[0]) and the note says so", + "description": "Get stack trace. The response includes stopReason — why the session is paused. For an implicit frameless stopped thread, a sibling with policy-recognized user frames is preferred and adopted, with the switch disclosed in note. Internal/runtime frames are filtered by default; when any are hidden the response carries hiddenFrames and a note. When the frame the program is paused in is itself internal, pausedFrame names it (kept: whether it was retained as stackFrames[0]) and the note says so. The note also says when the top frame's file is newer than the compiled file the debugger binds it through (a stale build)", "inputSchema": { "type": "object", "properties": { diff --git a/tests/core/unit/server/__snapshots__/tool-list/bp-assert.va-unset.container-true.json b/tests/core/unit/server/__snapshots__/tool-list/bp-assert.va-unset.container-true.json index 82ec30e8..1f15dd0e 100644 --- a/tests/core/unit/server/__snapshots__/tool-list/bp-assert.va-unset.container-true.json +++ b/tests/core/unit/server/__snapshots__/tool-list/bp-assert.va-unset.container-true.json @@ -213,7 +213,7 @@ }, { "name": "start_debugging", - "description": "Start debugging a script. Answers when the launched program stops, when it ends, or after a short hold — whichever comes first, and the same way in every language: \"paused\" with a reason (a breakpoint reached as the program starts is answered here), \"stopped\" with the exit code, or \"running\" with pending:true when the program has not stopped yet; the message says what is armed. The hold is not a limit on the program — nothing is cancelled when it passes and breakpoints stay armed: call wait_for_stop to block until the program stops or ends (for a server, after sending the request that reaches your breakpoint). A stopOnEntry launch is answered at its entry stop", + "description": "Start debugging a script. Answers when the launched program stops, when it ends, or after a short hold — whichever comes first, and the same way in every language: \"paused\" with a reason and, as wait_for_stop answers a stop, the location, its source context and a note (a breakpoint reached as the program starts is answered here), \"stopped\" with the exit code, or \"running\" with pending:true when the program has not stopped yet; the message says what is armed. The hold is not a limit on the program — nothing is cancelled when it passes and breakpoints stay armed: call wait_for_stop to block until the program stops or ends (for a server, after sending the request that reaches your breakpoint). A stopOnEntry launch is answered at its entry stop", "inputSchema": { "type": "object", "properties": { @@ -588,7 +588,7 @@ }, { "name": "get_stack_trace", - "description": "Get stack trace. The response includes stopReason — why the session is paused. For an implicit frameless stopped thread, a sibling with policy-recognized user frames is preferred and adopted, with the switch disclosed in note. Internal/runtime frames are filtered by default; when any are hidden the response carries hiddenFrames and a note. When the frame the program is paused in is itself internal, pausedFrame names it (kept: whether it was retained as stackFrames[0]) and the note says so", + "description": "Get stack trace. The response includes stopReason — why the session is paused. For an implicit frameless stopped thread, a sibling with policy-recognized user frames is preferred and adopted, with the switch disclosed in note. Internal/runtime frames are filtered by default; when any are hidden the response carries hiddenFrames and a note. When the frame the program is paused in is itself internal, pausedFrame names it (kept: whether it was retained as stackFrames[0]) and the note says so. The note also says when the top frame's file is newer than the compiled file the debugger binds it through (a stale build)", "inputSchema": { "type": "object", "properties": { diff --git a/tests/core/unit/server/__snapshots__/tool-list/bp-assert.va-unset.container-unset.json b/tests/core/unit/server/__snapshots__/tool-list/bp-assert.va-unset.container-unset.json index ef94f34a..2ff0a757 100644 --- a/tests/core/unit/server/__snapshots__/tool-list/bp-assert.va-unset.container-unset.json +++ b/tests/core/unit/server/__snapshots__/tool-list/bp-assert.va-unset.container-unset.json @@ -213,7 +213,7 @@ }, { "name": "start_debugging", - "description": "Start debugging a script. Answers when the launched program stops, when it ends, or after a short hold — whichever comes first, and the same way in every language: \"paused\" with a reason (a breakpoint reached as the program starts is answered here), \"stopped\" with the exit code, or \"running\" with pending:true when the program has not stopped yet; the message says what is armed. The hold is not a limit on the program — nothing is cancelled when it passes and breakpoints stay armed: call wait_for_stop to block until the program stops or ends (for a server, after sending the request that reaches your breakpoint). A stopOnEntry launch is answered at its entry stop", + "description": "Start debugging a script. Answers when the launched program stops, when it ends, or after a short hold — whichever comes first, and the same way in every language: \"paused\" with a reason and, as wait_for_stop answers a stop, the location, its source context and a note (a breakpoint reached as the program starts is answered here), \"stopped\" with the exit code, or \"running\" with pending:true when the program has not stopped yet; the message says what is armed. The hold is not a limit on the program — nothing is cancelled when it passes and breakpoints stay armed: call wait_for_stop to block until the program stops or ends (for a server, after sending the request that reaches your breakpoint). A stopOnEntry launch is answered at its entry stop", "inputSchema": { "type": "object", "properties": { @@ -588,7 +588,7 @@ }, { "name": "get_stack_trace", - "description": "Get stack trace. The response includes stopReason — why the session is paused. For an implicit frameless stopped thread, a sibling with policy-recognized user frames is preferred and adopted, with the switch disclosed in note. Internal/runtime frames are filtered by default; when any are hidden the response carries hiddenFrames and a note. When the frame the program is paused in is itself internal, pausedFrame names it (kept: whether it was retained as stackFrames[0]) and the note says so", + "description": "Get stack trace. The response includes stopReason — why the session is paused. For an implicit frameless stopped thread, a sibling with policy-recognized user frames is preferred and adopted, with the switch disclosed in note. Internal/runtime frames are filtered by default; when any are hidden the response carries hiddenFrames and a note. When the frame the program is paused in is itself internal, pausedFrame names it (kept: whether it was retained as stackFrames[0]) and the note says so. The note also says when the top frame's file is newer than the compiled file the debugger binds it through (a stale build)", "inputSchema": { "type": "object", "properties": { diff --git a/tests/core/unit/server/__snapshots__/tool-list/bp-content.va-explicit.container-true.json b/tests/core/unit/server/__snapshots__/tool-list/bp-content.va-explicit.container-true.json index 2f0150a2..a9404fe7 100644 --- a/tests/core/unit/server/__snapshots__/tool-list/bp-content.va-explicit.container-true.json +++ b/tests/core/unit/server/__snapshots__/tool-list/bp-content.va-explicit.container-true.json @@ -223,7 +223,7 @@ }, { "name": "start_debugging", - "description": "Start debugging a script. Answers when the launched program stops, when it ends, or after a short hold — whichever comes first, and the same way in every language: \"paused\" with a reason (a breakpoint reached as the program starts is answered here), \"stopped\" with the exit code, or \"running\" with pending:true when the program has not stopped yet; the message says what is armed. The hold is not a limit on the program — nothing is cancelled when it passes and breakpoints stay armed: call wait_for_stop to block until the program stops or ends (for a server, after sending the request that reaches your breakpoint). A stopOnEntry launch is answered at its entry stop", + "description": "Start debugging a script. Answers when the launched program stops, when it ends, or after a short hold — whichever comes first, and the same way in every language: \"paused\" with a reason and, as wait_for_stop answers a stop, the location, its source context and a note (a breakpoint reached as the program starts is answered here), \"stopped\" with the exit code, or \"running\" with pending:true when the program has not stopped yet; the message says what is armed. The hold is not a limit on the program — nothing is cancelled when it passes and breakpoints stay armed: call wait_for_stop to block until the program stops or ends (for a server, after sending the request that reaches your breakpoint). A stopOnEntry launch is answered at its entry stop", "inputSchema": { "type": "object", "properties": { @@ -600,7 +600,7 @@ }, { "name": "get_stack_trace", - "description": "Get stack trace. The response includes stopReason — why the session is paused. For an implicit frameless stopped thread, a sibling with policy-recognized user frames is preferred and adopted, with the switch disclosed in note. Internal/runtime frames are filtered by default; when any are hidden the response carries hiddenFrames and a note. When the frame the program is paused in is itself internal, pausedFrame names it (kept: whether it was retained as stackFrames[0]) and the note says so", + "description": "Get stack trace. The response includes stopReason — why the session is paused. For an implicit frameless stopped thread, a sibling with policy-recognized user frames is preferred and adopted, with the switch disclosed in note. Internal/runtime frames are filtered by default; when any are hidden the response carries hiddenFrames and a note. When the frame the program is paused in is itself internal, pausedFrame names it (kept: whether it was retained as stackFrames[0]) and the note says so. The note also says when the top frame's file is newer than the compiled file the debugger binds it through (a stale build)", "inputSchema": { "type": "object", "properties": { diff --git a/tests/core/unit/server/__snapshots__/tool-list/bp-content.va-explicit.container-unset.json b/tests/core/unit/server/__snapshots__/tool-list/bp-content.va-explicit.container-unset.json index 1c25827b..e14f252b 100644 --- a/tests/core/unit/server/__snapshots__/tool-list/bp-content.va-explicit.container-unset.json +++ b/tests/core/unit/server/__snapshots__/tool-list/bp-content.va-explicit.container-unset.json @@ -223,7 +223,7 @@ }, { "name": "start_debugging", - "description": "Start debugging a script. Answers when the launched program stops, when it ends, or after a short hold — whichever comes first, and the same way in every language: \"paused\" with a reason (a breakpoint reached as the program starts is answered here), \"stopped\" with the exit code, or \"running\" with pending:true when the program has not stopped yet; the message says what is armed. The hold is not a limit on the program — nothing is cancelled when it passes and breakpoints stay armed: call wait_for_stop to block until the program stops or ends (for a server, after sending the request that reaches your breakpoint). A stopOnEntry launch is answered at its entry stop", + "description": "Start debugging a script. Answers when the launched program stops, when it ends, or after a short hold — whichever comes first, and the same way in every language: \"paused\" with a reason and, as wait_for_stop answers a stop, the location, its source context and a note (a breakpoint reached as the program starts is answered here), \"stopped\" with the exit code, or \"running\" with pending:true when the program has not stopped yet; the message says what is armed. The hold is not a limit on the program — nothing is cancelled when it passes and breakpoints stay armed: call wait_for_stop to block until the program stops or ends (for a server, after sending the request that reaches your breakpoint). A stopOnEntry launch is answered at its entry stop", "inputSchema": { "type": "object", "properties": { @@ -600,7 +600,7 @@ }, { "name": "get_stack_trace", - "description": "Get stack trace. The response includes stopReason — why the session is paused. For an implicit frameless stopped thread, a sibling with policy-recognized user frames is preferred and adopted, with the switch disclosed in note. Internal/runtime frames are filtered by default; when any are hidden the response carries hiddenFrames and a note. When the frame the program is paused in is itself internal, pausedFrame names it (kept: whether it was retained as stackFrames[0]) and the note says so", + "description": "Get stack trace. The response includes stopReason — why the session is paused. For an implicit frameless stopped thread, a sibling with policy-recognized user frames is preferred and adopted, with the switch disclosed in note. Internal/runtime frames are filtered by default; when any are hidden the response carries hiddenFrames and a note. When the frame the program is paused in is itself internal, pausedFrame names it (kept: whether it was retained as stackFrames[0]) and the note says so. The note also says when the top frame's file is newer than the compiled file the debugger binds it through (a stale build)", "inputSchema": { "type": "object", "properties": { diff --git a/tests/core/unit/server/__snapshots__/tool-list/bp-content.va-open.container-true.json b/tests/core/unit/server/__snapshots__/tool-list/bp-content.va-open.container-true.json index 3c515bbc..9358f57a 100644 --- a/tests/core/unit/server/__snapshots__/tool-list/bp-content.va-open.container-true.json +++ b/tests/core/unit/server/__snapshots__/tool-list/bp-content.va-open.container-true.json @@ -223,7 +223,7 @@ }, { "name": "start_debugging", - "description": "Start debugging a script. Answers when the launched program stops, when it ends, or after a short hold — whichever comes first, and the same way in every language: \"paused\" with a reason (a breakpoint reached as the program starts is answered here), \"stopped\" with the exit code, or \"running\" with pending:true when the program has not stopped yet; the message says what is armed. The hold is not a limit on the program — nothing is cancelled when it passes and breakpoints stay armed: call wait_for_stop to block until the program stops or ends (for a server, after sending the request that reaches your breakpoint). A stopOnEntry launch is answered at its entry stop", + "description": "Start debugging a script. Answers when the launched program stops, when it ends, or after a short hold — whichever comes first, and the same way in every language: \"paused\" with a reason and, as wait_for_stop answers a stop, the location, its source context and a note (a breakpoint reached as the program starts is answered here), \"stopped\" with the exit code, or \"running\" with pending:true when the program has not stopped yet; the message says what is armed. The hold is not a limit on the program — nothing is cancelled when it passes and breakpoints stay armed: call wait_for_stop to block until the program stops or ends (for a server, after sending the request that reaches your breakpoint). A stopOnEntry launch is answered at its entry stop", "inputSchema": { "type": "object", "properties": { @@ -598,7 +598,7 @@ }, { "name": "get_stack_trace", - "description": "Get stack trace. The response includes stopReason — why the session is paused. For an implicit frameless stopped thread, a sibling with policy-recognized user frames is preferred and adopted, with the switch disclosed in note. Internal/runtime frames are filtered by default; when any are hidden the response carries hiddenFrames and a note. When the frame the program is paused in is itself internal, pausedFrame names it (kept: whether it was retained as stackFrames[0]) and the note says so", + "description": "Get stack trace. The response includes stopReason — why the session is paused. For an implicit frameless stopped thread, a sibling with policy-recognized user frames is preferred and adopted, with the switch disclosed in note. Internal/runtime frames are filtered by default; when any are hidden the response carries hiddenFrames and a note. When the frame the program is paused in is itself internal, pausedFrame names it (kept: whether it was retained as stackFrames[0]) and the note says so. The note also says when the top frame's file is newer than the compiled file the debugger binds it through (a stale build)", "inputSchema": { "type": "object", "properties": { diff --git a/tests/core/unit/server/__snapshots__/tool-list/bp-content.va-open.container-unset.json b/tests/core/unit/server/__snapshots__/tool-list/bp-content.va-open.container-unset.json index 1463d200..b6114fc7 100644 --- a/tests/core/unit/server/__snapshots__/tool-list/bp-content.va-open.container-unset.json +++ b/tests/core/unit/server/__snapshots__/tool-list/bp-content.va-open.container-unset.json @@ -223,7 +223,7 @@ }, { "name": "start_debugging", - "description": "Start debugging a script. Answers when the launched program stops, when it ends, or after a short hold — whichever comes first, and the same way in every language: \"paused\" with a reason (a breakpoint reached as the program starts is answered here), \"stopped\" with the exit code, or \"running\" with pending:true when the program has not stopped yet; the message says what is armed. The hold is not a limit on the program — nothing is cancelled when it passes and breakpoints stay armed: call wait_for_stop to block until the program stops or ends (for a server, after sending the request that reaches your breakpoint). A stopOnEntry launch is answered at its entry stop", + "description": "Start debugging a script. Answers when the launched program stops, when it ends, or after a short hold — whichever comes first, and the same way in every language: \"paused\" with a reason and, as wait_for_stop answers a stop, the location, its source context and a note (a breakpoint reached as the program starts is answered here), \"stopped\" with the exit code, or \"running\" with pending:true when the program has not stopped yet; the message says what is armed. The hold is not a limit on the program — nothing is cancelled when it passes and breakpoints stay armed: call wait_for_stop to block until the program stops or ends (for a server, after sending the request that reaches your breakpoint). A stopOnEntry launch is answered at its entry stop", "inputSchema": { "type": "object", "properties": { @@ -598,7 +598,7 @@ }, { "name": "get_stack_trace", - "description": "Get stack trace. The response includes stopReason — why the session is paused. For an implicit frameless stopped thread, a sibling with policy-recognized user frames is preferred and adopted, with the switch disclosed in note. Internal/runtime frames are filtered by default; when any are hidden the response carries hiddenFrames and a note. When the frame the program is paused in is itself internal, pausedFrame names it (kept: whether it was retained as stackFrames[0]) and the note says so", + "description": "Get stack trace. The response includes stopReason — why the session is paused. For an implicit frameless stopped thread, a sibling with policy-recognized user frames is preferred and adopted, with the switch disclosed in note. Internal/runtime frames are filtered by default; when any are hidden the response carries hiddenFrames and a note. When the frame the program is paused in is itself internal, pausedFrame names it (kept: whether it was retained as stackFrames[0]) and the note says so. The note also says when the top frame's file is newer than the compiled file the debugger binds it through (a stale build)", "inputSchema": { "type": "object", "properties": { diff --git a/tests/core/unit/server/__snapshots__/tool-list/bp-content.va-unset.container-true.json b/tests/core/unit/server/__snapshots__/tool-list/bp-content.va-unset.container-true.json index 3c515bbc..9358f57a 100644 --- a/tests/core/unit/server/__snapshots__/tool-list/bp-content.va-unset.container-true.json +++ b/tests/core/unit/server/__snapshots__/tool-list/bp-content.va-unset.container-true.json @@ -223,7 +223,7 @@ }, { "name": "start_debugging", - "description": "Start debugging a script. Answers when the launched program stops, when it ends, or after a short hold — whichever comes first, and the same way in every language: \"paused\" with a reason (a breakpoint reached as the program starts is answered here), \"stopped\" with the exit code, or \"running\" with pending:true when the program has not stopped yet; the message says what is armed. The hold is not a limit on the program — nothing is cancelled when it passes and breakpoints stay armed: call wait_for_stop to block until the program stops or ends (for a server, after sending the request that reaches your breakpoint). A stopOnEntry launch is answered at its entry stop", + "description": "Start debugging a script. Answers when the launched program stops, when it ends, or after a short hold — whichever comes first, and the same way in every language: \"paused\" with a reason and, as wait_for_stop answers a stop, the location, its source context and a note (a breakpoint reached as the program starts is answered here), \"stopped\" with the exit code, or \"running\" with pending:true when the program has not stopped yet; the message says what is armed. The hold is not a limit on the program — nothing is cancelled when it passes and breakpoints stay armed: call wait_for_stop to block until the program stops or ends (for a server, after sending the request that reaches your breakpoint). A stopOnEntry launch is answered at its entry stop", "inputSchema": { "type": "object", "properties": { @@ -598,7 +598,7 @@ }, { "name": "get_stack_trace", - "description": "Get stack trace. The response includes stopReason — why the session is paused. For an implicit frameless stopped thread, a sibling with policy-recognized user frames is preferred and adopted, with the switch disclosed in note. Internal/runtime frames are filtered by default; when any are hidden the response carries hiddenFrames and a note. When the frame the program is paused in is itself internal, pausedFrame names it (kept: whether it was retained as stackFrames[0]) and the note says so", + "description": "Get stack trace. The response includes stopReason — why the session is paused. For an implicit frameless stopped thread, a sibling with policy-recognized user frames is preferred and adopted, with the switch disclosed in note. Internal/runtime frames are filtered by default; when any are hidden the response carries hiddenFrames and a note. When the frame the program is paused in is itself internal, pausedFrame names it (kept: whether it was retained as stackFrames[0]) and the note says so. The note also says when the top frame's file is newer than the compiled file the debugger binds it through (a stale build)", "inputSchema": { "type": "object", "properties": { diff --git a/tests/core/unit/server/__snapshots__/tool-list/bp-content.va-unset.container-unset.json b/tests/core/unit/server/__snapshots__/tool-list/bp-content.va-unset.container-unset.json index 1463d200..b6114fc7 100644 --- a/tests/core/unit/server/__snapshots__/tool-list/bp-content.va-unset.container-unset.json +++ b/tests/core/unit/server/__snapshots__/tool-list/bp-content.va-unset.container-unset.json @@ -223,7 +223,7 @@ }, { "name": "start_debugging", - "description": "Start debugging a script. Answers when the launched program stops, when it ends, or after a short hold — whichever comes first, and the same way in every language: \"paused\" with a reason (a breakpoint reached as the program starts is answered here), \"stopped\" with the exit code, or \"running\" with pending:true when the program has not stopped yet; the message says what is armed. The hold is not a limit on the program — nothing is cancelled when it passes and breakpoints stay armed: call wait_for_stop to block until the program stops or ends (for a server, after sending the request that reaches your breakpoint). A stopOnEntry launch is answered at its entry stop", + "description": "Start debugging a script. Answers when the launched program stops, when it ends, or after a short hold — whichever comes first, and the same way in every language: \"paused\" with a reason and, as wait_for_stop answers a stop, the location, its source context and a note (a breakpoint reached as the program starts is answered here), \"stopped\" with the exit code, or \"running\" with pending:true when the program has not stopped yet; the message says what is armed. The hold is not a limit on the program — nothing is cancelled when it passes and breakpoints stay armed: call wait_for_stop to block until the program stops or ends (for a server, after sending the request that reaches your breakpoint). A stopOnEntry launch is answered at its entry stop", "inputSchema": { "type": "object", "properties": { @@ -598,7 +598,7 @@ }, { "name": "get_stack_trace", - "description": "Get stack trace. The response includes stopReason — why the session is paused. For an implicit frameless stopped thread, a sibling with policy-recognized user frames is preferred and adopted, with the switch disclosed in note. Internal/runtime frames are filtered by default; when any are hidden the response carries hiddenFrames and a note. When the frame the program is paused in is itself internal, pausedFrame names it (kept: whether it was retained as stackFrames[0]) and the note says so", + "description": "Get stack trace. The response includes stopReason — why the session is paused. For an implicit frameless stopped thread, a sibling with policy-recognized user frames is preferred and adopted, with the switch disclosed in note. Internal/runtime frames are filtered by default; when any are hidden the response carries hiddenFrames and a note. When the frame the program is paused in is itself internal, pausedFrame names it (kept: whether it was retained as stackFrames[0]) and the note says so. The note also says when the top frame's file is newer than the compiled file the debugger binds it through (a stale build)", "inputSchema": { "type": "object", "properties": { diff --git a/tests/core/unit/server/__snapshots__/tool-list/bp-line.va-explicit.container-true.json b/tests/core/unit/server/__snapshots__/tool-list/bp-line.va-explicit.container-true.json index b5b51884..055c581d 100644 --- a/tests/core/unit/server/__snapshots__/tool-list/bp-line.va-explicit.container-true.json +++ b/tests/core/unit/server/__snapshots__/tool-list/bp-line.va-explicit.container-true.json @@ -209,7 +209,7 @@ }, { "name": "start_debugging", - "description": "Start debugging a script. Answers when the launched program stops, when it ends, or after a short hold — whichever comes first, and the same way in every language: \"paused\" with a reason (a breakpoint reached as the program starts is answered here), \"stopped\" with the exit code, or \"running\" with pending:true when the program has not stopped yet; the message says what is armed. The hold is not a limit on the program — nothing is cancelled when it passes and breakpoints stay armed: call wait_for_stop to block until the program stops or ends (for a server, after sending the request that reaches your breakpoint). A stopOnEntry launch is answered at its entry stop", + "description": "Start debugging a script. Answers when the launched program stops, when it ends, or after a short hold — whichever comes first, and the same way in every language: \"paused\" with a reason and, as wait_for_stop answers a stop, the location, its source context and a note (a breakpoint reached as the program starts is answered here), \"stopped\" with the exit code, or \"running\" with pending:true when the program has not stopped yet; the message says what is armed. The hold is not a limit on the program — nothing is cancelled when it passes and breakpoints stay armed: call wait_for_stop to block until the program stops or ends (for a server, after sending the request that reaches your breakpoint). A stopOnEntry launch is answered at its entry stop", "inputSchema": { "type": "object", "properties": { @@ -586,7 +586,7 @@ }, { "name": "get_stack_trace", - "description": "Get stack trace. The response includes stopReason — why the session is paused. For an implicit frameless stopped thread, a sibling with policy-recognized user frames is preferred and adopted, with the switch disclosed in note. Internal/runtime frames are filtered by default; when any are hidden the response carries hiddenFrames and a note. When the frame the program is paused in is itself internal, pausedFrame names it (kept: whether it was retained as stackFrames[0]) and the note says so", + "description": "Get stack trace. The response includes stopReason — why the session is paused. For an implicit frameless stopped thread, a sibling with policy-recognized user frames is preferred and adopted, with the switch disclosed in note. Internal/runtime frames are filtered by default; when any are hidden the response carries hiddenFrames and a note. When the frame the program is paused in is itself internal, pausedFrame names it (kept: whether it was retained as stackFrames[0]) and the note says so. The note also says when the top frame's file is newer than the compiled file the debugger binds it through (a stale build)", "inputSchema": { "type": "object", "properties": { diff --git a/tests/core/unit/server/__snapshots__/tool-list/bp-line.va-explicit.container-unset.json b/tests/core/unit/server/__snapshots__/tool-list/bp-line.va-explicit.container-unset.json index e3058194..8fe23695 100644 --- a/tests/core/unit/server/__snapshots__/tool-list/bp-line.va-explicit.container-unset.json +++ b/tests/core/unit/server/__snapshots__/tool-list/bp-line.va-explicit.container-unset.json @@ -209,7 +209,7 @@ }, { "name": "start_debugging", - "description": "Start debugging a script. Answers when the launched program stops, when it ends, or after a short hold — whichever comes first, and the same way in every language: \"paused\" with a reason (a breakpoint reached as the program starts is answered here), \"stopped\" with the exit code, or \"running\" with pending:true when the program has not stopped yet; the message says what is armed. The hold is not a limit on the program — nothing is cancelled when it passes and breakpoints stay armed: call wait_for_stop to block until the program stops or ends (for a server, after sending the request that reaches your breakpoint). A stopOnEntry launch is answered at its entry stop", + "description": "Start debugging a script. Answers when the launched program stops, when it ends, or after a short hold — whichever comes first, and the same way in every language: \"paused\" with a reason and, as wait_for_stop answers a stop, the location, its source context and a note (a breakpoint reached as the program starts is answered here), \"stopped\" with the exit code, or \"running\" with pending:true when the program has not stopped yet; the message says what is armed. The hold is not a limit on the program — nothing is cancelled when it passes and breakpoints stay armed: call wait_for_stop to block until the program stops or ends (for a server, after sending the request that reaches your breakpoint). A stopOnEntry launch is answered at its entry stop", "inputSchema": { "type": "object", "properties": { @@ -586,7 +586,7 @@ }, { "name": "get_stack_trace", - "description": "Get stack trace. The response includes stopReason — why the session is paused. For an implicit frameless stopped thread, a sibling with policy-recognized user frames is preferred and adopted, with the switch disclosed in note. Internal/runtime frames are filtered by default; when any are hidden the response carries hiddenFrames and a note. When the frame the program is paused in is itself internal, pausedFrame names it (kept: whether it was retained as stackFrames[0]) and the note says so", + "description": "Get stack trace. The response includes stopReason — why the session is paused. For an implicit frameless stopped thread, a sibling with policy-recognized user frames is preferred and adopted, with the switch disclosed in note. Internal/runtime frames are filtered by default; when any are hidden the response carries hiddenFrames and a note. When the frame the program is paused in is itself internal, pausedFrame names it (kept: whether it was retained as stackFrames[0]) and the note says so. The note also says when the top frame's file is newer than the compiled file the debugger binds it through (a stale build)", "inputSchema": { "type": "object", "properties": { diff --git a/tests/core/unit/server/__snapshots__/tool-list/bp-line.va-open.container-true.json b/tests/core/unit/server/__snapshots__/tool-list/bp-line.va-open.container-true.json index 348b13d2..eaf48e18 100644 --- a/tests/core/unit/server/__snapshots__/tool-list/bp-line.va-open.container-true.json +++ b/tests/core/unit/server/__snapshots__/tool-list/bp-line.va-open.container-true.json @@ -209,7 +209,7 @@ }, { "name": "start_debugging", - "description": "Start debugging a script. Answers when the launched program stops, when it ends, or after a short hold — whichever comes first, and the same way in every language: \"paused\" with a reason (a breakpoint reached as the program starts is answered here), \"stopped\" with the exit code, or \"running\" with pending:true when the program has not stopped yet; the message says what is armed. The hold is not a limit on the program — nothing is cancelled when it passes and breakpoints stay armed: call wait_for_stop to block until the program stops or ends (for a server, after sending the request that reaches your breakpoint). A stopOnEntry launch is answered at its entry stop", + "description": "Start debugging a script. Answers when the launched program stops, when it ends, or after a short hold — whichever comes first, and the same way in every language: \"paused\" with a reason and, as wait_for_stop answers a stop, the location, its source context and a note (a breakpoint reached as the program starts is answered here), \"stopped\" with the exit code, or \"running\" with pending:true when the program has not stopped yet; the message says what is armed. The hold is not a limit on the program — nothing is cancelled when it passes and breakpoints stay armed: call wait_for_stop to block until the program stops or ends (for a server, after sending the request that reaches your breakpoint). A stopOnEntry launch is answered at its entry stop", "inputSchema": { "type": "object", "properties": { @@ -584,7 +584,7 @@ }, { "name": "get_stack_trace", - "description": "Get stack trace. The response includes stopReason — why the session is paused. For an implicit frameless stopped thread, a sibling with policy-recognized user frames is preferred and adopted, with the switch disclosed in note. Internal/runtime frames are filtered by default; when any are hidden the response carries hiddenFrames and a note. When the frame the program is paused in is itself internal, pausedFrame names it (kept: whether it was retained as stackFrames[0]) and the note says so", + "description": "Get stack trace. The response includes stopReason — why the session is paused. For an implicit frameless stopped thread, a sibling with policy-recognized user frames is preferred and adopted, with the switch disclosed in note. Internal/runtime frames are filtered by default; when any are hidden the response carries hiddenFrames and a note. When the frame the program is paused in is itself internal, pausedFrame names it (kept: whether it was retained as stackFrames[0]) and the note says so. The note also says when the top frame's file is newer than the compiled file the debugger binds it through (a stale build)", "inputSchema": { "type": "object", "properties": { diff --git a/tests/core/unit/server/__snapshots__/tool-list/bp-line.va-open.container-unset.json b/tests/core/unit/server/__snapshots__/tool-list/bp-line.va-open.container-unset.json index 3c106235..c14d425a 100644 --- a/tests/core/unit/server/__snapshots__/tool-list/bp-line.va-open.container-unset.json +++ b/tests/core/unit/server/__snapshots__/tool-list/bp-line.va-open.container-unset.json @@ -209,7 +209,7 @@ }, { "name": "start_debugging", - "description": "Start debugging a script. Answers when the launched program stops, when it ends, or after a short hold — whichever comes first, and the same way in every language: \"paused\" with a reason (a breakpoint reached as the program starts is answered here), \"stopped\" with the exit code, or \"running\" with pending:true when the program has not stopped yet; the message says what is armed. The hold is not a limit on the program — nothing is cancelled when it passes and breakpoints stay armed: call wait_for_stop to block until the program stops or ends (for a server, after sending the request that reaches your breakpoint). A stopOnEntry launch is answered at its entry stop", + "description": "Start debugging a script. Answers when the launched program stops, when it ends, or after a short hold — whichever comes first, and the same way in every language: \"paused\" with a reason and, as wait_for_stop answers a stop, the location, its source context and a note (a breakpoint reached as the program starts is answered here), \"stopped\" with the exit code, or \"running\" with pending:true when the program has not stopped yet; the message says what is armed. The hold is not a limit on the program — nothing is cancelled when it passes and breakpoints stay armed: call wait_for_stop to block until the program stops or ends (for a server, after sending the request that reaches your breakpoint). A stopOnEntry launch is answered at its entry stop", "inputSchema": { "type": "object", "properties": { @@ -584,7 +584,7 @@ }, { "name": "get_stack_trace", - "description": "Get stack trace. The response includes stopReason — why the session is paused. For an implicit frameless stopped thread, a sibling with policy-recognized user frames is preferred and adopted, with the switch disclosed in note. Internal/runtime frames are filtered by default; when any are hidden the response carries hiddenFrames and a note. When the frame the program is paused in is itself internal, pausedFrame names it (kept: whether it was retained as stackFrames[0]) and the note says so", + "description": "Get stack trace. The response includes stopReason — why the session is paused. For an implicit frameless stopped thread, a sibling with policy-recognized user frames is preferred and adopted, with the switch disclosed in note. Internal/runtime frames are filtered by default; when any are hidden the response carries hiddenFrames and a note. When the frame the program is paused in is itself internal, pausedFrame names it (kept: whether it was retained as stackFrames[0]) and the note says so. The note also says when the top frame's file is newer than the compiled file the debugger binds it through (a stale build)", "inputSchema": { "type": "object", "properties": { diff --git a/tests/core/unit/server/__snapshots__/tool-list/bp-line.va-unset.container-true.json b/tests/core/unit/server/__snapshots__/tool-list/bp-line.va-unset.container-true.json index 348b13d2..eaf48e18 100644 --- a/tests/core/unit/server/__snapshots__/tool-list/bp-line.va-unset.container-true.json +++ b/tests/core/unit/server/__snapshots__/tool-list/bp-line.va-unset.container-true.json @@ -209,7 +209,7 @@ }, { "name": "start_debugging", - "description": "Start debugging a script. Answers when the launched program stops, when it ends, or after a short hold — whichever comes first, and the same way in every language: \"paused\" with a reason (a breakpoint reached as the program starts is answered here), \"stopped\" with the exit code, or \"running\" with pending:true when the program has not stopped yet; the message says what is armed. The hold is not a limit on the program — nothing is cancelled when it passes and breakpoints stay armed: call wait_for_stop to block until the program stops or ends (for a server, after sending the request that reaches your breakpoint). A stopOnEntry launch is answered at its entry stop", + "description": "Start debugging a script. Answers when the launched program stops, when it ends, or after a short hold — whichever comes first, and the same way in every language: \"paused\" with a reason and, as wait_for_stop answers a stop, the location, its source context and a note (a breakpoint reached as the program starts is answered here), \"stopped\" with the exit code, or \"running\" with pending:true when the program has not stopped yet; the message says what is armed. The hold is not a limit on the program — nothing is cancelled when it passes and breakpoints stay armed: call wait_for_stop to block until the program stops or ends (for a server, after sending the request that reaches your breakpoint). A stopOnEntry launch is answered at its entry stop", "inputSchema": { "type": "object", "properties": { @@ -584,7 +584,7 @@ }, { "name": "get_stack_trace", - "description": "Get stack trace. The response includes stopReason — why the session is paused. For an implicit frameless stopped thread, a sibling with policy-recognized user frames is preferred and adopted, with the switch disclosed in note. Internal/runtime frames are filtered by default; when any are hidden the response carries hiddenFrames and a note. When the frame the program is paused in is itself internal, pausedFrame names it (kept: whether it was retained as stackFrames[0]) and the note says so", + "description": "Get stack trace. The response includes stopReason — why the session is paused. For an implicit frameless stopped thread, a sibling with policy-recognized user frames is preferred and adopted, with the switch disclosed in note. Internal/runtime frames are filtered by default; when any are hidden the response carries hiddenFrames and a note. When the frame the program is paused in is itself internal, pausedFrame names it (kept: whether it was retained as stackFrames[0]) and the note says so. The note also says when the top frame's file is newer than the compiled file the debugger binds it through (a stale build)", "inputSchema": { "type": "object", "properties": { diff --git a/tests/core/unit/server/__snapshots__/tool-list/bp-line.va-unset.container-unset.json b/tests/core/unit/server/__snapshots__/tool-list/bp-line.va-unset.container-unset.json index 3c106235..c14d425a 100644 --- a/tests/core/unit/server/__snapshots__/tool-list/bp-line.va-unset.container-unset.json +++ b/tests/core/unit/server/__snapshots__/tool-list/bp-line.va-unset.container-unset.json @@ -209,7 +209,7 @@ }, { "name": "start_debugging", - "description": "Start debugging a script. Answers when the launched program stops, when it ends, or after a short hold — whichever comes first, and the same way in every language: \"paused\" with a reason (a breakpoint reached as the program starts is answered here), \"stopped\" with the exit code, or \"running\" with pending:true when the program has not stopped yet; the message says what is armed. The hold is not a limit on the program — nothing is cancelled when it passes and breakpoints stay armed: call wait_for_stop to block until the program stops or ends (for a server, after sending the request that reaches your breakpoint). A stopOnEntry launch is answered at its entry stop", + "description": "Start debugging a script. Answers when the launched program stops, when it ends, or after a short hold — whichever comes first, and the same way in every language: \"paused\" with a reason and, as wait_for_stop answers a stop, the location, its source context and a note (a breakpoint reached as the program starts is answered here), \"stopped\" with the exit code, or \"running\" with pending:true when the program has not stopped yet; the message says what is armed. The hold is not a limit on the program — nothing is cancelled when it passes and breakpoints stay armed: call wait_for_stop to block until the program stops or ends (for a server, after sending the request that reaches your breakpoint). A stopOnEntry launch is answered at its entry stop", "inputSchema": { "type": "object", "properties": { @@ -584,7 +584,7 @@ }, { "name": "get_stack_trace", - "description": "Get stack trace. The response includes stopReason — why the session is paused. For an implicit frameless stopped thread, a sibling with policy-recognized user frames is preferred and adopted, with the switch disclosed in note. Internal/runtime frames are filtered by default; when any are hidden the response carries hiddenFrames and a note. When the frame the program is paused in is itself internal, pausedFrame names it (kept: whether it was retained as stackFrames[0]) and the note says so", + "description": "Get stack trace. The response includes stopReason — why the session is paused. For an implicit frameless stopped thread, a sibling with policy-recognized user frames is preferred and adopted, with the switch disclosed in note. Internal/runtime frames are filtered by default; when any are hidden the response carries hiddenFrames and a note. When the frame the program is paused in is itself internal, pausedFrame names it (kept: whether it was retained as stackFrames[0]) and the note says so. The note also says when the top frame's file is newer than the compiled file the debugger binds it through (a stale build)", "inputSchema": { "type": "object", "properties": { diff --git a/tests/core/unit/server/__snapshots__/tool-list/bp-unset.va-explicit.container-true.json b/tests/core/unit/server/__snapshots__/tool-list/bp-unset.va-explicit.container-true.json index 2f0150a2..a9404fe7 100644 --- a/tests/core/unit/server/__snapshots__/tool-list/bp-unset.va-explicit.container-true.json +++ b/tests/core/unit/server/__snapshots__/tool-list/bp-unset.va-explicit.container-true.json @@ -223,7 +223,7 @@ }, { "name": "start_debugging", - "description": "Start debugging a script. Answers when the launched program stops, when it ends, or after a short hold — whichever comes first, and the same way in every language: \"paused\" with a reason (a breakpoint reached as the program starts is answered here), \"stopped\" with the exit code, or \"running\" with pending:true when the program has not stopped yet; the message says what is armed. The hold is not a limit on the program — nothing is cancelled when it passes and breakpoints stay armed: call wait_for_stop to block until the program stops or ends (for a server, after sending the request that reaches your breakpoint). A stopOnEntry launch is answered at its entry stop", + "description": "Start debugging a script. Answers when the launched program stops, when it ends, or after a short hold — whichever comes first, and the same way in every language: \"paused\" with a reason and, as wait_for_stop answers a stop, the location, its source context and a note (a breakpoint reached as the program starts is answered here), \"stopped\" with the exit code, or \"running\" with pending:true when the program has not stopped yet; the message says what is armed. The hold is not a limit on the program — nothing is cancelled when it passes and breakpoints stay armed: call wait_for_stop to block until the program stops or ends (for a server, after sending the request that reaches your breakpoint). A stopOnEntry launch is answered at its entry stop", "inputSchema": { "type": "object", "properties": { @@ -600,7 +600,7 @@ }, { "name": "get_stack_trace", - "description": "Get stack trace. The response includes stopReason — why the session is paused. For an implicit frameless stopped thread, a sibling with policy-recognized user frames is preferred and adopted, with the switch disclosed in note. Internal/runtime frames are filtered by default; when any are hidden the response carries hiddenFrames and a note. When the frame the program is paused in is itself internal, pausedFrame names it (kept: whether it was retained as stackFrames[0]) and the note says so", + "description": "Get stack trace. The response includes stopReason — why the session is paused. For an implicit frameless stopped thread, a sibling with policy-recognized user frames is preferred and adopted, with the switch disclosed in note. Internal/runtime frames are filtered by default; when any are hidden the response carries hiddenFrames and a note. When the frame the program is paused in is itself internal, pausedFrame names it (kept: whether it was retained as stackFrames[0]) and the note says so. The note also says when the top frame's file is newer than the compiled file the debugger binds it through (a stale build)", "inputSchema": { "type": "object", "properties": { diff --git a/tests/core/unit/server/__snapshots__/tool-list/bp-unset.va-explicit.container-unset.json b/tests/core/unit/server/__snapshots__/tool-list/bp-unset.va-explicit.container-unset.json index 1c25827b..e14f252b 100644 --- a/tests/core/unit/server/__snapshots__/tool-list/bp-unset.va-explicit.container-unset.json +++ b/tests/core/unit/server/__snapshots__/tool-list/bp-unset.va-explicit.container-unset.json @@ -223,7 +223,7 @@ }, { "name": "start_debugging", - "description": "Start debugging a script. Answers when the launched program stops, when it ends, or after a short hold — whichever comes first, and the same way in every language: \"paused\" with a reason (a breakpoint reached as the program starts is answered here), \"stopped\" with the exit code, or \"running\" with pending:true when the program has not stopped yet; the message says what is armed. The hold is not a limit on the program — nothing is cancelled when it passes and breakpoints stay armed: call wait_for_stop to block until the program stops or ends (for a server, after sending the request that reaches your breakpoint). A stopOnEntry launch is answered at its entry stop", + "description": "Start debugging a script. Answers when the launched program stops, when it ends, or after a short hold — whichever comes first, and the same way in every language: \"paused\" with a reason and, as wait_for_stop answers a stop, the location, its source context and a note (a breakpoint reached as the program starts is answered here), \"stopped\" with the exit code, or \"running\" with pending:true when the program has not stopped yet; the message says what is armed. The hold is not a limit on the program — nothing is cancelled when it passes and breakpoints stay armed: call wait_for_stop to block until the program stops or ends (for a server, after sending the request that reaches your breakpoint). A stopOnEntry launch is answered at its entry stop", "inputSchema": { "type": "object", "properties": { @@ -600,7 +600,7 @@ }, { "name": "get_stack_trace", - "description": "Get stack trace. The response includes stopReason — why the session is paused. For an implicit frameless stopped thread, a sibling with policy-recognized user frames is preferred and adopted, with the switch disclosed in note. Internal/runtime frames are filtered by default; when any are hidden the response carries hiddenFrames and a note. When the frame the program is paused in is itself internal, pausedFrame names it (kept: whether it was retained as stackFrames[0]) and the note says so", + "description": "Get stack trace. The response includes stopReason — why the session is paused. For an implicit frameless stopped thread, a sibling with policy-recognized user frames is preferred and adopted, with the switch disclosed in note. Internal/runtime frames are filtered by default; when any are hidden the response carries hiddenFrames and a note. When the frame the program is paused in is itself internal, pausedFrame names it (kept: whether it was retained as stackFrames[0]) and the note says so. The note also says when the top frame's file is newer than the compiled file the debugger binds it through (a stale build)", "inputSchema": { "type": "object", "properties": { diff --git a/tests/core/unit/server/__snapshots__/tool-list/bp-unset.va-open.container-true.json b/tests/core/unit/server/__snapshots__/tool-list/bp-unset.va-open.container-true.json index 3c515bbc..9358f57a 100644 --- a/tests/core/unit/server/__snapshots__/tool-list/bp-unset.va-open.container-true.json +++ b/tests/core/unit/server/__snapshots__/tool-list/bp-unset.va-open.container-true.json @@ -223,7 +223,7 @@ }, { "name": "start_debugging", - "description": "Start debugging a script. Answers when the launched program stops, when it ends, or after a short hold — whichever comes first, and the same way in every language: \"paused\" with a reason (a breakpoint reached as the program starts is answered here), \"stopped\" with the exit code, or \"running\" with pending:true when the program has not stopped yet; the message says what is armed. The hold is not a limit on the program — nothing is cancelled when it passes and breakpoints stay armed: call wait_for_stop to block until the program stops or ends (for a server, after sending the request that reaches your breakpoint). A stopOnEntry launch is answered at its entry stop", + "description": "Start debugging a script. Answers when the launched program stops, when it ends, or after a short hold — whichever comes first, and the same way in every language: \"paused\" with a reason and, as wait_for_stop answers a stop, the location, its source context and a note (a breakpoint reached as the program starts is answered here), \"stopped\" with the exit code, or \"running\" with pending:true when the program has not stopped yet; the message says what is armed. The hold is not a limit on the program — nothing is cancelled when it passes and breakpoints stay armed: call wait_for_stop to block until the program stops or ends (for a server, after sending the request that reaches your breakpoint). A stopOnEntry launch is answered at its entry stop", "inputSchema": { "type": "object", "properties": { @@ -598,7 +598,7 @@ }, { "name": "get_stack_trace", - "description": "Get stack trace. The response includes stopReason — why the session is paused. For an implicit frameless stopped thread, a sibling with policy-recognized user frames is preferred and adopted, with the switch disclosed in note. Internal/runtime frames are filtered by default; when any are hidden the response carries hiddenFrames and a note. When the frame the program is paused in is itself internal, pausedFrame names it (kept: whether it was retained as stackFrames[0]) and the note says so", + "description": "Get stack trace. The response includes stopReason — why the session is paused. For an implicit frameless stopped thread, a sibling with policy-recognized user frames is preferred and adopted, with the switch disclosed in note. Internal/runtime frames are filtered by default; when any are hidden the response carries hiddenFrames and a note. When the frame the program is paused in is itself internal, pausedFrame names it (kept: whether it was retained as stackFrames[0]) and the note says so. The note also says when the top frame's file is newer than the compiled file the debugger binds it through (a stale build)", "inputSchema": { "type": "object", "properties": { diff --git a/tests/core/unit/server/__snapshots__/tool-list/bp-unset.va-open.container-unset.json b/tests/core/unit/server/__snapshots__/tool-list/bp-unset.va-open.container-unset.json index 1463d200..b6114fc7 100644 --- a/tests/core/unit/server/__snapshots__/tool-list/bp-unset.va-open.container-unset.json +++ b/tests/core/unit/server/__snapshots__/tool-list/bp-unset.va-open.container-unset.json @@ -223,7 +223,7 @@ }, { "name": "start_debugging", - "description": "Start debugging a script. Answers when the launched program stops, when it ends, or after a short hold — whichever comes first, and the same way in every language: \"paused\" with a reason (a breakpoint reached as the program starts is answered here), \"stopped\" with the exit code, or \"running\" with pending:true when the program has not stopped yet; the message says what is armed. The hold is not a limit on the program — nothing is cancelled when it passes and breakpoints stay armed: call wait_for_stop to block until the program stops or ends (for a server, after sending the request that reaches your breakpoint). A stopOnEntry launch is answered at its entry stop", + "description": "Start debugging a script. Answers when the launched program stops, when it ends, or after a short hold — whichever comes first, and the same way in every language: \"paused\" with a reason and, as wait_for_stop answers a stop, the location, its source context and a note (a breakpoint reached as the program starts is answered here), \"stopped\" with the exit code, or \"running\" with pending:true when the program has not stopped yet; the message says what is armed. The hold is not a limit on the program — nothing is cancelled when it passes and breakpoints stay armed: call wait_for_stop to block until the program stops or ends (for a server, after sending the request that reaches your breakpoint). A stopOnEntry launch is answered at its entry stop", "inputSchema": { "type": "object", "properties": { @@ -598,7 +598,7 @@ }, { "name": "get_stack_trace", - "description": "Get stack trace. The response includes stopReason — why the session is paused. For an implicit frameless stopped thread, a sibling with policy-recognized user frames is preferred and adopted, with the switch disclosed in note. Internal/runtime frames are filtered by default; when any are hidden the response carries hiddenFrames and a note. When the frame the program is paused in is itself internal, pausedFrame names it (kept: whether it was retained as stackFrames[0]) and the note says so", + "description": "Get stack trace. The response includes stopReason — why the session is paused. For an implicit frameless stopped thread, a sibling with policy-recognized user frames is preferred and adopted, with the switch disclosed in note. Internal/runtime frames are filtered by default; when any are hidden the response carries hiddenFrames and a note. When the frame the program is paused in is itself internal, pausedFrame names it (kept: whether it was retained as stackFrames[0]) and the note says so. The note also says when the top frame's file is newer than the compiled file the debugger binds it through (a stale build)", "inputSchema": { "type": "object", "properties": { diff --git a/tests/core/unit/server/__snapshots__/tool-list/bp-unset.va-unset.container-true.json b/tests/core/unit/server/__snapshots__/tool-list/bp-unset.va-unset.container-true.json index 3c515bbc..9358f57a 100644 --- a/tests/core/unit/server/__snapshots__/tool-list/bp-unset.va-unset.container-true.json +++ b/tests/core/unit/server/__snapshots__/tool-list/bp-unset.va-unset.container-true.json @@ -223,7 +223,7 @@ }, { "name": "start_debugging", - "description": "Start debugging a script. Answers when the launched program stops, when it ends, or after a short hold — whichever comes first, and the same way in every language: \"paused\" with a reason (a breakpoint reached as the program starts is answered here), \"stopped\" with the exit code, or \"running\" with pending:true when the program has not stopped yet; the message says what is armed. The hold is not a limit on the program — nothing is cancelled when it passes and breakpoints stay armed: call wait_for_stop to block until the program stops or ends (for a server, after sending the request that reaches your breakpoint). A stopOnEntry launch is answered at its entry stop", + "description": "Start debugging a script. Answers when the launched program stops, when it ends, or after a short hold — whichever comes first, and the same way in every language: \"paused\" with a reason and, as wait_for_stop answers a stop, the location, its source context and a note (a breakpoint reached as the program starts is answered here), \"stopped\" with the exit code, or \"running\" with pending:true when the program has not stopped yet; the message says what is armed. The hold is not a limit on the program — nothing is cancelled when it passes and breakpoints stay armed: call wait_for_stop to block until the program stops or ends (for a server, after sending the request that reaches your breakpoint). A stopOnEntry launch is answered at its entry stop", "inputSchema": { "type": "object", "properties": { @@ -598,7 +598,7 @@ }, { "name": "get_stack_trace", - "description": "Get stack trace. The response includes stopReason — why the session is paused. For an implicit frameless stopped thread, a sibling with policy-recognized user frames is preferred and adopted, with the switch disclosed in note. Internal/runtime frames are filtered by default; when any are hidden the response carries hiddenFrames and a note. When the frame the program is paused in is itself internal, pausedFrame names it (kept: whether it was retained as stackFrames[0]) and the note says so", + "description": "Get stack trace. The response includes stopReason — why the session is paused. For an implicit frameless stopped thread, a sibling with policy-recognized user frames is preferred and adopted, with the switch disclosed in note. Internal/runtime frames are filtered by default; when any are hidden the response carries hiddenFrames and a note. When the frame the program is paused in is itself internal, pausedFrame names it (kept: whether it was retained as stackFrames[0]) and the note says so. The note also says when the top frame's file is newer than the compiled file the debugger binds it through (a stale build)", "inputSchema": { "type": "object", "properties": { diff --git a/tests/core/unit/server/__snapshots__/tool-list/bp-unset.va-unset.container-unset.json b/tests/core/unit/server/__snapshots__/tool-list/bp-unset.va-unset.container-unset.json index 1463d200..b6114fc7 100644 --- a/tests/core/unit/server/__snapshots__/tool-list/bp-unset.va-unset.container-unset.json +++ b/tests/core/unit/server/__snapshots__/tool-list/bp-unset.va-unset.container-unset.json @@ -223,7 +223,7 @@ }, { "name": "start_debugging", - "description": "Start debugging a script. Answers when the launched program stops, when it ends, or after a short hold — whichever comes first, and the same way in every language: \"paused\" with a reason (a breakpoint reached as the program starts is answered here), \"stopped\" with the exit code, or \"running\" with pending:true when the program has not stopped yet; the message says what is armed. The hold is not a limit on the program — nothing is cancelled when it passes and breakpoints stay armed: call wait_for_stop to block until the program stops or ends (for a server, after sending the request that reaches your breakpoint). A stopOnEntry launch is answered at its entry stop", + "description": "Start debugging a script. Answers when the launched program stops, when it ends, or after a short hold — whichever comes first, and the same way in every language: \"paused\" with a reason and, as wait_for_stop answers a stop, the location, its source context and a note (a breakpoint reached as the program starts is answered here), \"stopped\" with the exit code, or \"running\" with pending:true when the program has not stopped yet; the message says what is armed. The hold is not a limit on the program — nothing is cancelled when it passes and breakpoints stay armed: call wait_for_stop to block until the program stops or ends (for a server, after sending the request that reaches your breakpoint). A stopOnEntry launch is answered at its entry stop", "inputSchema": { "type": "object", "properties": { @@ -598,7 +598,7 @@ }, { "name": "get_stack_trace", - "description": "Get stack trace. The response includes stopReason — why the session is paused. For an implicit frameless stopped thread, a sibling with policy-recognized user frames is preferred and adopted, with the switch disclosed in note. Internal/runtime frames are filtered by default; when any are hidden the response carries hiddenFrames and a note. When the frame the program is paused in is itself internal, pausedFrame names it (kept: whether it was retained as stackFrames[0]) and the note says so", + "description": "Get stack trace. The response includes stopReason — why the session is paused. For an implicit frameless stopped thread, a sibling with policy-recognized user frames is preferred and adopted, with the switch disclosed in note. Internal/runtime frames are filtered by default; when any are hidden the response carries hiddenFrames and a note. When the frame the program is paused in is itself internal, pausedFrame names it (kept: whether it was retained as stackFrames[0]) and the note says so. The note also says when the top frame's file is newer than the compiled file the debugger binds it through (a stale build)", "inputSchema": { "type": "object", "properties": { diff --git a/tests/core/unit/server/handlers/debuggee-tools.test.ts b/tests/core/unit/server/handlers/debuggee-tools.test.ts new file mode 100644 index 00000000..a3a128b8 --- /dev/null +++ b/tests/core/unit/server/handlers/debuggee-tools.test.ts @@ -0,0 +1,115 @@ +/** + * Debuggee lifecycle tool handlers (start_debugging, restart_debugging), + * driven directly against a ToolContext. The landing a launch that stopped + * reports (issue #951) is hoisted and annotated the way the step tools, + * wait_for_stop and pause_execution hoist theirs: `location`, `threadId` and + * `note` beside `state`, the source line as `context`, and the stale-build + * sentence (issues #938/#950) appended to the note. + */ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { DotnetAdapterPolicy, SessionLifecycleState } from '@debugmcp/shared'; +import { startDebuggingTool, restartDebuggingTool } from '../../../../../src/server/handlers/debuggee-tools.js'; +import { createMockToolContext } from '../server-test-helpers.js'; + +// DebugMcpServer builds its dependencies in the constructor; mock the container +// so createMockToolContext() never opens a real logger transport or session dir. +vi.mock('../../../../../src/container/dependencies.js'); +vi.mock('../../../../../src/session/session-manager.js'); + +describe('debuggee lifecycle tool handlers', () => { + let ctx: any; + const location = { file: '/app/Program.cs', line: 8, column: 0 }; + /** What the adapter's transform handed the engine for the current launch (issue #950). */ + const launched = { program: '/app/bin/app.dll', launchConfig: { program: '/app/bin/app.dll' } }; + const evidence = { + sourceFile: '/app/Program.cs', + sourceMtimeMs: 20_000, + generatedFile: '/app/bin/app.dll', + generatedMtimeMs: 10_000, + linkedBy: 'launch' as const + }; + const context = { + lineContent: ' int result = a + b;', + surrounding: [{ line: 8, content: ' int result = a + b;' }] + }; + + type Answer = (data: Record, state?: string) => Promise<{ content: Array<{ text: string }> }>; + const answers: Array<[string, Answer]> = [ + ['start_debugging', (data, state = 'paused') => { + ctx.sessionManager.startDebugging.mockResolvedValue({ success: true, state, data }); + return startDebuggingTool(ctx, { sessionId: 'test-session', scriptPath: '/app/Program.cs' }, 'start_debugging'); + }], + ['restart_debugging', (data, state = 'paused') => { + ctx.sessionManager.restartDebugging.mockResolvedValue({ success: true, state, data }); + return restartDebuggingTool(ctx, { sessionId: 'test-session' }, 'restart_debugging'); + }] + ]; + + beforeEach(() => { + ctx = createMockToolContext(); + ctx.sessionManager.getSession.mockReturnValue({ + id: 'test-session', + sessionLifecycle: SessionLifecycleState.ACTIVE, + transformedLaunch: launched + }); + ctx.sessionManager.getSessionPolicy.mockReturnValue(DotnetAdapterPolicy); + ctx.fileChecker = { checkExists: vi.fn().mockResolvedValue({ exists: true, effectivePath: '/app/Program.cs' }) }; + ctx.lineReader = { getLineContext: vi.fn().mockResolvedValue(context) }; + ctx.buildStaleness = { check: vi.fn().mockResolvedValue(undefined) }; + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + describe('where the launch stopped (issue #951)', () => { + for (const [tool, answer] of answers) { + it(`${tool} hoists location, threadId and note beside state and reads the line, as wait_for_stop does`, async () => { + const data = { + message: 'Debugging started for /app/Program.cs. Current state: paused', + reason: 'breakpoint', + location, + threadId: 7, + note: 'existing' + }; + + const payload = JSON.parse((await answer(data)).content[0].text); + + expect(payload).toMatchObject({ success: true, state: 'paused', location, threadId: 7, note: 'existing', context }); + expect(ctx.lineReader.getLineContext).toHaveBeenCalledWith('/app/Program.cs', 8, { contextLines: 2 }); + // The session layer's own copy stays beneath, as for pause_execution. + expect(payload.data.location).toEqual(location); + expect(payload.data.reason).toBe('breakpoint'); + }); + + it(`${tool} appends the stale-build sentence to its note when the artefact the launch runs is older than the source`, async () => { + ctx.buildStaleness = { check: vi.fn().mockResolvedValue(evidence) }; + + const payload = JSON.parse((await answer({ message: 'Paused', reason: 'breakpoint', location, note: 'existing' })).content[0].text); + + expect(ctx.buildStaleness.check).toHaveBeenCalledWith('/app/Program.cs', DotnetAdapterPolicy, launched); + expect(payload.note).toMatch(/^existing The compiled file .* may be stale/); + }); + + it(`${tool} says the sentence alone when the stop had nothing else to note`, async () => { + ctx.buildStaleness = { check: vi.fn().mockResolvedValue(evidence) }; + + const payload = JSON.parse((await answer({ message: 'Paused', reason: 'breakpoint', location })).content[0].text); + + expect(payload.note).toMatch(/^The compiled file .* may be stale/); + expect(payload.note).toContain('/app/bin/app.dll'); + }); + + it(`${tool} hoists nothing and reads nothing for an answer without a location`, async () => { + const payload = JSON.parse((await answer({ message: 'still running', pending: true }, 'running')).content[0].text); + + expect(payload).toMatchObject({ success: true, state: 'running', pending: true }); + expect(payload).not.toHaveProperty('location'); + expect(payload).not.toHaveProperty('note'); + expect(payload).not.toHaveProperty('context'); + expect(ctx.lineReader.getLineContext).not.toHaveBeenCalled(); + expect(ctx.buildStaleness.check).not.toHaveBeenCalled(); + }); + } + }); +}); diff --git a/tests/core/unit/server/handlers/inspection-tools.test.ts b/tests/core/unit/server/handlers/inspection-tools.test.ts index ca4b6261..edf333d5 100644 --- a/tests/core/unit/server/handlers/inspection-tools.test.ts +++ b/tests/core/unit/server/handlers/inspection-tools.test.ts @@ -6,8 +6,9 @@ */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { McpError, ErrorCode as McpErrorCode } from '@modelcontextprotocol/sdk/types.js'; -import { SessionLifecycleState } from '@debugmcp/shared'; +import { DotnetAdapterPolicy, SessionLifecycleState } from '@debugmcp/shared'; import { + getStackTraceTool, handleEvaluateExpression, handleGetSourceContext, handleGetLocalVariables @@ -32,6 +33,107 @@ describe('inspection tool handlers', () => { vi.clearAllMocks(); }); + describe('get_stack_trace carries the stale-build note for its top frame (issue #951)', () => { + /** What the adapter's transform handed the engine for the current launch (issue #950). */ + const launched = { program: '/app/bin/app.dll', launchConfig: { program: '/app/bin/app.dll' } }; + const evidence = { + sourceFile: '/app/Program.cs', + sourceMtimeMs: 20_000, + generatedFile: '/app/bin/app.dll', + generatedMtimeMs: 10_000, + linkedBy: 'launch' as const + }; + const programFrame = { id: 1, name: 'HelloWorld.Add', file: '/app/Program.cs', line: 8, column: 0 }; + const callerFrame = { id: 2, name: 'HelloWorld.Main', file: '/app/Program.cs', line: 17, column: 0 }; + + function stackOf(frames: Array>, extra: Record = {}) { + ctx.getStackTrace = vi.fn().mockResolvedValue({ + frames, + threadId: 1, + totalFrameCount: frames.length, + hiddenFrameCount: 0, + allFramesInternal: false, + ...extra + }); + } + + async function getStackTrace() { + const result = await getStackTraceTool(ctx, { sessionId: 'test-session' }, 'get_stack_trace'); + return JSON.parse(result.content[0].text); + } + + beforeEach(() => { + ctx.sessionManager.getSession.mockReturnValue({ + id: 'test-session', + sessionLifecycle: SessionLifecycleState.ACTIVE, + state: 'paused', + transformedLaunch: launched + }); + ctx.sessionManager.getSessionPolicy.mockReturnValue(DotnetAdapterPolicy); + ctx.buildStaleness = { check: vi.fn().mockResolvedValue(evidence) }; + }); + + it('appends the sentence when the file of the top frame is newer than the artefact the launch runs', async () => { + stackOf([programFrame, callerFrame]); + + const payload = await getStackTrace(); + + expect(payload.success).toBe(true); + expect(payload.count).toBe(2); + expect(ctx.buildStaleness.check).toHaveBeenCalledTimes(1); + expect(ctx.buildStaleness.check).toHaveBeenCalledWith('/app/Program.cs', DotnetAdapterPolicy, launched); + expect(payload.note).toMatch(/^The compiled file .* may be stale/); + expect(payload.note).toContain('/app/bin/app.dll'); + }); + + it('puts the sentence after the notes the stack itself needed', async () => { + stackOf([programFrame], { hiddenFrameCount: 2, totalFrameCount: 3 }); + + const payload = await getStackTrace(); + + expect(payload.hiddenFrames).toBe(2); + expect(payload.note).toMatch(/^2 internal frame\(s\) hidden — pass includeInternals: true to see them\. The compiled file .* may be stale/); + }); + + it('carries no note when the build is current', async () => { + ctx.buildStaleness = { check: vi.fn().mockResolvedValue(undefined) }; + stackOf([programFrame]); + + const payload = await getStackTrace(); + + expect(ctx.buildStaleness.check).toHaveBeenCalledWith('/app/Program.cs', DotnetAdapterPolicy, launched); + expect(payload).not.toHaveProperty('note'); + }); + + it('does not probe a top frame whose file is a label rather than a path', async () => { + stackOf([{ id: 1, name: 'idle', file: '', line: 0, column: 0 }]); + + const payload = await getStackTrace(); + + expect(ctx.buildStaleness.check).not.toHaveBeenCalled(); + expect(payload.note ?? '').not.toMatch(/may be stale/); + }); + + it('does not probe a top frame with no openable source on this host', async () => { + stackOf([{ ...programFrame, file: '../src/Program.cs', unresolvedSource: true }]); + + const payload = await getStackTrace(); + + expect(ctx.buildStaleness.check).not.toHaveBeenCalled(); + expect(payload.note).toMatch(/unresolvedSource: true/); + expect(payload.note).not.toMatch(/may be stale/); + }); + + it('probes nothing for an empty stack', async () => { + stackOf([]); + + const payload = await getStackTrace(); + + expect(payload.count).toBe(0); + expect(ctx.buildStaleness.check).not.toHaveBeenCalled(); + }); + }); + describe('handleEvaluateExpression', () => { it('should handle expression evaluation in terminated session', async () => { ctx.sessionManager.getSession.mockReturnValue({ diff --git a/tests/core/unit/session/debug-result-data.test.ts b/tests/core/unit/session/debug-result-data.test.ts index 65ef7916..4c86d8c1 100644 --- a/tests/core/unit/session/debug-result-data.test.ts +++ b/tests/core/unit/session/debug-result-data.test.ts @@ -1,5 +1,5 @@ import { describe, expectTypeOf, it } from 'vitest'; -import type { DebugResultData } from '../../../../src/session/session-manager-core.js'; +import type { DebugResultData, LaunchResultData } from '../../../../src/session/session-manager-core.js'; describe('DebugResultData type contract (issue #590)', () => { it('stays closed over the result keys written by the session layer', () => { @@ -24,4 +24,8 @@ describe('DebugResultData type contract (issue #590)', () => { | 'resumedBeforeDetach' >(); }); + + it('the launch answer adds only the landing fields the stop answers share (issue #951)', () => { + expectTypeOf().toEqualTypeOf(); + }); }); diff --git a/tests/core/unit/session/session-manager-launch-contract.test.ts b/tests/core/unit/session/session-manager-launch-contract.test.ts index 402fa8a5..5fe0e442 100644 --- a/tests/core/unit/session/session-manager-launch-contract.test.ts +++ b/tests/core/unit/session/session-manager-launch-contract.test.ts @@ -13,7 +13,7 @@ import { SessionManager, SessionManagerConfig } from '../../../../src/session/se import { DebugLanguage, SessionState } from '@debugmcp/shared'; import { createMockDependencies, setMockProxyRunning } from './session-manager-test-utils.js'; import { MockProxyManager } from '../../../test-utils/mocks/mock-proxy-manager.js'; -import type { DebugResult } from '../../../../src/session/session-manager-core.js'; +import type { DebugResult, LaunchResultData } from '../../../../src/session/session-manager-core.js'; /** The hold these tests reason about; asserted below so a retune has to look here. */ const HOLD_MS = 1_000; @@ -47,7 +47,7 @@ describe('SessionManager - launch contract (issues #823, #826, #851)', () => { /** Start a launch without awaiting it, and expose whether it has answered. */ function launch(sessionId: string, dapLaunchArgs: Record = {}, script = 'test.py') { - let result: DebugResult | undefined; + let result: DebugResult | undefined; const promise = sessionManager .startDebugging(sessionId, script, [], { stopOnEntry: false, ...dapLaunchArgs }) .then((settled) => { result = settled; return settled; }); @@ -360,6 +360,89 @@ describe('SessionManager - launch contract (issues #823, #826, #851)', () => { }); }); + describe('where the launch stopped (issue #951)', () => { + const stackTraceCalls = () => proxy().dapRequestCalls.filter((call) => call.command === 'stackTrace'); + + it('answers a breakpoint stop with the location of the first visible frame, the way wait_for_stop does', async () => { + const sessionId = await createSession(); + await sessionManager.setBreakpoint(sessionId, { file: 'test.py', line: 5 }); + const started = launch(sessionId); + await vi.advanceTimersByTimeAsync(300); + proxy().simulateStopped(1, 'breakpoint'); + await vi.advanceTimersByTimeAsync(50); + + const result = started.result(); + expect(result).toMatchObject({ success: true, state: SessionState.PAUSED }); + expect(result?.data?.reason).toBe('breakpoint'); + // The mock's stack: one frame at test.py:10. + expect(result?.data?.location).toEqual({ file: 'test.py', line: 10, column: 0 }); + // Anchored on the reported thread with nothing to explain: no threadId, no note. + expect(result?.data?.threadId).toBeUndefined(); + expect(result?.data?.note).toBeUndefined(); + // The message is unchanged; the structured location is the addition. + expect(result?.data?.message).toBe('Debugging started for test.py. Current state: paused'); + expect(stackTraceCalls().length).toBeGreaterThan(0); + }); + + it('answers a requested entry stop with its location', async () => { + const sessionId = await createSession(); + const started = launch(sessionId, { stopOnEntry: true }); + await vi.advanceTimersByTimeAsync(50); + + const result = started.result(); + expect(result).toMatchObject({ success: true, state: SessionState.PAUSED }); + expect(result?.data?.reason).toBe('entry'); + expect(result?.data?.location).toEqual({ file: 'test.py', line: 10, column: 0 }); + }); + + it('reads no stack for a launch answered while the program still runs', async () => { + const sessionId = await createSession(); + await sessionManager.setBreakpoint(sessionId, { file: 'test.py', line: 5 }); + const started = launch(sessionId); + await vi.advanceTimersByTimeAsync(HOLD_MS + 100); + + const result = started.result(); + expect(result?.data?.pending).toBe(true); + expect(result?.data?.location).toBeUndefined(); + expect(stackTraceCalls()).toHaveLength(0); + }); + + it('reads no stack for a program that ended inside the hold', async () => { + const sessionId = await createSession(); + const started = launch(sessionId); + await vi.advanceTimersByTimeAsync(200); + proxy().simulateExited(0); + await vi.advanceTimersByTimeAsync(50); + + const result = started.result(); + expect(result).toMatchObject({ success: true, state: SessionState.STOPPED }); + expect(result?.data?.location).toBeUndefined(); + expect(stackTraceCalls()).toHaveLength(0); + }); + + it("restart_debugging carries the relaunch's landing the same way", async () => { + const sessionId = await createSession(); + const relaunchProxy = new MockProxyManager(); + vi.mocked(dependencies.proxyManagerFactory.create) + .mockReturnValueOnce(proxy()) + .mockReturnValueOnce(relaunchProxy); + await sessionManager.setBreakpoint(sessionId, { file: 'test.py', line: 5 }); + const first = launch(sessionId); + await vi.advanceTimersByTimeAsync(HOLD_MS + 100); + expect(first.result()?.data?.pending).toBe(true); + + let restarted: DebugResult | undefined; + void sessionManager.restartDebugging(sessionId).then((settled) => { restarted = settled; }); + await vi.advanceTimersByTimeAsync(300); + relaunchProxy.simulateStopped(1, 'breakpoint'); + await vi.advanceTimersByTimeAsync(50); + + expect(restarted, JSON.stringify(restarted)).toMatchObject({ success: true, state: SessionState.PAUSED }); + expect(restarted?.data?.location).toEqual({ file: 'test.py', line: 10, column: 0 }); + expect(restarted?.data?.breakpointsReapplied).toBe(1); + }); + }); + it('applies the same contract to restart_debugging', async () => { const sessionId = await createSession(); // A proxy per launch, as in production: the shared mock would deliver the diff --git a/tests/e2e/mcp-server-smoke-cpp.test.ts b/tests/e2e/mcp-server-smoke-cpp.test.ts index 761ac6e9..996ecc82 100644 --- a/tests/e2e/mcp-server-smoke-cpp.test.ts +++ b/tests/e2e/mcp-server-smoke-cpp.test.ts @@ -444,6 +444,19 @@ describe.skipIf(SKIP_CPP)('MCP Server C/C++ Debugging Smoke Test @requires-cpp', })); expect(stop.state, JSON.stringify(stop)).toBe('paused'); expect(String(stop.note ?? ''), JSON.stringify(stop)).toMatch(/may be stale/); + + // get_stack_trace is the other carrier (issue #951); and when the + // launch itself answered the stop, that answer already said so. + const stack = parseSdkToolResult(await mcpClient!.callTool({ + name: 'get_stack_trace', + arguments: { sessionId } + })); + expect(String(stack.note ?? ''), JSON.stringify(stack)).toMatch(/may be stale/); + expect(stack.note).toContain(binaryPath); + if (startResponse.state === 'paused') { + expect(startResponse.location, JSON.stringify(startResponse)).toBeDefined(); + expect(String(startResponse.note ?? ''), JSON.stringify(startResponse)).toMatch(/may be stale/); + } } finally { utimesSync(binaryPath, stamp.atime, stamp.mtime); } diff --git a/tests/e2e/mcp-server-smoke-dotnet.test.ts b/tests/e2e/mcp-server-smoke-dotnet.test.ts index 69339c34..158dca5b 100644 --- a/tests/e2e/mcp-server-smoke-dotnet.test.ts +++ b/tests/e2e/mcp-server-smoke-dotnet.test.ts @@ -155,7 +155,7 @@ describe.skipIf(SKIP_DOTNET)('.NET Adapter Smoke Test', () => { console.log('[.NET Smoke Test] dotnet language is available'); }); - it('warns on set_breakpoint once the launch has named the DLL, and notes on the stop, when dotnet.dll is older than Program.cs (issue #950)', async (ctx) => { + it('warns on set_breakpoint once the launch has named the DLL, and notes on the launch answer, the stop and the stack, when dotnet.dll is older than Program.cs (issues #950, #951)', async (ctx) => { const dllPath = ensureDotnetBuild(); const sourceFile = path.resolve(ROOT, 'examples', 'dotnet', 'Program.cs'); const stamp = statSync(dllPath); @@ -187,6 +187,21 @@ describe.skipIf(SKIP_DOTNET)('.NET Adapter Smoke Test', () => { throw new Error(`start_debugging failed: ${JSON.stringify(startResponse, null, 2)}`); } + // A fast program is paused at the breakpoint when the launch answers, + // and that answer says where and carries the note (issue #951) — the + // call a caller reads before any wait_for_stop. A machine slow enough to + // outlast the hold answers pending instead; the wait then carries both. + const launchStop = startResponse.state === 'paused' + ? startResponse + : parseSdkToolResult(await mcpClient!.callTool({ + name: 'wait_for_stop', + arguments: { sessionId, timeout: 20000 } + })); + expect(launchStop.state, JSON.stringify(launchStop)).toBe('paused'); + expect((launchStop.location as { line?: number } | undefined)?.line, JSON.stringify(launchStop)).toBe(14); + expect(String(launchStop.note ?? ''), JSON.stringify(launchStop)).toMatch(/may be stale/); + expect(launchStop.note).toContain(dllPath); + // The launch named the DLL: a breakpoint set now carries the warning, naming both files. const after = parseSdkToolResult(await mcpClient!.callTool({ name: 'set_breakpoint', @@ -204,6 +219,14 @@ describe.skipIf(SKIP_DOTNET)('.NET Adapter Smoke Test', () => { })); expect(stop.state, JSON.stringify(stop)).toBe('paused'); expect(String(stop.note ?? ''), JSON.stringify(stop)).toMatch(/may be stale/); + + // And get_stack_trace, the other carrier (issue #951). + const stack = parseSdkToolResult(await mcpClient!.callTool({ + name: 'get_stack_trace', + arguments: { sessionId } + })); + expect(String(stack.note ?? ''), JSON.stringify(stack)).toMatch(/may be stale/); + expect(stack.note).toContain(dllPath); } finally { utimesSync(dllPath, stamp.atime, stamp.mtime); } diff --git a/tests/e2e/mcp-server-smoke-java.test.ts b/tests/e2e/mcp-server-smoke-java.test.ts index 45b0b181..9d0b4622 100644 --- a/tests/e2e/mcp-server-smoke-java.test.ts +++ b/tests/e2e/mcp-server-smoke-java.test.ts @@ -182,6 +182,15 @@ describe('MCP Server Java Debugging Smoke Test @requires-java', () => { })); expect(stop.state, JSON.stringify(stop)).toBe('paused'); expect(String(stop.note ?? ''), JSON.stringify(stop)).toMatch(/may be stale/); + + // get_stack_trace is the other carrier (issue #951): its top frame is in + // the aged class's source, so the same sentence rides its note. + const stack = parseSdkToolResult(await mcpClient!.callTool({ + name: 'get_stack_trace', + arguments: { sessionId } + })); + expect(String(stack.note ?? ''), JSON.stringify(stack)).toMatch(/may be stale/); + expect(stack.note).toContain(classFile); } finally { utimesSync(classFile, stamp.atime, stamp.mtime); } diff --git a/tests/e2e/mcp-server-smoke-javascript.test.ts b/tests/e2e/mcp-server-smoke-javascript.test.ts index 7e2e50cc..9808e80a 100644 --- a/tests/e2e/mcp-server-smoke-javascript.test.ts +++ b/tests/e2e/mcp-server-smoke-javascript.test.ts @@ -749,7 +749,7 @@ describe('JavaScript Debugging - module-load breakpoints in source-mapped TypeSc * fixture is ES2015 CommonJS, so a step from line 91's downlevelled `await` * can land in the `__awaiter` trampoline rather than on a `.ts` line. */ - it('warns on set_breakpoint, and notes on the stop, when the generated .js is older than the .ts (issue #938)', async () => { + it('warns on set_breakpoint, and notes on the launch answer, the stop and the stack, when the generated .js is older than the .ts (issues #938, #951)', async () => { /** `console.log("Test 4: Complex data structures");` — a plain line after the awaits. */ const LATER_LINE = 96; const dir = transpile('commonjs'); @@ -789,6 +789,9 @@ describe('JavaScript Debugging - module-load breakpoints in source-mapped TypeSc arguments: { sessionId, scriptPath: jsFile, args: [], dapLaunchArgs: { stopOnEntry: false, justMyCode: true } } })); expect(start.state, JSON.stringify(start)).toBe('paused'); + // The launch answered the stop, so it says where, with the note (issue #951). + expect((start.location as { line?: number } | undefined)?.line, JSON.stringify(start)).toBe(MODULE_LOAD_LINE); + expect(String(start.note ?? ''), JSON.stringify(start)).toMatch(/may be stale/); await mcpClient!.callTool({ name: 'continue_execution', arguments: { sessionId } }); const stop = parseSdkToolResult(await mcpClient!.callTool({ @@ -799,6 +802,10 @@ describe('JavaScript Debugging - module-load breakpoints in source-mapped TypeSc expect((stop.location as { line?: number } | undefined)?.line, JSON.stringify(stop)).toBe(LATER_LINE); expect(String(stop.note ?? ''), JSON.stringify(stop)).toMatch(/may be stale/); + // get_stack_trace is the other carrier (issue #951). + const stack = parseSdkToolResult(await mcpClient!.callTool({ name: 'get_stack_trace', arguments: { sessionId } })); + expect(String(stack.note ?? ''), JSON.stringify(stack)).toMatch(/may be stale/); + await mcpClient!.callTool({ name: 'continue_execution', arguments: { sessionId } }); }, 60000); diff --git a/tests/integration/javascript-launch-config.test.ts b/tests/integration/javascript-launch-config.test.ts index debcd302..ebd6fc68 100644 --- a/tests/integration/javascript-launch-config.test.ts +++ b/tests/integration/javascript-launch-config.test.ts @@ -15,6 +15,7 @@ interface LastStop { reason?: string; rawReason?: string; threadId?: number; des interface Listed { id: string; state: string; exitCode?: number; lastStop?: LastStop } interface Waited extends Result { pending?: boolean; exitCode?: number; lastStop?: LastStop; location?: { file: string; line: number }; + context?: { lineContent: string }; } interface BreakpointRecord { id: string; line: number; verified: boolean; verifiedBy?: string; adapterId?: number; message?: string; boundFile?: string; boundLine?: number; @@ -201,6 +202,11 @@ describe('JavaScript launch configuration', () => { const evidence = JSON.stringify({ launched, stopped, lastStop: session?.lastStop, breakpoints: listed.breakpoints, top: stack.stackFrames[0] }); expect(session?.lastStop?.reason, evidence).toBe('breakpoint'); expect(stack.stackFrames[0]?.line, evidence).toBe(9); + // The answer that reported the stop — the launch itself, or the wait — + // names where it is (issue #951): the launch answer carries the same + // location (and context) wait_for_stop does. + expect(stopped.location?.line, evidence).toBe(9); + expect(stopped.context?.lineContent, evidence).toContain("process.argv.includes('--exit')"); }, 60_000); it('wait_for_stop answers pending on an idle program, then blocks until a breakpoint set on it is hit (issue #849)', async () => { diff --git a/tests/unit/session-manager-operations-coverage.test.ts b/tests/unit/session-manager-operations-coverage.test.ts index e623caba..2796002c 100644 --- a/tests/unit/session-manager-operations-coverage.test.ts +++ b/tests/unit/session-manager-operations-coverage.test.ts @@ -1897,7 +1897,9 @@ describe('Session Manager Operations Coverage - Error Paths and Edge Cases', () const startPromise = operations.startDebugging('test-session', 'server.py'); await vi.advanceTimersByTimeAsync(Math.floor(holdMs() / 2)); moveSessionTo(SessionState.PAUSED, { reason: 'breakpoint', threadId: 1, timestamp: Date.now() }); - await vi.advanceTimersByTimeAsync(0); + // A launch that stopped reads where (issue #951): the landing read's + // short settle has to elapse before the answer is built. + await vi.advanceTimersByTimeAsync(50); const result = await startPromise; expect(result.state).toBe(SessionState.PAUSED); From 1a5e566ab8131d4b4e96e0db84c5d0811a410bed Mon Sep 17 00:00:00 2001 From: JF Date: Mon, 19 Oct 2026 11:57:38 -0400 Subject: [PATCH 2/3] test(server): the two #951 carriers through the full server and the real checker; CLAUDE.md names the launch as a landing-read caller The plan named full-server .NET cases for the launch answer and get_stack_trace beside the handler-level ones; they are in server-stale-build.test.ts now (aged DLL, transformedLaunch on the session, the real BuildStalenessChecker over the fake fs). The hoistStopLanding doc says the launch handlers call it on every answer and why that is a no-op without a location. CLAUDE.md's landing-read sentence lists the launch as its fifth caller and the handler helper. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Y3ULnoVSoCZ5t2Y8d98Ctv --- CLAUDE.md | 2 +- src/server/handlers/shared.ts | 5 +- .../unit/server/server-stale-build.test.ts | 58 +++++++++++++++++++ 3 files changed, 62 insertions(+), 3 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index b9414466..b5451351 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -245,7 +245,7 @@ The codebase follows a **layered architecture with dependency injection** and ** - `breakpoints/breakpoint-controller.ts` (`BreakpointController`): set/remove/clear plus the replace-all DAP re-sends - `breakpoints/anchor-resolution.ts` (`reresolveAnchors`) and `breakpoints/launch-warnings.ts` (pure warning builders) - `execution/execution-controller.ts` (`ExecutionController`): stepping (table-driven over `STEP_KINDS`), continue, pause, thread list, `waitForStop`; `execution/session-state-wait.ts` (`waitForSessionState`): the one level-triggered wait on a session's state — it re-reads the state in a microtask after each change (`OperationsContext.onStateChange`, fed by `_updateSessionState`), so an entry stop the core auto-continues within one call stack is never reported (issue #849) - - `inspection/stop-anchor.ts` (`selectStopAnchor`: the one verdict on which thread a stop is anchored on, shared by the attach controller and the frame-anchor resolver — a `pause` reported on a thread with no program frame moves to the first sibling whose stack reaches the program and says so; breakpoint/step/exception/entry stops never move; issues #904/#898), `inspection/stop-location.ts` (`describeStopLocation`: the one landing read behind the pause, step, wait_for_stop and attach answers — `location`, and `threadId`/`note` when the answer is anchored off the reported thread or no frame is program code), `inspection/expression-evaluator.ts` (`ExpressionEvaluator`), `jvm/redefine-classes-controller.ts` (`RedefineClassesController`), `mirror/mirror-controller.ts` (`MirrorController`) + - `inspection/stop-anchor.ts` (`selectStopAnchor`: the one verdict on which thread a stop is anchored on, shared by the attach controller and the frame-anchor resolver — a `pause` reported on a thread with no program frame moves to the first sibling whose stack reaches the program and says so; breakpoint/step/exception/entry stops never move; issues #904/#898), `inspection/stop-location.ts` (`describeStopLocation`: the one landing read behind the pause, step, wait_for_stop, attach and launch answers — a `start_debugging`/`restart_debugging` that is paused when it answers reads it too, issue #951 — `location`, and `threadId`/`note` when the answer is anchored off the reported thread or no frame is program code; the handlers hoist all three through `handlers/shared.ts` `hoistStopLanding`, which also reads the line context and the stale-build sentence), `inspection/expression-evaluator.ts` (`ExpressionEvaluator`), `jvm/redefine-classes-controller.ts` (`RedefineClassesController`), `mirror/mirror-controller.ts` (`MirrorController`) - Collaborators see the facade through `OperationsContext` (`operations-context.ts`), whose members are late-bound arrows and getters — never captured references — so reassigning a facade method or writing a tunable on a live instance is visible to them; each takes the narrowest `Pick<>` slice it uses. Collaborator-to-collaborator dependencies are constructor arguments (`DebugLauncher(ctx, proxyLauncher, breakpoints, inFlight)`, `AttachController(ctx, proxyLauncher, breakpoints, pauseCoordinator, inFlight)`) held as captured instance references: their methods resolve at call time, so `vi.spyOn(internals(ops).proxyLauncher, 'start')` intercepts, but reassigning a collaborator field after construction is not observed. The facade owns one shared `InFlightGuard` (`in-flight-guard.ts`, issue #711) that the launcher and the attach controller both take: `startDebugging`, `restartDebugging`, `attachToProcess` and `detachFromProcess` each claim the session before their first await and release it in `finally`, so one launch-shaped call per session at a time; `restartDebugging` replays through the launcher's private `launch()` under its own claim, never through the facade's `startDebugging`. Shared per-request DAP helpers live in `dap-request-helpers.ts` - Coordinates ProxyManager instances (one per session) - Handles breakpoint management and queuing diff --git a/src/server/handlers/shared.ts b/src/server/handlers/shared.ts index 43344562..d9bbf9b8 100644 --- a/src/server/handlers/shared.ts +++ b/src/server/handlers/shared.ts @@ -147,8 +147,9 @@ export function appendNote(response: Record, note: string | und * running code may be an older build than the source it was read from * (issues #938/#950) — that sentence is appended to the note, so it always * comes after what the session layer had to say. Nothing happens without a - * location: an answer that is pending or that reports a program's end keeps - * exactly the fields it had. + * location, so the launch handlers call this on every answer: one that is + * pending, a dry run, a failed launch or a program's end keeps exactly the + * fields it had. */ export async function hoistStopLanding( ctx: ToolContext, diff --git a/tests/core/unit/server/server-stale-build.test.ts b/tests/core/unit/server/server-stale-build.test.ts index 6067d446..7d8dfce0 100644 --- a/tests/core/unit/server/server-stale-build.test.ts +++ b/tests/core/unit/server/server-stale-build.test.ts @@ -5,6 +5,8 @@ * source gets a hedged warning naming both files; a current build, a * non-TypeScript file, or a session whose adapter does not bind through a * map gets none. The warning rides beside the loud-snapping one, after it. + * The compiled languages' cases (issue #950) and the two carriers #951 added + * (the launch answer, get_stack_trace) are at the end. */ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import * as path from 'path'; @@ -204,5 +206,61 @@ describe('set_breakpoint stale-build warning (#938)', () => { }); expect((await setBreakpoint(CS, {})).warning ?? '').not.toMatch(/may be stale/); }); + + // The two carriers #951 added, through the full server and the real + // checker: the launch answer, when the program is paused in the source + // as the launch answers, and the stack, when its top frame is there. + it('start_debugging carries the sentence as note when the launch answers paused in the source (issue #951)', async () => { + mockSessionManager.getSession.mockReturnValue({ id: 'test-session', sessionLifecycle: 'active', transformedLaunch: launched }); + mockSessionManager.startDebugging.mockResolvedValue({ + success: true, + state: 'paused', + data: { message: 'Debugging started', reason: 'breakpoint', location: { file: CS, line: 4, column: 1 } } + }); + + const result = await callToolHandler({ + method: 'tools/call', + params: { name: 'start_debugging', arguments: { sessionId: 'test-session', scriptPath: DLL } } + }); + const content = JSON.parse(result.content[0].text); + + expect(content.success, JSON.stringify(content)).toBe(true); + expect(content.state).toBe('paused'); + expect(content.location).toEqual({ file: CS, line: 4, column: 1 }); + expect(content.context?.lineContent).toBe(' for (const p of prices) sum += p;'); + expect(content.note).toMatch(/may be stale/); + expect(content.note).toContain(DLL); + expect(content.note).toContain(CS); + }); + + it('get_stack_trace carries the sentence as note when its top frame is in the source (issue #951)', async () => { + // Paused with a current thread: the server reads the stack through the + // session layer's detailed read, which the mock answers below. + mockSessionManager.getSession.mockReturnValue({ + id: 'test-session', + sessionLifecycle: 'active', + state: 'paused', + proxyManager: { getCurrentThreadId: () => 1, setCurrentThreadId: vi.fn() }, + transformedLaunch: launched + }); + mockSessionManager.getStackTraceDetailed.mockResolvedValue({ + frames: [{ id: 1, name: 'HelloWorld.Add', file: CS, line: 4, column: 1 }], + threadId: 1, + totalFrameCount: 1, + hiddenFrameCount: 0, + allFramesInternal: false + }); + + const result = await callToolHandler({ + method: 'tools/call', + params: { name: 'get_stack_trace', arguments: { sessionId: 'test-session' } } + }); + const content = JSON.parse(result.content[0].text); + + expect(content.success, JSON.stringify(content)).toBe(true); + expect(content.stackFrames[0].file).toBe(CS); + expect(content.note).toMatch(/may be stale/); + expect(content.note).toContain(DLL); + }); }); }); From e8a9937bf2c530c2e10e64c21c4f651379987034 Mon Sep 17 00:00:00 2001 From: JF Date: Mon, 19 Oct 2026 12:18:30 -0400 Subject: [PATCH 3/3] fix(handlers): one openable-path rule for every stop carrier; drop the vacuous lastStop gate and the false comments behind it (#951 review) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Own-PR review findings, all seven addressed: - debug-launcher.ts: "PAUSED without lastStop is the auto-continue transient" was false — _updateSessionState has refused PAUSED without lastStop since #598 and the auto-continue path records the stop before entering PAUSED. The gate on finalSession.lastStop was therefore vacuous and is gone; the new comment names the real protection (the readiness wait's microtask re-read, #849), and the pre-existing `reason` fallback comment that said the same false thing is corrected. - shared.ts: hasOpenableSourcePath(file) — not a `<…>` label and absolute. hoistStopLanding now skips the line read and the stale probe for any other file, so a step landing on a JDI package-relative label such as java/io/PrintStream.java no longer runs the Java candidate walk and a stat against the server's cwd (pre-existing since #938); get_stack_trace applies the same rule beside its unresolvedSource check. Two cases per stop tool in execution-tools.test.ts. - The helper's doc no longer claims nothing happens without a location (threadId/note are copied first; the launch's non-stop arms carry none). - Carrier lists that omitted the launch: stop-location.ts header, StopLandingFields doc, docs/architecture/component-design.md. - docs/tool-reference.md: data.note lacks the appended stale sentence, so the placement sentence says so; the launch response example now shows the real swap_vars.py lines with the five-entry surrounding the line reader produces. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Y3ULnoVSoCZ5t2Y8d98Ctv --- docs/architecture/component-design.md | 2 +- docs/tool-reference.md | 16 ++++++--- src/server/handlers/inspection-tools.ts | 13 +++++-- src/server/handlers/shared.ts | 34 ++++++++++++++----- src/session/inspection/stop-location.ts | 3 +- src/session/launch/debug-launcher.ts | 20 +++++------ src/session/session-manager-core.ts | 4 +-- .../server/handlers/execution-tools.test.ts | 19 +++++++++++ 8 files changed, 81 insertions(+), 30 deletions(-) diff --git a/docs/architecture/component-design.md b/docs/architecture/component-design.md index d19bde98..42c153fc 100644 --- a/docs/architecture/component-design.md +++ b/docs/architecture/component-design.md @@ -34,7 +34,7 @@ can be spied on or replaced: | `ExpressionEvaluator` | `src/session/inspection/expression-evaluator.ts` | `evaluate_expression`, including frame resolution and secret redaction | | `FrameAnchorResolver` | `src/session/inspection/frame-anchor-resolver.ts` | The inspection anchor shared by stack trace, locals, and evaluation: thread selection, bounded stack readiness, sibling-thread adoption, policy frame filtering. Constructed by `SessionManagerData` and passed to `ExpressionEvaluator`; both sibling-thread scans (a frameless stop, and a `pause` reported on a thread with no program frame, issue #898) go through `selectStopAnchor` | | `selectStopAnchor` | `src/session/inspection/stop-anchor.ts` | The one verdict on which thread a stop is anchored on (issue #904), shared by `AttachController` and `FrameAnchorResolver`: candidates ordered reported thread, `main`-named, listed; a thread reaches the program when a frame both names a source and is one the policy calls the program's; bounds (threads, per-unwind timeout, budget) injected by the caller. It decides, the callers move the anchor and word the disclosure | -| `describeStopLocation` | `src/session/inspection/stop-location.ts` | The one landing read behind the pause, step, wait_for_stop and attach answers (issue #904): the resolver's detailed result (with the readiness option for a `pause`, plainly for every other stop) turned into the shared `StopLandingFields` — `location`, `threadId` only when anchored off the reported thread, `note` for an adopted thread or a stack with no program frame | +| `describeStopLocation` | `src/session/inspection/stop-location.ts` | The one landing read behind the pause, step, wait_for_stop, attach and launch answers (issues #904, #951): the resolver's detailed result (with the readiness option for a `pause`, plainly for every other stop) turned into the shared `StopLandingFields` — `location`, `threadId` only when anchored off the reported thread, `note` for an adopted thread or a stack with no program frame | | `RedefineClassesController` | `src/session/jvm/redefine-classes-controller.ts` | JVM hot swap plus the post-swap anchor re-resolution and breakpoint re-send | | `MirrorController` | `src/session/mirror/mirror-controller.ts` | `expose_session` / `unexpose_session` | diff --git a/docs/tool-reference.md b/docs/tool-reference.md index ca95aa7c..17f2bc69 100644 --- a/docs/tool-reference.md +++ b/docs/tool-reference.md @@ -400,22 +400,28 @@ Starts debugging a script. "success": true, "state": "paused", "message": "Debugging started for /abs/path/examples/python_simple_swap/swap_vars.py. Current state: paused", - "location": { "file": "/abs/path/examples/python_simple_swap/swap_vars.py", "line": 12, "column": 1 }, + "location": { "file": "/abs/path/examples/python_simple_swap/swap_vars.py", "line": 10, "column": 1 }, "context": { - "lineContent": " a, b = b, a", - "surrounding": [ { "line": 10, "content": " print(a, b)" }, { "line": 11, "content": "" }, { "line": 12, "content": " a, b = b, a" } ] + "lineContent": " a = b # Bug: 'a' loses its original value here", + "surrounding": [ + { "line": 8, "content": " # Or Python's tuple assignment: a, b = b, a" }, + { "line": 9, "content": " " }, + { "line": 10, "content": " a = b # Bug: 'a' loses its original value here" }, + { "line": 11, "content": " b = a # Bug: 'b' gets the new value of 'a' (which is original 'b')" }, + { "line": 12, "content": " " } + ] }, "data": { "message": "Debugging started for /abs/path/examples/python_simple_swap/swap_vars.py. Current state: paused", "reason": "breakpoint", - "location": { "file": "/abs/path/examples/python_simple_swap/swap_vars.py", "line": 12, "column": 1 } + "location": { "file": "/abs/path/examples/python_simple_swap/swap_vars.py", "line": 10, "column": 1 } } } ``` **When the launch answers:** once the program is launched, `start_debugging` holds its answer for a short window and reports the session as it is when the first of three things happens — the same way in every language (issues #815, #823, #826): -- **The program stops** — `state: "paused"` with `reason`, and where, the way `wait_for_stop` answers a stop (issue #951): `location` is the first visible frame, `context` the source line around it, and `threadId` and `note` appear when there is something to say — the answer anchored on a thread other than the reported one, no frame of the stack being program code (as for `pause_execution`), or the compiled file the debugger binds the location through being older than its source (the stale-build sentence `set_breakpoint` warns with, issues #938/#950). `location`, `threadId` and `note` sit at the top level and in `data`; `context` at the top level only. A breakpoint reached as the program starts is answered here, in the one call (the example above). +- **The program stops** — `state: "paused"` with `reason`, and where, the way `wait_for_stop` answers a stop (issue #951): `location` is the first visible frame, `context` the source line around it, and `threadId` and `note` appear when there is something to say — the answer anchored on a thread other than the reported one, no frame of the stack being program code (as for `pause_execution`), or the compiled file the debugger binds the location through being older than its source (the stale-build sentence `set_breakpoint` warns with, issues #938/#950). `location` and `threadId` sit at the top level and in `data`; `context` and the full `note` at the top level (`data.note` is the session layer's part of the note, without the stale-build sentence the handler appends). A breakpoint reached as the program starts is answered here, in the one call (the example above). - **The program ends** — `state: "stopped"` with the run-to-completion summary and `exitCode` below. - **The hold elapses** — `state: "running"` with **`pending: true`** (top level and in `data`, like `step_over`) and a `message` that says what is armed: `…has not reached 2 breakpoint(s) yet. Nothing was cancelled: what is armed stays armed…`; or, with nothing armed (the `"uncaught"` default does not count — a crash is not a stop you are waiting for), `…nothing is armed to stop it (no breakpoints, no entry stop, no caught-exception filter)…`; or, for a `noDebug` launch, that the debugger is off. Logpoints the adapter runs on never stop the program, so they are named apart: `…1 logpoint(s) that log without stopping is armed (read get_output for the messages), and nothing is armed to stop it (no pausing breakpoints, …)…`. What is armed is read when the answer is built, so a breakpoint set while the launch was in flight is named. diff --git a/src/server/handlers/inspection-tools.ts b/src/server/handlers/inspection-tools.ts index 539d5fca..a9aae351 100644 --- a/src/server/handlers/inspection-tools.ts +++ b/src/server/handlers/inspection-tools.ts @@ -8,7 +8,13 @@ import { SessionTerminatedError } from '../../errors/debug-errors.js'; import { ErrorMessages } from '../../utils/error-messages.js'; import type { ToolContext, ToolHandler } from '../tool-context.js'; import { enforceExplicitNames, requireSessionId } from '../tool-validation.js'; -import { carriesLastStop, debuggerOffWhyFor, describeStaleBuild, variablePayloadExtras } from './shared.js'; +import { + carriesLastStop, + debuggerOffWhyFor, + describeStaleBuild, + hasOpenableSourcePath, + variablePayloadExtras +} from './shared.js'; import { failureResult, jsonResult, @@ -144,9 +150,10 @@ export const getStackTraceTool: ToolHandler = async (ctx, args) => { // frame was read from (issues #938/#950): the sentence the stop answers // carry, here on the tool the fast-program flow always calls (issue // #951). The top frame only, and only one with an openable path — a - // `<…>` label or an unresolvedSource frame names no file on this host. + // `<…>` label, a relative spelling or an unresolvedSource frame names no + // file on this host (the same rule the stop answers apply). const top = stackTrace.frames[0]; - if (top && !top.unresolvedSource && !top.file.startsWith('<')) { + if (top && !top.unresolvedSource && hasOpenableSourcePath(top.file)) { const staleBuild = await describeStaleBuild(ctx, args.sessionId, top.file, 'stack trace'); if (staleBuild) { notes.push(staleBuild); diff --git a/src/server/handlers/shared.ts b/src/server/handlers/shared.ts index d9bbf9b8..63e0f084 100644 --- a/src/server/handlers/shared.ts +++ b/src/server/handlers/shared.ts @@ -4,6 +4,7 @@ * extras, the attach warning both attach paths surface, and the one * AttachRequest both attach entry points send. */ +import * as path from 'path'; import { isSettledSessionState, REDACTION_NOTICE, SessionState, Variable } from '@debugmcp/shared'; import type { LineContext } from '../../utils/line-reader.js'; import { buildTruncationNotice, VariableTruncationSummary } from '../../session/variable-caps.js'; @@ -136,20 +137,34 @@ export function appendNote(response: Record, note: string | und response.note = typeof existing === 'string' && existing.length > 0 ? `${existing} ${note}` : note; } +/** + * Whether a frame's `file` is a path this host can open: not a `<…>` label, + * and not the relative spelling an adapter uses for a source it could not + * locate (a JDI package path such as `java/io/PrintStream.java`, a runtime + * built elsewhere under CodeLLDB). Such a frame names no file here, so + * neither the line context nor the stale-build check is read for it — a + * relative path would otherwise be resolved against the server's cwd. + */ +export function hasOpenableSourcePath(file: string): boolean { + return !file.startsWith('<') && path.isAbsolute(file); +} + /** * Hoist where a stop landed to the top level of a tool answer and annotate * it — the one shape every stop answer has: the step tools, pause_execution, * wait_for_stop, and a launch or relaunch that stopped (issue #951). * * Beyond the frame (issue #904): the thread the answer is anchored on when it - * is not the reported one, and the note about it. Then the location, and two - * reads of its file, overlapped: the source line around it, and whether the - * running code may be an older build than the source it was read from - * (issues #938/#950) — that sentence is appended to the note, so it always - * comes after what the session layer had to say. Nothing happens without a - * location, so the launch handlers call this on every answer: one that is - * pending, a dry run, a failed launch or a program's end keeps exactly the - * fields it had. + * is not the reported one, and the note about it — copied whenever the + * session layer set them, location or not (a resolver note on a stack that + * came back empty). Then the location and, when its file is a path this host + * can open, two reads of it, overlapped: the source line around it, and + * whether the running code may be an older build than the source it was read + * from (issues #938/#950) — that sentence is appended to the note, so it + * always comes after what the session layer had to say. The launch handlers + * call this on every answer: a pending one, a dry run, a failed launch or a + * program's end carries none of these fields, so it keeps exactly the fields + * it had. */ export async function hoistStopLanding( ctx: ToolContext, @@ -169,6 +184,9 @@ export async function hoistStopLanding( return; } response.location = location; + if (!hasOpenableSourcePath(location.file)) { + return; + } const [context, staleBuild] = await Promise.all([ readLineContext(ctx, location.file, location.line, logLabel), describeStaleBuild(ctx, sessionId, location.file, logLabel) diff --git a/src/session/inspection/stop-location.ts b/src/session/inspection/stop-location.ts index afb08877..4c96ce6c 100644 --- a/src/session/inspection/stop-location.ts +++ b/src/session/inspection/stop-location.ts @@ -1,6 +1,7 @@ /** * Where a stop landed, for the answers that name a location (issue #904): - * pause, the steps, wait_for_stop and the attach. One read through the + * pause, the steps, wait_for_stop, the attach, and a launch or relaunch that + * is paused when it answers (issue #951). One read through the * frame-anchor resolver, so an answer sees the resolver's verdict — which * thread it is anchored on, whether any frame is program code, the note * that explains a switch — instead of the top frame alone, which used to be diff --git a/src/session/launch/debug-launcher.ts b/src/session/launch/debug-launcher.ts index 04d3dd13..90d76f7b 100644 --- a/src/session/launch/debug-launcher.ts +++ b/src/session/launch/debug-launcher.ts @@ -641,11 +641,12 @@ export class DebugLauncher { // stack read a step, a pause or wait_for_stop makes, so the answer // carries `location` — and `threadId`/`note` when there is something // to say — and the handler can read the line and the build behind it. - // Only for a stop the core kept: PAUSED without lastStop is the - // auto-continue transient (below), whose line the program is about to - // leave. Best effort, like every landing read: a stack that cannot be - // read leaves the answer as it was. - const landing = finalState === SessionState.PAUSED && finalSession.lastStop + // An entry stop the core auto-continues never gets here: the readiness + // wait reads the state through waitForSessionState, whose microtask + // re-read (issue #849) sees the RUNNING the continue sets in the same + // call stack. Best effort, like every landing read: a stack that cannot + // be read leaves the answer as it was. + const landing = finalState === SessionState.PAUSED ? await describeStopLocation(this.ctx, sessionId, 'launch') : {}; @@ -752,11 +753,10 @@ export class DebugLauncher { ...(runToCompletion?.data ?? {}), // Prefer the actual DAP stop reason (issue #214) — the first stop is // not always a breakpoint (e.g. an uncaught exception before any - // breakpoint is hit). handleStopped records lastStop synchronously - // before every user-visible PAUSED transition, so PAUSED without - // lastStop is only the auto-continue transient (an entry stop being - // auto-continued); report 'unknown' rather than fabricating - // 'breakpoint' there (issue #255 residual). + // breakpoint is hit). A PAUSED session always carries lastStop + // (_updateSessionState enforces it, issue #598), so the fallback is + // for the type alone: 'entry' for a stopOnEntry launch, 'unknown' + // otherwise — never a fabricated 'breakpoint' (issue #255). reason: finalState === SessionState.PAUSED ? finalSession.lastStop?.reason ?? diff --git a/src/session/session-manager-core.ts b/src/session/session-manager-core.ts index 1dc68f81..c8e9d335 100644 --- a/src/session/session-manager-core.ts +++ b/src/session/session-manager-core.ts @@ -123,8 +123,8 @@ export interface DebugResultData extends ProxyFailureDiagnostics { export type StopLocation = Pick; /** - * What a location-bearing answer (pause, step, wait_for_stop) carries about - * where the stop landed, read through the frame-anchor resolver (issue + * What a location-bearing answer (pause, step, wait_for_stop, and a launch + * that stopped) carries about where the stop landed, read through the frame-anchor resolver (issue * #904): the first visible frame when the stack could be read; the thread * the answer is anchored on, only when it is not the thread the stop was * reported on; and a note when there is something to say — a sibling diff --git a/tests/core/unit/server/handlers/execution-tools.test.ts b/tests/core/unit/server/handlers/execution-tools.test.ts index 0d13cf55..86da1445 100644 --- a/tests/core/unit/server/handlers/execution-tools.test.ts +++ b/tests/core/unit/server/handlers/execution-tools.test.ts @@ -270,6 +270,25 @@ describe('execution tool handlers', () => { expect(payload.note).toBe('existing'); }); + + // A frame whose file is a `<…>` label or an adapter's relative spelling + // of a source it could not locate (a JDI package path) names no file on + // this host (issue #951): the location is still reported, but neither + // the line nor the build is read for it — a relative path would be + // resolved against the server's cwd. + for (const file of ['', 'java/io/PrintStream.java']) { + it(`${tool} reads neither the line nor the build for a location whose file is not an openable path (${file})`, async () => { + ctx.buildStaleness = { check: vi.fn().mockResolvedValue(evidence) }; + + const payload = JSON.parse((await answer({ message: 'Paused', location: { file, line: 1 } })).content[0].text); + + expect(payload.location).toEqual({ file, line: 1 }); + expect(payload).not.toHaveProperty('context'); + expect(payload).not.toHaveProperty('note'); + expect(ctx.lineReader.getLineContext).not.toHaveBeenCalled(); + expect(ctx.buildStaleness.check).not.toHaveBeenCalled(); + }); + } } });