Skip to content

Commit 9601be0

Browse files
committed
fix: detect and handle truncated write_to_file operations for large files
- Add detection for files >7000 lines when line_count is missing - Add validation to compare actual vs predicted line count with 10% tolerance - Provide clear error messages when truncation is detected - Add comprehensive tests for large file handling Fixes #6247
1 parent 6216075 commit 9601be0

File tree

2 files changed

+131
-1
lines changed

2 files changed

+131
-1
lines changed

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

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -339,11 +339,82 @@ describe("writeToFileTool", () => {
339339
})
340340

341341
it("processes files with very large line counts", async () => {
342-
await executeWriteFileTool({ line_count: "999999" })
342+
// Create content that matches the line count to avoid mismatch error
343+
const largeContent = Array(999).fill("Line content").join("\n")
344+
await executeWriteFileTool({
345+
content: largeContent,
346+
line_count: "999",
347+
})
343348

344349
// Should process normally without issues
345350
expect(mockCline.consecutiveMistakeCount).toBe(0)
346351
})
352+
353+
it("detects potential truncation when line count is 0 and content exceeds 7000 lines", async () => {
354+
// Create content with more than 7000 lines
355+
const largeContent = Array(7500).fill("Line content").join("\n")
356+
357+
await executeWriteFileTool({
358+
content: largeContent,
359+
line_count: "0", // Missing line count
360+
})
361+
362+
expect(mockCline.consecutiveMistakeCount).toBe(1)
363+
expect(mockCline.recordToolError).toHaveBeenCalledWith("write_to_file")
364+
expect(mockCline.say).toHaveBeenCalledWith("error", expect.stringContaining("7500 lines"))
365+
expect(mockCline.diffViewProvider.reset).toHaveBeenCalled()
366+
})
367+
368+
it("detects line count mismatch indicating truncation", async () => {
369+
// Create content with 7001 lines but claim it should have 10000
370+
const truncatedContent = Array(7001).fill("Line content").join("\n")
371+
372+
// Need to capture the tool result
373+
mockPushToolResult = vi.fn((result: ToolResponse) => {
374+
toolResult = result
375+
})
376+
377+
await executeWriteFileTool({
378+
content: truncatedContent,
379+
line_count: "10000", // Expected more lines
380+
})
381+
382+
expect(mockCline.consecutiveMistakeCount).toBe(1)
383+
expect(mockCline.recordToolError).toHaveBeenCalledWith("write_to_file")
384+
expect(mockCline.say).toHaveBeenCalledWith("error", expect.stringContaining("Line count mismatch"))
385+
// The error message should mention truncation since we have >7000 lines
386+
expect(toolResult).toContain("Content appears to be truncated")
387+
expect(toolResult).toContain("7001 lines")
388+
expect(mockCline.diffViewProvider.revertChanges).toHaveBeenCalled()
389+
})
390+
391+
it("allows small line count differences within tolerance", async () => {
392+
// Create content with 95 lines when expecting 100 (5% difference)
393+
const content = Array(95).fill("Line content").join("\n")
394+
395+
await executeWriteFileTool({
396+
content: content,
397+
line_count: "100",
398+
})
399+
400+
// Should process normally as difference is within 10% tolerance
401+
expect(mockCline.consecutiveMistakeCount).toBe(0)
402+
expect(mockCline.diffViewProvider.saveChanges).toHaveBeenCalled()
403+
})
404+
405+
it("handles exact line count match for large files", async () => {
406+
// Create content with exactly 8000 lines
407+
const largeContent = Array(8000).fill("Line content").join("\n")
408+
409+
await executeWriteFileTool({
410+
content: largeContent,
411+
line_count: "8000",
412+
})
413+
414+
// Should process normally
415+
expect(mockCline.consecutiveMistakeCount).toBe(0)
416+
expect(mockCline.diffViewProvider.saveChanges).toHaveBeenCalled()
417+
})
347418
})
348419

349420
describe("partial block handling", () => {

src/core/tools/writeToFileTool.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,29 @@ export async function writeToFileTool(
7171
cline.diffViewProvider.editType = fileExists ? "modify" : "create"
7272
}
7373

74+
// Check for potential truncation in large files
75+
const actualLineCount = newContent.split("\n").length
76+
const LARGE_FILE_THRESHOLD = 7000
77+
78+
// If the file has many lines and no line_count was provided, it might be truncated
79+
if (actualLineCount > LARGE_FILE_THRESHOLD && predictedLineCount === 0) {
80+
cline.consecutiveMistakeCount++
81+
cline.recordToolError("write_to_file")
82+
83+
await cline.say(
84+
"error",
85+
`The file content appears to be very large (${actualLineCount} lines). The assistant may have reached its output limit and the content could be truncated. Please verify the file is complete or consider using a different approach for large files.`,
86+
)
87+
88+
pushToolResult(
89+
formatResponse.toolError(
90+
`Large file detected (${actualLineCount} lines). The content may be truncated due to output limits. Consider breaking the file into smaller chunks or using a different approach.`,
91+
),
92+
)
93+
await cline.diffViewProvider.reset()
94+
return
95+
}
96+
7497
// pre-processing newContent for cases where weaker models might add artifacts like markdown codeblock markers (deepseek/llama) or extra escape characters (gemini)
7598
if (newContent.startsWith("```")) {
7699
// cline handles cases where it includes language specifiers like ```python ```js
@@ -147,6 +170,42 @@ export async function writeToFileTool(
147170
return
148171
}
149172

173+
// Validate line count matches actual content
174+
const actualLineCount = newContent.split("\n").length
175+
const lineCountMismatchThreshold = 0.1 // 10% tolerance
176+
const lineCountDifference = Math.abs(actualLineCount - predictedLineCount)
177+
const lineCountDifferencePercent = lineCountDifference / predictedLineCount
178+
179+
// Check for significant mismatch that might indicate truncation
180+
if (predictedLineCount > 100 && lineCountDifferencePercent > lineCountMismatchThreshold) {
181+
cline.consecutiveMistakeCount++
182+
cline.recordToolError("write_to_file")
183+
184+
await cline.say(
185+
"error",
186+
`Line count mismatch detected. Expected ${predictedLineCount} lines but got ${actualLineCount} lines. This may indicate the content was truncated.`,
187+
)
188+
189+
// Check if this looks like truncation at output limit
190+
const TYPICAL_OUTPUT_LIMIT_LINES = 7000
191+
if (actualLineCount > TYPICAL_OUTPUT_LIMIT_LINES && actualLineCount < predictedLineCount) {
192+
pushToolResult(
193+
formatResponse.toolError(
194+
`Content appears to be truncated. The file should have ${predictedLineCount} lines but only ${actualLineCount} lines were provided. This often happens when files exceed ~7000 lines due to output token limits. Consider using apply_diff for large file modifications or breaking the content into smaller chunks.`,
195+
),
196+
)
197+
} else {
198+
pushToolResult(
199+
formatResponse.toolError(
200+
`Line count mismatch: expected ${predictedLineCount} lines but got ${actualLineCount} lines. Please verify the content is complete.`,
201+
),
202+
)
203+
}
204+
205+
await cline.diffViewProvider.revertChanges()
206+
return
207+
}
208+
150209
cline.consecutiveMistakeCount = 0
151210

152211
// if isEditingFile false, that means we have the full contents of the file already.

0 commit comments

Comments
 (0)