mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-22 07:02:16 +03:00
* 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)
90 lines
2.7 KiB
TypeScript
90 lines
2.7 KiB
TypeScript
export interface TextBlock {
|
|
type?: string;
|
|
text?: string;
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
export interface ChatMessageLike {
|
|
role: string;
|
|
content?: string | TextBlock[] | unknown[];
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
export function isTextBlock(value: unknown): value is TextBlock {
|
|
return (
|
|
!!value &&
|
|
typeof value === "object" &&
|
|
"text" in value &&
|
|
typeof (value as TextBlock).text === "string" &&
|
|
((value as TextBlock).type === undefined ||
|
|
(value as TextBlock).type === "text" ||
|
|
(value as TextBlock).type === "input_text")
|
|
);
|
|
}
|
|
|
|
export function extractTextContent(content: ChatMessageLike["content"]): string {
|
|
if (typeof content === "string") return content;
|
|
if (!Array.isArray(content)) return "";
|
|
|
|
const textParts: string[] = [];
|
|
for (const part of content) {
|
|
if (isTextBlock(part) && part.text) {
|
|
textParts.push(part.text);
|
|
}
|
|
}
|
|
return textParts.join("\n");
|
|
}
|
|
|
|
export function mapTextContent(
|
|
msg: ChatMessageLike,
|
|
transform: (text: string, index: number) => string
|
|
): ChatMessageLike {
|
|
if (typeof msg.content === "string") {
|
|
return { ...msg, content: transform(msg.content, 0) };
|
|
}
|
|
if (!Array.isArray(msg.content)) return msg;
|
|
|
|
let textIndex = 0;
|
|
let changed = false;
|
|
const content = msg.content.map((part) => {
|
|
if (!isTextBlock(part)) return part;
|
|
const nextText = transform(part.text ?? "", textIndex);
|
|
textIndex++;
|
|
if (nextText === part.text) return part;
|
|
changed = true;
|
|
return { ...part, text: nextText };
|
|
});
|
|
|
|
return changed ? { ...msg, content } : msg;
|
|
}
|
|
|
|
export function replaceTextContent(msg: ChatMessageLike, newText: string): ChatMessageLike {
|
|
if (typeof msg.content === "string" || !Array.isArray(msg.content)) {
|
|
return { ...msg, content: newText };
|
|
}
|
|
|
|
// The first text block receives `newText`. Trailing text blocks are normally
|
|
// already subsumed by `newText` (callers build it from the JOIN of all text
|
|
// blocks via extractTextContent), so we drop them to avoid duplicating content.
|
|
// But if a caller's `newText` does NOT contain a trailing block's text, dropping
|
|
// it would silently lose content the model can no longer see (B-AGG-TEXTDROP) —
|
|
// so in that case we keep the trailing block instead of returning [].
|
|
let replaced = false;
|
|
const content = msg.content.flatMap((part) => {
|
|
if (!isTextBlock(part)) return [part];
|
|
if (!replaced) {
|
|
replaced = true;
|
|
return [{ ...part, text: newText }];
|
|
}
|
|
const partText = part.text ?? "";
|
|
if (partText && !newText.includes(partText)) return [part];
|
|
return [];
|
|
});
|
|
|
|
if (!replaced) {
|
|
return { ...msg, content: [{ type: "text", text: newText }, ...msg.content] };
|
|
}
|
|
|
|
return { ...msg, content };
|
|
}
|