From f8620742095f97c91f2896f3f65e7d48990a5e8e Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Mon, 29 Jun 2026 00:16:14 -0300 Subject: [PATCH] =?UTF-8?q?feat(compression):=20composite-command=20splitt?= =?UTF-8?q?er=20for=20RTK=20detection=20=E2=80=94=20roadmap=20#16=20(#5283?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit roadmap #16: composite-command splitter for RTK detection (opt-in, fail-open). Integrated into release/v3.8.40. --- CHANGELOG.md | 1 + .../engines/rtk/commandDetector.ts | 7 +- .../engines/rtk/splitCompositeCommand.ts | 111 ++++++++++++++++++ .../compression/rtk-command-detector.test.ts | 11 ++ .../compression/splitCompositeCommand.test.ts | 64 ++++++++++ 5 files changed, 192 insertions(+), 2 deletions(-) create mode 100644 open-sse/services/compression/engines/rtk/splitCompositeCommand.ts create mode 100644 tests/unit/compression/splitCompositeCommand.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 7cfd7c8a87..4480471603 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ _In development — bullets added per PR; finalized at release._ ### ✨ New Features +- **feat(compression): composite-command splitter for RTK detection** — `cd /x && git status` now detects as `git-status` (previously the whole string was treated as one command and matched no filter). A quote-aware top-level tokenizer splits on `&&`/`||`/`;` (never inside quotes or `$(…)`/backtick subshells) and feeds the **last** segment to RTK command detection, so every RTK filter/renderer fires on commands wrapped in `cd … &&`/`||`/`;` chains. O(n), no RegExp over the command (ReDoS-safe). Tier-3 item of the compression feature-extraction roadmap (#16). - **feat(mcp): `omniroute_tool_search` tool + one-line TS signatures** — new MCP tool that does lexical keyword search over every MCP tool's name/description and returns the top matches as compact one-line TypeScript signatures (~half the JSON-schema token cost), so agents discover tools on demand instead of carrying all ~88 schemas every turn. Search is ReDoS-safe (substring scoring, never `new RegExp` on the query) and deterministic; `tools/list` stays complete (no hidden tools). Adds the `read:tools` scope. Tier-1 item of the compression feature-extraction roadmap. ([#5269](https://github.com/diegosouzapw/OmniRoute/pull/5269)) - **feat(compression): RTK semantic command-output renderers (opt-in)** — adds a second, opt-in compaction layer to the RTK engine that rewrites structured command output into a far more compact semantic form: `git diff` → file headers + `@@` hunks + changed lines only; an all-green `pytest`/`jest`/`vitest`/`eslint` run → its one-line summary; `terraform`/`tofu plan` → `Plan: +N ~M -K` plus the resource list; `kubectl`/`aws` JSON arrays → a minimal table. Each renderer is conservative (no-op when the shape doesn't match) and the integration is fail-open; the test-green renderer never collapses output that carries any failure signal. Gated by `RtkConfig.enableRenderers` (default off → zero behavioral change). Eighth item of the compression feature-extraction roadmap. ([#5268](https://github.com/diegosouzapw/OmniRoute/pull/5268)) - **feat(compression): QuantumLock cache-prefix stabilization (opt-in, default off)** — recovers upstream prompt-cache hits that a volatile fragment in the system prompt would otherwise bust. When a caller injects a session UUID, unix timestamp, request-id, JWT, API-key shape, or long hex digest into the `role:system` message every turn, the longest common prefix across turns ends at that changing byte → the whole system prompt after it is re-billed and re-processed each turn. QuantumLock replaces each non-semantic volatile fragment with a **positional, value-independent** placeholder `⟦Q{i}⟧` and appends the real values in a delimited `⟦QUANTUMLOCK⟧` tail. The rewrite is **sent to the model** (lossless — not restored), so the system-prompt body becomes **byte-identical across turns** and the provider caches the long stable prefix while only the small tail differs. Opt-in, default off, applied only for caching providers (`isCachingProvider && config.quantumLock.enabled`); bounded ReDoS-safe patterns; idempotent; **no date/time patterns** (semantically meaningful — explicit non-goal). Studio gets a toggle + a "🔒 N volatile fragment(s) stabilized" dry-run badge. Seventh item of the compression feature-extraction roadmap (bench: [#5080](https://github.com/diegosouzapw/OmniRoute/pull/5080), gate: [#5127](https://github.com/diegosouzapw/OmniRoute/pull/5127), fuzzy: [#5143](https://github.com/diegosouzapw/OmniRoute/pull/5143), ionizer: [#5148](https://github.com/diegosouzapw/OmniRoute/pull/5148), TOON: [#5163](https://github.com/diegosouzapw/OmniRoute/pull/5163), CCR ranged: [#5187](https://github.com/diegosouzapw/OmniRoute/pull/5187), risk-gate: [#5243](https://github.com/diegosouzapw/OmniRoute/pull/5243)). ([#5260](https://github.com/diegosouzapw/OmniRoute/pull/5260)) diff --git a/open-sse/services/compression/engines/rtk/commandDetector.ts b/open-sse/services/compression/engines/rtk/commandDetector.ts index 459e165320..6f58a87b6a 100644 --- a/open-sse/services/compression/engines/rtk/commandDetector.ts +++ b/open-sse/services/compression/engines/rtk/commandDetector.ts @@ -1,3 +1,5 @@ +import { lastCommandSegment } from "./splitCompositeCommand.ts"; + export interface CommandDetectionResult { type: string; command: string | null; @@ -412,14 +414,15 @@ export function detectCommandFromText(text: string): string | null { const trimmed = line.trim().replace(/^\$\s+/, ""); if (!trimmed) continue; if (COMMAND_PREFIX_PATTERN.test(trimmed)) { - return trimmed; + return lastCommandSegment(trimmed); } } return null; } export function detectCommandType(text: string, command?: string | null): CommandDetectionResult { - const detectedCommand = command?.trim() || detectCommandFromText(text); + const detectedCommand = + lastCommandSegment(command?.trim() || detectCommandFromText(text) || "") || null; let best: CommandDetectionResult | null = null; for (const detector of DETECTORS) { diff --git a/open-sse/services/compression/engines/rtk/splitCompositeCommand.ts b/open-sse/services/compression/engines/rtk/splitCompositeCommand.ts new file mode 100644 index 0000000000..58d62e2c8c --- /dev/null +++ b/open-sse/services/compression/engines/rtk/splitCompositeCommand.ts @@ -0,0 +1,111 @@ +/** + * Quote-aware composite-command splitter. + * + * Splits a shell command string on top-level `&&`, `||`, or `;` separators + * (i.e. those NOT inside single quotes, double quotes, backtick subshells, + * or `$(...)` subshells) and returns the LAST significant segment (trimmed). + * + * - No separator found → returns the input unchanged. + * - Last segment is empty (e.g. trailing `&&`) → falls back to the previous non-empty one. + * - O(n) char-by-char scan; zero RegExp over the full input (anti-ReDoS). + */ +export function lastCommandSegment(command: string): string { + if (!command) return command; + + const segments: string[] = []; + let current = 0; // start index of the current segment + let depth = 0; // nesting depth for $(...) / (...) + let inSingle = false; + let inDouble = false; + let inBacktick = false; + + const push = (end: number): void => { + segments.push(command.slice(current, end)); + }; + + for (let i = 0; i < command.length; i++) { + const ch = command[i]; + + // ── quote / subshell state tracking ────────────────────────────────── + if (inSingle) { + if (ch === "'") inSingle = false; + continue; + } + if (inBacktick) { + if (ch === "`") inBacktick = false; + continue; + } + if (inDouble) { + if (ch === '"') inDouble = false; + // $( inside double-quotes still opens a subshell + if (ch === "$" && command[i + 1] === "(") { + depth++; + i++; // skip the '(' + } + continue; + } + if (depth > 0) { + if (ch === "(") depth++; + else if (ch === ")") depth--; + continue; + } + + // ── open new quote / subshell context ──────────────────────────────── + if (ch === "'") { + inSingle = true; + continue; + } + if (ch === '"') { + inDouble = true; + continue; + } + if (ch === "`") { + inBacktick = true; + continue; + } + if (ch === "$" && command[i + 1] === "(") { + depth++; + i++; // skip the '(' + continue; + } + if (ch === "(") { + depth++; + continue; + } + + // ── top-level separator detection ───────────────────────────────────── + if (ch === "&" && command[i + 1] === "&") { + push(i); + i += 1; // skip second '&' + current = i + 1; + continue; + } + if (ch === "|" && command[i + 1] === "|") { + push(i); + i += 1; // skip second '|' + current = i + 1; + continue; + } + if (ch === ";") { + push(i); + current = i + 1; + continue; + } + } + + // push the remainder + push(command.length); + + if (segments.length === 1) { + // no top-level separator found → return unchanged + return command; + } + + // find the last non-empty (after trim) segment + for (let i = segments.length - 1; i >= 0; i--) { + const trimmed = segments[i].trim(); + if (trimmed) return trimmed; + } + + return command; +} diff --git a/tests/unit/compression/rtk-command-detector.test.ts b/tests/unit/compression/rtk-command-detector.test.ts index 36b99c8417..997a93e0ca 100644 --- a/tests/unit/compression/rtk-command-detector.test.ts +++ b/tests/unit/compression/rtk-command-detector.test.ts @@ -97,6 +97,17 @@ describe("RTK command detector", () => { } }); + it("detects git-status when command is a composite like 'cd /x && git status'", () => { + assert.equal( + detectCommandType(fixture("git-status-sample.txt"), "cd /x && git status").type, + "git-status" + ); + assert.equal( + detectCommandType(fixture("git-diff-sample.txt"), "cd /repo && git diff").type, + "git-diff" + ); + }); + it("returns unknown for generic text and exposes planned alias", () => { const detection = detectCommandOutput("ordinary prose without command output"); diff --git a/tests/unit/compression/splitCompositeCommand.test.ts b/tests/unit/compression/splitCompositeCommand.test.ts new file mode 100644 index 0000000000..f670ebb0c9 --- /dev/null +++ b/tests/unit/compression/splitCompositeCommand.test.ts @@ -0,0 +1,64 @@ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; + +import { lastCommandSegment } from "../../../open-sse/services/compression/engines/rtk/splitCompositeCommand.ts"; + +describe("lastCommandSegment", () => { + it("splits on && and returns the last segment", () => { + assert.equal(lastCommandSegment("cd /x && git status"), "git status"); + }); + + it("splits on || and returns the last segment", () => { + assert.equal(lastCommandSegment("npm i || yarn"), "yarn"); + }); + + it("splits on ; and returns the last segment", () => { + assert.equal(lastCommandSegment("make; git status"), "git status"); + }); + + it("does not split inside double quotes", () => { + assert.equal(lastCommandSegment('cd "a && b" && git log'), "git log"); + }); + + it("does not split inside single quotes", () => { + assert.equal(lastCommandSegment("cd 'a && b' && git log"), "git log"); + }); + + it("does not split inside backtick subshell", () => { + assert.equal(lastCommandSegment("echo `git rev-parse` && git status"), "git status"); + }); + + it("does not split inside $(...) subshell", () => { + assert.equal(lastCommandSegment("echo $(git rev-parse) && git status"), "git status"); + }); + + it("returns input unchanged when no top-level separator exists", () => { + assert.equal(lastCommandSegment("git status"), "git status"); + assert.equal(lastCommandSegment("npm install"), "npm install"); + }); + + it("returns input unchanged for empty string", () => { + assert.equal(lastCommandSegment(""), ""); + }); + + it("falls back to previous segment when last segment is empty", () => { + // trailing separator — last segment is empty, should fall back + assert.equal(lastCommandSegment("git status &&"), "git status"); + }); + + it("trims whitespace from returned segment", () => { + assert.equal(lastCommandSegment("cd /tmp && git log"), "git log"); + }); + + it("handles chained operators returning the very last non-empty segment", () => { + assert.equal(lastCommandSegment("a && b && git diff"), "git diff"); + }); + + it("returns fast on a pathological ReDoS-style string (< 50ms)", () => { + const pathological = "a".repeat(10000) + " && " + "b".repeat(10000); + const start = Date.now(); + lastCommandSegment(pathological); + const elapsed = Date.now() - start; + assert.ok(elapsed < 50, `took ${elapsed}ms, expected < 50ms`); + }); +});