microsoft / microsoft/vscode

Debug: "Add to Chat" on a stack frame with column 0 (per-spec "unknown") corrupts Range and permanently poisons the chat session

Open
#329,777 0 comments 0 reactions 1 assignee Claimed by @connor4312 View on GitHub
bug
Dominant language
TypeScript
Stars
193k
Forks
42.4k
PR merge metrics
PR metrics pending

Description

Does this issue occur when all extensions are disabled?: Yes/No

- VS Code Version:
- OS Version:

Steps to Reproduce:

1.
2.
## Title
Debug: "Add to Chat" on a stack frame with column 0 (per-spec "unknown") corrupts Range and permanently poisons the chat session

## Environment
- VS Code: 1.132.0 (Stable)
- GitHub Copilot Chat: 0.60.0
- Debug adapter: GDB-based (via OpenOCD, embedded ARM target), but root cause applies to any DAP adapter that reports column: 0

## Summary
Right-clicking a variable in the Run and Debug -> Variables view and choosing Add to Chat while stopped in a stack frame whose debug adapter reports column: 0 (a value the DAP spec explicitly defines and requires clients to treat as "unknown/ignore") causes VS Code to construct an invalid Range with a negative character offset. This isn't just a failed action -- it permanently corrupts the entire chat session, causing every subsequent prompt to fail with "Illegal argument: character must be non-negative", with no in-session recovery and no trace in the DevTools console.

## Steps to Reproduce
1. Debug any program via a GDB-based (or other column-agnostic) debug adapter.
2. Set a breakpoint on a local variable declaration, e.g.:
volatile uint32_t dbg_uid_word0 = uid->word0;
3. Start debugging, hit the breakpoint.
4. In Run and Debug -> Variables, right-click the local variable and choose Add to Chat.
5. Send any chat prompt.

(Generic, hardware-free repro for maintainers: any adapter/scenario producing a StackFrame with column: 0 will reproduce this -- this is a documented, spec-legal value, not an edge case specific to embedded debugging.)

## Actual Behavior
- The chat request fails immediately: "Illegal argument: character must be non-negative".
- Nothing appears in the DevTools console -- the exception is only visible in the Extension Host log (exthost*/remoteexthost.log).
- Every subsequent prompt in the same chat session fails identically, even in turns that never reference the variable again, and even after removing the attachment chip from the input box.
- The only recovery is abandoning the chat session entirely.

## Extension Host Stack Trace
Error: Illegal argument: character must be non-negative
at Position/Range construction (extHostTypes)
at Object.i [as toReferences]
at i.prepareHistoryTurns
at i._createRequest
at i.$invokeAgent

## Root Cause

Step 1 -- src/vs/workbench/contrib/debug/common/debugModel.ts, Thread.getCallStackImpl():

return response.body.stackFrames.map((rsf, index) => {
const source = this.session.getSource(rsf.source);
return new StackFrame(this, rsf.id, source, rsf.name, rsf.presentationHint,
new Range(rsf.line, rsf.column, rsf.endLine || rsf.line, rsf.endColumn || rsf.column),
startFrame + index, ...);
});

The raw DAP line/column values are passed directly into new Range(...) with no validation.

Per the official Debug Adapter Protocol schema (StackFrame.column in debugAdapterProtocol.json):
"Start position of the range covered by the stack frame... If attribute source is missing or doesn't exist, column is 0 and should be ignored by the client."

(The same applies to StackFrame.line.) This is a documented, spec-legal sentinel value -- not malformed adapter output. GDB-based and other adapters that don't track column-level debug info commonly report column: 0 for exactly this reason. debugModel.ts never checks for it.

Step 2 -- src/vs/workbench/contrib/debug/browser/debugChatIntegration.ts, createPausedLocationEntry():

function createPausedLocationEntry(stackFrame: IStackFrame): IChatRequestFileEntry {
const uri = stackFrame.source.uri;
let range = Range.lift(stackFrame.range);
if (range.isEmpty()) {
range = range.setEndPosition(range.startLineNumber + 1, 1);
}
return { kind: 'file', value: { uri, range }, ... };
}

This only guards against an empty range, not a zero-column one. Critically, this "paused location" entry is attached automatically alongside every debug-variable Add to Chat action (see createDebugAttachments()), not just when a user explicitly attaches the paused location -- so the invalid range enters chat context silently as a side effect of a completely unrelated user action.

Step 3 -- src/vs/workbench/api/common/extHostChatAgents2.ts, prepareHistoryTurns():

varsWithoutTools.push(...typeConvert.ChatPromptReference.toReferences(
v, this.getDiagnosticsWhenEnabled(extension), this._logService));

This runs on every new chat request to rebuild the full conversation history, converting the persisted (already 1-based-to-0-based-converted) column: 0 -> character: -1 into a real vscode.Position, which throws synchronously. Because this reconstruction is unconditional and unguarded per-entry, one bad historical reference aborts every future request in the session.

## Ramifications
1. Whole-session poisoning from a single action -- not a one-off failure; every future turn in the session fails identically, with no way to recover except starting a new chat.
2. Silent failure mode -- the only diagnostic trace lives in the Extension Host log file, invisible via DevTools; users have no way to self-diagnose.
3. Not an edge case -- column: 0 is explicit, documented DAP behavior for adapters without column-level granularity, meaning any GDB-based or similarly limited debug adapter can trigger this via a single ordinary Add to Chat click.
4. Broader surface -- the same failure mode likely affects any other reference source producing a Location with an unvalidated line/column (e.g., other DAP-derived views), not just Debug Variables.

## Proposed Fix

Primary fix -- validate/clamp per the DAP spec's own documented semantics, in debugModel.ts:

- new Range(rsf.line, rsf.column, rsf.endLine || rsf.line, rsf.endColumn || rsf.column),
+ new Range(rsf.line || 1, rsf.column || 1, rsf.endLine || rsf.line || 1, rsf.endColumn || rsf.column || 1),

Secondary fix -- guard the empty-or-invalid case in createPausedLocationEntry():

let range = Range.lift(stackFrame.range);
- if (range.isEmpty()) {
+ if (range.isEmpty() || range.startColumn < 1 || range.startLineNumber < 1) {
range = range.setEndPosition(range.startLineNumber + 1, 1);
}

Defense-in-depth -- make history reconstruction resilient to any future malformed reference, in extHostChatAgents2.ts:

for (const v of h.request.variables.variables) {
if (v.kind === 'tool') {
toolReferences.push(typeConvert.ChatLanguageModelToolReference.to(v));
} else if (v.kind === 'toolset') {
toolReferences.push(...v.value.map(typeConvert.ChatLanguageModelToolReference.to));
} else {
+ try {
varsWithoutTools.push(...typeConvert.ChatPromptReference.toReferences(
v, this.getDiagnosticsWhenEnabled(extension), this._logService));
+ } catch (err) {
+ this._logService.warn('Dropping malformed chat history reference', err);
+ }
}
}

This last change is the one I would consider highest priority even independent of the root cause fix above -- it ensures that no single malformed reference (from this bug or any future one) can ever again take down an entire chat session.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.