From b93729008d43330f12d93c0145a1efadd98da639 Mon Sep 17 00:00:00 2001 From: kami Date: Sun, 12 Jul 2026 12:55:12 +0400 Subject: [PATCH] fix(infra): single-read file_read (no double disk I/O for hash) + bail health poll on dead process --- .../inference/llama/cpp/DefaultModelManager.kt | 4 +++- .../infrastructure/tools/filesystem/FileReadTool.kt | 11 +++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/infrastructure/inference/llama_cpp/src/main/kotlin/com/correx/infrastructure/inference/llama/cpp/DefaultModelManager.kt b/infrastructure/inference/llama_cpp/src/main/kotlin/com/correx/infrastructure/inference/llama/cpp/DefaultModelManager.kt index b5013b49..5d978899 100644 --- a/infrastructure/inference/llama_cpp/src/main/kotlin/com/correx/infrastructure/inference/llama/cpp/DefaultModelManager.kt +++ b/infrastructure/inference/llama_cpp/src/main/kotlin/com/correx/infrastructure/inference/llama/cpp/DefaultModelManager.kt @@ -151,10 +151,12 @@ class DefaultModelManager( currentDescriptor = null } - @Suppress("UnusedParameter") private suspend fun waitForHealthy(process: LlamaProcess): Boolean { val endTime = Clock.System.now().toEpochMilliseconds() + healthTimeoutMs while (Clock.System.now().toEpochMilliseconds() < endTime) { + // A crashed process (bad --model, port clash) never gets healthy; polling the full + // timeout is pure dead time. Bail as soon as it's gone. + if (!process.isAlive) return false try { val response = httpClient.get("http://$host:$port/health").body() if (response.contains("\"status\":\"healthy\"") || response.contains("ok")) { diff --git a/infrastructure/tools/filesystem/src/main/kotlin/com/correx/infrastructure/tools/filesystem/FileReadTool.kt b/infrastructure/tools/filesystem/src/main/kotlin/com/correx/infrastructure/tools/filesystem/FileReadTool.kt index 2b4d6868..9c5c6277 100644 --- a/infrastructure/tools/filesystem/src/main/kotlin/com/correx/infrastructure/tools/filesystem/FileReadTool.kt +++ b/infrastructure/tools/filesystem/src/main/kotlin/com/correx/infrastructure/tools/filesystem/FileReadTool.kt @@ -141,7 +141,10 @@ class FileReadTool( } private fun readFile(path: Path, startLine: Int?, endLine: Int?, request: ToolRequest): ToolResult = runCatching { - val lines = Files.readAllLines(path) + // Read the file once. Lines and the (whole-read) content hash both derive from these bytes, + // so a full read no longer hits the disk twice (readAllLines + readAllBytes for the hash). + val bytes = Files.readAllBytes(path) + val lines = bytes.inputStream().bufferedReader().readLines() val start = ((startLine ?: 1) - 1).coerceAtLeast(0) val end = (endLine ?: lines.size).coerceAtMost(lines.size) val selected = lines.subList(start, end) @@ -166,7 +169,7 @@ class FileReadTool( // against what the agent actually saw. A partial or truncated view establishes no baseline — // the agent must read the relevant range before editing. val sawWholeFile = startLine == null && endLine == null && !lineCapped && !charCapped - val metadata = if (sawWholeFile) mapOf("contentHash" to sha256(path)) else emptyMap() + val metadata = if (sawWholeFile) mapOf("contentHash" to sha256(bytes)) else emptyMap() ToolResult.Success( invocationId = request.invocationId, output = content + note, @@ -180,8 +183,8 @@ class FileReadTool( ) } - private fun sha256(path: Path): String = - java.security.MessageDigest.getInstance("SHA-256").digest(Files.readAllBytes(path)) + private fun sha256(bytes: ByteArray): String = + java.security.MessageDigest.getInstance("SHA-256").digest(bytes) .joinToString("") { "%02x".format(it) } private fun listDir(path: Path, request: ToolRequest): ToolResult = runCatching {