fix(tools): atomic file_edit writes + bound the patch subprocess

This commit is contained in:
2026-07-12 12:48:30 +04:00
parent c2336ae6f7
commit 46fbd597c3
@@ -24,7 +24,6 @@ import java.nio.file.Files
import java.nio.file.InvalidPathException
import java.nio.file.Path
import java.nio.file.Paths
import java.nio.file.StandardOpenOption
@Suppress("TooManyFunctions")
class FileEditTool(
@@ -242,7 +241,9 @@ class FileEditTool(
private fun append(path: Path, request: ToolRequest): ToolResult {
val content = request.parameters["content"] as String
Files.writeString(path, content, StandardOpenOption.APPEND)
// Read-modify-atomic-swap instead of an in-place APPEND: a crash mid-append leaves the file
// whole (old content), never truncated. Same crash-safety FileWriteTool gets.
AtomicFileWriter.write(path, (Files.readString(path) + content).toByteArray(Charsets.UTF_8))
return ToolResult.Success(
invocationId = request.invocationId,
output = "Content appended to ${path.toAbsolutePath()}",
@@ -258,7 +259,7 @@ class FileEditTool(
return when (occurrences) {
1 -> {
val newContent = currentContent.replace(target, replacement)
Files.writeString(path, newContent)
AtomicFileWriter.write(path, newContent.toByteArray(Charsets.UTF_8))
ToolResult.Success(
invocationId = request.invocationId,
output = "Target replaced in $pathString",
@@ -338,7 +339,18 @@ class FileEditTool(
return try {
process.outputStream.use { it.write(patchContent.toByteArray()) }
val output = process.inputStream.bufferedReader().use { it.readText() }
val exitCode = process.waitFor()
if (!process.waitFor(PATCH_TIMEOUT_SECONDS, java.util.concurrent.TimeUnit.SECONDS)) {
// A hung `patch` (e.g. prompting for a reject-file name it never gets) would block the
// stage forever. Kill it and let the model fall back to replace/file_write.
process.destroyForcibly()
return ToolResult.Failure(
invocationId = request.invocationId,
reason = "Patch timed out after ${PATCH_TIMEOUT_SECONDS}s. " +
"Consider using operation 'replace' (exact string swap) or file_write instead.",
recoverable = true,
)
}
val exitCode = process.exitValue()
if (exitCode == 0) {
ToolResult.Success(
@@ -389,5 +401,6 @@ class FileEditTool(
const val MIN_ANCHOR = 3
const val MAX_CMP = 400
const val MIN_SIMILARITY = 0.5
const val PATCH_TIMEOUT_SECONDS = 30L
}
}