fix(infra): single-read file_read (no double disk I/O for hash) + bail health poll on dead process

This commit is contained in:
2026-07-12 12:55:12 +04:00
parent 363d880a69
commit b93729008d
2 changed files with 10 additions and 5 deletions
@@ -151,10 +151,12 @@ class DefaultModelManager(
currentDescriptor = null currentDescriptor = null
} }
@Suppress("UnusedParameter")
private suspend fun waitForHealthy(process: LlamaProcess): Boolean { private suspend fun waitForHealthy(process: LlamaProcess): Boolean {
val endTime = Clock.System.now().toEpochMilliseconds() + healthTimeoutMs val endTime = Clock.System.now().toEpochMilliseconds() + healthTimeoutMs
while (Clock.System.now().toEpochMilliseconds() < endTime) { 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 { try {
val response = httpClient.get("http://$host:$port/health").body<String>() val response = httpClient.get("http://$host:$port/health").body<String>()
if (response.contains("\"status\":\"healthy\"") || response.contains("ok")) { if (response.contains("\"status\":\"healthy\"") || response.contains("ok")) {
@@ -141,7 +141,10 @@ class FileReadTool(
} }
private fun readFile(path: Path, startLine: Int?, endLine: Int?, request: ToolRequest): ToolResult = runCatching { 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 start = ((startLine ?: 1) - 1).coerceAtLeast(0)
val end = (endLine ?: lines.size).coerceAtMost(lines.size) val end = (endLine ?: lines.size).coerceAtMost(lines.size)
val selected = lines.subList(start, end) 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 — // against what the agent actually saw. A partial or truncated view establishes no baseline —
// the agent must read the relevant range before editing. // the agent must read the relevant range before editing.
val sawWholeFile = startLine == null && endLine == null && !lineCapped && !charCapped 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( ToolResult.Success(
invocationId = request.invocationId, invocationId = request.invocationId,
output = content + note, output = content + note,
@@ -180,8 +183,8 @@ class FileReadTool(
) )
} }
private fun sha256(path: Path): String = private fun sha256(bytes: ByteArray): String =
java.security.MessageDigest.getInstance("SHA-256").digest(Files.readAllBytes(path)) java.security.MessageDigest.getInstance("SHA-256").digest(bytes)
.joinToString("") { "%02x".format(it) } .joinToString("") { "%02x".format(it) }
private fun listDir(path: Path, request: ToolRequest): ToolResult = runCatching { private fun listDir(path: Path, request: ToolRequest): ToolResult = runCatching {