From f860d1b8af7f0e7cd4ad061d05c0557a951c4e00 Mon Sep 17 00:00:00 2001 From: kami Date: Mon, 20 Jul 2026 18:17:42 +0400 Subject: [PATCH] feat(context): compact successful write/edit tool results to receipts (#289) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A successful file_write/file_edit echoes the whole file body (+ diff) back in its tool result — up to ~30k chars, which pushed the traced Gemma4 request past its context window. The write already happened and its full output is durable in the event log/CAS; the model only needs a receipt that it landed. Replace each exit=0 write result with a one-line receipt (path + elision marker) in the context builder; reads, gate output, and nonzero-exit writes stay verbatim. Derived-only pass over the transcript — authoritative events are untouched, so it's replay-safe. Co-Authored-By: Claude Opus 4.8 --- .../builder/DefaultContextPackBuilder.kt | 43 ++++++++++- .../test/kotlin/WriteReceiptCompactionTest.kt | 72 +++++++++++++++++++ 2 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 testing/deterministic/src/test/kotlin/WriteReceiptCompactionTest.kt diff --git a/core/context/src/main/kotlin/com/correx/core/context/builder/DefaultContextPackBuilder.kt b/core/context/src/main/kotlin/com/correx/core/context/builder/DefaultContextPackBuilder.kt index e8075ac8..4119863c 100644 --- a/core/context/src/main/kotlin/com/correx/core/context/builder/DefaultContextPackBuilder.kt +++ b/core/context/src/main/kotlin/com/correx/core/context/builder/DefaultContextPackBuilder.kt @@ -94,16 +94,23 @@ class DefaultContextPackBuilder( // raw ToolCallRequest JSON into a dotted-line format the fingerprint parser can't read. val deduped = dedupeRepeatedToolCalls(stamped) + // #289: a successful file_write/file_edit echoes the whole file body (+ diff) back in its + // tool result — up to ~30k chars, which pushed the traced Gemma4 request past its context + // window. The write already happened and its full output is durable in the event log/CAS; + // the model only needs a receipt that it landed. Compact those results to a one-line receipt + // (reads and gate output stay verbatim). Derived-only — authoritative events are untouched. + val compacted = compactWriteReceipts(deduped) + // Stage 1 (FORMAT_COMPRESS): lossless json→dotted-line compaction of structured entries. val formatted = if (policy.enabled(CompressionStage.FORMAT_COMPRESS)) { - deduped.map { entry -> + compacted.map { entry -> if (classifier.classify(entry) == ContextClass.STRUCTURED) { reencode(entry, formatCompressor.compress(entry.content)) } else { entry } } - } else deduped + } else compacted // Stage 3 (TOKEN_PRUNE): prune freeform prose, preserving protected spans. When TIER_SPLIT // is on, the newest TIER0_TURNS freeform turns are left full-fidelity (tier 0). @@ -283,6 +290,38 @@ class DefaultContextPackBuilder( } } + // A successful tool result is framed "[ exit=]\n..." by renderToolResult; failures + // use ERROR:/FATAL: sentinels (never matched here). Only exit=0 writes are compacted. + private val successFrame = Regex("""^\[(\S+) exit=(\d+)]""") + private val writeToolNames = setOf("file_write", "file_edit") + + private fun compactWriteReceipts(entries: List): List { + val writeCallPaths = entries + .filter { it.sourceType == "assistantToolCall" && toolCallName(it.content) in writeToolNames } + .associate { it.sourceId to toolCallPath(it.content) } + if (writeCallPaths.isEmpty()) return entries + return entries.map { entry -> + if (entry.sourceType != "toolResult" || entry.sourceId !in writeCallPaths) return@map entry + val header = entry.content.substringBefore('\n') + val match = successFrame.find(header) ?: return@map entry + // A nonzero-exit write is a Success carrying a real advisory (e.g. a partial patch) — + // keep it verbatim; only a clean exit=0 write body is pure echo we can drop. + if (match.groupValues[2] != "0") return@map entry + val path = writeCallPaths[entry.sourceId].orEmpty() + reencode(entry, "$header wrote $path — succeeded; body elided (full result in event log)".trim()) + } + } + + private fun toolCallName(content: String): String? = runCatching { + Json.parseToJsonElement(content).jsonObject["function"]?.jsonObject?.get("name")?.jsonPrimitive?.content + }.getOrNull() + + private fun toolCallPath(content: String): String? = runCatching { + val args = Json.parseToJsonElement(content).jsonObject["function"]?.jsonObject + ?.get("arguments")?.jsonPrimitive?.content ?: return null + Json.parseToJsonElement(args).jsonObject["path"]?.jsonPrimitive?.content + }.getOrNull() + private fun toolCallFingerprint(content: String): String? = runCatching { val obj = Json.parseToJsonElement(content).jsonObject val fn = obj["function"]?.jsonObject ?: return null diff --git a/testing/deterministic/src/test/kotlin/WriteReceiptCompactionTest.kt b/testing/deterministic/src/test/kotlin/WriteReceiptCompactionTest.kt new file mode 100644 index 00000000..42c82133 --- /dev/null +++ b/testing/deterministic/src/test/kotlin/WriteReceiptCompactionTest.kt @@ -0,0 +1,72 @@ +import com.correx.core.context.builder.DefaultContextPackBuilder +import com.correx.core.context.compression.DefaultContextCompressor +import com.correx.core.context.model.ContextEntry +import com.correx.core.context.model.ContextLayer +import com.correx.core.context.model.EntryRole +import com.correx.core.context.model.TokenBudget +import com.correx.core.events.types.ContextEntryId +import com.correx.core.events.types.ContextPackId +import com.correx.core.events.types.SessionId +import com.correx.core.events.types.StageId +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +// #289: a successful file_write/file_edit result echoes the whole file body back — it must be +// compacted to a receipt so it can't blow the context window, while reads and failures stay full. +class WriteReceiptCompactionTest { + + private val builder = DefaultContextPackBuilder(DefaultContextCompressor()) + private val bigBody = "x".repeat(20_000) + + private fun call(sourceId: String, name: String, path: String) = ContextEntry( + id = ContextEntryId("call-$sourceId"), + layer = ContextLayer.L2, + content = """{"id":"$sourceId","function":{"name":"$name","arguments":"{\"path\":\"$path\"}"}}""", + sourceType = "assistantToolCall", + sourceId = sourceId, + tokenEstimate = 40, + role = EntryRole.ASSISTANT, + ) + + private fun result(sourceId: String, content: String) = ContextEntry( + id = ContextEntryId("res-$sourceId"), + layer = ContextLayer.L2, + content = content, + sourceType = "toolResult", + sourceId = sourceId, + tokenEstimate = content.length / 4, + role = EntryRole.TOOL, + ) + + private suspend fun build(entries: List) = + builder.build(ContextPackId("p"), SessionId("s"), StageId("st"), entries, TokenBudget(limit = 1_000_000)) + + @Test + fun `successful write result is compacted to a receipt`(): Unit = runBlocking { + val entries = listOf( + call("tc1", "file_write", "src/A.kt"), + result("tc1", "[file_write exit=0]\n$bigBody"), + ) + val res = build(entries).layers.values.flatten().first { it.sourceType == "toolResult" } + assertFalse(res.content.contains(bigBody), "body should be elided from the receipt") + assertTrue(res.content.contains("src/A.kt"), "receipt names the path") + assertTrue(res.content.contains("body elided"), "receipt marks the elision") + } + + @Test + fun `read results and failed writes are left verbatim`(): Unit = runBlocking { + val readBody = "[file_read exit=0]\n$bigBody" + val failBody = "ERROR: could not write src/B.kt" + val entries = listOf( + call("r1", "file_read", "src/A.kt"), + result("r1", readBody), + call("w1", "file_write", "src/B.kt"), + result("w1", failBody), + ) + val results = build(entries).layers.values.flatten().filter { it.sourceType == "toolResult" } + assertTrue(results.any { it.content == readBody }, "read result stays verbatim") + assertTrue(results.any { it.content == failBody }, "failed write stays verbatim") + } +}