From 1817aaaada4e224127eda127dc35465ba8e3291b Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 30 Oct 2026 21:51:58 +0000 Subject: [PATCH 1/3] test(server): reproduce #27854 decline feedback reaching Claude as a bare tool error Declining a Claude tool approval with feedback forwards the user's text as the whole deny message, so the agent cannot tell it is a user message attached to a decline. This test fails until the message says so. Co-Authored-By: Claude Opus 5.5 --- .../Adapters/ClaudeAdapterV2.test.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts index d758984f6a5c3..6c38c3858db5c 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts @@ -803,6 +803,23 @@ describe("ClaudeAdapterV2 session permissions", () => { assert.equal(result.interrupt, undefined); }); + it("tells Claude that decline feedback is the user's message, not a tool error", () => { + const feedback = "Use the dry-run flag first."; + const result = ClaudeAdapterV2.permissionResultFromDecision({ + toolName: "Bash", + decision: "decline", + toolInput: { command: "make deploy" }, + toolUseID: "declined-with-feedback", + feedback, + }); + assert.equal(result.behavior, "deny"); + if (result.behavior !== "deny") return; + assert.include(result.message, feedback); + assert.notEqual(result.message, feedback, "feedback was forwarded as a bare tool error"); + assert.match(result.message, /user/i); + assert.match(result.message, /declin|reject/i); + }); + it("forces suggested permission updates to session scope", () => { const result = ClaudeAdapterV2.permissionResultFromDecision({ toolName: "Bash", From 50770a54f1aa28bad56f0e486d17037886a308a6 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 30 Oct 2026 21:57:36 +0000 Subject: [PATCH 2/3] fix(server): tell Claude that decline feedback is the user's message Declining a Claude tool approval with feedback sent the user's raw text as the whole deny message, so Claude read it as a tool error and could not tell the user had rejected the call. The deny message now says the user declined the tool call and then quotes their words. The no-feedback defaults are unchanged. Fixes pingdotgg/t3code#27854 Co-Authored-By: Claude Opus 5.5 --- .../src/orchestration-v2/Adapters/ClaudeAdapterV2.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts index 1d51bd7ef0ce1..09c78516a72d4 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts @@ -3432,13 +3432,14 @@ export function permissionResultFromDecision(input: { }; } + const feedback = input.feedback?.trim(); return { behavior: "deny", - message: - input.feedback?.trim() || - (input.decision === "cancel" + message: feedback + ? `The user declined this tool call and said:\n${feedback}` + : input.decision === "cancel" ? "User cancelled tool execution." - : "User declined tool execution."), + : "User declined tool execution.", toolUseID: input.toolUseID, decisionClassification: "user_reject", ...(input.decision === "cancel" ? { interrupt: true } : {}), From 8dcf47be85db7442f44c0dbccad41472edbe6d2b Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 11 Nov 2026 00:10:16 +0000 Subject: [PATCH 3/3] test(server): fold #27854 decline-feedback case into the user_reject test AI-assisted; verified with the tests and tools listed above. --- .../Adapters/ClaudeAdapterV2.test.ts | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts index 6c38c3858db5c..fa903c1cf2145 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts @@ -801,23 +801,22 @@ describe("ClaudeAdapterV2 session permissions", () => { assert.equal(result.decisionClassification, "user_reject"); assert.equal(result.message, "User declined tool execution."); assert.equal(result.interrupt, undefined); - }); - it("tells Claude that decline feedback is the user's message, not a tool error", () => { - const feedback = "Use the dry-run flag first."; - const result = ClaudeAdapterV2.permissionResultFromDecision({ + // regression: #27854 bare feedback read to Claude as a tool error, not the user's words + const withFeedback = ClaudeAdapterV2.permissionResultFromDecision({ toolName: "Bash", decision: "decline", - toolInput: { command: "make deploy" }, - toolUseID: "declined-with-feedback", - feedback, + toolInput: { command: "make" }, + toolUseID: "denied-build", + feedback: " Use the dry-run flag first. ", }); - assert.equal(result.behavior, "deny"); - if (result.behavior !== "deny") return; - assert.include(result.message, feedback); - assert.notEqual(result.message, feedback, "feedback was forwarded as a bare tool error"); - assert.match(result.message, /user/i); - assert.match(result.message, /declin|reject/i); + assert.equal(withFeedback.behavior, "deny"); + if (withFeedback.behavior !== "deny") return; + assert.equal(withFeedback.decisionClassification, "user_reject"); + assert.equal( + withFeedback.message, + "The user declined this tool call and said:\nUse the dry-run flag first.", + ); }); it("forces suggested permission updates to session scope", () => {