From 368a099cc9b5cc3cf7bb1f2298718824a1ab1118 Mon Sep 17 00:00:00 2001 From: Laxman Reddy <141967714+laileni-aws@users.noreply.github.com> Date: Tue, 18 Aug 2026 12:47:36 -0700 Subject: [PATCH 01/13] fix(amazonq): retry truncated tool-use streams instead of dropping them silently (#2841) When a tool-use content block is streamed but the response ends before its terminating `stop` event (e.g. the output-token limit is reached mid tool-input), the tool-use input is never JSON-parsed and no error is recorded, so the parser reports success. The agentic loop keeps only stopped tool uses as pending, so the unterminated one is filtered out, the turn is reported as Succeeded, and the loop breaks -- the tool never runs and the user sees no error or retry. Add AgenticChatEventParser.finalize(), called once the response stream is fully consumed: any tool use still lacking a stop is marked stopped and given an incomplete-input error (reusing the malformed-JSON prefix) so it survives the pending-tool-use filter and is routed into the existing recovery path, which re-prompts the model to split the work into smaller tool uses. User cancellation is unaffected (aborts throw before finalize runs). --- .../agenticChat/agenticChatController.ts | 5 +- .../agenticChatEventParser.test.ts | 52 +++++++++++++++++++ .../agenticChat/agenticChatEventParser.ts | 38 ++++++++++++++ 3 files changed, 94 insertions(+), 1 deletion(-) diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts index be67753637..269ba474bf 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts @@ -4668,7 +4668,10 @@ export class AgenticChatController implements ChatHandlers { cwsprChatResponseLength: chatEventParser.body?.length ?? 0, }) - return chatEventParser.getResult() + // Use finalize() (not getResult()) so a tool use whose stream ended before its + // terminating `stop` event is surfaced as an incomplete-input error and retried, + // rather than being silently dropped and reported as a successful turn. + return chatEventParser.finalize() } /** diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatEventParser.test.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatEventParser.test.ts index 917ae28179..a772fc808c 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatEventParser.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatEventParser.test.ts @@ -304,4 +304,56 @@ describe('AgenticChatEventParser', () => { assert.deepStrictEqual(chatEventParser.getResult(), { success: true, data: expectedData }) }) + + it('finalize flags an unterminated tool use (no stop event) as incomplete and marks it stopped', () => { + const chatEventParser = new AgenticChatEventParser(mockMessageId, new Metric(), logging) + + // A tool use whose input streams in fragments but never receives a `stop` event + // (e.g. the response hit the output-token limit mid tool-input). + chatEventParser.processPartialEvent({ + toolUseEvent: { + toolUseId: 'tool-1', + name: 'fsWrite', + input: '{"path":"out.py","fileText":"import os', + stop: false, + }, + }) + const midStream = chatEventParser.processPartialEvent({ + toolUseEvent: { + toolUseId: 'tool-1', + name: 'fsWrite', + input: '\\nprint(1)', + stop: false, + }, + }) + + // Mid-stream this looks like a clean success and would be filtered out (dropped) by the loop. + assert.strictEqual(midStream.success, true) + assert.strictEqual(midStream.data?.toolUses['tool-1'].stop, false) + + // After the stream ends, finalize() surfaces it as an error and marks it stopped so it + // survives the pending-tool-use filter and is routed into the existing retry path. + const result = chatEventParser.finalize() + assert.strictEqual(result.success, false) + assert.ok(result.error?.startsWith('ToolUse input is invalid JSON:')) + assert.strictEqual(result.data?.toolUses['tool-1'].stop, true) + }) + + it('finalize leaves a properly stopped tool use untouched', () => { + const chatEventParser = new AgenticChatEventParser(mockMessageId, new Metric(), logging) + + chatEventParser.processPartialEvent({ + toolUseEvent: { + toolUseId: 'tool-1', + name: 'fsWrite', + input: '{"path":"out.py","fileText":"print(1)"}', + stop: true, + }, + }) + + const result = chatEventParser.finalize() + assert.strictEqual(result.success, true) + assert.strictEqual(result.data?.toolUses['tool-1'].stop, true) + assert.deepStrictEqual(result.data?.toolUses['tool-1'].input, { path: 'out.py', fileText: 'print(1)' }) + }) }) diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatEventParser.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatEventParser.ts index 6878e19caf..c424524992 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatEventParser.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatEventParser.ts @@ -229,4 +229,42 @@ export class AgenticChatEventParser implements ChatResult { data: chatResultWithMetadata, } } + + /** + * Finalizes parsing after the response stream has been fully consumed. + * + * A tool use only becomes executable once its terminating `stop` event arrives — that + * is also the only point at which its streamed input is JSON-parsed. If the stream ends + * before that event (for example, the response reaches the output-token limit mid + * tool-input), the tool use is left with `stop === false` and no error is recorded, so + * `getResult()` reports success. The agentic loop then filters it out (only stopped tool + * uses are treated as pending) and completes the turn without ever running the tool — a + * silent no-op with no error and no retry. + * + * To avoid that, treat any still-unstopped tool use as incomplete/truncated input: + * record an error (reusing the malformed-JSON prefix so it is handled by the existing + * recovery branch) and mark it `stop` so it survives the pending-tool-use filter and the + * model is re-prompted to split the work into smaller tool uses. + */ + public finalize(): Result { + for (const toolUseId of Object.keys(this.toolUses)) { + const toolUse = this.toolUses[toolUseId] + if (!toolUse.stop) { + const received = typeof toolUse.input === 'string' ? toolUse.input.length : 0 + this.#logging.error( + `ToolUse ${toolUseId} (${toolUse.name}) stream ended before a stop event after ${received} characters; input was truncated` + ) + // Reuse the malformed-JSON error prefix so this routes into the same recovery + // branch in the agentic loop. Keep the message short so the (potentially very + // large) partial input is not echoed back to the model in the tool result. + this.error = `ToolUse input is invalid JSON: incomplete tool input, stream ended after ${received} characters before completion.` + this.toolUses[toolUseId] = { + ...toolUse, + input: {}, + stop: true, + } + } + } + return this.getResult() + } } From 37dd2d1ccc5a2048821f6a8ba6281d6a06f9b934 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 18 Aug 2026 15:34:11 -0700 Subject: [PATCH 02/13] chore(release): release packages from branch main (#2839) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .release-please-manifest.json | 14 +++++++------- chat-client/CHANGELOG.md | 7 +++++++ chat-client/package.json | 2 +- core/aws-lsp-core/CHANGELOG.md | 7 +++++++ core/aws-lsp-core/package.json | 2 +- package-lock.json | 22 +++++++++++----------- server/aws-lsp-antlr4/CHANGELOG.md | 14 ++++++++++++++ server/aws-lsp-antlr4/package.json | 4 ++-- server/aws-lsp-codewhisperer/CHANGELOG.md | 20 ++++++++++++++++++++ server/aws-lsp-codewhisperer/package.json | 4 ++-- server/aws-lsp-json/CHANGELOG.md | 14 ++++++++++++++ server/aws-lsp-json/package.json | 4 ++-- server/aws-lsp-partiql/CHANGELOG.md | 7 +++++++ server/aws-lsp-partiql/package.json | 2 +- server/aws-lsp-yaml/CHANGELOG.md | 14 ++++++++++++++ server/aws-lsp-yaml/package.json | 4 ++-- 16 files changed, 112 insertions(+), 29 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index e2ae92303c..90080d6ba5 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,9 +1,9 @@ { - "chat-client": "0.1.55", - "core/aws-lsp-core": "0.0.21", - "server/aws-lsp-antlr4": "0.1.25", - "server/aws-lsp-codewhisperer": "0.0.124", - "server/aws-lsp-json": "0.1.26", - "server/aws-lsp-partiql": "0.0.23", - "server/aws-lsp-yaml": "0.1.26" + "chat-client": "0.1.56", + "core/aws-lsp-core": "0.0.22", + "server/aws-lsp-antlr4": "0.1.26", + "server/aws-lsp-codewhisperer": "0.0.125", + "server/aws-lsp-json": "0.1.27", + "server/aws-lsp-partiql": "0.0.24", + "server/aws-lsp-yaml": "0.1.27" } diff --git a/chat-client/CHANGELOG.md b/chat-client/CHANGELOG.md index b6ec7991b8..ab8096741e 100644 --- a/chat-client/CHANGELOG.md +++ b/chat-client/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [0.1.56](https://github.com/Amazon-Q-Developer/language-servers/compare/chat-client/v0.1.55...chat-client/v0.1.56) (2026-08-18) + + +### Bug Fixes + +* repoint CODEOWNERS and package repository URLs to the new org ([#2838](https://github.com/Amazon-Q-Developer/language-servers/issues/2838)) ([43e44dc](https://github.com/Amazon-Q-Developer/language-servers/commit/43e44dc5af94703f51456cd8a913f0d61af7570e)) + ## [0.1.55](https://github.com/aws/language-servers/compare/chat-client/v0.1.54...chat-client/v0.1.55) (2026-06-30) diff --git a/chat-client/package.json b/chat-client/package.json index 1def952379..b3a1aa46b7 100644 --- a/chat-client/package.json +++ b/chat-client/package.json @@ -1,6 +1,6 @@ { "name": "@aws/chat-client", - "version": "0.1.55", + "version": "0.1.56", "description": "AWS Chat Client", "main": "out/index.js", "repository": { diff --git a/core/aws-lsp-core/CHANGELOG.md b/core/aws-lsp-core/CHANGELOG.md index 868606d0ac..9967096253 100644 --- a/core/aws-lsp-core/CHANGELOG.md +++ b/core/aws-lsp-core/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [0.0.22](https://github.com/Amazon-Q-Developer/language-servers/compare/lsp-core/v0.0.21...lsp-core/v0.0.22) (2026-08-18) + + +### Bug Fixes + +* repoint CODEOWNERS and package repository URLs to the new org ([#2838](https://github.com/Amazon-Q-Developer/language-servers/issues/2838)) ([43e44dc](https://github.com/Amazon-Q-Developer/language-servers/commit/43e44dc5af94703f51456cd8a913f0d61af7570e)) + ## [0.0.21](https://github.com/aws/language-servers/compare/lsp-core/v0.0.20...lsp-core/v0.0.21) (2026-02-17) diff --git a/core/aws-lsp-core/package.json b/core/aws-lsp-core/package.json index 0874682d04..9aec44109c 100644 --- a/core/aws-lsp-core/package.json +++ b/core/aws-lsp-core/package.json @@ -1,6 +1,6 @@ { "name": "@aws/lsp-core", - "version": "0.0.21", + "version": "0.0.22", "description": "Core library, contains common code and utilities", "main": "out/index.js", "repository": { diff --git a/package-lock.json b/package-lock.json index 3f193aa49e..13c9c30348 100644 --- a/package-lock.json +++ b/package-lock.json @@ -251,7 +251,7 @@ }, "chat-client": { "name": "@aws/chat-client", - "version": "0.1.55", + "version": "0.1.56", "license": "Apache-2.0", "dependencies": { "@aws/chat-client-ui-types": "0.1.71", @@ -302,7 +302,7 @@ }, "core/aws-lsp-core": { "name": "@aws/lsp-core", - "version": "0.0.21", + "version": "0.0.22", "license": "Apache-2.0", "dependencies": { "@aws/language-server-runtimes": "^0.3.14", @@ -30287,11 +30287,11 @@ }, "server/aws-lsp-antlr4": { "name": "@aws/lsp-antlr4", - "version": "0.1.25", + "version": "0.1.26", "license": "Apache-2.0", "dependencies": { "@aws/language-server-runtimes": "^0.3.14", - "@aws/lsp-core": "^0.0.21" + "@aws/lsp-core": "^0.0.22" }, "devDependencies": { "@babel/plugin-transform-modules-commonjs": "^7.24.1", @@ -30351,7 +30351,7 @@ }, "server/aws-lsp-codewhisperer": { "name": "@aws/lsp-codewhisperer", - "version": "0.0.124", + "version": "0.0.125", "bundleDependencies": [ "@amzn/codewhisperer", "@amzn/codewhisperer-runtime", @@ -30372,7 +30372,7 @@ "@aws-sdk/util-retry": "^3.374.0", "@aws/chat-client-ui-types": "0.1.71", "@aws/language-server-runtimes": "^0.3.21", - "@aws/lsp-core": "^0.0.21", + "@aws/lsp-core": "^0.0.22", "@modelcontextprotocol/sdk": "^1.23.0", "@mozilla/readability": "^0.6.0", "@smithy/node-http-handler": "^2.5.0", @@ -30642,11 +30642,11 @@ }, "server/aws-lsp-json": { "name": "@aws/lsp-json", - "version": "0.1.26", + "version": "0.1.27", "license": "Apache-2.0", "dependencies": { "@aws/language-server-runtimes": "^0.3.14", - "@aws/lsp-core": "^0.0.21", + "@aws/lsp-core": "^0.0.22", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.8" }, @@ -30718,7 +30718,7 @@ }, "server/aws-lsp-partiql": { "name": "@aws/lsp-partiql", - "version": "0.0.23", + "version": "0.0.24", "license": "Apache-2.0", "dependencies": { "@aws/language-server-runtimes": "^0.3.14", @@ -30768,12 +30768,12 @@ }, "server/aws-lsp-yaml": { "name": "@aws/lsp-yaml", - "version": "0.1.26", + "version": "0.1.27", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { "@aws/language-server-runtimes": "^0.3.14", - "@aws/lsp-core": "^0.0.21", + "@aws/lsp-core": "^0.0.22", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.8", "yaml-language-server": "1.13.0" diff --git a/server/aws-lsp-antlr4/CHANGELOG.md b/server/aws-lsp-antlr4/CHANGELOG.md index 85c4542684..ad6bffd9a3 100644 --- a/server/aws-lsp-antlr4/CHANGELOG.md +++ b/server/aws-lsp-antlr4/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## [0.1.26](https://github.com/Amazon-Q-Developer/language-servers/compare/lsp-antlr4/v0.1.25...lsp-antlr4/v0.1.26) (2026-08-18) + + +### Bug Fixes + +* repoint CODEOWNERS and package repository URLs to the new org ([#2838](https://github.com/Amazon-Q-Developer/language-servers/issues/2838)) ([43e44dc](https://github.com/Amazon-Q-Developer/language-servers/commit/43e44dc5af94703f51456cd8a913f0d61af7570e)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @aws/lsp-core bumped from ^0.0.21 to ^0.0.22 + ## [0.1.25](https://github.com/aws/language-servers/compare/lsp-antlr4/v0.1.24...lsp-antlr4/v0.1.25) (2026-02-17) diff --git a/server/aws-lsp-antlr4/package.json b/server/aws-lsp-antlr4/package.json index 918f2ef5f7..ac12cf53b5 100644 --- a/server/aws-lsp-antlr4/package.json +++ b/server/aws-lsp-antlr4/package.json @@ -1,6 +1,6 @@ { "name": "@aws/lsp-antlr4", - "version": "0.1.25", + "version": "0.1.26", "description": "ANTLR4 language server", "main": "out/index.js", "repository": { @@ -29,7 +29,7 @@ }, "dependencies": { "@aws/language-server-runtimes": "^0.3.14", - "@aws/lsp-core": "^0.0.21" + "@aws/lsp-core": "^0.0.22" }, "peerDependencies": { "antlr4-c3": ">=3.4 < 4", diff --git a/server/aws-lsp-codewhisperer/CHANGELOG.md b/server/aws-lsp-codewhisperer/CHANGELOG.md index 22eb2e02df..17d1d801a6 100644 --- a/server/aws-lsp-codewhisperer/CHANGELOG.md +++ b/server/aws-lsp-codewhisperer/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog +## [0.0.125](https://github.com/Amazon-Q-Developer/language-servers/compare/lsp-codewhisperer/v0.0.124...lsp-codewhisperer/v0.0.125) (2026-08-18) + + +### Features + +* **amazonq:** initialize Q services when credentials arrive ([#2840](https://github.com/Amazon-Q-Developer/language-servers/issues/2840)) ([4e0d3b0](https://github.com/Amazon-Q-Developer/language-servers/commit/4e0d3b0d69e0aa7a230f64a1b168a09bcfd4b7cd)) + + +### Bug Fixes + +* **amazonq:** retry truncated tool-use streams instead of dropping them silently ([#2841](https://github.com/Amazon-Q-Developer/language-servers/issues/2841)) ([368a099](https://github.com/Amazon-Q-Developer/language-servers/commit/368a099cc9b5cc3cf7bb1f2298718824a1ab1118)) +* repoint CODEOWNERS and package repository URLs to the new org ([#2838](https://github.com/Amazon-Q-Developer/language-servers/issues/2838)) ([43e44dc](https://github.com/Amazon-Q-Developer/language-servers/commit/43e44dc5af94703f51456cd8a913f0d61af7570e)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @aws/lsp-core bumped from ^0.0.21 to ^0.0.22 + ## [0.0.124](https://github.com/aws/language-servers/compare/lsp-codewhisperer/v0.0.123...lsp-codewhisperer/v0.0.124) (2026-08-13) diff --git a/server/aws-lsp-codewhisperer/package.json b/server/aws-lsp-codewhisperer/package.json index 120744dc41..201e72d402 100644 --- a/server/aws-lsp-codewhisperer/package.json +++ b/server/aws-lsp-codewhisperer/package.json @@ -1,6 +1,6 @@ { "name": "@aws/lsp-codewhisperer", - "version": "0.0.124", + "version": "0.0.125", "description": "CodeWhisperer Language Server", "main": "out/index.js", "repository": { @@ -39,7 +39,7 @@ "@aws-sdk/util-retry": "^3.374.0", "@aws/chat-client-ui-types": "0.1.71", "@aws/language-server-runtimes": "^0.3.21", - "@aws/lsp-core": "^0.0.21", + "@aws/lsp-core": "^0.0.22", "@modelcontextprotocol/sdk": "^1.23.0", "@mozilla/readability": "^0.6.0", "@smithy/node-http-handler": "^2.5.0", diff --git a/server/aws-lsp-json/CHANGELOG.md b/server/aws-lsp-json/CHANGELOG.md index 15449049b7..977d7dda14 100644 --- a/server/aws-lsp-json/CHANGELOG.md +++ b/server/aws-lsp-json/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## [0.1.27](https://github.com/Amazon-Q-Developer/language-servers/compare/lsp-json/v0.1.26...lsp-json/v0.1.27) (2026-08-18) + + +### Bug Fixes + +* repoint CODEOWNERS and package repository URLs to the new org ([#2838](https://github.com/Amazon-Q-Developer/language-servers/issues/2838)) ([43e44dc](https://github.com/Amazon-Q-Developer/language-servers/commit/43e44dc5af94703f51456cd8a913f0d61af7570e)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @aws/lsp-core bumped from ^0.0.21 to ^0.0.22 + ## [0.1.26](https://github.com/aws/language-servers/compare/lsp-json/v0.1.25...lsp-json/v0.1.26) (2026-02-17) diff --git a/server/aws-lsp-json/package.json b/server/aws-lsp-json/package.json index b1c22c3601..b9f6ea357c 100644 --- a/server/aws-lsp-json/package.json +++ b/server/aws-lsp-json/package.json @@ -1,6 +1,6 @@ { "name": "@aws/lsp-json", - "version": "0.1.26", + "version": "0.1.27", "description": "JSON Language Server", "main": "out/index.js", "repository": { @@ -27,7 +27,7 @@ }, "dependencies": { "@aws/language-server-runtimes": "^0.3.14", - "@aws/lsp-core": "^0.0.21", + "@aws/lsp-core": "^0.0.22", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.8" }, diff --git a/server/aws-lsp-partiql/CHANGELOG.md b/server/aws-lsp-partiql/CHANGELOG.md index ddd53ffcdf..8577629e33 100644 --- a/server/aws-lsp-partiql/CHANGELOG.md +++ b/server/aws-lsp-partiql/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [0.0.24](https://github.com/Amazon-Q-Developer/language-servers/compare/lsp-partiql/v0.0.23...lsp-partiql/v0.0.24) (2026-08-18) + + +### Bug Fixes + +* repoint CODEOWNERS and package repository URLs to the new org ([#2838](https://github.com/Amazon-Q-Developer/language-servers/issues/2838)) ([43e44dc](https://github.com/Amazon-Q-Developer/language-servers/commit/43e44dc5af94703f51456cd8a913f0d61af7570e)) + ## [0.0.23](https://github.com/aws/language-servers/compare/lsp-partiql/v0.0.22...lsp-partiql/v0.0.23) (2026-02-17) diff --git a/server/aws-lsp-partiql/package.json b/server/aws-lsp-partiql/package.json index cf735c6184..12cf012af8 100644 --- a/server/aws-lsp-partiql/package.json +++ b/server/aws-lsp-partiql/package.json @@ -3,7 +3,7 @@ "author": "Amazon Web Services", "license": "Apache-2.0", "description": "PartiQL language server", - "version": "0.0.23", + "version": "0.0.24", "repository": { "type": "git", "url": "https://github.com/Amazon-Q-Developer/language-servers" diff --git a/server/aws-lsp-yaml/CHANGELOG.md b/server/aws-lsp-yaml/CHANGELOG.md index 4b20d35253..fe6850ee84 100644 --- a/server/aws-lsp-yaml/CHANGELOG.md +++ b/server/aws-lsp-yaml/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## [0.1.27](https://github.com/Amazon-Q-Developer/language-servers/compare/lsp-yaml/v0.1.26...lsp-yaml/v0.1.27) (2026-08-18) + + +### Bug Fixes + +* repoint CODEOWNERS and package repository URLs to the new org ([#2838](https://github.com/Amazon-Q-Developer/language-servers/issues/2838)) ([43e44dc](https://github.com/Amazon-Q-Developer/language-servers/commit/43e44dc5af94703f51456cd8a913f0d61af7570e)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @aws/lsp-core bumped from ^0.0.21 to ^0.0.22 + ## [0.1.26](https://github.com/aws/language-servers/compare/lsp-yaml/v0.1.25...lsp-yaml/v0.1.26) (2026-02-17) diff --git a/server/aws-lsp-yaml/package.json b/server/aws-lsp-yaml/package.json index 64119bdcf2..cd5348b36b 100644 --- a/server/aws-lsp-yaml/package.json +++ b/server/aws-lsp-yaml/package.json @@ -1,6 +1,6 @@ { "name": "@aws/lsp-yaml", - "version": "0.1.26", + "version": "0.1.27", "description": "YAML Language Server", "main": "out/index.js", "repository": { @@ -27,7 +27,7 @@ }, "dependencies": { "@aws/language-server-runtimes": "^0.3.14", - "@aws/lsp-core": "^0.0.21", + "@aws/lsp-core": "^0.0.22", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.8", "yaml-language-server": "1.13.0" From 9f8bb8943c41edc105c460d4b2c60836183cdd2b Mon Sep 17 00:00:00 2001 From: Rajanna-Karthik Date: Wed, 19 Aug 2026 08:52:39 -0700 Subject: [PATCH 03/13] fix: scope and forward stepId for the planning-branch LBV HITL (#2835) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: scope and forward stepId for the planning-branch lbv hitl The AWAITING_HUMAN_INPUT path already scopes the local-build-verification HITL to the loaded beamed repo and forwards its plan-step id, but the PLANNING branch did neither: it picked the first LBV HITL in the list and returned it without StepInformation. On a multi-repo beam where the job sits in PLANNING at build time, that surfaces a sibling's (or untagged) HITL, so the IDE scope guard cannot confirm ownership and defers the build indefinitely. Bring the PLANNING branch to parity with AWAITING_HUMAN_INPUT: - prefer the in-scope LBV via selectScopedLbvHitl (no-op when scope is empty, so non-beam behavior is unchanged) - forward StepInformation.StepId when present Verified live on a 3-repo beam: each loaded repo now builds and siblings are correctly deferred. * fix: suppress out-of-scope sibling LBVs in the planning branch Brings the PLANNING branch of getTransformInfo to full parity with the EXECUTING / getHitlAgentArtifact paths for multi-repo beam. It already preferred the loaded repo's in-scope LBV and forwarded its stepId, but it did not suppress out-of-scope sibling LBVs: when scope was set with no in-scope LBV it could still surface a sibling's LBV HITL to the IDE. Mirror the sibling branches: - when scope is set and every pending HITL is a sibling's LBV, return plan-only (surface nothing) - otherwise drop out-of-scope LBVs from the fallback pool so a sibling's LBV can't be picked in a mixed pending set Not a live false-green (the IDE scope guard already rejects a HITL whose stepId is out of the loaded subtree) — this restores the LSP-side layer so all three branches behave identically. Non-beam is byte-identical: the new logic is gated on a non-empty beam scope. * fix: harden beam LSP stepId coalescing and tidy beam docs - getStepId uses || so an empty-string id falls through to the next spelling (was ?? which let an empty id defeat coalescing and scope matching) - move the normalizeBeamRepo JSDoc onto normalizeBeamRepo (was stranded above getStepId) * test: cover beam LBV planning-scope and getStepId coalescing - PLANNING branch: a sibling repo's out-of-scope LBV is not surfaced - getStepId: empty stepId falls through to planStepId/parentStepId (|| not ??) --- .../netTransform/atxTransformHandler.ts | 61 +++++++++++++++---- .../tests/atxTransformHandler.test.ts | 33 ++++++++++ 2 files changed, 83 insertions(+), 11 deletions(-) diff --git a/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxTransformHandler.ts b/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxTransformHandler.ts index 70649ff0fc..6566297acf 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxTransformHandler.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxTransformHandler.ts @@ -841,11 +841,6 @@ export class ATXTransformHandler { } } - /** - * Normalize a beam-map repo item into the PascalCase shape the C# IDE consumes, - * tolerant of key-name variants from the web writer (repoName/repo/name, - * artifactId/beamArtifactId, stepId/planStepId, targetFramework/tfm). - */ /** * The wire contract with the web orchestrator / FES is not yet pinned to a single * spelling: a plan-step / HITL id arrives as stepId, planStepId, or parentStepId @@ -855,7 +850,9 @@ export class ATXTransformHandler { */ private getStepId(obj: any): string | undefined { if (obj == null || typeof obj !== 'object') return undefined - return obj.stepId ?? obj.planStepId ?? obj.parentStepId ?? undefined + // Use || (not ??) so an empty-string id falls through to the next spelling; an empty + // stepId must not short-circuit coalescing and then fail every scope match. + return obj.stepId || obj.planStepId || obj.parentStepId || undefined } /** @@ -879,6 +876,11 @@ export class ATXTransformHandler { return { scopedLbv, allOutOfScopeLbv } } + /** + * Normalize a beam-map repo item into the PascalCase shape the C# IDE consumes, + * tolerant of key-name variants from the web writer (repoName/repo/name, + * artifactId/beamArtifactId, stepId/planStepId, targetFramework/tfm). + */ private normalizeBeamRepo(r: BeamMapRepo | null | undefined): Omit { const o: BeamMapRepo = r || {} return { @@ -2001,13 +2003,46 @@ export class ATXTransformHandler { // Plan not available yet } - // Check for pending HITL tasks (e.g. missing packages) - job stays in PLANNING while HITL is pending + // Check for pending HITL tasks (e.g. missing packages) - job stays in PLANNING while HITL is pending. + // Beam multi-repo: several repos can have a pending LBV HITL at once; prefer the loaded repo's + // in-scope one (parity with AWAITING_HUMAN_INPUT). No-op when scope is empty, so non-beam is unchanged. const hitls = await this.listHitls(request.WorkspaceId, request.TransformationJobId) if (hitls && hitls.length > 0) { + const planScopeStepIds = (request.beamScopeStepIds || '') + .split(',') + .map(s => s.trim()) + .filter(s => s.length > 0) + const { scopedLbv: scopedPlanLbv, allOutOfScopeLbv: planAllOutOfScopeLbv } = + this.selectScopedLbvHitl(hitls, planScopeStepIds) + // Beam multi-repo parity with EXECUTING / getHitlAgentArtifact: if scope is set and every + // pending HITL is a sibling repo's LBV (none in the loaded repo's subtree), do NOT surface a + // sibling's LBV — return plan-only. Prevents handing the IDE another repo's build HITL. + if (planScopeStepIds.length > 0 && planAllOutOfScopeLbv) { + this.logging.log( + 'ATX: PLANNING job — all pending HITLs are sibling-repo LBV (none in loaded scope); not surfacing' + ) + return { + TransformationJob: { + WorkspaceId: request.WorkspaceId, + JobId: request.TransformationJobId, + Status: jobStatus, + } as AtxTransformationJob, + TransformationPlan: plan, + } as AtxGetTransformInfoResponse + } + // When scope is set but no in-scope LBV matched, drop out-of-scope LBVs from the fallback + // pool so the plain find() below can't select a sibling's LBV (mixed pending-set case). + const planFallbackPool = + planScopeStepIds.length > 0 && !scopedPlanLbv + ? hitls.filter(h => h.tag !== 'local-build-verification') + : hitls const hitl = - hitls.find(h => h.tag === 'local-build-verification') || - hitls.find(h => h.tag === 'missing-packages' || h.tag === 'handle_missing_packages_hitl') || - hitls[0] + scopedPlanLbv || + planFallbackPool.find(h => h.tag === 'local-build-verification') || + planFallbackPool.find( + h => h.tag === 'missing-packages' || h.tag === 'handle_missing_packages_hitl' + ) || + planFallbackPool[0] this.logging.log(`ATX: Found HITL task - tag: ${hitl.tag}, hasArtifact: ${!!hitl.agentArtifact}`) // For missing packages HITL, try to download artifact if available if (hitl.tag === 'handle_missing_packages_hitl' || hitl.tag === 'missing-packages') { @@ -2036,8 +2071,11 @@ export class ATXTransformHandler { } if (hitl.tag === 'local-build-verification') { this.jobsPastLocalBuild.add(request.TransformationJobId) + // Forward the HITL's plan-step id so a multi-repo beam IDE can confirm this LBV + // belongs to the loaded repo (parity with AWAITING_HUMAN_INPUT). Undefined when absent. + const planLbvStepId = this.getStepId(hitl) this.logging.log( - `ATX: ${jobStatus} job has pending LBV HITL — taskId=${hitl.taskId}; surfacing AWAITING_HUMAN_INPUT to IDE` + `ATX: ${jobStatus} job has pending LBV HITL — taskId=${hitl.taskId} stepId=${planLbvStepId ?? ''}; surfacing AWAITING_HUMAN_INPUT to IDE` ) return { TransformationJob: { @@ -2047,6 +2085,7 @@ export class ATXTransformHandler { } as AtxTransformationJob, HitlTag: hitl.tag, HitlTaskId: hitl.taskId, + StepInformation: planLbvStepId ? { StepId: planLbvStepId } : undefined, TransformationPlan: plan, } as AtxGetTransformInfoResponse } diff --git a/server/aws-lsp-codewhisperer/src/language-server/netTransform/tests/atxTransformHandler.test.ts b/server/aws-lsp-codewhisperer/src/language-server/netTransform/tests/atxTransformHandler.test.ts index 7f9b4fddbd..b8a0894b5c 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/netTransform/tests/atxTransformHandler.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/netTransform/tests/atxTransformHandler.test.ts @@ -497,6 +497,30 @@ describe('ATXTransformHandler - getTransformInfo', () => { expect((handler as any).jobsPastLocalBuild.has('job-123')).to.be.true }) + it('should NOT surface a sibling repo LBV when status is PLANNING and all pending LBVs are out of the loaded scope', async () => { + // Beam multi-repo: the IDE loaded one repo (beamScopeStepIds = its subtree). Every pending + // HITL is a local-build-verification for a DIFFERENT (sibling) repo's plan step. Surfacing + // one would make the IDE build the loaded solution against a sibling's HITL → false-green. + // Mirrors the EXECUTING/getHitlAgentArtifact guard: return the plan-only view instead. + getJobStub.resolves({ statusDetails: { status: 'PLANNING' } }) + getTransformationPlanStub.resolves({ Root: { Children: [] } }) + listHitlsStub.resolves([ + { tag: 'local-build-verification', taskId: 'task-sib1', stepId: 'sibling-step-1' }, + { tag: 'local-build-verification', taskId: 'task-sib2', stepId: 'sibling-step-2' }, + ]) + + const result = await handler.getTransformInfo({ + ...baseRequest, + beamScopeStepIds: 'loaded-step-a,loaded-step-b', + }) + + // Plan-only view: original job status preserved, no HITL surfaced to the IDE. + expect(result?.TransformationJob.Status).to.equal('PLANNING') + expect(result?.HitlTag).to.be.undefined + expect(result?.HitlTaskId).to.be.undefined + expect(result?.TransformationPlan).to.deep.equal({ Root: { Children: [] } }) + }) + it('should filter pre-job mode-selection -checkpoint HITL before LBV has run', async () => { getJobStub.resolves({ statusDetails: { status: 'PLANNING' } }) getTransformationPlanStub.resolves({ Root: { Children: [] } }) @@ -4167,6 +4191,15 @@ describe('ATXTransformHandler - Beam to IDE', () => { expect(g('str')).to.be.undefined expect(g({ other: 'x' })).to.be.undefined }) + + it('skips an empty-string stepId and falls through to planStepId / parentStepId (|| not ??)', () => { + const g = (o: any) => (handler as any).getStepId(o) + // Empty stepId must NOT short-circuit coalescing (the ??→|| hardening) — an empty + // string is falsy under ||, so the non-empty planStepId is returned. + expect(g({ stepId: '', planStepId: 'step-1' })).to.equal('step-1') + // Empty stepId + empty planStepId → fall all the way through to parentStepId. + expect(g({ stepId: '', planStepId: '', parentStepId: 'parent-1' })).to.equal('parent-1') + }) }) // --- normalizeBeamRepo: tolerant wire-shape mapping --- From 1eb2f9f270c479ca457658e22f2ef0d06f6517ca Mon Sep 17 00:00:00 2001 From: Laxman Reddy <141967714+laileni-aws@users.noreply.github.com> Date: Thu, 20 Aug 2026 11:25:36 -0700 Subject: [PATCH 04/13] fix(amazonq): narrow tool-use truncation detection and classify retries in telemetry (#2847) * fix(amazonq): only retry tool-use streams that were genuinely truncated Treating every response stream that ends without a terminating tool-use `stop` event as a truncated tool input is too broad. A stream also ends without `stop` when the request is aborted (response-processing timeout or cancellation), and when the model announced a tool use but streamed no input at all. Reporting those as failures and re-prompting the model produces failed intermediate stream events for turns that were not broken, and can re-run the agent loop without making progress. - finalize() now only reports an incomplete tool input when partial input was actually received and the request was not aborted; other unterminated tool uses are left unstopped and filtered out downstream, as before. - The abort state is passed into finalize() from the response processor. - Consecutive incomplete tool-use retries are now bounded (MAX_INCOMPLETE_TOOL_USE_RETRIES). On exceeding the limit the agent loop stops and surfaces an actionable error instead of retrying indefinitely. Genuine truncation (partial input present, request not aborted) still routes into the existing recovery path and is retried. * fix(amazonq): allow 3 incomplete tool-use retries before giving up * feat(amazonq): classify incomplete tool-use retries in invokeLLM telemetry A response stream whose tool-use input is cut off is retried inside the agent loop and usually recovers within the same user turn. Every one of those iterations emits amazonq_invokeLLM with result='Failed', so a per-call success rate built on that metric drops even though the user was unaffected. Report a `reason` alongside the existing result so the two cases can be told apart downstream: INCOMPLETE_TOOL_USE_RETRYING retry budget remains; transient, recovers INCOMPLETE_TOOL_USE_EXHAUSTED retries used up; the user sees an error The classification is computed before the emit. Because the retry budget is bounded, whether this iteration will be retried is already known at that point, so no post-hoc correlation is needed. result stays 'Failed' in both cases, so raw failure counts are unchanged and remain available for diagnostics. Consumers can now exclude the transient class from success-rate calculations while still counting the terminal give-up. The reason values are consumed by ToolkitTelemetryLambda to emit a separate EMF counter; renaming them requires updating that transform first. Retry behaviour is unchanged: incrementing the counter before the emit and testing `count <= MAX_INCOMPLETE_TOOL_USE_RETRIES` preserves the existing off-by-one, so 3 retries still follow the initial failure. * chore(amazonq): describe the telemetry reason consumer generically The do-not-rename note on the reason constants pointed at a specific internal consumer by name. Describe it as a downstream metrics pipeline instead: the warning is what matters to anyone editing these values, and the constants are part of a contract rather than a link to one particular implementation. No functional change. --- .../agenticChat/agenticChatController.ts | 50 ++++++++++++++-- .../agenticChatEventParser.test.ts | 38 ++++++++++++ .../agenticChat/agenticChatEventParser.ts | 56 ++++++++++++------ .../agenticChat/constants/constants.ts | 20 +++++++ .../telemetry/chatTelemetryController.test.ts | 58 +++++++++++++++++++ .../chat/telemetry/chatTelemetryController.ts | 4 +- .../src/shared/telemetry/types.ts | 7 +++ 7 files changed, 210 insertions(+), 23 deletions(-) diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts index 269ba474bf..ac991235ed 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts @@ -180,6 +180,10 @@ import { OUTPUT_LIMIT_EXCEEDS_PARTIAL_MSG, RESPONSE_TIMEOUT_MS, RESPONSE_TIMEOUT_PARTIAL_MSG, + INCOMPLETE_TOOL_USE_RETRY_LIMIT_MSG, + MAX_INCOMPLETE_TOOL_USE_RETRIES, + INVOKE_LLM_REASON_INCOMPLETE_TOOL_USE_RETRYING, + INVOKE_LLM_REASON_INCOMPLETE_TOOL_USE_EXHAUSTED, COMPACTION_BODY, COMPACTION_HEADER_BODY, DEFAULT_MACOS_RUN_SHORTCUT, @@ -1427,6 +1431,9 @@ export class AgenticChatController implements ChatHandlers { let iterationCount = 0 let shouldDisplayMessage = true let currentRequestCount = 0 + // Number of consecutive responses that produced an incomplete tool-use input. Bounded + // so a model that keeps truncating cannot keep the agent loop running indefinitely. + let consecutiveIncompleteToolUses = 0 const pinnedContext = additionalContext?.filter(item => item.pinned) metric.recordStart() @@ -1645,6 +1652,8 @@ export class AgenticChatController implements ChatHandlers { let toolResults: ToolResult[] session.setConversationType('AgenticChatWithToolUse') if (result.success) { + // A complete tool use was received, so the incomplete-input streak is over. + consecutiveIncompleteToolUses = 0 // Process tool uses and update the request input for the next iteration toolResults = await this.processToolUses( pendingToolUses, @@ -1689,6 +1698,21 @@ export class AgenticChatController implements ChatHandlers { status: ToolResultStatus.ERROR, content: [{ text: result.error }], })) + // Classify the failure *before* emitting telemetry. An incomplete tool-use input is + // retried inside this loop and usually recovers within the same user turn, so it is + // not a user-visible failure and should not depress a per-call success rate. Because + // the retry budget is bounded we already know here whether this iteration will be + // retried, which lets us distinguish the transient case from the terminal give-up. + const isIncompleteToolUse = result.error.startsWith('ToolUse input is invalid JSON:') + let willRetryIncompleteToolUse = false + let invokeLlmReason: string | undefined + if (isIncompleteToolUse) { + consecutiveIncompleteToolUses++ + willRetryIncompleteToolUse = consecutiveIncompleteToolUses <= MAX_INCOMPLETE_TOOL_USE_RETRIES + invokeLlmReason = willRetryIncompleteToolUse + ? INVOKE_LLM_REASON_INCOMPLETE_TOOL_USE_RETRYING + : INVOKE_LLM_REASON_INCOMPLETE_TOOL_USE_EXHAUSTED + } this.#telemetryController.emitAgencticLoop_InvokeLLM( response.$metadata.requestId!, conversationId, @@ -1704,9 +1728,25 @@ export class AgenticChatController implements ChatHandlers { this.#timeBetweenChunks, session.pairProgrammingMode, this.#abTestingAllocation?.experimentName, - this.#abTestingAllocation?.userVariation + this.#abTestingAllocation?.userVariation, + invokeLlmReason ) - if (result.error.startsWith('ToolUse input is invalid JSON:')) { + if (isIncompleteToolUse) { + if (!willRetryIncompleteToolUse) { + // The model has failed to produce a complete tool request several times + // in a row. Retrying again is unlikely to help and would keep the agent + // loop running, so stop and surface a real error to the user instead. + this.#features.logging.error( + `Giving up after ${consecutiveIncompleteToolUses} consecutive incomplete tool uses: ${result.error}` + ) + await chatResultStream.updateOngoingProgressResult('Error') + finalResult = { + success: false, + error: INCOMPLETE_TOOL_USE_RETRY_LIMIT_MSG, + data: result.data, + } + break + } content = 'Your toolUse input is incomplete, try again. If the error happens consistently, break this task down into multiple tool uses with smaller input. Do not apologize.' shouldDisplayMessage = false @@ -4670,8 +4710,10 @@ export class AgenticChatController implements ChatHandlers { // Use finalize() (not getResult()) so a tool use whose stream ended before its // terminating `stop` event is surfaced as an incomplete-input error and retried, - // rather than being silently dropped and reported as a successful turn. - return chatEventParser.finalize() + // rather than being silently dropped and reported as a successful turn. The abort + // state is passed in so a timed-out/cancelled request is not misreported as a + // truncated tool input. + return chatEventParser.finalize({ aborted: abortSignal?.aborted === true }) } /** diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatEventParser.test.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatEventParser.test.ts index a772fc808c..0fb7640106 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatEventParser.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatEventParser.test.ts @@ -356,4 +356,42 @@ describe('AgenticChatEventParser', () => { assert.strictEqual(result.data?.toolUses['tool-1'].stop, true) assert.deepStrictEqual(result.data?.toolUses['tool-1'].input, { path: 'out.py', fileText: 'print(1)' }) }) + + it('finalize does not flag an unterminated tool use when no input was received', () => { + const chatEventParser = new AgenticChatEventParser(mockMessageId, new Metric(), logging) + + // The model announced a tool use but never streamed any input before the stream ended. + // There is nothing to truncate, so this must be dropped rather than retried. + chatEventParser.processPartialEvent({ + toolUseEvent: { + toolUseId: 'tool-1', + name: 'fsWrite', + input: '', + stop: false, + }, + }) + + const result = chatEventParser.finalize() + assert.strictEqual(result.success, true) + assert.strictEqual(result.data?.toolUses['tool-1'].stop, false) + }) + + it('finalize does not flag an unterminated tool use when the request was aborted', () => { + const chatEventParser = new AgenticChatEventParser(mockMessageId, new Metric(), logging) + + chatEventParser.processPartialEvent({ + toolUseEvent: { + toolUseId: 'tool-1', + name: 'fsWrite', + input: '{"path":"out.py","fileText":"import os', + stop: false, + }, + }) + + // An aborted request (response-processing timeout or cancellation) is expected to end + // without a stop event, so it must not be reported as a truncated tool input. + const result = chatEventParser.finalize({ aborted: true }) + assert.strictEqual(result.success, true) + assert.strictEqual(result.data?.toolUses['tool-1'].stop, false) + }) }) diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatEventParser.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatEventParser.ts index c424524992..12da3654e7 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatEventParser.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatEventParser.ts @@ -241,28 +241,48 @@ export class AgenticChatEventParser implements ChatResult { * uses are treated as pending) and completes the turn without ever running the tool — a * silent no-op with no error and no retry. * - * To avoid that, treat any still-unstopped tool use as incomplete/truncated input: - * record an error (reusing the malformed-JSON prefix so it is handled by the existing - * recovery branch) and mark it `stop` so it survives the pending-tool-use filter and the - * model is re-prompted to split the work into smaller tool uses. + * To avoid that, a genuinely *truncated tool input* is recorded as an error (reusing the + * malformed-JSON prefix so it is handled by the existing recovery branch) and marked + * `stop` so it survives the pending-tool-use filter and the model is re-prompted to split + * the work into smaller tool uses. + * + * Two other situations also reach this point and must keep the previous behaviour of being + * dropped, otherwise a benign end-of-stream is reported as a failure and re-prompted — + * which inflates failure metrics and can re-run the agent loop without making progress: + * + * - the request was aborted (response-processing timeout or cancellation): the missing + * `stop` event is expected, and the abort is already surfaced by its own path; + * - no tool input was received at all: the model never committed to a tool call, so + * there is nothing to truncate and nothing to retry. */ - public finalize(): Result { + public finalize(options?: { aborted?: boolean }): Result { for (const toolUseId of Object.keys(this.toolUses)) { const toolUse = this.toolUses[toolUseId] - if (!toolUse.stop) { - const received = typeof toolUse.input === 'string' ? toolUse.input.length : 0 - this.#logging.error( - `ToolUse ${toolUseId} (${toolUse.name}) stream ended before a stop event after ${received} characters; input was truncated` + if (toolUse.stop) { + continue + } + + const partialInput = typeof toolUse.input === 'string' ? toolUse.input : '' + + if (options?.aborted || partialInput.length === 0) { + // Leave the tool use unstopped so it is filtered out downstream, as before. + this.#logging.debug( + `ToolUse ${toolUseId} (${toolUse.name}) ended without a stop event and is not a truncated input (aborted=${!!options?.aborted}, receivedCharacters=${partialInput.length}); dropping it` ) - // Reuse the malformed-JSON error prefix so this routes into the same recovery - // branch in the agentic loop. Keep the message short so the (potentially very - // large) partial input is not echoed back to the model in the tool result. - this.error = `ToolUse input is invalid JSON: incomplete tool input, stream ended after ${received} characters before completion.` - this.toolUses[toolUseId] = { - ...toolUse, - input: {}, - stop: true, - } + continue + } + + this.#logging.error( + `ToolUse ${toolUseId} (${toolUse.name}) stream ended before a stop event after ${partialInput.length} characters; input was truncated` + ) + // Reuse the malformed-JSON error prefix so this routes into the same recovery + // branch in the agentic loop. Keep the message short so the (potentially very + // large) partial input is not echoed back to the model in the tool result. + this.error = `ToolUse input is invalid JSON: incomplete tool input, stream ended after ${partialInput.length} characters before completion.` + this.toolUses[toolUseId] = { + ...toolUse, + input: {}, + stop: true, } } return this.getResult() diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/constants/constants.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/constants/constants.ts index 901e08964b..34e5a0aecb 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/constants/constants.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/constants/constants.ts @@ -2,6 +2,26 @@ export const GENERIC_ERROR_MS = 'An unexpected error occurred, check the logs for more information.' export const OUTPUT_LIMIT_EXCEEDS_PARTIAL_MSG = 'output exceeds maximum character limit of' export const RESPONSE_TIMEOUT_PARTIAL_MSG = 'Response processing timed out after' +export const INCOMPLETE_TOOL_USE_RETRY_LIMIT_MSG = + 'Amazon Q could not complete a tool request because its input was cut off repeatedly. Try asking for a smaller change, or generating the content in sections.' + +// Retry limits +// Maximum number of consecutive responses with an incomplete tool-use input that will be +// retried before giving up and surfacing an error to the user. +export const MAX_INCOMPLETE_TOOL_USE_RETRIES = 3 + +// Telemetry reason codes reported on the amazonq_invokeLLM metric. +// +// An incomplete tool-use input is retried inside the agent loop and usually recovers within the +// same user turn, so it is not a user-visible failure. These codes let downstream metrics exclude +// those transient iterations from per-call success rates while still counting the terminal +// give-up, which the user does see, as a genuine failure. `result` stays 'Failed' in both cases +// so raw failure counts remain intact for diagnostics. +// +// NOTE: these string values form part of the telemetry contract and are consumed by a downstream +// metrics pipeline. Do not rename either value without updating that consumer first. +export const INVOKE_LLM_REASON_INCOMPLETE_TOOL_USE_RETRYING = 'INCOMPLETE_TOOL_USE_RETRYING' +export const INVOKE_LLM_REASON_INCOMPLETE_TOOL_USE_EXHAUSTED = 'INCOMPLETE_TOOL_USE_EXHAUSTED' // Time Constants export const LOADING_THRESHOLD_MS = 2000 diff --git a/server/aws-lsp-codewhisperer/src/language-server/chat/telemetry/chatTelemetryController.test.ts b/server/aws-lsp-codewhisperer/src/language-server/chat/telemetry/chatTelemetryController.test.ts index 07cac09984..2c99b4dcf1 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/chat/telemetry/chatTelemetryController.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/chat/telemetry/chatTelemetryController.test.ts @@ -5,6 +5,10 @@ import { CONVERSATION_ID_METRIC_KEY, ChatTelemetryController } from './chatTelem import assert = require('assert') import { ChatUIEventName } from './clientTelemetry' import { TelemetryService } from '../../../shared/telemetry/telemetryService' +import { + INVOKE_LLM_REASON_INCOMPLETE_TOOL_USE_EXHAUSTED, + INVOKE_LLM_REASON_INCOMPLETE_TOOL_USE_RETRYING, +} from '../../agenticChat/constants/constants' describe('TelemetryController', () => { const mockTabId = 'mockTabId' @@ -163,6 +167,60 @@ describe('TelemetryController', () => { }) }) + describe('emitAgencticLoop_InvokeLLM reason', () => { + // The `reason` field lets downstream metrics tell a transient, self-recovering agent-loop + // failure apart from a terminal one, so it must survive the emit unchanged. + const emitInvokeLLM = (reason?: string) => + telemetryController.emitAgencticLoop_InvokeLLM( + 'mockRequestId', + mockConversationId, + 'AgenticChatWithToolUse', + undefined, + undefined, + 'Failed', + '1.0.0', + 'mockModelId', + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + reason + ) + + it('forwards the reason when one is supplied', () => { + emitInvokeLLM(INVOKE_LLM_REASON_INCOMPLETE_TOOL_USE_RETRYING) + + sinon.assert.calledOnce(testFeatures.telemetry.emitMetric) + const emitted = testFeatures.telemetry.emitMetric.firstCall.firstArg + assert.strictEqual(emitted.name, ChatTelemetryEventName.AgencticLoop_InvokeLLM) + assert.strictEqual(emitted.data.reason, INVOKE_LLM_REASON_INCOMPLETE_TOOL_USE_RETRYING) + // `result` must stay 'Failed' so raw failure counts are unchanged for diagnostics. + assert.strictEqual(emitted.data.result, 'Failed') + }) + + it('distinguishes the terminal give-up from the retrying case', () => { + emitInvokeLLM(INVOKE_LLM_REASON_INCOMPLETE_TOOL_USE_EXHAUSTED) + + const emitted = testFeatures.telemetry.emitMetric.firstCall.firstArg + assert.strictEqual(emitted.data.reason, INVOKE_LLM_REASON_INCOMPLETE_TOOL_USE_EXHAUSTED) + assert.notStrictEqual( + INVOKE_LLM_REASON_INCOMPLETE_TOOL_USE_EXHAUSTED, + INVOKE_LLM_REASON_INCOMPLETE_TOOL_USE_RETRYING + ) + }) + + it('leaves reason undefined for failures with no specific classification', () => { + emitInvokeLLM(undefined) + + const emitted = testFeatures.telemetry.emitMetric.firstCall.firstArg + assert.strictEqual(emitted.data.reason, undefined) + assert.strictEqual(emitted.data.result, 'Failed') + }) + }) + describe('enqueueCodeDiffEntry', () => { const mockTextDocumentUri = 'file:///path/to/file.ts' const mockCode = 'const x = 42;' diff --git a/server/aws-lsp-codewhisperer/src/language-server/chat/telemetry/chatTelemetryController.ts b/server/aws-lsp-codewhisperer/src/language-server/chat/telemetry/chatTelemetryController.ts index e2f336e0ae..ed4634c69e 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/chat/telemetry/chatTelemetryController.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/chat/telemetry/chatTelemetryController.ts @@ -197,7 +197,8 @@ export class ChatTelemetryController { cwsprChatTimeBetweenChunks?: number[], agenticCodingMode?: boolean, experimentName?: string, - userVariation?: string + userVariation?: string, + reason?: string ) { this.#telemetry.emitMetric({ name: ChatTelemetryEventName.AgencticLoop_InvokeLLM, @@ -218,6 +219,7 @@ export class ChatTelemetryController { modelId, experimentName: experimentName, userVariation: userVariation, + reason, }, }) } diff --git a/server/aws-lsp-codewhisperer/src/shared/telemetry/types.ts b/server/aws-lsp-codewhisperer/src/shared/telemetry/types.ts index 8534f907f9..260fe677a8 100644 --- a/server/aws-lsp-codewhisperer/src/shared/telemetry/types.ts +++ b/server/aws-lsp-codewhisperer/src/shared/telemetry/types.ts @@ -253,6 +253,13 @@ export type AgencticLoop_InvokeLLMEvent = { enabled?: boolean languageServerVersion?: string latency?: string + /** + * Classifies *why* a call failed, for failures that are handled inside the agent loop and are + * therefore not user-visible. Used by downstream metrics to exclude transient, self-recovering + * iterations from per-call success rates. Absent when `result` is 'Succeeded', and absent for + * failures that have no specific classification. + */ + reason?: string } export type ToolUseSuggestedEvent = { From b3b204049f080595c767775baabb13f612ed9ea0 Mon Sep 17 00:00:00 2001 From: invictus <149003065+ashishrp-aws@users.noreply.github.com> Date: Thu, 20 Aug 2026 12:35:16 -0700 Subject: [PATCH 05/13] chore: bump agentic version: 1.76.0 (#2844) Co-authored-by: aws-toolkit-automation <> --- app/aws-lsp-codewhisperer-runtimes/src/version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/aws-lsp-codewhisperer-runtimes/src/version.json b/app/aws-lsp-codewhisperer-runtimes/src/version.json index 8f307b2695..6649bef78f 100644 --- a/app/aws-lsp-codewhisperer-runtimes/src/version.json +++ b/app/aws-lsp-codewhisperer-runtimes/src/version.json @@ -1,3 +1,3 @@ { - "agenticChat": "1.75.0" + "agenticChat": "1.76.0" } From f42b58c5fab9b4e26bff78e6cc8685f63eec3988 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 20 Aug 2026 13:18:27 -0700 Subject: [PATCH 06/13] chore(release): release packages from branch main (#2845) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Laxman Reddy <141967714+laileni-aws@users.noreply.github.com> --- .release-please-manifest.json | 2 +- package-lock.json | 2 +- server/aws-lsp-codewhisperer/CHANGELOG.md | 8 ++++++++ server/aws-lsp-codewhisperer/package.json | 2 +- 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 90080d6ba5..bb45dcb8fd 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -2,7 +2,7 @@ "chat-client": "0.1.56", "core/aws-lsp-core": "0.0.22", "server/aws-lsp-antlr4": "0.1.26", - "server/aws-lsp-codewhisperer": "0.0.125", + "server/aws-lsp-codewhisperer": "0.0.126", "server/aws-lsp-json": "0.1.27", "server/aws-lsp-partiql": "0.0.24", "server/aws-lsp-yaml": "0.1.27" diff --git a/package-lock.json b/package-lock.json index 13c9c30348..4aed07ac92 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30351,7 +30351,7 @@ }, "server/aws-lsp-codewhisperer": { "name": "@aws/lsp-codewhisperer", - "version": "0.0.125", + "version": "0.0.126", "bundleDependencies": [ "@amzn/codewhisperer", "@amzn/codewhisperer-runtime", diff --git a/server/aws-lsp-codewhisperer/CHANGELOG.md b/server/aws-lsp-codewhisperer/CHANGELOG.md index 17d1d801a6..b7c1051c23 100644 --- a/server/aws-lsp-codewhisperer/CHANGELOG.md +++ b/server/aws-lsp-codewhisperer/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [0.0.126](https://github.com/Amazon-Q-Developer/language-servers/compare/lsp-codewhisperer/v0.0.125...lsp-codewhisperer/v0.0.126) (2026-08-20) + + +### Bug Fixes + +* **amazonq:** narrow tool-use truncation detection and classify retries in telemetry ([#2847](https://github.com/Amazon-Q-Developer/language-servers/issues/2847)) ([1eb2f9f](https://github.com/Amazon-Q-Developer/language-servers/commit/1eb2f9f270c479ca457658e22f2ef0d06f6517ca)) +* scope and forward stepId for the planning-branch LBV HITL ([#2835](https://github.com/Amazon-Q-Developer/language-servers/issues/2835)) ([9f8bb89](https://github.com/Amazon-Q-Developer/language-servers/commit/9f8bb8943c41edc105c460d4b2c60836183cdd2b)) + ## [0.0.125](https://github.com/Amazon-Q-Developer/language-servers/compare/lsp-codewhisperer/v0.0.124...lsp-codewhisperer/v0.0.125) (2026-08-18) diff --git a/server/aws-lsp-codewhisperer/package.json b/server/aws-lsp-codewhisperer/package.json index 201e72d402..4c2a195da7 100644 --- a/server/aws-lsp-codewhisperer/package.json +++ b/server/aws-lsp-codewhisperer/package.json @@ -1,6 +1,6 @@ { "name": "@aws/lsp-codewhisperer", - "version": "0.0.125", + "version": "0.0.126", "description": "CodeWhisperer Language Server", "main": "out/index.js", "repository": { From 55af91ea31dff0b0d119ab4d2d79bdb22cf3e35d Mon Sep 17 00:00:00 2001 From: xinyiww1 Date: Thu, 20 Aug 2026 19:03:33 -0700 Subject: [PATCH 07/13] chore: bump agentic version: 1.77.0 (#2848) Co-authored-by: aws-toolkit-automation <> --- app/aws-lsp-codewhisperer-runtimes/src/version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/aws-lsp-codewhisperer-runtimes/src/version.json b/app/aws-lsp-codewhisperer-runtimes/src/version.json index 6649bef78f..9fb2d82896 100644 --- a/app/aws-lsp-codewhisperer-runtimes/src/version.json +++ b/app/aws-lsp-codewhisperer-runtimes/src/version.json @@ -1,3 +1,3 @@ { - "agenticChat": "1.76.0" + "agenticChat": "1.77.0" } From 863c5bf00a6c30ec7658056a155f1ca1005127e6 Mon Sep 17 00:00:00 2001 From: Rajanna-Karthik Date: Mon, 24 Aug 2026 10:35:27 -0700 Subject: [PATCH 08/13] fix: beam - flat-named transformed zips + lightweight discovery + IsLbvPending (#2849) - listBeamedRepos: derive repo name from both subdir (_/file.zip) and flat (_[_suffix].zip) transformed-source shapes, so a flat-named repo is no longer dropped from the beamed list. - Add optional Lightweight param: the IDE poll-refresh sets it to skip the beam-map download scan (throttle-safe); the IDE preserves stepId across lightweight ticks. - Add IsLbvPending (beamed but LBV HITL not created yet) via isRepoLbvHitlPending, so the IDE gates Load (IsLbvOpen && !IsLbvPending) until the HITL exists. --- .../netTransform/atxNetTransformServer.ts | 10 +++- .../netTransform/atxTransformHandler.ts | 52 +++++++++++++++---- 2 files changed, 50 insertions(+), 12 deletions(-) diff --git a/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxNetTransformServer.ts b/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxNetTransformServer.ts index 65e29c2642..b74e6a8155 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxNetTransformServer.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxNetTransformServer.ts @@ -276,11 +276,17 @@ export const AtxNetTransformServerToken = // ---- Beam to IDE ---- case AtxListBeamedReposCommand: { // PascalCase params/return to match the C# ExecuteCommandParams convention. - const { WorkspaceId, ParentJobId } = params as any + // Lightweight (optional): the poll-tick refresh sets it to skip the beam-map + // download scan (throttle-safe). + const { WorkspaceId, ParentJobId, Lightweight } = params as any if (!WorkspaceId || !ParentJobId) { throw new Error('WorkspaceId and ParentJobId are required for listBeamedRepos') } - const repos = await atxTransformHandler.listBeamedRepos(WorkspaceId, ParentJobId) + const repos = await atxTransformHandler.listBeamedRepos( + WorkspaceId, + ParentJobId, + Lightweight === true + ) return { Repos: repos } } case AtxDownloadBeamArtifactCommand: { diff --git a/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxTransformHandler.ts b/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxTransformHandler.ts index 6566297acf..f0e9abfe2e 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxTransformHandler.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxTransformHandler.ts @@ -107,6 +107,9 @@ interface BeamedRepoInfo { BeamTargetFramework: string BeamScenario: string IsLbvOpen: boolean + // Beamed but LBV HITL not created yet (no beamed node / no LBV child / no plan). IDE shows Load + // only when IsLbvOpen && !IsLbvPending, so a repo can't be Loaded before its HITL exists. + IsLbvPending: boolean } // Bounds for the beam-map candidate scan (serial download+parse per candidate). @@ -572,9 +575,9 @@ export class ATXTransformHandler { * (category HITL_FROM_AGENT) listing repos the user chose to beam. We find it * by listing the parent job's artifacts and picking the beam-map JSON. */ - async listBeamedRepos(workspaceId: string, parentJobId: string): Promise { + async listBeamedRepos(workspaceId: string, parentJobId: string, lightweight = false): Promise { try { - this.logging.log(`ATX: listBeamedRepos for parentJobId=${parentJobId}`) + this.logging.log(`ATX: listBeamedRepos for parentJobId=${parentJobId} lightweight=${lightweight}`) if (!this.atxClient && !(await this.initializeAtxClient())) { throw new Error('ATX client not initialized') } @@ -623,7 +626,15 @@ export class ATXTransformHandler { for (const a of transformedZips) { const path = a.fileMetadata?.path || '' const m = path.match(ZIP_RE) - const repoName = m ? m[2] : (path.split('/')[0] || '').replace(/_[0-9a-f]{6,}$/i, '') + // Repo name from the transformed-source zip, handling both producer shapes: subdir + // "_/.zip" (ZIP_RE) and flat "_[_suffix].zip". Flat must + // be handled or a flat-named repo never matches its beam-status entry and drops. + const repoName = m + ? m[2] + : (path.split('/').pop() || '') + .replace(/\.zip$/i, '') + .replace(/_(transformed[_-]?source|transformed|migrated|output|source)$/i, '') + .replace(/_[0-9a-f]{6,}$/i, '') if (!repoName || !a.artifactId) continue const key = repoName.toLowerCase() if (!zipByRepo.has(key)) { @@ -654,10 +665,15 @@ export class ATXTransformHandler { // source bundles, whose paths match "_/.zip" (ZIP_RE). Anything // else (including an unnamed/empty-path zip = the beam-map) is a candidate. let beamMapRepos: BeamMapRepo[] | null = null - const allCandidates = artifacts.filter(a => { - const p = (a.fileMetadata?.path || '').toLowerCase() - return !ZIP_RE.test(p) // keep non-transformed-bundle artifacts (incl. the beam-map zip) - }) + // Lightweight (poll refresh): skip the beam-map download scan (throttle-safe) — use only + // beam-status + one plan fetch. stepId isn't resolved here; the IDE keeps the one from full + // discovery / resolves it at Load. + const allCandidates = lightweight + ? [] + : artifacts.filter(a => { + const p = (a.fileMetadata?.path || '').toLowerCase() + return !ZIP_RE.test(p) // keep non-transformed-bundle artifacts (incl. the beam-map zip) + }) // Bound the scan: each candidate is a serial download+parse, so an artifact-heavy // job (logs, metadata) could otherwise stall the IDE's discovery UI. Cap the number // scanned and enforce an overall deadline; the beam-map is written early and is @@ -755,6 +771,7 @@ export class ATXTransformHandler { ) } const lbvOpen = planRoot ? this.isRepoLbvOpen(planRoot, nr.RepositoryName) : true + const lbvPending = planRoot ? this.isRepoLbvHitlPending(planRoot, nr.RepositoryName) : true beamed.push({ RepositoryName: nr.RepositoryName, BeamArtifactId: artifactId, @@ -762,9 +779,10 @@ export class ATXTransformHandler { BeamTargetFramework: nr.BeamTargetFramework || '', BeamScenario: nr.BeamScenario || 'transformed', IsLbvOpen: lbvOpen, + IsLbvPending: lbvPending, }) this.logging.log( - `[BEAM-PKG] beamed repo (from beam-map) | repo=${nr.RepositoryName} artifact=${artifactId} stepId=${nr.BeamStepId || ''} scenario=${nr.BeamScenario || 'transformed'} lbvOpen=${lbvOpen}` + `[BEAM-PKG] beamed repo (from beam-map) | repo=${nr.RepositoryName} artifact=${artifactId} stepId=${nr.BeamStepId || ''} scenario=${nr.BeamScenario || 'transformed'} lbvOpen=${lbvOpen} lbvPending=${lbvPending}` ) } } else { @@ -809,6 +827,7 @@ export class ATXTransformHandler { continue } const lbvOpen = planRoot ? this.isRepoLbvOpen(planRoot, repoName) : true + const lbvPending = planRoot ? this.isRepoLbvHitlPending(planRoot, repoName) : true beamed.push({ RepositoryName: repoName, BeamArtifactId: zip.artifactId, @@ -816,9 +835,10 @@ export class ATXTransformHandler { BeamTargetFramework: '', BeamScenario: 'transformed', IsLbvOpen: lbvOpen, + IsLbvPending: lbvPending, }) this.logging.log( - `[BEAM-PKG] beamed repo (from beam-status) | repo=${repoName} artifact=${zip.artifactId} path=${zip.path} lbvOpen=${lbvOpen}` + `[BEAM-PKG] beamed repo (from beam-status) | repo=${repoName} artifact=${zip.artifactId} path=${zip.path} lbvOpen=${lbvOpen} lbvPending=${lbvPending}` ) } this.logging.log( @@ -881,7 +901,7 @@ export class ATXTransformHandler { * tolerant of key-name variants from the web writer (repoName/repo/name, * artifactId/beamArtifactId, stepId/planStepId, targetFramework/tfm). */ - private normalizeBeamRepo(r: BeamMapRepo | null | undefined): Omit { + private normalizeBeamRepo(r: BeamMapRepo | null | undefined): Omit { const o: BeamMapRepo = r || {} return { RepositoryName: o.repoName ?? o.repositoryName ?? o.repo ?? o.name ?? '', @@ -2380,6 +2400,18 @@ export class ATXTransformHandler { return !terminal } + /** + * Is this repo's LBV HITL NOT created yet? TRUE when the beamed node isn't in the plan tree, or + * has no LBV child. FALSE once an LBV node exists (open or terminal — IsLbvOpen distinguishes + * those). Gates the Load button (isLbvOpen && !isLbvPending) so the IDE never Loads before the + * HITL exists. Same node resolution as isRepoLbvOpen. + */ + private isRepoLbvHitlPending(planRoot: AtxPlanStep, repoName: string): boolean { + const repoNode = this.findBeamedRepoNode(planRoot, repoName) + if (!repoNode) return true + return !this.findLbvNode(repoNode) + } + /** * Match the beamed repo's plan node. CRITICAL: the plan tree has MULTIPLE nodes named for a * repo — the ORIGINAL TRANSFORM node named plain "" (top-level, SUCCEEDED, NO LBV child) From 836b756ee2d8a553d9b26cf095e12979141e2367 Mon Sep 17 00:00:00 2001 From: Chris Date: Mon, 24 Aug 2026 14:39:27 -0700 Subject: [PATCH 09/13] fix: surface backend interactive mode in getTransformInfo getTransformInfo already resolves the interactive mode from the job objective (cachedInteractiveMode) but never returned it. Add the InteractiveMode field to AtxGetTransformInfoResponse and populate it at the getTransformInfo wrapper so the IDE can restore the correct mode after a restart instead of trusting its local settings store. Paired with the IDE change in aws-toolkit-visual-studio-staging. --- .../src/language-server/netTransform/atxModels.ts | 4 ++++ .../src/language-server/netTransform/atxTransformHandler.ts | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxModels.ts b/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxModels.ts index 21622fea61..f010011550 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxModels.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxModels.ts @@ -146,6 +146,10 @@ export interface AtxGetTransformInfoResponse { MissingPackageJsonPath?: string | null DiffApplyFailed?: boolean DiffApplyFailedStepIds?: string[] + // Interactive mode as resolved from the backend job objective (interactive_mode). + // Surfaced so the IDE can restore the correct mode instead of relying on its local + // settings store, which may be missing/stale (e.g. cold restart on another machine). + InteractiveMode?: InteractiveMode } /** diff --git a/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxTransformHandler.ts b/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxTransformHandler.ts index f0e9abfe2e..04de8649da 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxTransformHandler.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxTransformHandler.ts @@ -1770,6 +1770,12 @@ export class ATXTransformHandler { result.DiffApplyFailed = true result.DiffApplyFailedStepIds = diffContext.failedStepIds } + // Surface the backend-resolved interactive mode (from job.objective) on every + // response so the IDE can restore it. Single injection point covers all internal + // return paths. cachedInteractiveMode is populated in _getTransformInfoInternal. + if (result && this.cachedInteractiveMode) { + result.InteractiveMode = this.cachedInteractiveMode + } return result } finally { this._currentDiffContext = null From 529aed43259503cf71b475fb3496de7bfab25f17 Mon Sep 17 00:00:00 2001 From: chungjac Date: Mon, 24 Aug 2026 23:57:13 +0000 Subject: [PATCH 10/13] fix(amazonq): cover merged env and headers in MCP consent fingerprint (#2851) (#2853) --- .../tools/mcp/mcpConsentStore.test.ts | 92 ++++++++++++++++++- .../agenticChat/tools/mcp/mcpConsentStore.ts | 42 ++++++++- .../agenticChat/tools/mcp/mcpManager.ts | 27 +++++- 3 files changed, 155 insertions(+), 6 deletions(-) diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpConsentStore.test.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpConsentStore.test.ts index 79696b5f9d..b638968139 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpConsentStore.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpConsentStore.test.ts @@ -8,6 +8,8 @@ import * as fs from 'fs' import * as os from 'os' import * as path from 'path' import { + effectiveEnv, + effectiveHeaders, fingerprintServerConfig, fingerprintWorkspace, hasApproval, @@ -74,6 +76,71 @@ describe('mcpConsentStore', () => { const b: MCPServerConfig = { url: 'https://b.example' } expect(fingerprintServerConfig(a)).to.not.equal(fingerprintServerConfig(b)) }) + + // __additionalEnv__ / __additionalHeaders__ are merged into the spawn env/headers, + // so they must be covered by the fingerprint alongside the raw fields — otherwise a + // post-approval config edit to those fields would not re-prompt. + it('differs when __additionalEnv__ is added', () => { + const a: MCPServerConfig = { command: 'npx', args: ['-y', 's'], env: { LOG_LEVEL: 'info' } } + const b: MCPServerConfig = { + ...a, + __additionalEnv__: { EXTRA: '1' }, + } + expect(fingerprintServerConfig(a)).to.not.equal(fingerprintServerConfig(b)) + }) + + it('differs when __additionalEnv__ overrides an existing env value', () => { + const a: MCPServerConfig = { command: 'sh', args: [], env: { FOO: '1' } } + const b: MCPServerConfig = { command: 'sh', args: [], env: { FOO: '1' }, __additionalEnv__: { FOO: '2' } } + expect(fingerprintServerConfig(a)).to.not.equal(fingerprintServerConfig(b)) + }) + + it('differs when headers change', () => { + const a: MCPServerConfig = { url: 'https://a.example', headers: { Authorization: 'Bearer good' } } + const b: MCPServerConfig = { url: 'https://a.example', headers: { Authorization: 'Bearer attacker' } } + expect(fingerprintServerConfig(a)).to.not.equal(fingerprintServerConfig(b)) + }) + + it('differs when __additionalHeaders__ overrides an existing header', () => { + const a: MCPServerConfig = { url: 'https://a.example', headers: { Authorization: 'Bearer good' } } + const b: MCPServerConfig = { + url: 'https://a.example', + headers: { Authorization: 'Bearer good' }, + __additionalHeaders__: { Authorization: 'Bearer attacker' }, + } + expect(fingerprintServerConfig(a)).to.not.equal(fingerprintServerConfig(b)) + }) + + // Consent is about what will execute, not how the config is spelled: two configs + // that spawn the process with the identical effective env share a fingerprint. + it('hashes the merged spawn env, so the same effective env matches either field', () => { + const viaEnv: MCPServerConfig = { command: 'sh', args: [], env: { FOO: '1' } } + const viaAdditional: MCPServerConfig = { command: 'sh', args: [], __additionalEnv__: { FOO: '1' } } + expect(fingerprintServerConfig(viaEnv)).to.equal(fingerprintServerConfig(viaAdditional)) + }) + + it('is stable regardless of __additionalEnv__ key order', () => { + const a: MCPServerConfig = { command: 'sh', args: [], __additionalEnv__: { A: '1', B: '2' } } + const b: MCPServerConfig = { command: 'sh', args: [], __additionalEnv__: { B: '2', A: '1' } } + expect(fingerprintServerConfig(a)).to.equal(fingerprintServerConfig(b)) + }) + }) + + describe('effectiveEnv / effectiveHeaders', () => { + it('merges __additionalEnv__ over env, matching the spawn-time merge', () => { + const cfg: MCPServerConfig = { env: { A: '1', B: '2' }, __additionalEnv__: { B: 'override', C: '3' } } + expect(effectiveEnv(cfg)).to.deep.equal({ A: '1', B: 'override', C: '3' }) + }) + + it('merges __additionalHeaders__ over headers', () => { + const cfg: MCPServerConfig = { headers: { X: '1' }, __additionalHeaders__: { X: '2', Y: '3' } } + expect(effectiveHeaders(cfg)).to.deep.equal({ X: '2', Y: '3' }) + }) + + it('returns an empty object when nothing is set', () => { + expect(effectiveEnv({})).to.deep.equal({}) + expect(effectiveHeaders({})).to.deep.equal({}) + }) }) describe('fingerprintWorkspace', () => { @@ -183,11 +250,34 @@ describe('mcpConsentStore', () => { const storeDir = path.join(tmpHome, '.aws', 'amazonq') fs.mkdirSync(storeDir, { recursive: true }) fs.writeFileSync(path.join(storeDir, 'mcp-approvals.json'), JSON.stringify({ version: 999, approvals: [] })) - // record should still work (overwrites with v1) + // record should still work (overwrites with the current version) await recordApproval(workspace, logger, 'poc', cfg, configPath) expect(await hasApproval(workspace, logger, 'poc', cfg, configPath)).to.be.true }) + // STORE_VERSION 1 -> 2: v1 fingerprints were computed over a narrower field set and + // cannot be trusted to cover the merged env/headers, so they are discarded and the + // user is re-prompted once per workspace-scoped server after upgrade. + it('discards legacy v1 approvals so the user is re-prompted once after upgrade', async () => { + const storeDir = path.join(tmpHome, '.aws', 'amazonq') + fs.mkdirSync(storeDir, { recursive: true }) + fs.writeFileSync( + path.join(storeDir, 'mcp-approvals.json'), + JSON.stringify({ + version: 1, + approvals: [ + { + serverName: 'poc', + fingerprint: fingerprintServerConfig(cfg), + workspaceHash: fingerprintWorkspace(configPath), + approvedAt: new Date().toISOString(), + }, + ], + }) + ) + expect(await hasApproval(workspace, logger, 'poc', cfg, configPath)).to.be.false + }) + it('treats a malformed store as empty', async () => { const storeDir = path.join(tmpHome, '.aws', 'amazonq') fs.mkdirSync(storeDir, { recursive: true }) diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpConsentStore.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpConsentStore.ts index b9230a6655..45a4f252fc 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpConsentStore.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpConsentStore.ts @@ -9,7 +9,11 @@ import type { Workspace, Logging } from '@aws/language-server-runtimes/server-in import type { MCPServerConfig } from './mcpTypes' const APPROVALS_FILE = 'mcp-approvals.json' -const STORE_VERSION = 1 +// v2: the fingerprint now covers the fully-merged spawn environment and headers +// (see fingerprintServerConfig). Bumping the version discards v1 approvals, which +// were computed over a narrower field set, so every workspace-scoped server is +// re-consented once after upgrade. +const STORE_VERSION = 2 interface Approval { serverName: string @@ -23,17 +27,49 @@ interface ApprovalStore { approvals: Approval[] } +function sortedRecord(rec?: Record): Record { + return rec ? Object.fromEntries(Object.entries(rec).sort(([a], [b]) => a.localeCompare(b))) : {} +} + +/** + * The environment the stdio transport will actually spawn the server with, as far + * as the config controls it. Mirrors the merge in McpManager (`cfg.env` overlaid by + * `cfg.__additionalEnv__`). + * + * `__additionalEnv__` carries workspace/agent-level `env` for registry servers and is + * NOT folded into `cfg.env`, so it must be merged here or it escapes the fingerprint. + */ +export function effectiveEnv(cfg: MCPServerConfig): Record { + return sortedRecord({ ...(cfg.env ?? {}), ...(cfg.__additionalEnv__ ?? {}) }) +} + +/** + * The headers the HTTP/SSE transport will actually send, as far as the config + * controls it. Mirrors the merge in McpManager (`cfg.headers` overlaid by + * `cfg.__additionalHeaders__`). + */ +export function effectiveHeaders(cfg: MCPServerConfig): Record { + return sortedRecord({ ...(cfg.headers ?? {}), ...(cfg.__additionalHeaders__ ?? {}) }) +} + /** * SHA-256 of a canonical JSON form of the server's execution-relevant fields. - * Any change to command/args/env/url yields a new fingerprint, invalidating + * Any change to command/args/env/url/headers yields a new fingerprint, invalidating * prior approvals — so mutation of the config re-prompts. + * + * `env` and `headers` are hashed in their *merged* form (see effectiveEnv / + * effectiveHeaders) so every field that reaches the spawned process is covered by + * consent. Two configs that spawn an identical process share a fingerprint regardless + * of which field supplied a value: consent is about what will execute, not how the + * config is spelled. */ export function fingerprintServerConfig(cfg: MCPServerConfig): string { const canonical = { command: cfg.command ?? null, args: cfg.args ?? [], - env: cfg.env ? Object.fromEntries(Object.entries(cfg.env).sort(([a], [b]) => a.localeCompare(b))) : {}, + env: effectiveEnv(cfg), url: cfg.url ?? null, + headers: effectiveHeaders(cfg), } return 'sha256:' + createHash('sha256').update(JSON.stringify(canonical)).digest('hex') } diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.ts index 61e7ea75fe..bde82719d7 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.ts @@ -42,7 +42,14 @@ import { Mutex } from 'async-mutex' import path = require('path') import { URI } from 'vscode-uri' import { MessageType } from '@aws/language-server-runtimes/protocol' -import { hasApproval, recordApproval, removeApproval, fingerprintServerConfig } from './mcpConsentStore' +import { + hasApproval, + recordApproval, + removeApproval, + fingerprintServerConfig, + effectiveEnv, + effectiveHeaders, +} from './mcpConsentStore' import { sanitizeInput } from '../../../../shared/utils' import { ProfileStatusMonitor } from './profileStatusMonitor' import { OAuthClient } from './mcpOauthClient' @@ -440,6 +447,17 @@ export class McpManager { ) if (!approved) { const cmdLine = [cfg.command ?? cfg.url ?? '(none)', ...(cfg.args ?? [])].join(' ').slice(0, 200) + // Surface the environment variables and headers the server will actually be + // launched with. Names only, never values: a config may legitimately hold + // tokens, and this string is shown in a dialog and written to logs. Values are + // covered by the fingerprint, so any value change re-prompts. + const envKeys = Object.keys(effectiveEnv(cfg)) + const headerNames = Object.keys(effectiveHeaders(cfg)) + const envLine = + envKeys.length > 0 + ? `Environment variables: ${envKeys.join(', ').slice(0, 200)}\n` + : `Environment variables: (none)\n` + const headerLine = headerNames.length > 0 ? `Headers: ${headerNames.join(', ').slice(0, 200)}\n` : '' const allowBtn = { title: 'Allow for this server' } const denyBtn = { title: 'Deny' } let choice: { title: string } | null | undefined @@ -451,8 +469,13 @@ export class McpManager { `A workspace configuration file wants to start an MCP server.\n` + `Server: ${serverName}\n` + `Command: ${cmdLine}\n` + + envLine + + headerLine + `Source: ${configPath}\n\n` + - `Running this server executes the above command on your machine. ` + + `Running this server executes the above command on your machine, ` + + `with the environment variables listed above. ` + + `Review them in the configuration file if you are unsure — variables such as ` + + `NODE_OPTIONS can cause additional code to run. ` + `Only allow if you trust the authors of this workspace.\n\n` + `Your choice will be remembered for this workspace. ` + `If you allow, you won't be asked again unless the server configuration changes.`, From d493511cc333dd6a1b7d40b056ea807e41cd8274 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 24 Aug 2026 17:55:49 -0700 Subject: [PATCH 11/13] chore(release): release packages from branch main (#2850) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .release-please-manifest.json | 2 +- package-lock.json | 2 +- server/aws-lsp-codewhisperer/CHANGELOG.md | 9 +++++++++ server/aws-lsp-codewhisperer/package.json | 2 +- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index bb45dcb8fd..c2a9a56c93 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -2,7 +2,7 @@ "chat-client": "0.1.56", "core/aws-lsp-core": "0.0.22", "server/aws-lsp-antlr4": "0.1.26", - "server/aws-lsp-codewhisperer": "0.0.126", + "server/aws-lsp-codewhisperer": "0.0.127", "server/aws-lsp-json": "0.1.27", "server/aws-lsp-partiql": "0.0.24", "server/aws-lsp-yaml": "0.1.27" diff --git a/package-lock.json b/package-lock.json index 4aed07ac92..81579468b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30351,7 +30351,7 @@ }, "server/aws-lsp-codewhisperer": { "name": "@aws/lsp-codewhisperer", - "version": "0.0.126", + "version": "0.0.127", "bundleDependencies": [ "@amzn/codewhisperer", "@amzn/codewhisperer-runtime", diff --git a/server/aws-lsp-codewhisperer/CHANGELOG.md b/server/aws-lsp-codewhisperer/CHANGELOG.md index b7c1051c23..19f7b5fd2e 100644 --- a/server/aws-lsp-codewhisperer/CHANGELOG.md +++ b/server/aws-lsp-codewhisperer/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## [0.0.127](https://github.com/Amazon-Q-Developer/language-servers/compare/lsp-codewhisperer/v0.0.126...lsp-codewhisperer/v0.0.127) (2026-08-24) + + +### Bug Fixes + +* **amazonq:** cover merged env and headers in MCP consent fingerprint ([#2851](https://github.com/Amazon-Q-Developer/language-servers/issues/2851)) ([#2853](https://github.com/Amazon-Q-Developer/language-servers/issues/2853)) ([529aed4](https://github.com/Amazon-Q-Developer/language-servers/commit/529aed43259503cf71b475fb3496de7bfab25f17)) +* beam - flat-named transformed zips + lightweight discovery + IsLbvPending ([#2849](https://github.com/Amazon-Q-Developer/language-servers/issues/2849)) ([863c5bf](https://github.com/Amazon-Q-Developer/language-servers/commit/863c5bf00a6c30ec7658056a155f1ca1005127e6)) +* surface backend interactive mode in getTransformInfo ([836b756](https://github.com/Amazon-Q-Developer/language-servers/commit/836b756ee2d8a553d9b26cf095e12979141e2367)) + ## [0.0.126](https://github.com/Amazon-Q-Developer/language-servers/compare/lsp-codewhisperer/v0.0.125...lsp-codewhisperer/v0.0.126) (2026-08-20) diff --git a/server/aws-lsp-codewhisperer/package.json b/server/aws-lsp-codewhisperer/package.json index 4c2a195da7..af7b4aaa17 100644 --- a/server/aws-lsp-codewhisperer/package.json +++ b/server/aws-lsp-codewhisperer/package.json @@ -1,6 +1,6 @@ { "name": "@aws/lsp-codewhisperer", - "version": "0.0.126", + "version": "0.0.127", "description": "CodeWhisperer Language Server", "main": "out/index.js", "repository": { From 2b5b9e7d3511284eb4a4775c90a2e2f229a531de Mon Sep 17 00:00:00 2001 From: XiaowenMaoA <107279155+XiaowenMaoA@users.noreply.github.com> Date: Tue, 25 Aug 2026 12:44:37 -0700 Subject: [PATCH 12/13] chore: bump agentic version: 1.78.0 (#2856) Co-authored-by: aws-toolkit-automation <> --- app/aws-lsp-codewhisperer-runtimes/src/version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/aws-lsp-codewhisperer-runtimes/src/version.json b/app/aws-lsp-codewhisperer-runtimes/src/version.json index 9fb2d82896..e8fa26028c 100644 --- a/app/aws-lsp-codewhisperer-runtimes/src/version.json +++ b/app/aws-lsp-codewhisperer-runtimes/src/version.json @@ -1,3 +1,3 @@ { - "agenticChat": "1.77.0" + "agenticChat": "1.78.0" } From acd48a981cc74bd17626ddcd7512da57aa1672f5 Mon Sep 17 00:00:00 2001 From: Laxman Reddy <141967714+laileni-aws@users.noreply.github.com> Date: Fri, 4 Sep 2026 15:18:37 -0700 Subject: [PATCH 13/13] fix: pass untrusted event data via env in prerelease workflow (#2861) The prerelease workflow interpolated github.event values directly into inline `run:` script bodies. Because GitHub substitutes those expressions textually before the shell parses the script, a crafted branch name or workflow input could terminate the surrounding quoting and run arbitrary commands on the runner. Move each untrusted value (inputs.tag_name, workflow_run.head_branch, workflow_run.head_sha) into a step-level `env:` block and reference it as a shell variable, so the value is always treated as data. This matches the pattern the create-release job in this workflow already uses. No behavior change: tag and prerelease names are unchanged for main, feature/*, and release/agentic/* branches, and unsupported branches still fail the same way. --- .../create-agentic-github-prerelease.yml | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/.github/workflows/create-agentic-github-prerelease.yml b/.github/workflows/create-agentic-github-prerelease.yml index 48873b7503..3201ebc682 100644 --- a/.github/workflows/create-agentic-github-prerelease.yml +++ b/.github/workflows/create-agentic-github-prerelease.yml @@ -26,15 +26,23 @@ jobs: # if user ran this action manually - if: github.event_name == 'workflow_dispatch' + env: + # Pass untrusted event data through the environment instead of + # interpolating it directly into the script body. + INPUT_TAG_NAME: ${{ github.event.inputs.tag_name }} run: | - echo "TAG_NAME=${{ github.event.inputs.tag_name }}" >> $GITHUB_ENV - echo "PRERELEASE_NAME=${{ github.event.inputs.tag_name }}" >> $GITHUB_ENV + echo "TAG_NAME=$INPUT_TAG_NAME" >> $GITHUB_ENV + echo "PRERELEASE_NAME=$INPUT_TAG_NAME" >> $GITHUB_ENV # Otherwise a push to a branch triggered this action. # Set TAG_NAME and PRERELEASE_NAME based on branch name - if: github.event_name != 'workflow_dispatch' + env: + # Branch names are attacker-controllable, so read the value from the + # environment instead of interpolating it into the script body. + HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }} run: | - BRANCH_NAME="${{ github.event.workflow_run.head_branch }}" + BRANCH_NAME="$HEAD_BRANCH" if [[ "$BRANCH_NAME" == "main" ]]; then echo "TAG_NAME=agentic-alpha" >> $GITHUB_ENV echo "PRERELEASE_NAME=alpha" >> $GITHUB_ENV @@ -53,6 +61,8 @@ jobs: # Make a sever version that is "decorated" as prerelease - name: Create SERVER_VERSION + env: + HEAD_SHA: ${{ github.event.workflow_run.head_sha }} run: | # example: 1.0.999-pre-main.commitid # SERVER_VERSION - we're making "imitation" manifests that are accessible @@ -61,7 +71,7 @@ jobs: # in the version.json file. AGENTIC_VERSION=$(jq -r '.agenticChat' app/aws-lsp-codewhisperer-runtimes/src/version.json) - COMMIT_SHORT=$(echo "${{ github.event.workflow_run.head_sha }}" | cut -c1-8) + COMMIT_SHORT=$(echo "$HEAD_SHA" | cut -c1-8) echo "SERVER_VERSION=$AGENTIC_VERSION-$PRERELEASE_NAME.$COMMIT_SHORT" >> $GITHUB_ENV - name: Export outputs