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 <souzamiriamrodrigues790@gmail.com>
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-06-25 03:27:36 -03:00
committed by GitHub
parent d38bf09551
commit bf6c8ca3fd
3 changed files with 134 additions and 9 deletions

View File

@@ -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)
---

View File

@@ -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");

View File

@@ -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"
);
});
});