From fe85422d9c3411218c9d89850743745ad1105b75 Mon Sep 17 00:00:00 2001 From: daniel-lxs Date: Fri, 30 Jan 2026 23:00:49 -0500 Subject: [PATCH] fix: sanitize tool_use_id in tool_result blocks to match API history Tool IDs from providers like Gemini/OpenRouter contain special characters (e.g., 'functions.read_file:0') that are sanitized when saving tool_use blocks to API history. However, tool_result blocks were using the original unsanitized IDs, causing ToolResultIdMismatchError. This fix ensures tool_result blocks use sanitizeToolUseId() to match the sanitized tool_use IDs in conversation history. Fixes EXT-711 --- .../assistant-message/presentAssistantMessage.ts | 15 ++++++++------- src/utils/__tests__/tool-id.spec.ts | 8 ++++++++ 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/core/assistant-message/presentAssistantMessage.ts b/src/core/assistant-message/presentAssistantMessage.ts index 5b9e1c4840e..c22c369b42d 100644 --- a/src/core/assistant-message/presentAssistantMessage.ts +++ b/src/core/assistant-message/presentAssistantMessage.ts @@ -40,6 +40,7 @@ import { isValidToolName, validateToolUse } from "../tools/validateToolUse" import { codebaseSearchTool } from "../tools/CodebaseSearchTool" import { formatResponse } from "../prompts/responses" +import { sanitizeToolUseId } from "../../utils/tool-id" /** * Processes and presents assistant message content to the user interface. @@ -118,7 +119,7 @@ export async function presentAssistantMessage(cline: Task) { if (toolCallId) { cline.pushToolResultToUserContent({ type: "tool_result", - tool_use_id: toolCallId, + tool_use_id: sanitizeToolUseId(toolCallId), content: errorMessage, is_error: true, }) @@ -169,7 +170,7 @@ export async function presentAssistantMessage(cline: Task) { if (toolCallId) { cline.pushToolResultToUserContent({ type: "tool_result", - tool_use_id: toolCallId, + tool_use_id: sanitizeToolUseId(toolCallId), content: resultContent, }) @@ -410,7 +411,7 @@ export async function presentAssistantMessage(cline: Task) { cline.pushToolResultToUserContent({ type: "tool_result", - tool_use_id: toolCallId, + tool_use_id: sanitizeToolUseId(toolCallId), content: errorMessage, is_error: true, }) @@ -447,7 +448,7 @@ export async function presentAssistantMessage(cline: Task) { // continue gracefully. cline.pushToolResultToUserContent({ type: "tool_result", - tool_use_id: toolCallId, + tool_use_id: sanitizeToolUseId(toolCallId), content: formatResponse.toolError(errorMessage), is_error: true, }) @@ -493,7 +494,7 @@ export async function presentAssistantMessage(cline: Task) { cline.pushToolResultToUserContent({ type: "tool_result", - tool_use_id: toolCallId, + tool_use_id: sanitizeToolUseId(toolCallId), content: resultContent, }) @@ -644,7 +645,7 @@ export async function presentAssistantMessage(cline: Task) { // Push tool_result directly without setting didAlreadyUseTool cline.pushToolResultToUserContent({ type: "tool_result", - tool_use_id: toolCallId, + tool_use_id: sanitizeToolUseId(toolCallId), content: typeof errorContent === "string" ? errorContent : "(validation error)", is_error: true, }) @@ -947,7 +948,7 @@ export async function presentAssistantMessage(cline: Task) { // This prevents the stream from being interrupted with "Response interrupted by tool use result" cline.pushToolResultToUserContent({ type: "tool_result", - tool_use_id: toolCallId, + tool_use_id: sanitizeToolUseId(toolCallId), content: formatResponse.toolError(errorMessage), is_error: true, }) diff --git a/src/utils/__tests__/tool-id.spec.ts b/src/utils/__tests__/tool-id.spec.ts index c047184417a..2459786ceaf 100644 --- a/src/utils/__tests__/tool-id.spec.ts +++ b/src/utils/__tests__/tool-id.spec.ts @@ -47,6 +47,14 @@ describe("sanitizeToolUseId", () => { it("should replace multiple invalid characters", () => { expect(sanitizeToolUseId("mcp.server:tool/name")).toBe("mcp_server_tool_name") }) + + it("should sanitize Gemini/OpenRouter function call IDs with dots and colons", () => { + // This is the exact pattern seen in PostHog errors where tool_result IDs + // didn't match tool_use IDs due to missing sanitization + expect(sanitizeToolUseId("functions.read_file:0")).toBe("functions_read_file_0") + expect(sanitizeToolUseId("functions.write_to_file:1")).toBe("functions_write_to_file_1") + expect(sanitizeToolUseId("read_file:0")).toBe("read_file_0") + }) }) describe("real-world MCP tool use ID patterns", () => {