Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions changelog.d/951.added.md
Original file line number Diff line number Diff line change
@@ -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)
6 changes: 3 additions & 3 deletions docs/architecture/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<DebugResult>`
#### `startDebugging(sessionId, scriptPath, scriptArgs?, dapLaunchArgs?, dryRunSpawn?, adapterLaunchConfig?, breakOnExceptions?): Promise<DebugResult<LaunchResultData>>`
Starts (or dry-runs) a launch-mode debug session. The parameters are **positional**, not an options object:

```typescript
Expand All @@ -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<LaunchResultData>` — `{ 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<DebugResult>`
#### `restartDebugging(sessionId: string): Promise<DebugResult<LaunchResultData>>`
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 }>`
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture/component-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |

Expand Down
2 changes: 1 addition & 1 deletion docs/cobol/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion docs/cpp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion docs/dotnet/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading
Loading