diff --git a/changelog.d/fixes/9200-compression-off-reactive-compaction.md b/changelog.d/fixes/9200-compression-off-reactive-compaction.md new file mode 100644 index 0000000000..e4fefaf0f4 --- /dev/null +++ b/changelog.d/fixes/9200-compression-off-reactive-compaction.md @@ -0,0 +1 @@ +- **fix(compression):** honor the global compression-off setting for proactive and last-resort context compaction, preventing disabled compression from rewriting tool-call histories ([#9200](https://github.com/diegosouzapw/OmniRoute/pull/9200)) — thanks @joachimBrindeau diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts index 7421d89628..8fa9e71f9c 100644 --- a/open-sse/handlers/chatCore.ts +++ b/open-sse/handlers/chatCore.ts @@ -1092,6 +1092,10 @@ export async function handleChatCore({ // settings read below, then threaded to executor.execute() further down. Lives at // function scope because the read happens inside the per-message compression block. let contextEditingEnabled = false; + // The dashboard's global compression switch must also control the built-in + // reactive and last-resort compaction passes. Otherwise an operator selecting + // "off" still has large histories rewritten by trim_tools/purify_history. + let reactiveContextCompactionEnabled = false; // Hoisted to function scope (not just the compression-block scope below) so the // combo-resolved override survives to the final enforceOutputTokenBudget() call // further down — see #8378 (context limit resolved by the combo was silently @@ -1108,6 +1112,7 @@ export async function handleChatCore({ compressionSettings?.exclusions ); let promptCompressionEnabled = compressionSettingsResult.enabled && !compressionExcluded; + reactiveContextCompactionEnabled = compressionSettingsResult.enabled && !compressionExcluded; contextEditingEnabled = compressionSettingsResult.contextEditingEnabled; if (compressionExcluded) { void writeCompressionSkip( @@ -1757,7 +1762,7 @@ export async function handleChatCore({ // engines (Caveman/RTK). Codex Desktop / Responses clients need this path even // when those engines are off, otherwise multi-turn image sessions hard-reject // at the budget check below (#8560). - if (estimatedTokens > threshold) { + if (reactiveContextCompactionEnabled && estimatedTokens > threshold) { log?.info?.( "CONTEXT", `Proactive compression triggered: ${estimatedTokens} tokens > ${threshold} threshold (${contextLimit} limit)` @@ -1848,7 +1853,7 @@ export async function handleChatCore({ // Last-resort compaction against the concrete input budget (not the 70% threshold). // Covers cases where the proactive pass was skipped or still left the request oversized (#8560). - if (finalEstimatedInputTokens >= finalContextLimit && body) { + if (reactiveContextCompactionEnabled && finalEstimatedInputTokens >= finalContextLimit && body) { const lastResortTarget = Math.max(1, finalContextLimit - toolsReserve - 1); const lastResortAdapter = adaptBodyForCompression(body as Record); const lastResortResult = compressContext(lastResortAdapter.body, { diff --git a/tests/unit/reactive-context-compaction-policy.test.mjs b/tests/unit/reactive-context-compaction-policy.test.mjs new file mode 100644 index 0000000000..dec9833505 --- /dev/null +++ b/tests/unit/reactive-context-compaction-policy.test.mjs @@ -0,0 +1,20 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { readFileSync } from "node:fs"; + +const source = readFileSync( + new URL("../../open-sse/handlers/chatCore.ts", import.meta.url), + "utf8" +); + +test("global compression off gates reactive and last-resort context compaction", () => { + assert.match( + source, + /reactiveContextCompactionEnabled\s*=\s*compressionSettingsResult\.enabled\s*&&\s*!compressionExcluded;/ + ); + assert.match(source, /reactiveContextCompactionEnabled\s*&&\s*estimatedTokens\s*>\s*threshold/); + assert.match( + source, + /reactiveContextCompactionEnabled\s*&&\s*finalEstimatedInputTokens\s*>=\s*finalContextLimit/ + ); +});