Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 11 additions & 10 deletions src/core/condense/__tests__/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -482,25 +482,25 @@ describe("getKeepMessagesWithToolBlocks", () => {
expect(result.toolUseBlocksToPreserve).toContainEqual(toolUseBlock2)
})

it("should not crash when tool_result references tool_use beyond search boundary", () => {
it("should preserve tool_use even when it is far back in the conversation history", () => {
const toolResultBlock = {
type: "tool_result" as const,
tool_use_id: "toolu_beyond_boundary",
tool_use_id: "toolu_far_back",
content: "result",
}

// Tool_use is at ts:1, but with N_MESSAGES_TO_KEEP=3, we only search back 3 messages
// from startIndex-1. StartIndex is 7 (messages.length=10, keepCount=3, startIndex=7).
// So we search from index 6 down to index 4 (7-1 down to 7-3).
// The tool_use at index 0 (ts:1) is beyond the search boundary.
// Tool_use is at ts:1, far back in the conversation.
// The search now covers the entire condensed region (from index 0 to startIndex-1),
// so the tool_use WILL be found and preserved. This prevents the API error:
// "tool_use ids were found without tool_result blocks immediately after"
const messages: ApiMessage[] = [
{
role: "assistant",
content: [
{ type: "text" as const, text: "Way back..." },
{
type: "tool_use" as const,
id: "toolu_beyond_boundary",
id: "toolu_far_back",
name: "old_tool",
input: {},
},
Expand All @@ -522,7 +522,6 @@ describe("getKeepMessagesWithToolBlocks", () => {
{ role: "user", content: "Message 10", ts: 10 },
]

// Should not crash
const result = getKeepMessagesWithToolBlocks(messages, 3)

// keepMessages should be the last 3 messages
Expand All @@ -531,8 +530,10 @@ describe("getKeepMessagesWithToolBlocks", () => {
expect(result.keepMessages[1].ts).toBe(9)
expect(result.keepMessages[2].ts).toBe(10)

// Should not preserve the tool_use since it's beyond the search boundary
expect(result.toolUseBlocksToPreserve).toHaveLength(0)
// Should NOW preserve the tool_use - this is the fix for the API error
// where tool_result blocks were orphaned because their tool_use wasn't found
expect(result.toolUseBlocksToPreserve).toHaveLength(1)
expect(result.toolUseBlocksToPreserve[0].id).toBe("toolu_far_back")
})

it("should not duplicate tool_use blocks when same tool_result ID appears multiple times", () => {
Expand Down
63 changes: 38 additions & 25 deletions src/core/condense/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,41 +105,54 @@ export function getKeepMessagesWithToolBlocks(messages: ApiMessage[], keepCount:
const reasoningBlocksToPreserve: Anthropic.Messages.ContentBlockParam[] = []
const preservedToolUseIds = new Set<string>()

// Check ALL kept messages for tool_result blocks
// First, collect all tool_use_ids we need to find from kept messages
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor cleanup: The findLast import from ../../shared/array at line 10 is now unused since this refactoring replaced the linear search with a Map-based lookup. Consider removing it.

Fix it with Roo Code or mention @roomote and request a fix.

const toolUseIdsToFind = new Set<string>()
for (const keepMsg of keepMessages) {
if (!hasToolResultBlocks(keepMsg)) {
continue
}
for (const toolResult of getToolResultBlocks(keepMsg)) {
toolUseIdsToFind.add(toolResult.tool_use_id)
}
}

const toolResults = getToolResultBlocks(keepMsg)

for (const toolResult of toolResults) {
const toolUseId = toolResult.tool_use_id
// Early exit if no tool_results in kept messages
if (toolUseIdsToFind.size === 0) {
return { keepMessages, toolUseBlocksToPreserve: [], reasoningBlocksToPreserve: [] }
}

// Skip if we've already found this tool_use
if (preservedToolUseIds.has(toolUseId)) {
continue
// Build an index of tool_use ID -> message for the condensed region
// This avoids repeated linear searches through the entire history
// Only index assistant messages (which contain tool_use blocks)
const toolUseIndex = new Map<string, ApiMessage>()
for (let i = 0; i < startIndex; i++) {
const msg = messages[i]
if (msg.role !== "assistant") {
continue
}
for (const toolUse of getToolUseBlocks(msg)) {
// Only index IDs we're looking for
if (toolUseIdsToFind.has(toolUse.id)) {
toolUseIndex.set(toolUse.id, msg)
}
}
}

// Search backwards through the condensed region (bounded)
const searchStart = startIndex - 1
const searchEnd = Math.max(0, startIndex - N_MESSAGES_TO_KEEP)
const messagesToSearch = messages.slice(searchEnd, searchStart + 1)

// Find the message containing this tool_use
const messageWithToolUse = findLast(messagesToSearch, (msg) => {
return findToolUseBlockById(msg, toolUseId) !== undefined
})
// Now look up each tool_use we need
for (const toolUseId of toolUseIdsToFind) {
if (preservedToolUseIds.has(toolUseId)) {
continue
}

if (messageWithToolUse) {
const toolUse = findToolUseBlockById(messageWithToolUse, toolUseId)!
toolUseBlocksToPreserve.push(toolUse)
preservedToolUseIds.add(toolUseId)
const messageWithToolUse = toolUseIndex.get(toolUseId)
if (messageWithToolUse) {
const toolUse = findToolUseBlockById(messageWithToolUse, toolUseId)!
toolUseBlocksToPreserve.push(toolUse)
preservedToolUseIds.add(toolUseId)

// Also preserve reasoning blocks from that message
const reasoning = getReasoningBlocks(messageWithToolUse)
reasoningBlocksToPreserve.push(...reasoning)
}
// Also preserve reasoning blocks from that message
const reasoning = getReasoningBlocks(messageWithToolUse)
reasoningBlocksToPreserve.push(...reasoning)
}
}

Expand Down
Loading