fix(tools): atomic file_edit writes + bound the patch subprocess
This commit is contained in:
+17
-4
@@ -24,7 +24,6 @@ import java.nio.file.Files
|
|||||||
import java.nio.file.InvalidPathException
|
import java.nio.file.InvalidPathException
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.nio.file.Paths
|
import java.nio.file.Paths
|
||||||
import java.nio.file.StandardOpenOption
|
|
||||||
|
|
||||||
@Suppress("TooManyFunctions")
|
@Suppress("TooManyFunctions")
|
||||||
class FileEditTool(
|
class FileEditTool(
|
||||||
@@ -242,7 +241,9 @@ class FileEditTool(
|
|||||||
|
|
||||||
private fun append(path: Path, request: ToolRequest): ToolResult {
|
private fun append(path: Path, request: ToolRequest): ToolResult {
|
||||||
val content = request.parameters["content"] as String
|
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(
|
return ToolResult.Success(
|
||||||
invocationId = request.invocationId,
|
invocationId = request.invocationId,
|
||||||
output = "Content appended to ${path.toAbsolutePath()}",
|
output = "Content appended to ${path.toAbsolutePath()}",
|
||||||
@@ -258,7 +259,7 @@ class FileEditTool(
|
|||||||
return when (occurrences) {
|
return when (occurrences) {
|
||||||
1 -> {
|
1 -> {
|
||||||
val newContent = currentContent.replace(target, replacement)
|
val newContent = currentContent.replace(target, replacement)
|
||||||
Files.writeString(path, newContent)
|
AtomicFileWriter.write(path, newContent.toByteArray(Charsets.UTF_8))
|
||||||
ToolResult.Success(
|
ToolResult.Success(
|
||||||
invocationId = request.invocationId,
|
invocationId = request.invocationId,
|
||||||
output = "Target replaced in $pathString",
|
output = "Target replaced in $pathString",
|
||||||
@@ -338,7 +339,18 @@ class FileEditTool(
|
|||||||
return try {
|
return try {
|
||||||
process.outputStream.use { it.write(patchContent.toByteArray()) }
|
process.outputStream.use { it.write(patchContent.toByteArray()) }
|
||||||
val output = process.inputStream.bufferedReader().use { it.readText() }
|
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) {
|
if (exitCode == 0) {
|
||||||
ToolResult.Success(
|
ToolResult.Success(
|
||||||
@@ -389,5 +401,6 @@ class FileEditTool(
|
|||||||
const val MIN_ANCHOR = 3
|
const val MIN_ANCHOR = 3
|
||||||
const val MAX_CMP = 400
|
const val MAX_CMP = 400
|
||||||
const val MIN_SIMILARITY = 0.5
|
const val MIN_SIMILARITY = 0.5
|
||||||
|
const val PATCH_TIMEOUT_SECONDS = 30L
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user