mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-20 06:02:14 +03:00
roadmap #16: composite-command splitter for RTK detection (opt-in, fail-open). Integrated into release/v3.8.40.
This commit is contained in:
committed by
GitHub
parent
9b0adabb17
commit
f862074209
@@ -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))
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
|
||||
64
tests/unit/compression/splitCompositeCommand.test.ts
Normal file
64
tests/unit/compression/splitCompositeCommand.test.ts
Normal file
@@ -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`);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user