fix: prevent apply_diff from hanging on large/complex XML files

- Add performance optimizations to fuzzySearch function with early termination for exact matches
- Add bounds checking and maximum iteration limit to prevent infinite loops
- Add 30-second timeout mechanism to prevent indefinite hanging
- Add integration tests to verify performance on large XML files

Fixes #4852
This commit is contained in:
Roo Code 2025-07-21 18:28:19 +00:00
parent 2eb586b422
commit 2410003e4d
4 changed files with 716 additions and 6 deletions

View file

@ -0,0 +1,274 @@
import { MultiSearchReplaceDiffStrategy } from "../multi-search-replace"
import { MultiFileSearchReplaceDiffStrategy } from "../multi-file-search-replace"
import { DiffResult } from "../../../../shared/tools"
describe("MultiSearchReplaceDiffStrategy Hanging Issue #4852", () => {
describe("reproduce exact issue scenario", () => {
let strategy: MultiSearchReplaceDiffStrategy
let multiFileStrategy: MultiFileSearchReplaceDiffStrategy
beforeEach(() => {
// Use exact settings that might cause the issue
strategy = new MultiSearchReplaceDiffStrategy(1.0, 40) // Exact match, 40 line buffer
multiFileStrategy = new MultiFileSearchReplaceDiffStrategy(1.0, 40)
})
it("should handle the exact XML from issue #4852 without hanging", async () => {
// This is the exact XML content from the issue
const issueXmlContent = `<?xml version="1.0" encoding="UTF-8"?>
<root>
<level1>
<level2>
<level3>
<level4>
<level5>
<level6>
<level7>
<level8>
<level9>
<level10>
<data>
<item>Value 1</item>
<item>Value 2</item>
<item>Value 3</item>
<nested>
<subnested>
<subsubnested>
<deep>This is deeply nested content</deep>
<deep>More content here</deep>
<deep>Even more content</deep>
</subsubnested>
<subsubnested>
<deep>Another deep element</deep>
<deep>And another one</deep>
</subsubnested>
</subnested>
<subnested>
<subsubnested>
<deep>More deeply nested</deep>
<deep>Content continues</deep>
</subsubnested>
</subnested>
</nested>
<item>Value 4</item>
<item>Value 5</item>
<complexPattern>
<!-- This pattern is designed to cause backtracking -->
<a><b><c><d><e><f><g><h><i><j>
<content>Complex nested structure</content>
</j></i></h></g></f></e></d></c></b></a>
<a><b><c><d><e><f><g><h><i><j>
<content>Another complex structure</content>
</j></i></h></g></f></e></d></c></b></a>
</complexPattern>
</data>
<moreData>
<repeatingPattern>Pattern A</repeatingPattern>
<repeatingPattern>Pattern A</repeatingPattern>
<repeatingPattern>Pattern A</repeatingPattern>
<repeatingPattern>Pattern B</repeatingPattern>
<repeatingPattern>Pattern B</repeatingPattern>
<repeatingPattern>Pattern B</repeatingPattern>
<ambiguousContent>
This content has multiple possible matches
and can cause the regex to try many combinations
especially when looking for specific patterns
=======
This looks like a separator but it's not
>>>>>>>
These patterns can confuse the regex
<<<<<<<
Causing it to backtrack extensively
</ambiguousContent>
</moreData>
</level10>
</level9>
</level8>
</level7>
</level6>
</level5>
</level4>
</level3>
</level2>
</level1>
</root>`
// Test multiple concurrent edits as described in the issue
const diffItems = [
{
content: `<<<<<<< SEARCH
<repeatingPattern>Pattern A</repeatingPattern>
=======
<repeatingPattern>Pattern X</repeatingPattern>
>>>>>>> REPLACE`,
startLine: undefined,
},
{
content: `<<<<<<< SEARCH
<repeatingPattern>Pattern B</repeatingPattern>
=======
<repeatingPattern>Pattern Y</repeatingPattern>
>>>>>>> REPLACE`,
startLine: undefined,
},
{
content: `<<<<<<< SEARCH
<content>Complex nested structure</content>
=======
<content>Updated nested structure</content>
>>>>>>> REPLACE`,
startLine: undefined,
},
]
console.log("Starting multi-file diff application...")
const startTime = Date.now()
// Apply all diffs using the multi-file strategy
const result = await multiFileStrategy.applyDiff(issueXmlContent, diffItems)
const endTime = Date.now()
const duration = endTime - startTime
console.log(`Multi-file diff completed in ${duration}ms`)
// Check that it completed in reasonable time
expect(duration).toBeLessThan(2000) // 2 seconds max
// Verify the result
expect(result.success).toBe(true)
if (result.success && result.content) {
// Count occurrences
const patternACount = (result.content.match(/Pattern A/g) || []).length
const patternBCount = (result.content.match(/Pattern B/g) || []).length
const patternXCount = (result.content.match(/Pattern X/g) || []).length
const patternYCount = (result.content.match(/Pattern Y/g) || []).length
// Should have replaced all occurrences
expect(patternACount).toBe(0)
expect(patternBCount).toBe(0)
expect(patternXCount).toBe(3)
expect(patternYCount).toBe(3)
expect(result.content).toContain("Updated nested structure")
}
}, 10000) // 10 second timeout
it("should handle worst-case scenario with ambiguous patterns", async () => {
// Create a pathological case with many ambiguous patterns
const lines = []
// Add many similar lines that could match
for (let i = 0; i < 200; i++) {
lines.push(` <pattern>Similar content with slight variation ${i % 5}</pattern>`)
}
// Add the target in the middle
lines.splice(100, 0, ` <pattern>Target pattern to replace</pattern>`)
// Add more similar lines
for (let i = 0; i < 200; i++) {
lines.push(` <pattern>More similar content ${i % 5}</pattern>`)
}
const pathologicalContent = lines.join("\n")
// Try to replace without line number hint (worst case)
const diffContent = `<<<<<<< SEARCH
<pattern>Target pattern to replace</pattern>
=======
<pattern>Successfully replaced target</pattern>
>>>>>>> REPLACE`
console.log("Starting pathological case test...")
const startTime = Date.now()
const result = await strategy.applyDiff(pathologicalContent, diffContent)
const endTime = Date.now()
const duration = endTime - startTime
console.log(`Pathological case completed in ${duration}ms`)
// Should complete even in worst case
expect(duration).toBeLessThan(5000) // 5 seconds max
expect(result.success).toBe(true)
if (result.success && result.content) {
expect(result.content).toContain("Successfully replaced target")
expect(result.content).not.toContain("Target pattern to replace")
}
}, 10000)
it("should handle extremely deep nesting efficiently", async () => {
// Create extremely deep nesting that could cause stack issues
const depth = 100
let content = '<?xml version="1.0" encoding="UTF-8"?>\n'
// Open tags
for (let i = 0; i < depth; i++) {
content += `${" ".repeat(i)}<level${i}>\n`
}
// Add content at deepest level
content += `${" ".repeat(depth)}<data>Deep content to replace</data>\n`
// Close tags
for (let i = depth - 1; i >= 0; i--) {
content += `${" ".repeat(i)}</level${i}>\n`
}
const diffContent = `<<<<<<< SEARCH
${" ".repeat(depth)}<data>Deep content to replace</data>
=======
${" ".repeat(depth)}<data>Replaced deep content</data>
>>>>>>> REPLACE`
console.log("Starting deep nesting test...")
const startTime = Date.now()
const result = await strategy.applyDiff(content, diffContent)
const endTime = Date.now()
const duration = endTime - startTime
console.log(`Deep nesting test completed in ${duration}ms`)
expect(duration).toBeLessThan(1000) // Should be fast
expect(result.success).toBe(true)
if (result.success && result.content) {
expect(result.content).toContain("Replaced deep content")
}
}, 10000)
it("should handle the ambiguous content markers that look like diff markers", async () => {
const contentWithFakeMarkers = `<root>
<data>
<item>Normal content</item>
<ambiguous>
This has fake markers
=======
Not a real separator
>>>>>>>
Also not real
<<<<<<<
Just content
</ambiguous>
<target>Replace this content</target>
</data>
</root>`
const diffContent = `<<<<<<< SEARCH
<target>Replace this content</target>
=======
<target>Successfully replaced</target>
>>>>>>> REPLACE`
const result = await strategy.applyDiff(contentWithFakeMarkers, diffContent)
expect(result.success).toBe(true)
if (result.success && result.content) {
expect(result.content).toContain("Successfully replaced")
// Should not have affected the fake markers
expect(result.content).toContain("=======")
expect(result.content).toContain(">>>>>>>")
expect(result.content).toContain("<<<<<<<")
}
})
})
})

View file

@ -0,0 +1,354 @@
import { MultiSearchReplaceDiffStrategy } from "../multi-search-replace"
import { MultiFileSearchReplaceDiffStrategy } from "../multi-file-search-replace"
import { DiffResult } from "../../../../shared/tools"
describe("MultiSearchReplaceDiffStrategy Performance", () => {
describe("large XML file handling", () => {
let strategy: MultiSearchReplaceDiffStrategy
let multiFileStrategy: MultiFileSearchReplaceDiffStrategy
beforeEach(() => {
strategy = new MultiSearchReplaceDiffStrategy(1.0, 40) // Default settings
multiFileStrategy = new MultiFileSearchReplaceDiffStrategy(1.0, 40)
})
it("should handle large complex XML files without hanging", async () => {
// Generate the large XML content from the issue
const largeXmlContent = `<?xml version="1.0" encoding="UTF-8"?>
<root>
<level1>
<level2>
<level3>
<level4>
<level5>
<level6>
<level7>
<level8>
<level9>
<level10>
<data>
<item>Value 1</item>
<item>Value 2</item>
<item>Value 3</item>
<nested>
<subnested>
<subsubnested>
<deep>This is deeply nested content</deep>
<deep>More content here</deep>
<deep>Even more content</deep>
</subsubnested>
<subsubnested>
<deep>Another deep element</deep>
<deep>And another one</deep>
</subsubnested>
</subnested>
<subnested>
<subsubnested>
<deep>More deeply nested</deep>
<deep>Content continues</deep>
</subsubnested>
</subnested>
</nested>
<item>Value 4</item>
<item>Value 5</item>
<complexPattern>
<!-- This pattern is designed to cause backtracking -->
<a><b><c><d><e><f><g><h><i><j>
<content>Complex nested structure</content>
</j></i></h></g></f></e></d></c></b></a>
<a><b><c><d><e><f><g><h><i><j>
<content>Another complex structure</content>
</j></i></h></g></f></e></d></c></b></a>
</complexPattern>
</data>
<moreData>
<repeatingPattern>Pattern A</repeatingPattern>
<repeatingPattern>Pattern A</repeatingPattern>
<repeatingPattern>Pattern A</repeatingPattern>
<repeatingPattern>Pattern B</repeatingPattern>
<repeatingPattern>Pattern B</repeatingPattern>
<repeatingPattern>Pattern B</repeatingPattern>
<ambiguousContent>
This content has multiple possible matches
and can cause the regex to try many combinations
especially when looking for specific patterns
=======
This looks like a separator but it's not
>>>>>>>
These patterns can confuse the regex
<<<<<<<
Causing it to backtrack extensively
</ambiguousContent>
</moreData>
</level10>
</level9>
</level8>
</level7>
</level6>
</level5>
</level4>
</level3>
</level2>
</level1>
</root>`
// Create diff content to change Pattern A to Pattern X and Pattern B to Pattern Y
const diffContent = `
<<<<<<< SEARCH
<repeatingPattern>Pattern A</repeatingPattern>
<repeatingPattern>Pattern A</repeatingPattern>
<repeatingPattern>Pattern A</repeatingPattern>
<repeatingPattern>Pattern B</repeatingPattern>
<repeatingPattern>Pattern B</repeatingPattern>
<repeatingPattern>Pattern B</repeatingPattern>
=======
<repeatingPattern>Pattern X</repeatingPattern>
<repeatingPattern>Pattern X</repeatingPattern>
<repeatingPattern>Pattern X</repeatingPattern>
<repeatingPattern>Pattern Y</repeatingPattern>
<repeatingPattern>Pattern Y</repeatingPattern>
<repeatingPattern>Pattern Y</repeatingPattern>
>>>>>>> REPLACE
<<<<<<< SEARCH
<content>Complex nested structure</content>
=======
<content>Updated nested structure</content>
>>>>>>> REPLACE`
// Set a timeout to ensure the test doesn't hang indefinitely
const startTime = Date.now()
const timeout = 5000 // 5 seconds timeout
const resultPromise = strategy.applyDiff(largeXmlContent, diffContent)
// Use Promise.race to implement timeout
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error("Operation timed out")), timeout)
})
try {
const result = await Promise.race([resultPromise, timeoutPromise])
const endTime = Date.now()
const duration = endTime - startTime
// Ensure the operation completed within reasonable time
expect(duration).toBeLessThan(timeout)
// Verify the result
const diffResult = result as DiffResult
expect(diffResult).toBeDefined()
expect(diffResult.success).toBe(true)
if (diffResult.success && diffResult.content) {
expect(diffResult.content).toContain("Pattern X")
expect(diffResult.content).toContain("Pattern Y")
expect(diffResult.content).toContain("Updated nested structure")
expect(diffResult.content).not.toContain("Pattern A")
expect(diffResult.content).not.toContain("Pattern B")
expect(diffResult.content).not.toContain("Complex nested structure")
}
} catch (error) {
if (error instanceof Error && error.message === "Operation timed out") {
throw new Error("applyDiff operation timed out - this indicates the hanging issue")
} else {
throw error
}
}
}, 10000) // Jest timeout of 10 seconds
it("should handle multiple simultaneous edits on large XML files", async () => {
// Test the multi-file strategy with the same large XML content
const largeXmlContent = `<?xml version="1.0" encoding="UTF-8"?>
<root>
<level1>
<level2>
<level3>
<level4>
<level5>
<level6>
<level7>
<level8>
<level9>
<level10>
<data>
<item>Value 1</item>
<item>Value 2</item>
<item>Value 3</item>
<nested>
<subnested>
<subsubnested>
<deep>This is deeply nested content</deep>
<deep>More content here</deep>
<deep>Even more content</deep>
</subsubnested>
</subnested>
</nested>
<item>Value 4</item>
<item>Value 5</item>
<complexPattern>
<a><b><c><d><e><f><g><h><i><j>
<content>Complex nested structure</content>
</j></i></h></g></f></e></d></c></b></a>
</complexPattern>
</data>
<moreData>
<repeatingPattern>Pattern A</repeatingPattern>
<repeatingPattern>Pattern A</repeatingPattern>
<repeatingPattern>Pattern A</repeatingPattern>
<repeatingPattern>Pattern B</repeatingPattern>
<repeatingPattern>Pattern B</repeatingPattern>
<repeatingPattern>Pattern B</repeatingPattern>
</moreData>
</level10>
</level9>
</level8>
</level7>
</level6>
</level5>
</level4>
</level3>
</level2>
</level1>
</root>`
// Create multiple diff items
const diffItems = [
{
content: `<<<<<<< SEARCH
<repeatingPattern>Pattern A</repeatingPattern>
<repeatingPattern>Pattern A</repeatingPattern>
<repeatingPattern>Pattern A</repeatingPattern>
=======
<repeatingPattern>Pattern X</repeatingPattern>
<repeatingPattern>Pattern X</repeatingPattern>
<repeatingPattern>Pattern X</repeatingPattern>
>>>>>>> REPLACE`,
startLine: undefined,
},
{
content: `<<<<<<< SEARCH
<repeatingPattern>Pattern B</repeatingPattern>
<repeatingPattern>Pattern B</repeatingPattern>
<repeatingPattern>Pattern B</repeatingPattern>
=======
<repeatingPattern>Pattern Y</repeatingPattern>
<repeatingPattern>Pattern Y</repeatingPattern>
<repeatingPattern>Pattern Y</repeatingPattern>
>>>>>>> REPLACE`,
startLine: undefined,
},
{
content: `<<<<<<< SEARCH
<content>Complex nested structure</content>
=======
<content>Updated nested structure</content>
>>>>>>> REPLACE`,
startLine: undefined,
},
]
const startTime = Date.now()
const timeout = 5000 // 5 seconds timeout
const resultPromise = multiFileStrategy.applyDiff(largeXmlContent, diffItems)
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error("Operation timed out")), timeout)
})
try {
const result = await Promise.race([resultPromise, timeoutPromise])
const endTime = Date.now()
const duration = endTime - startTime
expect(duration).toBeLessThan(timeout)
const diffResult = result as DiffResult
expect(diffResult).toBeDefined()
expect(diffResult.success).toBe(true)
if (diffResult.success && diffResult.content) {
expect(diffResult.content).toContain("Pattern X")
expect(diffResult.content).toContain("Pattern Y")
expect(diffResult.content).toContain("Updated nested structure")
}
} catch (error) {
if (error instanceof Error && error.message === "Operation timed out") {
throw new Error("Multi-file applyDiff operation timed out - this indicates the hanging issue")
} else {
throw error
}
}
}, 10000)
it("should handle pathological cases with many similar patterns", async () => {
// Create content with many similar patterns that could cause excessive backtracking
const lines = []
for (let i = 0; i < 100; i++) {
lines.push(` <pattern>Similar content ${i % 10}</pattern>`)
}
const pathologicalContent = lines.join("\n")
// Try to replace a pattern in the middle
const diffContent = `
<<<<<<< SEARCH
<pattern>Similar content 5</pattern>
=======
<pattern>Updated content 5</pattern>
>>>>>>> REPLACE`
const startTime = Date.now()
const result = await strategy.applyDiff(pathologicalContent, diffContent)
const endTime = Date.now()
const duration = endTime - startTime
// Should complete quickly even with many similar patterns
expect(duration).toBeLessThan(1000) // 1 second max
expect(result.success).toBe(true)
if (result.success && result.content) {
// Should update exactly one occurrence
const updatedCount = (result.content.match(/Updated content 5/g) || []).length
expect(updatedCount).toBe(1)
}
})
it("should handle deeply nested content with line number hints efficiently", async () => {
const deeplyNestedXml = `<?xml version="1.0" encoding="UTF-8"?>
<root>
${Array(50)
.fill(0)
.map((_, i) => ` ${" ".repeat(i)}<level${i}>`)
.join("\n")}
${Array(50)
.fill(0)
.map((_, i) => ` ${" ".repeat(49 - i)}<data>Content at level ${49 - i}</data>`)
.join("\n")}
${Array(50)
.fill(0)
.map((_, i) => ` ${" ".repeat(49 - i)}</level${49 - i}>`)
.join("\n")}
</root>`
// Try to replace content at a specific level with line number hint
const diffContent = `
<<<<<<< SEARCH
:start_line:30
-------
<data>Content at level 20</data>
=======
<data>Updated content at level 20</data>
>>>>>>> REPLACE`
const startTime = Date.now()
const result = await strategy.applyDiff(deeplyNestedXml, diffContent)
const endTime = Date.now()
const duration = endTime - startTime
// Should be fast with line number hint
expect(duration).toBeLessThan(500) // 500ms max
expect(result.success).toBe(true)
if (result.success && result.content) {
expect(result.content).toContain("Updated content at level 20")
expect(result.content).not.toContain("<data>Content at level 20</data>")
}
})
})
})

View file

@ -40,34 +40,61 @@ function fuzzySearch(lines: string[], searchChunk: string, startIndex: number, e
const searchLen = searchChunk.split(/\r?\n/).length
// Early return if search range is invalid
if (startIndex < 0 || endIndex > lines.length || startIndex >= endIndex || searchLen > endIndex - startIndex) {
return { bestScore, bestMatchIndex, bestMatchContent }
}
// Middle-out from the midpoint
const midPoint = Math.floor((startIndex + endIndex) / 2)
let leftIndex = midPoint
let rightIndex = midPoint + 1
while (leftIndex >= startIndex || rightIndex <= endIndex - searchLen) {
if (leftIndex >= startIndex) {
// Add a maximum iteration count to prevent infinite loops
const maxIterations = endIndex - startIndex
let iterations = 0
while ((leftIndex >= startIndex || rightIndex <= endIndex - searchLen) && iterations < maxIterations) {
iterations++
// Check left side
if (leftIndex >= startIndex && leftIndex + searchLen <= endIndex) {
const originalChunk = lines.slice(leftIndex, leftIndex + searchLen).join("\n")
const similarity = getSimilarity(originalChunk, searchChunk)
// Early termination if we find an exact match
if (similarity === 1.0) {
return { bestScore: similarity, bestMatchIndex: leftIndex, bestMatchContent: originalChunk }
}
if (similarity > bestScore) {
bestScore = similarity
bestMatchIndex = leftIndex
bestMatchContent = originalChunk
}
leftIndex--
} else {
leftIndex = startIndex - 1 // Force it out of bounds
}
if (rightIndex <= endIndex - searchLen) {
// Check right side
if (rightIndex <= endIndex - searchLen && rightIndex >= startIndex) {
const originalChunk = lines.slice(rightIndex, rightIndex + searchLen).join("\n")
const similarity = getSimilarity(originalChunk, searchChunk)
// Early termination if we find an exact match
if (similarity === 1.0) {
return { bestScore: similarity, bestMatchIndex: rightIndex, bestMatchContent: originalChunk }
}
if (similarity > bestScore) {
bestScore = similarity
bestMatchIndex = rightIndex
bestMatchContent = originalChunk
}
rightIndex++
} else {
rightIndex = endIndex + 1 // Force it out of bounds
}
}
@ -446,6 +473,10 @@ Each file requires its own path, start_line, and diff elements.
diffContent: string,
_paramStartLine?: number,
): Promise<DiffResult> {
// Add a timeout mechanism to prevent indefinite hanging
const DIFF_TIMEOUT_MS = 30000 // 30 seconds timeout
const startTime = Date.now()
const validseq = this.validateMarkerSequencing(diffContent)
if (!validseq.success) {
return {
@ -494,6 +525,15 @@ Each file requires its own path, start_line, and diff elements.
.sort((a, b) => a.startLine - b.startLine)
for (const replacement of replacements) {
// Check for timeout
if (Date.now() - startTime > DIFF_TIMEOUT_MS) {
return {
success: false,
error: `Operation timed out after ${DIFF_TIMEOUT_MS / 1000} seconds. This may indicate the file is too large or complex for the current search pattern.`,
failParts: diffResults,
}
}
let { searchContent, replaceContent } = replacement
let startLine = replacement.startLine + (replacement.startLine === 0 ? 0 : delta)

View file

@ -42,32 +42,61 @@ function fuzzySearch(lines: string[], searchChunk: string, startIndex: number, e
let bestMatchContent = ""
const searchLen = searchChunk.split(/\r?\n/).length
// Early return if search range is invalid
if (startIndex < 0 || endIndex > lines.length || startIndex >= endIndex || searchLen > endIndex - startIndex) {
return { bestScore, bestMatchIndex, bestMatchContent }
}
// Middle-out from the midpoint
const midPoint = Math.floor((startIndex + endIndex) / 2)
let leftIndex = midPoint
let rightIndex = midPoint + 1
while (leftIndex >= startIndex || rightIndex <= endIndex - searchLen) {
if (leftIndex >= startIndex) {
// Add a maximum iteration count to prevent infinite loops
const maxIterations = endIndex - startIndex
let iterations = 0
while ((leftIndex >= startIndex || rightIndex <= endIndex - searchLen) && iterations < maxIterations) {
iterations++
// Check left side
if (leftIndex >= startIndex && leftIndex + searchLen <= endIndex) {
const originalChunk = lines.slice(leftIndex, leftIndex + searchLen).join("\n")
const similarity = getSimilarity(originalChunk, searchChunk)
// Early termination if we find an exact match
if (similarity === 1.0) {
return { bestScore: similarity, bestMatchIndex: leftIndex, bestMatchContent: originalChunk }
}
if (similarity > bestScore) {
bestScore = similarity
bestMatchIndex = leftIndex
bestMatchContent = originalChunk
}
leftIndex--
} else {
leftIndex = startIndex - 1 // Force it out of bounds
}
if (rightIndex <= endIndex - searchLen) {
// Check right side
if (rightIndex <= endIndex - searchLen && rightIndex >= startIndex) {
const originalChunk = lines.slice(rightIndex, rightIndex + searchLen).join("\n")
const similarity = getSimilarity(originalChunk, searchChunk)
// Early termination if we find an exact match
if (similarity === 1.0) {
return { bestScore: similarity, bestMatchIndex: rightIndex, bestMatchContent: originalChunk }
}
if (similarity > bestScore) {
bestScore = similarity
bestMatchIndex = rightIndex
bestMatchContent = originalChunk
}
rightIndex++
} else {
rightIndex = endIndex + 1 // Force it out of bounds
}
}
@ -337,6 +366,10 @@ Only use a single line of '=======' between search and replacement content, beca
_paramStartLine?: number,
_paramEndLine?: number,
): Promise<DiffResult> {
// Add a timeout mechanism to prevent indefinite hanging
const DIFF_TIMEOUT_MS = 30000 // 30 seconds timeout
const startTime = Date.now()
const validseq = this.validateMarkerSequencing(diffContent)
if (!validseq.success) {
return {
@ -403,6 +436,15 @@ Only use a single line of '=======' between search and replacement content, beca
.sort((a, b) => a.startLine - b.startLine)
for (const replacement of replacements) {
// Check for timeout
if (Date.now() - startTime > DIFF_TIMEOUT_MS) {
return {
success: false,
error: `Operation timed out after ${DIFF_TIMEOUT_MS / 1000} seconds. This may indicate the file is too large or complex for the current search pattern.`,
failParts: diffResults,
}
}
let { searchContent, replaceContent } = replacement
let startLine = replacement.startLine + (replacement.startLine === 0 ? 0 : delta)