Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

Commit 04fc2a0

Browse files
committed
fix: search entire condensed region for tool_use blocks (ROO-520)
The bounded search window in getKeepMessagesWithToolBlocks was too restrictive after multiple condensations. When tool_use blocks are preserved in earlier summary messages, they can end up outside the N_MESSAGES_TO_KEEP search window, causing orphaned tool_result blocks that trigger 400 errors from the API. Changed the search to cover the entire condensed region (from index 0 to startIndex-1) instead of just the last N_MESSAGES_TO_KEEP messages. Fixes: https://linear.app/roocode/issue/ROO-520
1 parent 4ee494b commit 04fc2a0

2 files changed

Lines changed: 95 additions & 19 deletions

File tree

src/core/condense/__tests__/index.spec.ts

Lines changed: 84 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -482,27 +482,29 @@ describe("getKeepMessagesWithToolBlocks", () => {
482482
expect(result.toolUseBlocksToPreserve).toContainEqual(toolUseBlock2)
483483
})
484484

485-
it("should not crash when tool_result references tool_use beyond search boundary", () => {
485+
it("should find tool_use even when it is far back in the message history (ROO-520 fix)", () => {
486+
// This test verifies the fix for https://linear.app/roocode/issue/ROO-520
487+
// The bug occurred when tool_use was preserved in an earlier summary message
488+
// after multiple condensations. The bounded search window was too small to find it,
489+
// resulting in orphaned tool_result blocks that caused 400 errors from the API.
486490
const toolResultBlock = {
487491
type: "tool_result" as const,
488-
tool_use_id: "toolu_beyond_boundary",
492+
tool_use_id: "toolu_early_in_history",
489493
content: "result",
490494
}
491495

492-
// Tool_use is at ts:1, but with N_MESSAGES_TO_KEEP=3, we only search back 3 messages
493-
// from startIndex-1. StartIndex is 7 (messages.length=10, keepCount=3, startIndex=7).
494-
// So we search from index 6 down to index 4 (7-1 down to 7-3).
495-
// The tool_use at index 0 (ts:1) is beyond the search boundary.
496+
// Tool_use is at index 0, tool_result is in the last 3 messages.
497+
// The search must cover the ENTIRE condensed region to find the tool_use.
496498
const messages: ApiMessage[] = [
497499
{
498500
role: "assistant",
499501
content: [
500-
{ type: "text" as const, text: "Way back..." },
502+
{ type: "text" as const, text: "Early tool call..." },
501503
{
502504
type: "tool_use" as const,
503-
id: "toolu_beyond_boundary",
504-
name: "old_tool",
505-
input: {},
505+
id: "toolu_early_in_history",
506+
name: "read_file",
507+
input: { path: "test.txt" },
506508
},
507509
],
508510
ts: 1,
@@ -522,7 +524,6 @@ describe("getKeepMessagesWithToolBlocks", () => {
522524
{ role: "user", content: "Message 10", ts: 10 },
523525
]
524526

525-
// Should not crash
526527
const result = getKeepMessagesWithToolBlocks(messages, 3)
527528

528529
// keepMessages should be the last 3 messages
@@ -531,8 +532,78 @@ describe("getKeepMessagesWithToolBlocks", () => {
531532
expect(result.keepMessages[1].ts).toBe(9)
532533
expect(result.keepMessages[2].ts).toBe(10)
533534

534-
// Should not preserve the tool_use since it's beyond the search boundary
535-
expect(result.toolUseBlocksToPreserve).toHaveLength(0)
535+
// With the fix, tool_use should be found even though it's far back in history
536+
expect(result.toolUseBlocksToPreserve).toHaveLength(1)
537+
expect(result.toolUseBlocksToPreserve[0].id).toBe("toolu_early_in_history")
538+
})
539+
540+
it("should find tool_use in previous summary message after multiple condensations (ROO-520)", () => {
541+
// This test simulates the exact scenario from ROO-520:
542+
// After first condensation, tool_use was preserved in summary1.
543+
// After second condensation, the bounded search couldn't find tool_use in summary1
544+
// because it was outside the N_MESSAGES_TO_KEEP search window.
545+
const toolUseBlock = {
546+
type: "tool_use" as const,
547+
id: "toolu_in_summary",
548+
name: "read_file",
549+
input: { path: "test.txt" },
550+
}
551+
const toolResultBlock = {
552+
type: "tool_result" as const,
553+
tool_use_id: "toolu_in_summary",
554+
content: "file contents",
555+
}
556+
557+
// Simulate state after first condensation:
558+
// summary1 contains a preserved tool_use block from the first condense
559+
const summaryMessage: ApiMessage = {
560+
role: "assistant",
561+
content: [{ type: "text" as const, text: "Summary of first conversation chunk" }, toolUseBlock],
562+
ts: 5,
563+
isSummary: true,
564+
condenseId: "summary-1",
565+
}
566+
567+
const messages: ApiMessage[] = [
568+
{ role: "user", content: "First message", ts: 1 },
569+
// Messages 2-4 are tagged with condenseParent from first condensation
570+
{ role: "assistant", content: "Message 2", ts: 2, condenseParent: "summary-1" },
571+
{ role: "user", content: "Message 3", ts: 3, condenseParent: "summary-1" },
572+
{ role: "assistant", content: "Message 4", ts: 4, condenseParent: "summary-1" },
573+
// summary1 with preserved tool_use
574+
summaryMessage,
575+
// Messages after first condensation
576+
{ role: "user", content: "Message 6", ts: 6 },
577+
{ role: "assistant", content: "Message 7", ts: 7 },
578+
{ role: "user", content: "Message 8", ts: 8 },
579+
{ role: "assistant", content: "Message 9", ts: 9 },
580+
{ role: "user", content: "Message 10", ts: 10 },
581+
{ role: "assistant", content: "Message 11", ts: 11 },
582+
// This user message has tool_result referencing the tool_use in summary1
583+
{
584+
role: "user",
585+
content: [toolResultBlock, { type: "text" as const, text: "Continue" }],
586+
ts: 12,
587+
},
588+
{ role: "assistant", content: "Message 13", ts: 13 },
589+
{ role: "user", content: "Message 14", ts: 14 },
590+
]
591+
592+
// Second condensation: keepCount=3 means we keep messages 12, 13, 14
593+
// startIndex = 14 - 3 = 11
594+
// Old bounded search would only look at messages[8:11] = [msg9, msg10, msg11]
595+
// The tool_use in summary1 at index 4 would NOT be found!
596+
const result = getKeepMessagesWithToolBlocks(messages, 3)
597+
598+
expect(result.keepMessages).toHaveLength(3)
599+
expect(result.keepMessages[0].ts).toBe(12) // Has tool_result
600+
expect(result.keepMessages[1].ts).toBe(13)
601+
expect(result.keepMessages[2].ts).toBe(14)
602+
603+
// With the fix, we search the ENTIRE condensed region (messages[0:11])
604+
// and find the tool_use in summary1
605+
expect(result.toolUseBlocksToPreserve).toHaveLength(1)
606+
expect(result.toolUseBlocksToPreserve[0].id).toBe("toolu_in_summary")
536607
})
537608

538609
it("should not duplicate tool_use blocks when same tool_result ID appears multiple times", () => {

src/core/condense/index.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,15 @@ export type KeepMessagesResult = {
8080

8181
/**
8282
* Extracts tool_use blocks that need to be preserved to match tool_result blocks in keepMessages.
83-
* Checks ALL kept messages for tool_result blocks and searches backwards through the condensed
84-
* region (bounded by N_MESSAGES_TO_KEEP) to find the matching tool_use blocks by ID.
83+
* Checks ALL kept messages for tool_result blocks and searches backwards through the ENTIRE
84+
* condensed region to find the matching tool_use blocks by ID.
8585
* These tool_use blocks will be appended to the summary message to maintain proper pairing.
8686
*
87+
* IMPORTANT: The search must cover the entire condensed region (from index 0 to startIndex-1),
88+
* not just a bounded window. After multiple condensations, tool_use blocks can be preserved in
89+
* earlier summary messages that are outside a bounded search window.
90+
* See: https://linear.app/roocode/issue/ROO-520
91+
*
8792
* Also extracts reasoning blocks from messages containing preserved tool_uses, which are required
8893
* by DeepSeek and Z.ai for interleaved thinking mode. Without these, the API returns a 400 error
8994
* "Missing reasoning_content field in the assistant message".
@@ -121,10 +126,10 @@ export function getKeepMessagesWithToolBlocks(messages: ApiMessage[], keepCount:
121126
continue
122127
}
123128

124-
// Search backwards through the condensed region (bounded)
125-
const searchStart = startIndex - 1
126-
const searchEnd = Math.max(0, startIndex - N_MESSAGES_TO_KEEP)
127-
const messagesToSearch = messages.slice(searchEnd, searchStart + 1)
129+
// Search backwards through the ENTIRE condensed region (from index 0 to startIndex-1)
130+
// This is critical because after multiple condensations, tool_use blocks may be
131+
// preserved in earlier summary messages that are outside a bounded search window.
132+
const messagesToSearch = messages.slice(0, startIndex)
128133

129134
// Find the message containing this tool_use
130135
const messageWithToolUse = findLast(messagesToSearch, (msg) => {

0 commit comments

Comments
 (0)