Skip to content

Commit 9f9bd71

Browse files
debugmcpdevcynarlabclaude
authored
fix(stack): explain unresolvedSource frames in the adapter's own terms (#816) (#822)
* fix(stack): explain unresolvedSource frames in the adapter's own terms (#816) get_stack_trace appended js-debug's remedy ("attach with adapterConfig.sourceMaps: false to see the generated .js paths") to the note of every session whose frames carry unresolvedSource: true. On a C/C++ attach that was nine native frames CodeLLDB names by symbol (@NtWaitForSingleObject, @BaseThreadInitThunk, …) because they have no debug info — nothing was source-mapped and sourceMaps is a key the C/C++ adapter does not know. Java's kept JDK label got the same sentence. - The note's first sentence is adapter-neutral ("N frame(s) have no source file on this host (unresolvedSource: true) — their file is a label, not an openable path; do not pass it to get_source_context"). - New optional policy hook describeUnresolvedSource({count, attachMode}) supplies what such a frame IS under the debugger: js-debug names the unshipped source map and spells the switch for the session's mode (adapterConfig.sourceMaps on attach, adapterLaunchConfig.sourceMaps on launch — the old sentence said "attach" on launches too); the CodeLLDB policies (cpp/rust/cobol share describeLldbUnresolvedSource) say the frames are native code without debug info and that the program's own frames are the ones with a path. Policies with nothing to add leave the neutral sentence alone; a throwing hook costs only the sentence. Measured (Windows cpp attach, CodeLLDB 1.11.8, DAP trace): the flagged frames are `source: {name: '@symbol', sourceReference: N}` with no path, so the resolver's rule (sourceReference != 0) is what flags them — the flag is right, the explanation was js-debug's. Closes #816 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(stack): word the unresolvedSource note from evidence; review follow-ups (#816) Review of #822: - The neutral sentence said "no source file on this host", which overclaims for the JDI bridge's case (a class no breakpoint was set in is named by package path whether or not its source is here); it now says "no openable source path on this host … a label, not a path". - describeUnresolvedSource receives the flagged frames' file labels (UnresolvedSourceContext.files), so a policy speaks from evidence: the CodeLLDB sentence says "native frames without debug info" only when every label is a `@symbol`, and otherwise names both cases (a symbol-only frame, or a runtime built elsewhere whose relative ../sysdeps/… path is not here — the kept paused frame of issue #672, which has debug info). - The Java policy gains its own account: classes the bridge names by package path because it has no file path for them; a breakpoint set in a class's source file teaches the bridge its path. - The unresolvedSource field doc and the resolver's rule-A comment name every adapter's case, not only js-debug's; the handler test asserts the flag reaches the payload again and types its fake policies; the COBOL guide points at the mechanism as #814 did for detach. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test(shared): pass the flagged frames' labels to describeUnresolvedSource in the cpp/rust policy tests (#816) The follow-up commit widened the hook's context with the flagged frames' file labels; these two suites were edited with it but left out of that commit's add set. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: JF <john.franklin@gmail.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
1 parent eceeaaf commit 9f9bd71

26 files changed

Lines changed: 361 additions & 44 deletions

‎changelog.d/816.fixed.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
**`get_stack_trace` explains source-less frames in the adapter's own terms** — the note for frames flagged `unresolvedSource: true` always ended with js-debug's remedy ("attach with `adapterConfig.sourceMaps: false` to see the generated .js paths"), on every adapter: a C/C++ attach showed it under nine native frames (`@NtWaitForSingleObject`, `@BaseThreadInitThunk`, …) that CodeLLDB names by symbol because they have no debug info — nothing was source-mapped and the key it named is one the C/C++ adapter does not know. The note is now adapter-neutral ("N frame(s) have no source file on this host … their file is a label, not an openable path; do not pass it to get_source_context") and the session's policy adds what such a frame is under its debugger through a new optional `describeUnresolvedSource` hook: js-debug names the unshipped source map and spells the switch for the session's mode (`adapterConfig.sourceMaps: false` on attach, `adapterLaunchConfig.sourceMaps: false` on launch); the CodeLLDB policies (C/C++, Rust, COBOL) say the frames are native code without debug info and that the program's own frames are the ones with a path; adapters with nothing to add leave the neutral sentence alone (#816)

‎docs/agent-debugging-guide.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ evaluate_expression(sessionId=session_id, expression="a + b") # Returns: "3"
125125
## Common Issues and Solutions
126126

127127
### Issue: JavaScript shows Node.js internals in stack trace
128-
**Solution:** Use `continue_execution` to move past internal frames. Stack trace filtering hides Node internals, `node_modules` dependency frames, and async separators by default; `includeInternals: true` shows them. A frame marked `unresolvedSource: true` is a source-map label, not a file you can open.
128+
**Solution:** Use `continue_execution` to move past internal frames. Stack trace filtering hides Node internals, `node_modules` dependency frames, and async separators by default; `includeInternals: true` shows them. A frame marked `unresolvedSource: true` has no file path you can open (for JavaScript, a source-map label for a file the package did not ship; for C/C++/Rust/COBOL, a native frame without debug info; for Java, a class the JDI bridge names by package path) — the `note` says which.
129129

130130
### Issue: Python shows "special variables" instead of actual variables
131131
**Solution:** This is normal hierarchical organization. Use the `variablesReference` to expand:

‎docs/architecture/adapter-policy-pattern.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ export interface AdapterPolicy {
8484
// === Stack frame filtering (optional) ===
8585
filterStackFrames?(frames: StackFrame[], includeInternals: boolean): StackFrame[];
8686
isInternalFrame?(frame: StackFrame): boolean;
87+
// What a frame flagged unresolvedSource IS under this debugger, for the get_stack_trace note (issue #816)
88+
describeUnresolvedSource?(info: { count: number; files: readonly string[]; attachMode: boolean }): string | undefined;
8789

8890
// === Variable extraction (optional) ===
8991
extractLocalVariables?(stackFrames, scopes, variables, includeSpecial?): LocalVariableExtraction;

‎docs/cobol/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ attach_to_process sessionId=... processId=<pid> adapterConfig={"manifestDirs": [
144144
- `manifestDirs` supplies ready `*.cobol-symbols.json` files instead (an earlier source launch's artifact directory, or a `cobc -C` run of your own); a manifest regenerated from `sources` outranks a `manifestDirs` entry for the same program. With neither, the session shows the engine's C view and stepping lands on any `.cob`/`.cpy` line.
145145
- cobc is not needed to attach with `manifestDirs`: the shim and the vendored CodeLLDB are all that runs.
146146
- On Windows the attach stop is reported on the break thread the OS injects (exception `0x80000003`), a pause may land on a runtime worker thread: when the reported thread has no COBOL frame, the shim reports the stop on the thread that is inside the COBOL program (the description names both), so `get_stack_trace` shows the program on first contact. Breakpoint, step, entry and runtime-error stops are always on the right thread and are left alone.
147-
- A batch job is usually paused inside libcob or a `C$SLEEP`/I/O call, a frame with no COBOL source: `get_scopes`, `get_local_variables` and `evaluate_expression` then serve the nearest COBOL program up the stack (the scope names say whose and how far up, `WORKING-STORAGE of PAYROLL (0000-MAIN, 3 frames up)`), so the data division is visible on first contact; `get_stack_trace` shows the COBOL frame's paragraph.
147+
- A batch job is usually paused inside libcob or a `C$SLEEP`/I/O call, a frame with no COBOL source: `get_scopes`, `get_local_variables` and `evaluate_expression` then serve the nearest COBOL program up the stack (the scope names say whose and how far up, `WORKING-STORAGE of PAYROLL (0000-MAIN, 3 frames up)`), so the data division is visible on first contact; `get_stack_trace` shows the COBOL frame's paragraph. Frames CodeLLDB names by symbol (`@…` — libcob, the C runtime or the OS without debug info) carry `unresolvedSource: true` and the `note` says they are native frames, not source maps (issue #816; the mechanism is in the C/C++ guide's inspect step).
148148
- When the client that started the session goes away (the server or the proxy dies), the shim detaches: an attached process is never terminated the way a launched one is.
149149
- The target is held paused after attach (`stopOnEntry` is a top-level `attach_to_process` parameter and defaults to `true`; pass `false` to resume immediately). That initial stop is reported as `lastStop.reason: "pause"`; on Windows the adapter's own words stay beside it as `rawReason: "exception"` and the `0x80000003` description (issue #817; the mechanism is in the C/C++ guide's attach section).
150150
- Recognised `adapterConfig` keys: `processId`/`pid`, `program` (CodeLLDB's explicit-binary hint; a relative path is resolved against `cwd` before it reaches CodeLLDB), `cwd`, `waitFor`, `manifestDirs`, `engineScopes`, `sources`, `dialect`, `format`, `copybookDirs`, `cobcFlags`, `runtimeChecks`, `forceRebuild`, `initCommands`, `preRunCommands`, `postRunCommands`, `exitCommands`, `targetCreateCommands`, `processCreateCommands`, `expressions`, `sourceMap`, `sourceLanguages`, `relativePathBase`, `breakpointMode`. The manifest keys (`sources` … `forceRebuild`, `manifestDirs`, `engineScopes`, `cwd`) are consumed by the adapter and never appear in the attach request, and are not reported as ignored. Unlisted keys are still forwarded, with a warning naming them.

‎docs/cpp/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ Data breakpoints (hardware watchpoints), disassembly view, instruction stepping,
8787
3. `set_breakpoint` on the source file (or `function: "name"` for function breakpoints — a bare `main` works fine in C/C++)
8888
4. `start_debugging` with the executable (or source) path
8989
5. Step (`step_over`/`step_into`/`step_out`), `continue_execution`, `pause_execution`
90-
6. Inspect: `get_stack_trace`, `get_local_variables`, `evaluate_expression` (LLDB expressions, e.g. `ptr->field`, `vec.size()`)
90+
6. Inspect: `get_stack_trace`, `get_local_variables`, `evaluate_expression` (LLDB expressions, e.g. `ptr->field`, `vec.size()`). Frames CodeLLDB names by symbol (`@NtWaitForSingleObject`, `@BaseThreadInitThunk`, `@__tmainCRTStartup`) are native code without debug info — system libraries and CRT start-up; they carry `unresolvedSource: true` and the `note` says so. The program's own frames are the ones with a file path (issue #816)
9191
7. `get_output` for captured stdout/stderr (Windows: forwarded via adapter stdio; POSIX: CodeLLDB output events)
9292
8. `close_debug_session`
9393

‎docs/stack-trace-filtering.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ The filtering is implemented using the existing `AdapterPolicy` system:
151151
- **All frames internal**: The filtered stack is never empty when the adapter reported frames. `FrameAnchorResolver` keeps the top (unfiltered) frame and sets `allFramesInternal`, so `get_scopes` and `evaluate_expression` always have a valid `frameId`; the `note` says so and points at `includeInternals: true` (issue #346). This guarantee is central and applies to every language, Go and .NET included. One policy additionally softens the result itself — Java returns the full unfiltered array (so a thread parked deep in JDK code still shows its stack)
152152
- **Paused frame internal, ancestors visible** (issue #672): when the stopped thread's top frame is filtered while others survive, the resolver reports it as `pausedFrame` and keeps it as `frames[0]` on a breakpoint or step stop, or when the policy's `isAsyncBoundaryFrame` finds a boundary between it and the first visible frame; otherwise it stays hidden with `kept: false`. The decision is made only for the thread the stop event named (or the current thread when the event named none) and only while that stop is still the current one — a new stop landing mid-request falls back to plain filtering. A kept frame whose `file` is a relative label rather than a path (the JDI bridge's `java/io/PrintStream.java`) carries `unresolvedSource: true`
153153
- **No frames**: Returns empty array as before
154-
- **Unresolvable source-mapped frames** (issue #655): when the adapter reports a frame's source as not-a-file-on-this-host (DAP `sourceReference != 0` with a real-looking path — js-debug does this for a source map's `../src/x.ts` that the package never shipped), the frame carries `unresolvedSource: true` and the `note` says its `file` is a label, not an openable path. These frames are the debuggee's own code and are never hidden. On js attach this is rare now: `resolveSourceMapLocations` excludes `node_modules` by default (so dependency maps are not applied and those frames report their real `.js` path) and `cwd` is defaulted so the debuggee's own relative map sources resolve
154+
- **Frames without an openable source** (issues #655/#816): when the adapter reports a frame's source as not-a-file-on-this-host (DAP `sourceReference != 0` and no openable path), the frame carries `unresolvedSource: true` and the `note` says its `file` is a label, not an openable path, then adds the policy's account of it (`describeUnresolvedSource`). What the flag means differs by debugger: js-debug reports a source map's `../src/x.ts` that the package never shipped — those frames are the debuggee's own code and are never hidden, and the remedy is `sourceMaps: false` (spelled for the session's mode); CodeLLDB reports native frames without debug info as `@symbol` labels with a `sourceReference` (`@NtWaitForSingleObject`, `@BaseThreadInitThunk`) — runtime plumbing, where the program's own frames are the ones with a path; the JDI bridge names a class no breakpoint was set in by its package path (the #672 kept-frame case above). The hook receives the flagged frames' `file` labels, so a policy can tell its cases apart (an LLDB frame with a relative glibc path is a runtime built elsewhere, not a symbol-only frame). On js attach the flag is rare now: `resolveSourceMapLocations` excludes `node_modules` by default (so dependency maps are not applied and those frames report their real `.js` path) and `cwd` is defaulted so the debuggee's own relative map sources resolve
155155
- **Python**: No filtering applied (Python's AdapterPolicy does not implement `filterStackFrames`)
156156
- **Other languages**: Any language whose AdapterPolicy implements `filterStackFrames` has filtering applied
157157

‎docs/tool-reference.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -729,7 +729,7 @@ Gets the current call stack.
729729
- `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.
730730
- Frame IDs are used with `get_scopes`
731731
- 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.
732-
- A frame whose source the adapter could not find on this host (js-debug: a source-mapped `.ts` the package did not ship) carries `unresolvedSource: true`, and the `note` says its `file` is a label rather than an openable path — do not pass it to `get_source_context`.
732+
- 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.
733733
- 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.
734734
- 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).
735735
- When an explicit thread reports no frames, the response remains anchored to that thread and its `note` suggests a frame-bearing alternative when one is available.

‎docs/usage.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,7 @@ You can also evaluate arbitrary expressions in the current debug context:
353353
- Narrow the request to escape the cap: pass `names: ["a", "b"]` to `get_variables` or `get_local_variables` to fetch specific variables in full. Requested names that were not found are listed in the response's `notFound`
354354

355355
### Stack Trace Filtering
356-
- `get_stack_trace` filters internal/runtime frames by default (for JavaScript: Node internals, `node_modules` dependencies, and async separators). When any are hidden the response carries `hiddenFrames` (the count) and a `note` saying so; pass `includeInternals: true` to get the full stack. A frame with `unresolvedSource: true` has a `file` that is a label, not an openable path
356+
- `get_stack_trace` filters internal/runtime frames by default (for JavaScript: Node internals, `node_modules` dependencies, and async separators). When any are hidden the response carries `hiddenFrames` (the count) and a `note` saying so; pass `includeInternals: true` to get the full stack. A frame with `unresolvedSource: true` has a `file` that is a label, not an openable path, and the `note` says what it is under that adapter (a js-debug source map the package did not ship, a CodeLLDB native frame without debug info, a class the JDI bridge names by package path)
357357

358358
### Breakpoint Behavior
359359
- Breakpoints initially show `"verified": false` because verification happens asynchronously by the debug adapter once the module is loaded (e.g., debugpy verifies after the script starts)

‎packages/shared/src/interfaces/adapter-policy-cobol.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
getLldbDapClientBehavior,
3232
getLldbAttachBehavior,
3333
isLldbInternalFrame,
34+
describeLldbUnresolvedSource,
3435
lldbShouldSuppressOutputEvent
3536
} from './lldb-policy-shared.js';
3637

@@ -163,6 +164,9 @@ export const CobolAdapterPolicy = {
163164

164165
filterStackFrames: filterCobolStackFrames,
165166
isInternalFrame: isCobolInternalFrame,
167+
// The engine is CodeLLDB: a `@symbol` frame is native code without debug
168+
// info, not a source map (issue #816).
169+
describeUnresolvedSource: describeLldbUnresolvedSource,
166170

167171
extractLocalVariables: extractCobolLocalVariables,
168172

‎packages/shared/src/interfaces/adapter-policy-cpp.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
getLldbAttachBehavior,
3131
isLldbInternalFrame,
3232
filterLldbStackFrames,
33+
describeLldbUnresolvedSource,
3334
lldbShouldSuppressOutputEvent
3435
} from './lldb-policy-shared.js';
3536

@@ -63,6 +64,9 @@ export const CppAdapterPolicy = {
6364
*/
6465
filterStackFrames: filterLldbStackFrames,
6566
isInternalFrame: isLldbInternalFrame,
67+
// A `@symbol` frame is native code without debug info, not a source map
68+
// (issue #816).
69+
describeUnresolvedSource: describeLldbUnresolvedSource,
6670

6771
extractLocalVariables: extractLldbLocalVariables,
6872

0 commit comments

Comments
 (0)