From e3d74f3b730c868c449b4193160884616c59fab5 Mon Sep 17 00:00:00 2001 From: Daniel Smolsky Date: Thu, 15 Jan 2026 12:17:14 -0500 Subject: [PATCH 1/2] fix: simplify injection timing to wait for assistant turn Replace provider-specific checks (GitHub Copilot, Anthropic reasoning detection) with a universal rule: never inject immediately following a user message. This avoids interfering with model reasoning/thinking phases across all providers. --- lib/messages/inject.ts | 32 ++++++++------------------------ lib/messages/utils.ts | 20 -------------------- 2 files changed, 8 insertions(+), 44 deletions(-) diff --git a/lib/messages/inject.ts b/lib/messages/inject.ts index 76c72dd..8b67186 100644 --- a/lib/messages/inject.ts +++ b/lib/messages/inject.ts @@ -8,7 +8,6 @@ import { buildToolIdList, createSyntheticAssistantMessageWithToolPart, isIgnoredUserMessage, - hasReasoningInCurrentAssistantTurn, } from "./utils" import { getFilePathFromParameters, isProtectedFilePath } from "../protected-file-patterns" import { getLastUserMessage } from "../shared-utils" @@ -139,31 +138,16 @@ export const insertPruneToolContext = ( return } - const userInfo = lastUserMessage.info as UserMessage - const providerID = userInfo.model.providerID - const modelID = userInfo.model.modelID - const isGitHubCopilot = - providerID === "github-copilot" || providerID === "github-copilot-enterprise" - - // TODO: This can probably be improved further to only trigger for the appropriate thinking settings - // This setting is also potentially only necessary for claude subscription, API seems to not need this - // validation. See more here: https://platform.claude.com/docs/en/build-with-claude/extended-thinking - const isAnthropic = modelID.includes("claude") - - if (isGitHubCopilot) { - const lastMessage = messages[messages.length - 1] - if (lastMessage?.info?.role === "user" && !isIgnoredUserMessage(lastMessage)) { - return - } - } - - // Anthropic extended thinking models require a thinking block at the start of its turn - if (isAnthropic) { - if (!hasReasoningInCurrentAssistantTurn(messages)) { - return - } + // Never inject immediately following a user message - wait until assistant has started its turn + // This avoids interfering with model reasoning/thinking phases + // TODO: This can be skipped if there is a good way to check if the model has reasoning, + // can't find a good way to do this yet + const lastMessage = messages[messages.length - 1] + if (lastMessage?.info?.role === "user" && !isIgnoredUserMessage(lastMessage)) { + return } + const userInfo = lastUserMessage.info as UserMessage const variant = state.variant ?? userInfo.variant messages.push( createSyntheticAssistantMessageWithToolPart(lastUserMessage, prunableToolsContent, variant), diff --git a/lib/messages/utils.ts b/lib/messages/utils.ts index 26fc29a..756ecc7 100644 --- a/lib/messages/utils.ts +++ b/lib/messages/utils.ts @@ -207,23 +207,3 @@ export const isIgnoredUserMessage = (message: WithParts): boolean => { return true } - -export const hasReasoningInCurrentAssistantTurn = (messages: WithParts[]): boolean => { - for (let i = messages.length - 1; i >= 0; i--) { - const message = messages[i] - if (message.info?.role === "user") { - if (isIgnoredUserMessage(message)) { - continue - } - return false - } - if (message.info?.role === "assistant" && message.parts) { - for (const part of message.parts) { - if (part.type === "reasoning") { - return true - } - } - } - } - return false -} From a2ae844cf74d12ccb3d193c6731cf7b7fb86787d Mon Sep 17 00:00:00 2001 From: Daniel Smolsky Date: Thu, 15 Jan 2026 13:44:55 -0500 Subject: [PATCH 2/2] fix: use text injection for Gemini models to avoid thought signature errors Gemini 3+ models have strict validation requiring thoughtSignature on functionCall parts. When the plugin injects synthetic assistant messages with tool parts, providers without robust signature handling will fail with 400 errors. This change detects Gemini models and injects as text parts instead of tool parts, avoiding the thought signature requirement entirely. --- lib/messages/utils.ts | 62 ++++++++++++++++++++++++++++++------------- 1 file changed, 44 insertions(+), 18 deletions(-) diff --git a/lib/messages/utils.ts b/lib/messages/utils.ts index 756ecc7..fafdccd 100644 --- a/lib/messages/utils.ts +++ b/lib/messages/utils.ts @@ -7,6 +7,11 @@ const SYNTHETIC_MESSAGE_ID = "msg_01234567890123456789012345" const SYNTHETIC_PART_ID = "prt_01234567890123456789012345" const SYNTHETIC_CALL_ID = "call_01234567890123456789012345" +const isGeminiModel = (modelID: string): boolean => { + const lowerModelID = modelID.toLowerCase() + return lowerModelID.includes("gemini") +} + export const createSyntheticAssistantMessageWithToolPart = ( baseMessage: WithParts, content: string, @@ -14,25 +19,46 @@ export const createSyntheticAssistantMessageWithToolPart = ( ): WithParts => { const userInfo = baseMessage.info as UserMessage const now = Date.now() - return { - info: { - id: SYNTHETIC_MESSAGE_ID, - sessionID: userInfo.sessionID, - role: "assistant", - agent: userInfo.agent || "code", - parentID: userInfo.id, - modelID: userInfo.model.modelID, - providerID: userInfo.model.providerID, - mode: "default", - path: { - cwd: "/", - root: "/", - }, - time: { created: now, completed: now }, - cost: 0, - tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, - ...(variant !== undefined && { variant }), + + const baseInfo = { + id: SYNTHETIC_MESSAGE_ID, + sessionID: userInfo.sessionID, + role: "assistant" as const, + agent: userInfo.agent || "code", + parentID: userInfo.id, + modelID: userInfo.model.modelID, + providerID: userInfo.model.providerID, + mode: "default", + path: { + cwd: "/", + root: "/", }, + time: { created: now, completed: now }, + cost: 0, + tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, + ...(variant !== undefined && { variant }), + } + + // For Gemini models, inject as text to avoid thought signature requirements + // Gemini 3+ has strict validation requiring thoughtSignature on functionCall parts + if (isGeminiModel(userInfo.model.modelID)) { + return { + info: baseInfo, + parts: [ + { + id: SYNTHETIC_PART_ID, + sessionID: userInfo.sessionID, + messageID: SYNTHETIC_MESSAGE_ID, + type: "text", + text: content, + }, + ], + } + } + + // For other models, use tool part for cleaner context + return { + info: baseInfo, parts: [ { id: SYNTHETIC_PART_ID,