Skip to content

Commit bd39fe6

Browse files
fix: correct tool repetition detector to not block first tool call when limit is 1 (#6836)
Co-authored-by: Roo Code <[email protected]>
1 parent b1300e5 commit bd39fe6

File tree

2 files changed

+96
-52
lines changed

2 files changed

+96
-52
lines changed

src/core/tools/ToolRepetitionDetector.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export class ToolRepetitionDetector {
3939
if (this.previousToolCallJson === currentToolCallJson) {
4040
this.consecutiveIdenticalToolCallCount++
4141
} else {
42-
this.consecutiveIdenticalToolCallCount = 1 // Start with 1 for the first occurrence
42+
this.consecutiveIdenticalToolCallCount = 0 // Reset to 0 for a new tool
4343
this.previousToolCallJson = currentToolCallJson
4444
}
4545

src/core/tools/__tests__/ToolRepetitionDetector.spec.ts

Lines changed: 95 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -32,30 +32,38 @@ describe("ToolRepetitionDetector", () => {
3232
const detector = new ToolRepetitionDetector()
3333
// We'll verify this through behavior in subsequent tests
3434

35-
// First call (counter = 1)
35+
// First call (counter = 0)
3636
const result1 = detector.check(createToolUse("test", "test-tool"))
3737
expect(result1.allowExecution).toBe(true)
3838

39-
// Second identical call (counter = 2)
39+
// Second identical call (counter = 1)
4040
const result2 = detector.check(createToolUse("test", "test-tool"))
4141
expect(result2.allowExecution).toBe(true)
4242

43-
// Third identical call (counter = 3) reaches the default limit
43+
// Third identical call (counter = 2)
4444
const result3 = detector.check(createToolUse("test", "test-tool"))
45-
expect(result3.allowExecution).toBe(false)
45+
expect(result3.allowExecution).toBe(true)
46+
47+
// Fourth identical call (counter = 3) reaches the default limit
48+
const result4 = detector.check(createToolUse("test", "test-tool"))
49+
expect(result4.allowExecution).toBe(false)
4650
})
4751

4852
it("should use the custom limit when provided", () => {
4953
const customLimit = 2
5054
const detector = new ToolRepetitionDetector(customLimit)
5155

52-
// First call (counter = 1)
56+
// First call (counter = 0)
5357
const result1 = detector.check(createToolUse("test", "test-tool"))
5458
expect(result1.allowExecution).toBe(true)
5559

56-
// Second identical call (counter = 2) reaches the custom limit
60+
// Second identical call (counter = 1)
5761
const result2 = detector.check(createToolUse("test", "test-tool"))
58-
expect(result2.allowExecution).toBe(false)
62+
expect(result2.allowExecution).toBe(true)
63+
64+
// Third identical call (counter = 2) reaches the custom limit
65+
const result3 = detector.check(createToolUse("test", "test-tool"))
66+
expect(result3.allowExecution).toBe(false)
5967
})
6068
})
6169

@@ -97,17 +105,21 @@ describe("ToolRepetitionDetector", () => {
97105
it("should allow execution when repetition is below limit and block when limit reached", () => {
98106
const detector = new ToolRepetitionDetector(3)
99107

100-
// First call (counter = 1)
108+
// First call (counter = 0)
101109
const result1 = detector.check(createToolUse("repeat", "repeat-tool"))
102110
expect(result1.allowExecution).toBe(true)
103111

104-
// Second identical call (counter = 2)
112+
// Second identical call (counter = 1)
105113
const result2 = detector.check(createToolUse("repeat", "repeat-tool"))
106114
expect(result2.allowExecution).toBe(true)
107115

108-
// Third identical call (counter = 3) reaches limit
116+
// Third identical call (counter = 2)
109117
const result3 = detector.check(createToolUse("repeat", "repeat-tool"))
110-
expect(result3.allowExecution).toBe(false)
118+
expect(result3.allowExecution).toBe(true)
119+
120+
// Fourth identical call (counter = 3) reaches limit
121+
const result4 = detector.check(createToolUse("repeat", "repeat-tool"))
122+
expect(result4.allowExecution).toBe(false)
111123
})
112124
})
113125

@@ -116,13 +128,16 @@ describe("ToolRepetitionDetector", () => {
116128
it("should block execution when repetition reaches the limit", () => {
117129
const detector = new ToolRepetitionDetector(3)
118130

119-
// First call (counter = 1)
131+
// First call (counter = 0)
132+
detector.check(createToolUse("repeat", "repeat-tool"))
133+
134+
// Second identical call (counter = 1)
120135
detector.check(createToolUse("repeat", "repeat-tool"))
121136

122-
// Second identical call (counter = 2)
137+
// Third identical call (counter = 2)
123138
detector.check(createToolUse("repeat", "repeat-tool"))
124139

125-
// Third identical call (counter = 3) - should reach limit
140+
// Fourth identical call (counter = 3) - should reach limit
126141
const result = detector.check(createToolUse("repeat", "repeat-tool"))
127142

128143
expect(result.allowExecution).toBe(false)
@@ -136,6 +151,7 @@ describe("ToolRepetitionDetector", () => {
136151

137152
// Reach the limit
138153
detector.check(createToolUse("repeat", "repeat-tool"))
154+
detector.check(createToolUse("repeat", "repeat-tool"))
139155
const limitResult = detector.check(createToolUse("repeat", "repeat-tool")) // This reaches limit
140156
expect(limitResult.allowExecution).toBe(false)
141157

@@ -152,6 +168,7 @@ describe("ToolRepetitionDetector", () => {
152168

153169
// Reach the limit with a specific tool
154170
detector.check(createToolUse("problem", "problem-tool"))
171+
detector.check(createToolUse("problem", "problem-tool"))
155172
const limitResult = detector.check(createToolUse("problem", "problem-tool")) // This reaches limit
156173
expect(limitResult.allowExecution).toBe(false)
157174

@@ -165,13 +182,17 @@ describe("ToolRepetitionDetector", () => {
165182

166183
// Reach the limit
167184
detector.check(createToolUse("repeat", "repeat-tool"))
185+
detector.check(createToolUse("repeat", "repeat-tool"))
168186
const limitResult = detector.check(createToolUse("repeat", "repeat-tool")) // This reaches limit
169187
expect(limitResult.allowExecution).toBe(false)
170188

171189
// First call after reset
172190
detector.check(createToolUse("repeat", "repeat-tool"))
173191

174-
// Second identical call (counter = 2) should reach limit again
192+
// Second call after reset
193+
detector.check(createToolUse("repeat", "repeat-tool"))
194+
195+
// Third identical call (counter = 2) should reach limit again
175196
const result = detector.check(createToolUse("repeat", "repeat-tool"))
176197
expect(result.allowExecution).toBe(false)
177198
expect(result.askUser).toBeDefined()
@@ -186,6 +207,7 @@ describe("ToolRepetitionDetector", () => {
186207

187208
// Reach the limit
188209
detector.check(createToolUse("test", toolName))
210+
detector.check(createToolUse("test", toolName))
189211
const result = detector.check(createToolUse("test", toolName))
190212

191213
expect(result.allowExecution).toBe(false)
@@ -201,7 +223,8 @@ describe("ToolRepetitionDetector", () => {
201223
// Create an empty tool call - a tool with no parameters
202224
// Use the empty tool directly in the check calls
203225
detector.check(createToolUse("empty-tool", "empty-tool"))
204-
const result = detector.check(createToolUse("empty-tool"))
226+
detector.check(createToolUse("empty-tool", "empty-tool"))
227+
const result = detector.check(createToolUse("empty-tool", "empty-tool"))
205228

206229
expect(result.allowExecution).toBe(false)
207230
expect(result.askUser).toBeDefined()
@@ -210,7 +233,7 @@ describe("ToolRepetitionDetector", () => {
210233
it("should handle different tool names with identical serialized JSON", () => {
211234
const detector = new ToolRepetitionDetector(2)
212235

213-
// First, call with tool-name-1 twice to set up the counter
236+
// First, call with tool-name-1 to set up the counter
214237
const toolUse1 = createToolUse("tool-name-1", "tool-name-1", { param: "value" })
215238
detector.check(toolUse1)
216239

@@ -223,21 +246,25 @@ describe("ToolRepetitionDetector", () => {
223246
;(detector as any).serializeToolUse = (tool: ToolUse) => {
224247
// Use string comparison for the name since it's technically an enum
225248
if (String(tool.name) === "tool-name-2") {
226-
return (detector as any).serializeToolUse(toolUse1) // Return the same JSON as toolUse1
249+
return originalSerialize.call(detector, toolUse1) // Return the same JSON as toolUse1
227250
}
228-
return originalSerialize(tool)
251+
return originalSerialize.call(detector, tool)
229252
}
230253

231-
// This should detect as a repetition now
232-
const result = detector.check(toolUse2)
254+
// Second call - this should be considered identical due to our mock
255+
const result2 = detector.check(toolUse2)
256+
expect(result2.allowExecution).toBe(true) // Still allowed (counter = 1)
257+
258+
// Third call - should be blocked (limit is 2)
259+
const result3 = detector.check(toolUse2)
233260

234261
// Restore the original method
235262
;(detector as any).serializeToolUse = originalSerialize
236263

237264
// Since we're directly manipulating the internal state for testing,
238-
// we still expect it to consider this a repetition
239-
expect(result.allowExecution).toBe(false)
240-
expect(result.askUser).toBeDefined()
265+
// we expect it to consider this a repetition
266+
expect(result3.allowExecution).toBe(false)
267+
expect(result3.askUser).toBeDefined()
241268
})
242269

243270
it("should treat tools with same parameters in different order as identical", () => {
@@ -247,11 +274,13 @@ describe("ToolRepetitionDetector", () => {
247274
const toolUse1 = createToolUse("same-tool", "same-tool", { a: "1", b: "2", c: "3" })
248275
detector.check(toolUse1)
249276

250-
// Create tool with same parameters but in different order
277+
// Second call with same parameters but in different order
251278
const toolUse2 = createToolUse("same-tool", "same-tool", { c: "3", a: "1", b: "2" })
279+
detector.check(toolUse2)
252280

253-
// This should still detect as a repetition due to canonical JSON with sorted keys
254-
const result = detector.check(toolUse2)
281+
// Third call - should be blocked (limit is 2)
282+
const toolUse3 = createToolUse("same-tool", "same-tool", { b: "2", c: "3", a: "1" })
283+
const result = detector.check(toolUse3)
255284

256285
// Since parameters are sorted alphabetically in the serialized JSON,
257286
// these should be considered identical
@@ -262,44 +291,56 @@ describe("ToolRepetitionDetector", () => {
262291

263292
// ===== Explicit Nth Call Blocking tests =====
264293
describe("explicit Nth call blocking behavior", () => {
265-
it("should block on the 1st call for limit 1", () => {
294+
it("should allow the 1st call but block on the 2nd call for limit 1", () => {
266295
const detector = new ToolRepetitionDetector(1)
267296

268-
// First call (counter = 1) should be blocked
269-
const result = detector.check(createToolUse("tool", "tool-name"))
297+
// First call (counter = 0) should be allowed
298+
const result1 = detector.check(createToolUse("tool", "tool-name"))
299+
expect(result1.allowExecution).toBe(true)
300+
expect(result1.askUser).toBeUndefined()
270301

271-
expect(result.allowExecution).toBe(false)
272-
expect(result.askUser).toBeDefined()
302+
// Second identical call (counter = 1) should be blocked
303+
const result2 = detector.check(createToolUse("tool", "tool-name"))
304+
expect(result2.allowExecution).toBe(false)
305+
expect(result2.askUser).toBeDefined()
273306
})
274307

275-
it("should block on the 2nd call for limit 2", () => {
308+
it("should allow first 2 calls but block on the 3rd call for limit 2", () => {
276309
const detector = new ToolRepetitionDetector(2)
277310

278-
// First call (counter = 1)
311+
// First call (counter = 0)
279312
const result1 = detector.check(createToolUse("tool", "tool-name"))
280313
expect(result1.allowExecution).toBe(true)
281314

282-
// Second call (counter = 2) should be blocked
315+
// Second identical call (counter = 1)
283316
const result2 = detector.check(createToolUse("tool", "tool-name"))
284-
expect(result2.allowExecution).toBe(false)
285-
expect(result2.askUser).toBeDefined()
317+
expect(result2.allowExecution).toBe(true)
318+
319+
// Third identical call (counter = 2) should be blocked
320+
const result3 = detector.check(createToolUse("tool", "tool-name"))
321+
expect(result3.allowExecution).toBe(false)
322+
expect(result3.askUser).toBeDefined()
286323
})
287324

288-
it("should block on the 3rd call for limit 3 (default)", () => {
325+
it("should allow first 3 calls but block on the 4th call for limit 3 (default)", () => {
289326
const detector = new ToolRepetitionDetector(3)
290327

291-
// First call (counter = 1)
328+
// First call (counter = 0)
292329
const result1 = detector.check(createToolUse("tool", "tool-name"))
293330
expect(result1.allowExecution).toBe(true)
294331

295-
// Second call (counter = 2)
332+
// Second identical call (counter = 1)
296333
const result2 = detector.check(createToolUse("tool", "tool-name"))
297334
expect(result2.allowExecution).toBe(true)
298335

299-
// Third call (counter = 3) should be blocked
336+
// Third identical call (counter = 2)
300337
const result3 = detector.check(createToolUse("tool", "tool-name"))
301-
expect(result3.allowExecution).toBe(false)
302-
expect(result3.askUser).toBeDefined()
338+
expect(result3.allowExecution).toBe(true)
339+
340+
// Fourth identical call (counter = 3) should be blocked
341+
const result4 = detector.check(createToolUse("tool", "tool-name"))
342+
expect(result4.allowExecution).toBe(false)
343+
expect(result4.askUser).toBeDefined()
303344
})
304345

305346
it("should never block when limit is 0 (unlimited)", () => {
@@ -318,18 +359,18 @@ describe("ToolRepetitionDetector", () => {
318359
const detector5 = new ToolRepetitionDetector(5)
319360
const tool = createToolUse("tool", "tool-name")
320361

321-
// First 4 calls should be allowed
322-
for (let i = 0; i < 4; i++) {
362+
// First 5 calls should be allowed
363+
for (let i = 0; i < 5; i++) {
323364
const result = detector5.check(tool)
324365
expect(result.allowExecution).toBe(true)
325366
expect(result.askUser).toBeUndefined()
326367
}
327368

328-
// 5th call should be blocked
329-
const result5 = detector5.check(tool)
330-
expect(result5.allowExecution).toBe(false)
331-
expect(result5.askUser).toBeDefined()
332-
expect(result5.askUser?.messageKey).toBe("mistake_limit_reached")
369+
// 6th call should be blocked
370+
const result6 = detector5.check(tool)
371+
expect(result6.allowExecution).toBe(false)
372+
expect(result6.askUser).toBeDefined()
373+
expect(result6.askUser?.messageKey).toBe("mistake_limit_reached")
333374
})
334375

335376
it("should reset counter after blocking and allow new attempts", () => {
@@ -339,7 +380,10 @@ describe("ToolRepetitionDetector", () => {
339380
// First call allowed
340381
expect(detector.check(tool).allowExecution).toBe(true)
341382

342-
// Second call should block (limit is 2)
383+
// Second call allowed
384+
expect(detector.check(tool).allowExecution).toBe(true)
385+
386+
// Third call should block (limit is 2)
343387
const blocked = detector.check(tool)
344388
expect(blocked.allowExecution).toBe(false)
345389

0 commit comments

Comments
 (0)