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

Commit 538ecae

Browse files
committed
fix: add path-based repetition detection for read_file tool
This enhances the ToolRepetitionDetector to detect when the same file is being read repeatedly with different parameters (e.g., different line ranges). Previously, the detector only caught byte-for-byte identical tool calls. This change adds secondary detection specifically for read_file calls that target the same file path, regardless of other parameters. This helps prevent models like GLM4.5 from getting stuck in loops where they read the same file over and over with slightly different line ranges. Fixes #11071
1 parent 40b2bdc commit 538ecae

3 files changed

Lines changed: 380 additions & 1 deletion

File tree

src/core/tools/ToolRepetitionDetector.ts

Lines changed: 122 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,28 @@ import { t } from "../../i18n"
55
/**
66
* Class for detecting consecutive identical tool calls
77
* to prevent the AI from getting stuck in a loop.
8+
*
9+
* Also includes path-based detection for read_file to catch cases where
10+
* the model reads the same file with different parameters (e.g., different line ranges).
811
*/
912
export class ToolRepetitionDetector {
1013
private previousToolCallJson: string | null = null
1114
private consecutiveIdenticalToolCallCount: number = 0
1215
private readonly consecutiveIdenticalToolCallLimit: number
1316

17+
// Path-based tracking for read_file
18+
private previousReadFilePaths: string | null = null
19+
private consecutiveReadFilePathCount: number = 0
20+
private readonly readFilePathLimit: number
21+
1422
/**
1523
* Creates a new ToolRepetitionDetector
1624
* @param limit The maximum number of identical consecutive tool calls allowed
25+
* @param readFilePathLimit The maximum number of consecutive read_file calls for the same file path (default: same as limit)
1726
*/
18-
constructor(limit: number = 3) {
27+
constructor(limit: number = 3, readFilePathLimit?: number) {
1928
this.consecutiveIdenticalToolCallLimit = limit
29+
this.readFilePathLimit = readFilePathLimit ?? limit
2030
}
2131

2232
/**
@@ -40,6 +50,12 @@ export class ToolRepetitionDetector {
4050
return { allowExecution: true }
4151
}
4252

53+
// Check path-based repetition for read_file
54+
const pathRepetitionResult = this.checkReadFilePathRepetition(currentToolCallBlock)
55+
if (!pathRepetitionResult.allowExecution) {
56+
return pathRepetitionResult
57+
}
58+
4359
// Serialize the block to a canonical JSON string for comparison
4460
const currentToolCallJson = this.serializeToolUse(currentToolCallBlock)
4561

@@ -74,6 +90,111 @@ export class ToolRepetitionDetector {
7490
return { allowExecution: true }
7591
}
7692

93+
/**
94+
* Checks for path-based repetition specifically for read_file tool.
95+
* This catches cases where the model reads the same file with different parameters
96+
* (e.g., different line ranges), which would not be caught by identical call detection.
97+
*
98+
* @param toolUse The ToolUse object to check
99+
* @returns Object indicating if execution is allowed and a message to show if not
100+
*/
101+
private checkReadFilePathRepetition(toolUse: ToolUse): {
102+
allowExecution: boolean
103+
askUser?: {
104+
messageKey: string
105+
messageDetail: string
106+
}
107+
} {
108+
// Only apply to read_file tool
109+
if (toolUse.name !== "read_file") {
110+
// Reset path tracking when switching to a different tool
111+
this.previousReadFilePaths = null
112+
this.consecutiveReadFilePathCount = 0
113+
return { allowExecution: true }
114+
}
115+
116+
// Extract file paths from the tool use
117+
const currentPaths = this.extractReadFilePaths(toolUse)
118+
119+
// Compare with previous paths
120+
if (this.previousReadFilePaths === currentPaths) {
121+
this.consecutiveReadFilePathCount++
122+
} else {
123+
this.consecutiveReadFilePathCount = 0
124+
this.previousReadFilePaths = currentPaths
125+
}
126+
127+
// Check if limit is reached (0 means unlimited)
128+
if (this.readFilePathLimit > 0 && this.consecutiveReadFilePathCount >= this.readFilePathLimit) {
129+
// Reset counters to allow recovery if user guides the AI past this point
130+
this.consecutiveReadFilePathCount = 0
131+
this.previousReadFilePaths = null
132+
133+
// Return result indicating execution should not be allowed
134+
return {
135+
allowExecution: false,
136+
askUser: {
137+
messageKey: "mistake_limit_reached",
138+
messageDetail: t("tools:readFilePathRepetitionLimitReached", { toolName: toolUse.name }),
139+
},
140+
}
141+
}
142+
143+
return { allowExecution: true }
144+
}
145+
146+
/**
147+
* Extracts file paths from a read_file tool use.
148+
* Handles both params-based and nativeArgs-based formats.
149+
*
150+
* @param toolUse The read_file ToolUse object
151+
* @returns A canonical string representation of the file paths
152+
*/
153+
private extractReadFilePaths(toolUse: ToolUse): string {
154+
const paths: string[] = []
155+
156+
// Check nativeArgs first (native protocol format)
157+
if (toolUse.nativeArgs && typeof toolUse.nativeArgs === "object") {
158+
const nativeArgs = toolUse.nativeArgs as { files?: Array<{ path?: string }> }
159+
if (nativeArgs.files && Array.isArray(nativeArgs.files)) {
160+
for (const file of nativeArgs.files) {
161+
if (file.path) {
162+
paths.push(file.path)
163+
}
164+
}
165+
}
166+
}
167+
168+
// Check params (legacy format or if nativeArgs didn't have files)
169+
if (paths.length === 0 && toolUse.params) {
170+
// Single file path
171+
if (toolUse.params.path) {
172+
paths.push(toolUse.params.path as string)
173+
}
174+
// Multiple files format (params.files is a JSON array string or array)
175+
if (toolUse.params.files) {
176+
const files = toolUse.params.files
177+
if (typeof files === "string") {
178+
try {
179+
const parsed = JSON.parse(files)
180+
if (Array.isArray(parsed)) {
181+
for (const file of parsed) {
182+
if (file.path) {
183+
paths.push(file.path)
184+
}
185+
}
186+
}
187+
} catch {
188+
// Ignore parse errors
189+
}
190+
}
191+
}
192+
}
193+
194+
// Sort paths for consistent comparison
195+
return paths.sort().join("|")
196+
}
197+
77198
/**
78199
* Checks if a tool use is a browser scroll action
79200
*

0 commit comments

Comments
 (0)