Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { lastCommandSegment } from "./splitCompositeCommand.ts";

export interface CommandDetectionResult {
type: string;
command: string | null;
Expand Down Expand Up @@ -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) {
Expand Down
111 changes: 111 additions & 0 deletions open-sse/services/compression/engines/rtk/splitCompositeCommand.ts
Original file line number Diff line number Diff line change
@@ -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;
}
11 changes: 11 additions & 0 deletions tests/unit/compression/rtk-command-detector.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down
64 changes: 64 additions & 0 deletions tests/unit/compression/splitCompositeCommand.test.ts
Original file line number Diff line number Diff line change
@@ -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`);
});
});
Loading