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
This commit is contained in:
Roo Code 2025-07-26 14:05:21 +00:00
parent 62160758fc
commit 9601be04ff
2 changed files with 131 additions and 1 deletions

View file

@ -339,11 +339,82 @@ describe("writeToFileTool", () => {
})
it("processes files with very large line counts", async () => {
await executeWriteFileTool({ line_count: "999999" })
// Create content that matches the line count to avoid mismatch error
const largeContent = Array(999).fill("Line content").join("\n")
await executeWriteFileTool({
content: largeContent,
line_count: "999",
})
// Should process normally without issues
expect(mockCline.consecutiveMistakeCount).toBe(0)
})
it("detects potential truncation when line count is 0 and content exceeds 7000 lines", async () => {
// Create content with more than 7000 lines
const largeContent = Array(7500).fill("Line content").join("\n")
await executeWriteFileTool({
content: largeContent,
line_count: "0", // Missing line count
})
expect(mockCline.consecutiveMistakeCount).toBe(1)
expect(mockCline.recordToolError).toHaveBeenCalledWith("write_to_file")
expect(mockCline.say).toHaveBeenCalledWith("error", expect.stringContaining("7500 lines"))
expect(mockCline.diffViewProvider.reset).toHaveBeenCalled()
})
it("detects line count mismatch indicating truncation", async () => {
// Create content with 7001 lines but claim it should have 10000
const truncatedContent = Array(7001).fill("Line content").join("\n")
// Need to capture the tool result
mockPushToolResult = vi.fn((result: ToolResponse) => {
toolResult = result
})
await executeWriteFileTool({
content: truncatedContent,
line_count: "10000", // Expected more lines
})
expect(mockCline.consecutiveMistakeCount).toBe(1)
expect(mockCline.recordToolError).toHaveBeenCalledWith("write_to_file")
expect(mockCline.say).toHaveBeenCalledWith("error", expect.stringContaining("Line count mismatch"))
// The error message should mention truncation since we have >7000 lines
expect(toolResult).toContain("Content appears to be truncated")
expect(toolResult).toContain("7001 lines")
expect(mockCline.diffViewProvider.revertChanges).toHaveBeenCalled()
})
it("allows small line count differences within tolerance", async () => {
// Create content with 95 lines when expecting 100 (5% difference)
const content = Array(95).fill("Line content").join("\n")
await executeWriteFileTool({
content: content,
line_count: "100",
})
// Should process normally as difference is within 10% tolerance
expect(mockCline.consecutiveMistakeCount).toBe(0)
expect(mockCline.diffViewProvider.saveChanges).toHaveBeenCalled()
})
it("handles exact line count match for large files", async () => {
// Create content with exactly 8000 lines
const largeContent = Array(8000).fill("Line content").join("\n")
await executeWriteFileTool({
content: largeContent,
line_count: "8000",
})
// Should process normally
expect(mockCline.consecutiveMistakeCount).toBe(0)
expect(mockCline.diffViewProvider.saveChanges).toHaveBeenCalled()
})
})
describe("partial block handling", () => {

View file

@ -71,6 +71,29 @@ export async function writeToFileTool(
cline.diffViewProvider.editType = fileExists ? "modify" : "create"
}
// Check for potential truncation in large files
const actualLineCount = newContent.split("\n").length
const LARGE_FILE_THRESHOLD = 7000
// If the file has many lines and no line_count was provided, it might be truncated
if (actualLineCount > LARGE_FILE_THRESHOLD && predictedLineCount === 0) {
cline.consecutiveMistakeCount++
cline.recordToolError("write_to_file")
await cline.say(
"error",
`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.`,
)
pushToolResult(
formatResponse.toolError(
`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.`,
),
)
await cline.diffViewProvider.reset()
return
}
// pre-processing newContent for cases where weaker models might add artifacts like markdown codeblock markers (deepseek/llama) or extra escape characters (gemini)
if (newContent.startsWith("```")) {
// cline handles cases where it includes language specifiers like ```python ```js
@ -147,6 +170,42 @@ export async function writeToFileTool(
return
}
// Validate line count matches actual content
const actualLineCount = newContent.split("\n").length
const lineCountMismatchThreshold = 0.1 // 10% tolerance
const lineCountDifference = Math.abs(actualLineCount - predictedLineCount)
const lineCountDifferencePercent = lineCountDifference / predictedLineCount
// Check for significant mismatch that might indicate truncation
if (predictedLineCount > 100 && lineCountDifferencePercent > lineCountMismatchThreshold) {
cline.consecutiveMistakeCount++
cline.recordToolError("write_to_file")
await cline.say(
"error",
`Line count mismatch detected. Expected ${predictedLineCount} lines but got ${actualLineCount} lines. This may indicate the content was truncated.`,
)
// Check if this looks like truncation at output limit
const TYPICAL_OUTPUT_LIMIT_LINES = 7000
if (actualLineCount > TYPICAL_OUTPUT_LIMIT_LINES && actualLineCount < predictedLineCount) {
pushToolResult(
formatResponse.toolError(
`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.`,
),
)
} else {
pushToolResult(
formatResponse.toolError(
`Line count mismatch: expected ${predictedLineCount} lines but got ${actualLineCount} lines. Please verify the content is complete.`,
),
)
}
await cline.diffViewProvider.revertChanges()
return
}
cline.consecutiveMistakeCount = 0
// if isEditingFile false, that means we have the full contents of the file already.