From bf6c8ca3fda6c556de147a586623f46a3d8dcd1c Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Thu, 25 Jun 2026 03:27:36 -0300 Subject: [PATCH] fix(compression): stop RTK over-truncating file-read tool results (#4559) (#4987) * fix(compression): stop RTK over-truncating file-read tool results (#4559) * chore(quality): trim #4559 comment to keep rtk/index.ts within size cap --------- Co-authored-by: Diego Rodrigues de Sa e Souza --- CHANGELOG.md | 2 +- .../services/compression/engines/rtk/index.ts | 33 ++++-- .../rtk-file-read-no-truncate-4559.test.ts | 108 ++++++++++++++++++ 3 files changed, 134 insertions(+), 9 deletions(-) create mode 100644 tests/unit/compression/rtk-file-read-no-truncate-4559.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c78f3bb33..195467d2ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ _In development — bullets added per PR; finalized at release._ ### 🔧 Bug Fixes - **fix(dashboard):** show custom provider given-name instead of internal id across dashboard pages — cache, combo health, compression analytics, cost overview, health/autopilot, provider stats, route explainability, provider utilization, runtime. Adds shared `resolveProviderName` resolver and `useProviderNodeMap` hook. (#4603) -- **fix(sse):** fail over on 400 responses carrying rate-limit text — providers like MiMoCode signal throttling with a non-standard 400 whose body reads `"Detected high-frequency non-compliant requests from you."`. These are now classified as fallback-worthy (`RATE_LIMIT_EXCEEDED`, connection-cooldown scope) so combo routing fails over to another free target instead of surfacing `[502]: fetch failed`. Malformed-400 detection still wins, preserving the #2101 infinite-loop guard. (#4976) +- **fix(compression):** stop RTK over-truncating file-read tool results — a tool returning a file's contents (e.g. a ~147-line code/prose file via a Read tool) is no longer head/tail-truncated by the generic-output fallback filter or the line/char hard-cap, which were silently dropping the middle. RTK now treats content with no detected command, an `unknown` type, and no error markers as a document read and skips those truncation paths; genuine repetitive command output (npm install, make, docker logs) is unaffected. (#4559) --- diff --git a/open-sse/services/compression/engines/rtk/index.ts b/open-sse/services/compression/engines/rtk/index.ts index 53e0522c33..2912ce2ce4 100644 --- a/open-sse/services/compression/engines/rtk/index.ts +++ b/open-sse/services/compression/engines/rtk/index.ts @@ -331,8 +331,21 @@ export function processRtkText( let result = text; const detection = detectCommandType(text, options.command); + // #4559: A document/file read (e.g. a Read tool returning a ~147-line code/prose + // file) is NOT repetitive command output, but the generic-output *fallback* filter + // and the final line/char hard-cap (designed for npm/make/docker logs) silently drop + // its middle. Treat content as a document read when RTK recognized no command, + // classified it "unknown", and it carries none of the generic error markers the + // generic-output filter keys on — then skip the truncating fallbacks. Genuine logs + // detect as a known command type (or carry a command / error markers), so RTK's + // value on those is preserved. + const hasGenericErrorMarkers = /Error:|Exception:|Traceback \(most recent call last\):/.test( + text + ); + const isDocumentLikeRead = + detection.type === "unknown" && !detection.command && !hasGenericErrorMarkers; let matchedFilterPatterns: string[] = []; - if (!options.skipFilters) { + if (!options.skipFilters && !isDocumentLikeRead) { const filter = matchRtkFilter(text, detection.command, { customFiltersEnabled: config.customFiltersEnabled, trustProjectFilters: config.trustProjectFilters, @@ -403,13 +416,17 @@ export function processRtkText( return []; } }); - const truncated = smartTruncate(result, { - maxLines: effectiveMaxLines(config.maxLinesPerResult, config.intensity), - maxChars: config.maxCharsPerResult, - preserveHead: config.intensity === "aggressive" ? 16 : 24, - preserveTail: config.intensity === "aggressive" ? 16 : 24, - priorityPatterns: [...defaultPriorityPatterns, ...filterPriorityPatterns], - }); + // #4559: skip the generic line/char hard-cap for document/file reads (see + // isDocumentLikeRead above) so the middle of a code/prose read is not dropped. + const truncated = isDocumentLikeRead + ? { text: result, truncated: false, droppedLines: 0 } + : smartTruncate(result, { + maxLines: effectiveMaxLines(config.maxLinesPerResult, config.intensity), + maxChars: config.maxCharsPerResult, + preserveHead: config.intensity === "aggressive" ? 16 : 24, + preserveTail: config.intensity === "aggressive" ? 16 : 24, + priorityPatterns: [...defaultPriorityPatterns, ...filterPriorityPatterns], + }); if (truncated.truncated) { result = truncated.text; techniquesUsed.push("rtk-truncate"); diff --git a/tests/unit/compression/rtk-file-read-no-truncate-4559.test.ts b/tests/unit/compression/rtk-file-read-no-truncate-4559.test.ts new file mode 100644 index 0000000000..7996a510c7 --- /dev/null +++ b/tests/unit/compression/rtk-file-read-no-truncate-4559.test.ts @@ -0,0 +1,108 @@ +/** + * TDD regression for #4559: RTK compression over-truncates tool-result content. + * + * A user reported that when a tool returns a file's contents (e.g. a ~147-line + * text/code file via a Read tool), RTK's default caps (maxLinesPerResult: 120, + * maxCharsPerResult: 12000) drop the middle and most lines are silently + * suppressed. Disabling compression OR the RTK "tool results" toggle fixes it; + * OpenRouter (no OmniRoute) is unaffected. + * + * Root cause: the final `smartTruncate` hard-cap in processRtkText fires for ANY + * tool result over 120 lines — including document/file reads — even though RTK's + * intent is repetitive *command output* (npm install logs, make, docker logs), + * NOT prose/code file reads. + * + * Fix: when the content is NOT a recognized repetitive command/log output + * (detection.type === "unknown" with no detected command — i.e. a document-style + * read), RTK must NOT apply the line/char truncation that drops the middle. + * Genuine repetitive logs (which detect as a known command type) are unaffected, + * preserving RTK's value there. + */ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; + +import { processRtkText } from "../../../open-sse/services/compression/engines/rtk/index.ts"; + +// A ~147-line mixed prose/code file read — every line is distinct content, NOT +// repetitive log noise. No shell-command prefix, so RTK detects it as "unknown". +function buildFileRead(): string { + const lines: string[] = []; + lines.push("# Module: payment processor"); + lines.push(""); + lines.push("This module reconciles charges against the ledger."); + lines.push(""); + lines.push("import { Decimal } from './decimal';"); + lines.push("import { Ledger } from './ledger';"); + lines.push(""); + for (let i = 0; i < 70; i++) { + lines.push(`export function step${i}(input: Input${i}): Result${i} {`); + lines.push(` // reconcile bucket ${i} against the prior settlement window`); + lines.push(` const value = computeBucket${i}(input.amount, input.currency);`); + lines.push(` const settled = applyDiscount${i}(value, input.coupon);`); + lines.push(` return { id: ${i}, value: settled, settledAt: input.timestamp${i} };`); + lines.push("}"); + lines.push(""); + } + return lines.join("\n"); +} + +describe("RTK file-read tool result — #4559 no over-truncation", () => { + it("does not drop the middle of a 147-line document/file read", () => { + const fileRead = buildFileRead(); + const totalLines = fileRead.split("\n").length; + assert.ok(totalLines > 120, `fixture should exceed default cap; got ${totalLines}`); + + // Default RTK tool-result path (applyToToolResults defaults true; this is the + // text path the Anthropic/OpenAI tool_result blocks feed into). + const result = processRtkText(fileRead, { + config: { maxLinesPerResult: 120, maxCharsPerResult: 12000 }, + }); + + // The hard-cap truncation must NOT have fired for a document/file read. + assert.ok( + !result.techniquesUsed.includes("rtk-truncate"), + `file-read content must not be hard-cap truncated; techniquesUsed: ${result.techniquesUsed.join( + ", " + )}` + ); + + // The truncation marker must be absent and the middle content retained. + assert.ok( + !result.text.includes("[rtk:truncated"), + "no truncation marker should be injected into a file read" + ); + + // Sample lines from the head, middle, and tail must all survive. + assert.ok(result.text.includes("step0("), "head content survives"); + assert.ok(result.text.includes("step35("), "middle content survives"); + assert.ok(result.text.includes("step69("), "tail content survives"); + + // Output retains the vast majority of the lines (not dropped below a + // reasonable threshold). + const outLines = result.text.split("\n").length; + assert.ok( + outLines >= totalLines * 0.9, + `expected to retain >=90% of lines; kept ${outLines}/${totalLines}` + ); + }); + + it("still truncates genuine repetitive command output (RTK value preserved)", () => { + // npm install-style repetitive log lines, with a detected command so RTK + // recognizes it as repetitive output worth truncating. + const noise = Array.from( + { length: 300 }, + (_, i) => `added package-${i}@1.0.0 to node_modules` + ).join("\n"); + const input = `$ npm install\n${noise}`; + + const result = processRtkText(input, { + command: "npm install", + config: { maxLinesPerResult: 120, maxCharsPerResult: 12000 }, + }); + + assert.ok( + result.compressed, + "repetitive npm install output should still be compressed" + ); + }); +});