Files
OmniRoute/open-sse/services/compression/toolResultCompressor.ts
Diego Rodrigues de Sa e Souza 5d89fa84e7 fix(compression): end-to-end audit — fixes across the whole compression flow (#4323)
* fix(compression): SLM worker resolves deps+worker file without import.meta.url (B-SLM)

The Next.js standalone bundle (webpack) replaces createRequire(import.meta.url)
with a stub that always throws MODULE_NOT_FOUND, and freezes import.meta.url to the
build-machine path. So depsAvailable() was always false (the worker never spawned)
and resolveWorkerFile() anchored on a path absent at runtime — the SLM silently
fell back to the aggressive summarizer in production. Confirmed by inspecting
dist/.build/next/server/chunks/26410.js (stub module 215743 + frozen file:// path).

Replace both with filesystem probing from runtime anchors (process.cwd(),
process.argv[1]) that survive the bundle. Necessary complement to #4286 (deps
co-location) for the SLM to actually engage in prod; still fail-open without it.
VPS live validation deferred (Rule #18); local resolver regression tests added.

* fix(compression): ultra heuristic preserves code blocks / inline code / URLs (B-ULTRA-CODE)

ultra.ts called pruneByScore on raw text with no tombstoning, so the token pruner
dropped low-score code tokens (`b)`, `{`, `+`) inside fenced blocks while leaving the
fence markers intact — output that looked like valid code but was syntactically
destroyed. caveman + llmlingua both extract/restore preserved blocks first; ultra was
the only pruning engine that didn't.

Add pruneProseOnly(): extractPreservedBlocks tombstones fenced code, inline code,
URLs, CONST_CASE, versions; only the prose between placeholders is pruned; preserved
blocks are re-stitched verbatim.

* fix(compression): GCF round-trips values containing the inline-array pattern [..]: (B-GCF-QUOTE)

A value like `ERR[404]: Not Found` / `[Speaker 1]: Hello` nested one level deep was
emitted bare and re-parsed by the decoder as an inline-array header → it threw
`count_mismatch` (or silently decoded wrong), losing the whole block. headroomEngine
.apply() ships such blobs in prod, so this was a reachable lossless violation.

Two complementary fixes, both per SPEC §2.4:
- encode: needsQuote() now quotes strings matching `[`…`]``:` (spec compliance / other
  decoders).
- decode: the inline-array branch only fires when the bracket is in the KEY position
  (no `=` before it), so a quoted `note="ERR[404]: …"` value falls through to key=value.

* fix(compression): aggressive fidelity — keep text blocks, compress Anthropic tool_result, don't corrupt JSON (B-AGG-*)

Three fidelity fixes in the aggressive path (each TDD, aggressive-fidelity.test.ts):
- B-AGG-TEXTDROP: replaceTextContent dropped 2nd+ text blocks unconditionally; now a
  trailing block is dropped only when its text is already subsumed by newText, else kept.
- B-AGG-ANTHROPIC-TR: tool-result compression only fired for OpenAI role:tool messages;
  now Anthropic-shape tool_result content blocks (inside user messages) are compressed
  too, preserving tool_use_id + block structure.
- B-AGG-JSONTAG: the [COMPRESSED:aging:*] prefix corrupted JSON/code payloads; pure JSON
  is now kept verbatim+untagged (stays parseable), fenced blocks get the tag on a
  preceding line.

* fix(compression): accessibility collapse preserves [ref] anchors + fires on interleaved trees (B-MCPA11Y-*)

- B-MCPA11Y-ANCHORS: collapseRepeated silently dropped the omitted middle siblings'
  [ref=eNN] anchors (the agent could no longer click them); now every omitted ref is
  kept alongside the collapse notice. Wires the previously-dead preserveRefPattern.
  Invariant: extractRefs(input) ⊆ extractRefs(output).
- B-MCPA11Y-COLLAPSE: noise removal blanked lines (replace→""), and a blank line broke
  the sibling run so collapse never fired on realistic interleaved trees; noise lines
  are now deleted, and the sibling walk skips stray blanks.

* fix(compression): rtk intensity scales the line budget (B-RTK-INTENSITY)

The intensity knob only set smartTruncate's preserveHead/Tail (16↔24), which rarely
fired because the matched filter capped lines first — so minimal/standard/aggressive
produced byte-identical output on filter-matched tool output. effectiveMaxLines() now
scales the effective line budget (minimal 1.5x, standard 1x, aggressive 0.5x) at both
the per-filter and engine-level truncation sites. Both go through smartTruncate with
priorityPatterns, so error/failure lines survive at every intensity (tested).

* fix(compression): robust language detection + auto-detect honors the detected pack (B-LANG-*)

- B-LANG-DETECTOR: detector was first-match-wins on a single keyword, and some hints are
  English-ambiguous ("configuration" in fr, "error" in es) → English text misclassified.
  Now score-based (count native-keyword hits, highest wins), and the two English-ambiguous
  words are removed from the hint lists, so a lone shared word never misclassifies while
  sparse-keyword languages (id) still detect on a single native word.
- B-LANG-DORMANT: with autoDetectLanguage on but enabledPacks ["en"], detected non-English
  text fell back to the English pack, whose `articles` rule deletes foreign articles
  (pt-BR "a"/"o"). Auto-detect now uses the detected pack directly (it always has rules);
  enabledPacks still gates manual selection.

* fix(compression): mode selection enables its engine + align stacked allowlist (B-MODE-ENGINE-DECOUPLE, B-PIPELINE-DIVERGENCE)

- B-MODE-ENGINE-DECOUPLE: picking the standard/rtk MODE now runs caveman/rtk regardless of
  the per-engine enabled flag — the mode selection is the enable signal (the per-engine flag
  still gates stacked pipeline steps). Previously an operator who picked a mode but left the
  engine toggle off got silent 0% compression.
- B-PIPELINE-DIVERGENCE: the global stackedPipeline normalizer stripped
  session-dedup/ccr/headroom/llmlingua (engines the combo path accepts via KNOWN_ENGINE_IDS).
  The allowlist now matches, so the global setting can use all registered engines.

* docs(compression): correct SLM "stable" claim + document partial packs / stacked telemetry limits

- The llmlingua `stable:true` comment claimed the bundle walk-up + deps-gate were
  "confirmed against the live install" — that was wrong (webpack froze import.meta.url and
  stubbed createRequire, so the worker never spawned in prod). Corrected to reflect B-SLM.
- COMPRESSION_ENGINES.md: add a Known limitations section (SLM dep co-location requirement,
  partial de/fr/ja packs, no-op engines absent from engineBreakdown).

* fix(compression): cast normalized engine id to CompressionPipelineStep['engine'] (typecheck)
2026-06-19 21:51:12 -03:00

284 lines
8.6 KiB
TypeScript

import type { ToolStrategiesConfig } from "./types.ts";
export interface CompressionResult {
compressed: string;
strategy: "fileContent" | "grepSearch" | "shellOutput" | "json" | "errorMessage" | "none";
saved: number;
}
const ANSI_RE = /\x1b\[[0-9;]*[a-zA-Z]/g;
const SHELL_PROMPT_RE = /\$\s/;
const JSON_PREFIX_RE = /^\s*[{[]/;
const COMPRESSED_MARKER_RE = /^\[COMPRESSED:/;
function isCodeLikeLine(rawLine: string): boolean {
const line = rawLine.trimStart();
return (
line.startsWith("import ") ||
line.startsWith("export ") ||
line.startsWith("function ") ||
line.startsWith("class ") ||
line.startsWith("const ") ||
line.startsWith("let ") ||
line.startsWith("var ") ||
line.startsWith("return ") ||
line.startsWith("if(") ||
line.startsWith("if (") ||
line.startsWith("for(") ||
line.startsWith("for (") ||
line.startsWith("while(") ||
line.startsWith("while (")
);
}
function parseGrepLinePath(line: string): string | null {
const firstColon = line.indexOf(":");
if (firstColon <= 0) return null;
const secondColon = line.indexOf(":", firstColon + 1);
if (secondColon === -1) return null;
const lineNumber = line.slice(firstColon + 1, secondColon);
if (!lineNumber || ![...lineNumber].every((char) => char >= "0" && char <= "9")) {
return null;
}
const filePath = line.slice(0, firstColon);
if (!filePath || /\s/.test(filePath)) return null;
return filePath;
}
function hasErrorLikeOutput(content: string): boolean {
const lower = content.toLowerCase();
return (
lower.includes("error:") ||
lower.includes("error ") ||
lower.includes("[error]") ||
lower.includes("exception:") ||
lower.includes("exception ") ||
lower.includes("[exception]") ||
lower.includes("traceback")
);
}
function compressFileContent(content: string): string | null {
const lines = content.split("\n");
if (lines.length < 3) return null;
if (!lines.some(isCodeLikeLine)) return null;
const keep = 20;
const tail = 5;
if (lines.length <= keep + tail) return content;
const head = lines.slice(0, keep).join("\n");
const tailLines = lines.slice(-tail).join("\n");
const elided = lines.length - keep - tail;
return `${head}\n… [${elided} lines elided] …\n${tailLines}`;
}
function compressGrepSearch(content: string): string | null {
const lines = content.split("\n");
const grepLines = lines.filter((line) => parseGrepLinePath(line) !== null);
if (grepLines.length === 0) return null;
const paths = new Set<string>();
for (const line of grepLines) {
const filePath = parseGrepLinePath(line);
if (filePath) paths.add(filePath);
}
const top30 = grepLines.slice(0, 30);
const remaining = grepLines.length - top30.length;
let result = top30.join("\n");
if (remaining > 0) {
result += `\n… [${remaining} more matches]`;
}
result += `\nFiles: ${[...paths].join(", ")}`;
return result;
}
function compressShellOutput(content: string): string | null {
const hasAnsi = ANSI_RE.test(content);
const hasPrompt = SHELL_PROMPT_RE.test(content);
if (!hasAnsi && !hasPrompt) return null;
let cleaned = content.replace(ANSI_RE, "");
const lines = cleaned.split("\n");
const last50 = lines.slice(-50);
const deduped: string[] = [];
for (const line of last50) {
if (deduped.length === 0 || line !== deduped[deduped.length - 1]) {
deduped.push(line);
}
}
return deduped.join("\n");
}
function compressJson(content: string): string | null {
if (content.length <= 2000) return null;
if (!JSON_PREFIX_RE.test(content)) return null;
let parsed: unknown;
try {
parsed = JSON.parse(content);
} catch {
return null;
}
if (Array.isArray(parsed)) {
const arr = parsed as unknown[];
if (arr.length <= 7) return content;
const head = arr.slice(0, 5);
const tail = arr.slice(-2);
return JSON.stringify({ type: "array", total: arr.length, first5: head, last2: tail }, null, 2);
}
if (typeof parsed === "object" && parsed !== null) {
const obj = parsed as Record<string, unknown>;
const keys = Object.keys(obj);
const summary: Record<string, unknown> = {};
for (const key of keys.slice(0, 20)) {
const val = obj[key];
if (typeof val === "object" && val !== null) {
summary[key] = `{…${Object.keys(val as Record<string, unknown>).length} keys}`;
} else {
summary[key] = val;
}
}
if (keys.length > 20) {
summary[`_remaining_${keys.length - 20}_keys`] = true;
}
return JSON.stringify(summary, null, 2);
}
return null;
}
function compressErrorMessage(content: string): string | null {
if (!hasErrorLikeOutput(content)) return null;
const lines = content.split("\n");
const errorLine = lines[0] || "";
const stackLines = lines.slice(1);
const head = stackLines.slice(0, 10);
const tail = stackLines.length > 10 ? stackLines.slice(-3) : [];
const middle = stackLines.length > 13 ? [`… [${stackLines.length - 13} frames elided] …`] : [];
const result = [errorLine, ...head, ...middle, ...tail].join("\n");
return result;
}
function estimateTokens(text: string): number {
return Math.ceil(text.length / 4);
}
/** Minimal shape of an Anthropic `tool_result` content block. */
export interface AnthropicToolResultBlock {
type: "tool_result";
tool_use_id?: string;
content?: string | Array<{ type?: string; text?: string; [key: string]: unknown }>;
[key: string]: unknown;
}
export function isAnthropicToolResultBlock(value: unknown): value is AnthropicToolResultBlock {
return (
!!value &&
typeof value === "object" &&
(value as { type?: unknown }).type === "tool_result"
);
}
/**
* Compress the text inside an Anthropic-shape `tool_result` content block,
* reusing the same per-type strategies as OpenAI-shape tool messages. The
* `tool_use_id` and block type are preserved exactly; only the inner text is
* compressed. Returns the (possibly unchanged) block plus tokens saved.
*/
export function compressAnthropicToolResultBlock(
block: AnthropicToolResultBlock,
opts: ToolStrategiesConfig
): { block: AnthropicToolResultBlock; saved: number } {
const content = block.content;
if (typeof content === "string") {
if (!content || COMPRESSED_MARKER_RE.test(content)) return { block, saved: 0 };
const result = compressToolResult(content, opts);
if (result.strategy === "none" || result.saved <= 0) return { block, saved: 0 };
return { block: { ...block, content: result.compressed }, saved: result.saved };
}
if (Array.isArray(content)) {
let saved = 0;
let changed = false;
const nextContent = content.map((part) => {
if (
!part ||
typeof part !== "object" ||
part.type !== "text" ||
typeof part.text !== "string"
) {
return part;
}
const text = part.text;
if (!text || COMPRESSED_MARKER_RE.test(text)) return part;
const result = compressToolResult(text, opts);
if (result.strategy === "none" || result.saved <= 0) return part;
saved += result.saved;
changed = true;
return { ...part, text: result.compressed };
});
if (!changed) return { block, saved: 0 };
return { block: { ...block, content: nextContent }, saved };
}
return { block, saved: 0 };
}
export function compressToolResult(content: string, opts: ToolStrategiesConfig): CompressionResult {
if (opts.fileContent) {
const result = compressFileContent(content);
if (result !== null) {
return {
compressed: result,
strategy: "fileContent",
saved: estimateTokens(content) - estimateTokens(result),
};
}
}
if (opts.grepSearch) {
const result = compressGrepSearch(content);
if (result !== null) {
return {
compressed: result,
strategy: "grepSearch",
saved: estimateTokens(content) - estimateTokens(result),
};
}
}
if (opts.shellOutput) {
const result = compressShellOutput(content);
if (result !== null) {
return {
compressed: result,
strategy: "shellOutput",
saved: estimateTokens(content) - estimateTokens(result),
};
}
}
if (opts.json) {
const result = compressJson(content);
if (result !== null) {
return {
compressed: result,
strategy: "json",
saved: estimateTokens(content) - estimateTokens(result),
};
}
}
if (opts.errorMessage) {
const result = compressErrorMessage(content);
if (result !== null) {
return {
compressed: result,
strategy: "errorMessage",
saved: estimateTokens(content) - estimateTokens(result),
};
}
}
return { compressed: content, strategy: "none", saved: 0 };
}