mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-11 22:51:26 +00:00
fix: filter orphan tool_result blocks after sliding window truncation (no summary)
When sliding window truncation removes assistant messages containing tool_use blocks, user messages with tool_result blocks referencing those removed IDs were being sent to the API, causing Anthropic to reject the request with 'unexpected tool_use_id in tool_result blocks'. The existing orphan filtering logic only ran in the summary (condensation) code path. This adds the same filtering to the no-summary code path in getEffectiveApiHistory(), ensuring tool_use/tool_result pairs remain atomic regardless of how truncation occurs. Fixes #11029
This commit is contained in:
parent
7607c684e9
commit
ac1d172e94
2 changed files with 166 additions and 1 deletions
|
|
@ -583,6 +583,134 @@ describe("getEffectiveApiHistory", () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe("getEffectiveApiHistory - orphan tool_result filtering after truncation (no summary)", () => {
|
||||
it("should filter orphan tool_result blocks when truncation removes assistant tool_use messages", () => {
|
||||
const truncationId = "trunc-1"
|
||||
const messages: ApiMessage[] = [
|
||||
// Truncation marker
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "[Previous context truncated]" }],
|
||||
isTruncationMarker: true,
|
||||
truncationId,
|
||||
},
|
||||
// Assistant message with tool_use, hidden by truncation
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "tool_use", id: "tool-1", name: "read_file", input: { path: "test.ts" } }],
|
||||
truncationParent: truncationId,
|
||||
},
|
||||
// User message with tool_result referencing truncated tool_use
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "tool_result", tool_use_id: "tool-1", content: "file contents" }],
|
||||
truncationParent: truncationId,
|
||||
},
|
||||
// Visible assistant message with tool_use
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "tool_use", id: "tool-2", name: "write_file", input: { path: "out.ts", content: "code" } },
|
||||
],
|
||||
},
|
||||
// Visible user message with tool_result for tool-2
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "tool_result", tool_use_id: "tool-2", content: "file written" }],
|
||||
},
|
||||
]
|
||||
|
||||
const result = getEffectiveApiHistory(messages)
|
||||
|
||||
// Should have: truncation marker, visible assistant, visible user (3 messages)
|
||||
// Truncated assistant and user are filtered by truncationParent
|
||||
expect(result).toHaveLength(3)
|
||||
expect(result[0].isTruncationMarker).toBe(true)
|
||||
expect(result[1].role).toBe("assistant")
|
||||
expect(result[2].role).toBe("user")
|
||||
const userContent = result[2].content as any[]
|
||||
expect(userContent[0].tool_use_id).toBe("tool-2")
|
||||
})
|
||||
|
||||
it("should filter orphan tool_result when user message survives truncation but referenced assistant is truncated", () => {
|
||||
const truncationId = "trunc-1"
|
||||
const messages: ApiMessage[] = [
|
||||
// Truncation marker
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "[Previous context truncated]" }],
|
||||
isTruncationMarker: true,
|
||||
truncationId,
|
||||
},
|
||||
// Assistant message with tool_use, hidden by truncation
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "tool_use", id: "tool-orphan", name: "read_file", input: { path: "test.ts" } }],
|
||||
truncationParent: truncationId,
|
||||
},
|
||||
// User message with orphan tool_result - NOT tagged with truncationParent
|
||||
// This is the bug scenario: truncation removed the assistant but not the user message
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "tool_result", tool_use_id: "tool-orphan", content: "file contents" }],
|
||||
},
|
||||
// Visible conversation continues
|
||||
{
|
||||
role: "assistant",
|
||||
content: "Here is the result.",
|
||||
},
|
||||
]
|
||||
|
||||
const result = getEffectiveApiHistory(messages)
|
||||
|
||||
// The orphan tool_result user message should be removed entirely
|
||||
// Result: truncation marker, assistant text (2 messages)
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result[0].isTruncationMarker).toBe(true)
|
||||
expect(result[1].role).toBe("assistant")
|
||||
expect(result[1].content).toBe("Here is the result.")
|
||||
})
|
||||
|
||||
it("should keep non-orphan content in mixed user message after truncation", () => {
|
||||
const truncationId = "trunc-1"
|
||||
const messages: ApiMessage[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "[Previous context truncated]" }],
|
||||
isTruncationMarker: true,
|
||||
truncationId,
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "tool_use", id: "tool-orphan", name: "read_file", input: { path: "test.ts" } }],
|
||||
truncationParent: truncationId,
|
||||
},
|
||||
// User message with both orphan tool_result AND text content
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "text", text: "Here's some context" },
|
||||
{ type: "tool_result", tool_use_id: "tool-orphan", content: "file contents" },
|
||||
],
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
content: "Got it.",
|
||||
},
|
||||
]
|
||||
|
||||
const result = getEffectiveApiHistory(messages)
|
||||
|
||||
// Should keep the user message but strip the orphan tool_result
|
||||
expect(result).toHaveLength(3)
|
||||
expect(result[0].isTruncationMarker).toBe(true)
|
||||
const userContent = result[1].content as any[]
|
||||
expect(userContent).toHaveLength(1)
|
||||
expect(userContent[0].type).toBe("text")
|
||||
expect(userContent[0].text).toBe("Here's some context")
|
||||
})
|
||||
})
|
||||
|
||||
describe("cleanupAfterTruncation", () => {
|
||||
it("should clear orphaned condenseParent references", () => {
|
||||
const orphanedCondenseId = "deleted-summary"
|
||||
|
|
|
|||
|
|
@ -489,7 +489,7 @@ export function getEffectiveApiHistory(messages: ApiMessage[]): ApiMessage[] {
|
|||
// Filter out messages whose condenseParent points to an existing summary
|
||||
// or whose truncationParent points to an existing truncation marker.
|
||||
// Messages with orphaned parents (summary/marker was deleted) are included.
|
||||
return messages.filter((msg) => {
|
||||
const visibleMessages = messages.filter((msg) => {
|
||||
// Filter out condensed messages if their summary exists
|
||||
if (msg.condenseParent && existingSummaryIds.has(msg.condenseParent)) {
|
||||
return false
|
||||
|
|
@ -500,6 +500,43 @@ export function getEffectiveApiHistory(messages: ApiMessage[]): ApiMessage[] {
|
|||
}
|
||||
return true
|
||||
})
|
||||
|
||||
// Collect all tool_use IDs from visible assistant messages.
|
||||
// This is needed to filter out orphan tool_result blocks that reference
|
||||
// tool_use IDs from messages that were truncated away.
|
||||
const toolUseIds = new Set<string>()
|
||||
for (const msg of visibleMessages) {
|
||||
if (msg.role === "assistant" && Array.isArray(msg.content)) {
|
||||
for (const block of msg.content) {
|
||||
if (block.type === "tool_use" && (block as Anthropic.Messages.ToolUseBlockParam).id) {
|
||||
toolUseIds.add((block as Anthropic.Messages.ToolUseBlockParam).id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filter out orphan tool_result blocks from user messages
|
||||
return visibleMessages
|
||||
.map((msg) => {
|
||||
if (msg.role === "user" && Array.isArray(msg.content)) {
|
||||
const filteredContent = msg.content.filter((block) => {
|
||||
if (block.type === "tool_result") {
|
||||
return toolUseIds.has((block as Anthropic.Messages.ToolResultBlockParam).tool_use_id)
|
||||
}
|
||||
return true
|
||||
})
|
||||
// If all content was filtered out, mark for removal
|
||||
if (filteredContent.length === 0) {
|
||||
return null
|
||||
}
|
||||
// If some content was filtered, return updated message
|
||||
if (filteredContent.length !== msg.content.length) {
|
||||
return { ...msg, content: filteredContent }
|
||||
}
|
||||
}
|
||||
return msg
|
||||
})
|
||||
.filter((msg): msg is ApiMessage => msg !== null)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue