fix: sanitize tool_use_id in tool_result blocks to match API history (#11131)

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
This commit is contained in:
Daniel 2026-01-30 23:15:25 -05:00 committed by GitHub
parent fa93109b76
commit 3400499917
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 16 additions and 7 deletions

View file

@ -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,
})

View file

@ -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", () => {