diff --git a/CHANGELOG.md b/CHANGELOG.md index f23f929847..d683801c9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ _In development — bullets added per PR; finalized at release._ - **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)) - **kilocode:** anonymous (no-auth) access to Kilo Code's free models, mirroring the `opencode`/`mimocode` pattern. With no Kilo account connected, requests now fall back to the gateway's anonymous tier (`Authorization: Bearer anonymous` on `api.kilo.ai/api/openrouter`) so the free models work without signup; a connected OAuth account is still used unchanged for the paid tier (#4019 — thanks @Theadd for the reference implementation) ### 🔧 Bug Fixes diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts index cd2a774956..a4a40ddf2f 100644 --- a/open-sse/handlers/chatCore.ts +++ b/open-sse/handlers/chatCore.ts @@ -1223,14 +1223,12 @@ export async function handleChatCore({ // #3890: in a caching context, never compress the system prompt (cacheable prefix) // even if the operator disabled preserveSystemPrompt — honors the cache-aware flag // that selectCompressionStrategy can only partially apply via the mode string. - const compressionConfig = resolveCacheAwareConfig(config, compressionInputBody, { - provider, - targetFormat, - model: effectiveModel, - }); + const cacheCtx = { provider, targetFormat, model: effectiveModel }; + const compressionConfig = resolveCacheAwareConfig(config, compressionInputBody, cacheCtx); const result = await applyCompressionAsync(compressionInputBody, mode, { model: effectiveModel, config: compressionConfig, + cachingContext: cacheCtx, principalId: apiKeyInfo?.id ? String(apiKeyInfo.id) : undefined, // F3.3: stream per-engine progress live (best-effort) before compression.completed. onEngineStep: (s) => { diff --git a/open-sse/services/compression/cacheAwareConfig.ts b/open-sse/services/compression/cacheAwareConfig.ts new file mode 100644 index 0000000000..5f0c01fb71 --- /dev/null +++ b/open-sse/services/compression/cacheAwareConfig.ts @@ -0,0 +1,23 @@ +import type { CompressionConfig } from "./types.ts"; +import type { CachingDetectionContext } from "./cachingAware.ts"; +import { detectCachingContext, getCacheAwareStrategy } from "./cachingAware.ts"; + +/** + * #3890: honor the cache-aware `skipSystemPrompt` decision that + * `getCacheAwareStrategy` already computes but `selectCompressionStrategy` + * cannot return. In a caching context the system prompt is part of the + * cacheable prefix, so compressing it breaks the upstream prompt cache. + */ +export function resolveCacheAwareConfig( + config: CompressionConfig, + body?: Record, + context?: CachingDetectionContext +): CompressionConfig { + if (!body) return config; + const ctx = detectCachingContext(body, context); + const cacheAware = getCacheAwareStrategy(config.defaultMode, ctx); + if (cacheAware.skipSystemPrompt && config.preserveSystemPrompt === false) { + return { ...config, preserveSystemPrompt: true }; + } + return config; +} diff --git a/open-sse/services/compression/entrypointWrap.ts b/open-sse/services/compression/entrypointWrap.ts new file mode 100644 index 0000000000..ee83860d67 --- /dev/null +++ b/open-sse/services/compression/entrypointWrap.ts @@ -0,0 +1,43 @@ +import type { CompressionConfig, CompressionResult } from "./types.ts"; +import type { CachingDetectionContext } from "./cachingAware.ts"; +import type { RiskGateConfig } from "./riskGate/riskGate.ts"; +import { resolveRiskGate, withRiskGate } from "./riskGate/strategyWrap.ts"; +import { + resolveQuantumLock, + quantumCachingContext, + withQuantumLock, + withQuantumLockAsync, +} from "./quantumLock/index.ts"; + +export interface CompressionEntrypointOptions { + config?: CompressionConfig; + riskGate?: RiskGateConfig; + cachingContext?: CachingDetectionContext; +} + +export function withCompressionEntrypointGuards( + body: Record, + options: T | undefined, + run: (body: Record) => CompressionResult +): CompressionResult { + return withQuantumLock( + body, + resolveQuantumLock(options), + quantumCachingContext(body, options), + (quantumBody) => + withRiskGate(quantumBody, resolveRiskGate(options), (riskBody) => run(riskBody)) + ); +} + +export function withCompressionEntrypointGuardsAsync( + body: Record, + options: T | undefined, + run: (body: Record) => Promise +): Promise { + return withQuantumLockAsync( + body, + resolveQuantumLock(options), + quantumCachingContext(body, options), + run + ); +} diff --git a/open-sse/services/compression/quantumLock/index.ts b/open-sse/services/compression/quantumLock/index.ts new file mode 100644 index 0000000000..e39cbf6bee --- /dev/null +++ b/open-sse/services/compression/quantumLock/index.ts @@ -0,0 +1,17 @@ +export { + QUANTUM_PATTERNS, + TAIL_DELIM, + placeholderFor, + type QuantumCategory, + type QuantumLockConfig, + type QuantumLockStats, + type VolatileSpan, +} from "./quantumPatterns.ts"; +export { detectVolatileSpans } from "./quantumLock.ts"; +export { applyQuantumLock } from "./quantumLockStep.ts"; +export { + resolveQuantumLock, + quantumCachingContext, + withQuantumLock, + withQuantumLockAsync, +} from "./strategyWrap.ts"; diff --git a/open-sse/services/compression/quantumLock/quantumLock.ts b/open-sse/services/compression/quantumLock/quantumLock.ts new file mode 100644 index 0000000000..2ce32dbbd6 --- /dev/null +++ b/open-sse/services/compression/quantumLock/quantumLock.ts @@ -0,0 +1,58 @@ +import { + QUANTUM_PATTERNS, + type QuantumLockConfig, + type VolatileSpan, +} from "./quantumPatterns.ts"; + +interface PrioritizedSpan extends VolatileSpan { + prio: number; +} + +/** + * Detect non-semantic volatile spans in `text`, in the FIXED pattern order, then merge + * overlapping spans so a token is never double-replaced. Pure + fail-open: a throwing + * pattern aborts the scan and returns [] (QuantumLock must never corrupt a request). + * + * Merge rule (widest wins): sort by start asc, then by width desc, then by priority asc + * (earlier pattern = higher precedence); greedily keep a span only if it starts at/after + * the last accepted span's end. + */ +export function detectVolatileSpans(text: string, cfg: QuantumLockConfig): VolatileSpan[] { + if (!text) return []; + const allow = cfg.categories && cfg.categories.length > 0 ? new Set(cfg.categories) : null; + const raw: PrioritizedSpan[] = []; + + try { + QUANTUM_PATTERNS.forEach(({ category, pattern }, prio) => { + if (allow && !allow.has(category)) return; + pattern.lastIndex = 0; + let m: RegExpExecArray | null; + while ((m = pattern.exec(text)) !== null) { + if (m[0].length === 0) { + pattern.lastIndex++; + continue; + } + raw.push({ start: m.index, end: m.index + m[0].length, category, prio }); + } + }); + } catch { + return []; + } + + raw.sort((a, b) => a.start - b.start || b.end - a.end || a.prio - b.prio); + + // Greedy non-overlapping sweep: accept a span only if it starts at/after the last accepted + // span's end. A nested or PARTIALLY-overlapping span is dropped whole (never split) — this is + // always SAFE (it can only under-stabilize, never corrupt text or shift placeholder numbering). + // With the current `\b`-anchored patterns true partial overlaps are unreachable; the drop rule + // is the conservative default if a future pattern can produce one. + const merged: VolatileSpan[] = []; + let lastEnd = -1; + for (const s of raw) { + if (s.start >= lastEnd) { + merged.push({ start: s.start, end: s.end, category: s.category }); + lastEnd = s.end; + } + } + return merged; +} diff --git a/open-sse/services/compression/quantumLock/quantumLockStep.ts b/open-sse/services/compression/quantumLock/quantumLockStep.ts new file mode 100644 index 0000000000..706ae7719e --- /dev/null +++ b/open-sse/services/compression/quantumLock/quantumLockStep.ts @@ -0,0 +1,72 @@ +import { detectVolatileSpans } from "./quantumLock.ts"; +import { + placeholderFor, + TAIL_DELIM, + type QuantumCategory, + type QuantumLockConfig, + type QuantumLockStats, +} from "./quantumPatterns.ts"; + +/** + * Fresh zero-stats per call. NOT a shared singleton on purpose: `applyQuantumLock` is a public + * export and `categories` is mutable, so returning one shared object would let a stats-aggregating + * caller corrupt the no-op result for every subsequent call in the process. + */ +const emptyStats = (): QuantumLockStats => ({ fragments: 0, categories: {} }); + +function isRecord(v: unknown): v is Record { + return v !== null && typeof v === "object" && !Array.isArray(v); +} + +/** v1 stabilizes string-content system messages only. Array/multimodal ⇒ no-op (follow-up). */ +function systemTextOf(msg: Record): string { + return typeof msg.content === "string" ? msg.content : ""; +} + +/** + * Stabilize volatile fragments in the FIRST role:system message: replace each detected span + * with a positional, value-independent placeholder (⟦Q{i}⟧) and append a delimited value-tail. + * The rewritten body is sent to the model (lossless — the tail carries every value). Pure: + * clones the touched message, never mutates input. Fail-open: returns the input body + zero + * stats on every no-op path. `ctx` is accepted for symmetry with the wrapper; unused here. + */ +export function applyQuantumLock( + body: Record, + cfg: QuantumLockConfig, + _ctx?: { isCachingProvider: boolean } +): { body: Record; stats: QuantumLockStats } { + const messages = body.messages; + if (!Array.isArray(messages)) return { body, stats: emptyStats() }; + + const idx = messages.findIndex((m) => isRecord(m) && m.role === "system"); + if (idx === -1) return { body, stats: emptyStats() }; + + const sys = messages[idx] as Record; + const text = systemTextOf(sys); + if (!text) return { body, stats: emptyStats() }; + if (text.includes(TAIL_DELIM)) return { body, stats: emptyStats() }; // idempotency + + const spans = detectVolatileSpans(text, cfg); + if (spans.length === 0) return { body, stats: emptyStats() }; + + let out = ""; + let cursor = 0; + const values: string[] = []; + const categories: Partial> = {}; + spans.forEach((span, i) => { + out += text.slice(cursor, span.start) + placeholderFor(i); + values.push(text.slice(span.start, span.end)); + categories[span.category] = (categories[span.category] ?? 0) + 1; + cursor = span.end; + }); + out += text.slice(cursor); + + const tail = `\n\n${TAIL_DELIM}\n${values.map((v, i) => `${placeholderFor(i)}=${v}`).join("\n")}`; + const newMessages = messages.slice(); + newMessages[idx] = { ...sys, content: out + tail }; + + return { + body: { ...body, messages: newMessages }, + stats: { fragments: values.length, categories }, + }; +} diff --git a/open-sse/services/compression/quantumLock/quantumPatterns.ts b/open-sse/services/compression/quantumLock/quantumPatterns.ts new file mode 100644 index 0000000000..22792ff36a --- /dev/null +++ b/open-sse/services/compression/quantumLock/quantumPatterns.ts @@ -0,0 +1,80 @@ +/** + * QuantumLock leaf module: category enum, span/config/stats types, constants, and the + * fixed-order, ReDoS-bounded detection patterns. No imports — keeps every consumer one-way + * (cycle-safe). See docs/superpowers/specs/2026-06-28-compression-quantumlock-design.md. + */ + +export type QuantumCategory = + | "uuid" + | "unix_ts" + | "long_hex" + | "jwt" + | "api_key_shape" + | "request_id"; + +export interface VolatileSpan { + start: number; // inclusive char offset into the system text + end: number; // exclusive + category: QuantumCategory; +} + +export interface QuantumLockConfig { + enabled: boolean; + /** Subset of categories to stabilize. Absent/empty ⇒ all categories. */ + categories?: QuantumCategory[]; +} + +export interface QuantumLockStats { + fragments: number; + categories: Partial>; +} + +/** Idempotency sentinel + tail header. Its presence in system text ⇒ already stabilized. */ +export const TAIL_DELIM = "⟦QUANTUMLOCK⟧"; + +/** Positional, value-independent placeholder. Depends ONLY on match index. */ +export const placeholderFor = (i: number): string => `⟦Q${i}⟧`; + +interface QuantumPattern { + category: QuantumCategory; + pattern: RegExp; +} + +/** + * Detection order is FIXED: most-specific / widest first so a token is never split. + * Every variable-length run is bounded ({N,M}) — no unbounded quantifier (anti-ReDoS). + */ +export const QUANTUM_PATTERNS: QuantumPattern[] = [ + // JWTs start with base64url of `{"` → "eyJ". Run first so the whole token wins. + // Trailing negative-lookahead (not \b): a JWT signature can END in base64url `-`/`_`, + // where \b would misfire (it needs a following word char) and let the token escape detection. + { + category: "jwt", + pattern: /\beyJ[A-Za-z0-9_-]{8,512}\.[A-Za-z0-9_-]{8,512}\.[A-Za-z0-9_-]{8,512}(?![A-Za-z0-9_-])/g, + }, + // Prefixed API keys (stripe/github/slack shapes). + { + category: "api_key_shape", + pattern: /\b(?:sk|pk|rk|ghp|gho|xox[baprs])[-_][A-Za-z0-9]{16,200}\b/g, + }, + // Bearer tokens. Bounded whitespace ([ \t]{1,4}) per the file convention (no unbounded \s+). + { + category: "api_key_shape", + pattern: /\bBearer[ \t]{1,4}[A-Za-z0-9._-]{16,400}\b/g, + }, + // Canonical UUID. Runs before long_hex so its inner hex is not re-claimed. + { + category: "uuid", + pattern: /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/gi, + }, + // Correlation / request ids. + { + category: "request_id", + pattern: /\b(?:req|trace|span|corr|request)[-_][A-Za-z0-9]{6,128}\b/gi, + }, + // Digests / SHAs. After uuid/jwt so it never eats their inner hex. + { category: "long_hex", pattern: /\b[0-9a-f]{16,128}\b/gi }, + // 10- or 13-digit unix epoch in the 2001–2033 window (leading 1). LAST so it never + // fragments a longer token already claimed above. + { category: "unix_ts", pattern: /\b1[0-9]{9}(?:[0-9]{3})?\b/g }, +]; diff --git a/open-sse/services/compression/quantumLock/strategyWrap.ts b/open-sse/services/compression/quantumLock/strategyWrap.ts new file mode 100644 index 0000000000..2bf289d452 --- /dev/null +++ b/open-sse/services/compression/quantumLock/strategyWrap.ts @@ -0,0 +1,72 @@ +import { detectCachingContext, type CachingDetectionContext } from "../cachingAware.ts"; +import type { CompressionConfig, CompressionResult, CompressionStats } from "../types.ts"; +import { applyQuantumLock } from "./quantumLockStep.ts"; +import type { QuantumLockConfig, QuantumLockStats } from "./quantumPatterns.ts"; + +/** The QuantumLock config to apply, or undefined when absent/disabled. */ +export function resolveQuantumLock(options?: { config?: CompressionConfig }): QuantumLockConfig | undefined { + const ql = options?.config?.quantumLock; + return ql?.enabled ? ql : undefined; +} + +/** + * Resolve the caching gate. Production passes `options.model` (provider inferred from the + * model slug) or an explicit `options.cachingContext`; the studio dry-run forces a caching + * context so the operator can see what WOULD be stabilized. + */ +export function quantumCachingContext( + body: Record, + options?: { model?: string; cachingContext?: CachingDetectionContext } +): { isCachingProvider: boolean } { + const ctx = detectCachingContext(body, options?.cachingContext ?? { model: options?.model }); + return { isCachingProvider: ctx.isCachingProvider }; +} + +/** Attach QuantumLock stats to a result, creating a minimal stats carrier when needed. */ +function attachQuantumLockStats( + result: CompressionResult, + qlStats: QuantumLockStats +): CompressionResult { + if (result.stats) { + result.stats.quantumLock = qlStats; + return result; + } + // Downstream compression produced no stats (e.g. message too short to compress). + // Create a passthrough carrier so the quantumLock field is not lost. + const carrier: CompressionStats = { + originalTokens: 0, + compressedTokens: 0, + savingsPercent: 0, + techniquesUsed: ["quantum-lock"], + mode: "off", + timestamp: Date.now(), + quantumLock: qlStats, + }; + return { ...result, stats: carrier }; +} + +export function withQuantumLock( + body: Record, + ql: QuantumLockConfig | undefined, + ctx: { isCachingProvider: boolean }, + run: (b: Record) => CompressionResult +): CompressionResult { + if (!ql || !ctx.isCachingProvider) return run(body); + const { body: locked, stats } = applyQuantumLock(body, ql, ctx); + const result = run(locked); + if (stats.fragments > 0) return attachQuantumLockStats(result, stats); + return result; +} + +export async function withQuantumLockAsync( + body: Record, + ql: QuantumLockConfig | undefined, + ctx: { isCachingProvider: boolean }, + run: (b: Record) => Promise +): Promise { + if (!ql || !ctx.isCachingProvider) return run(body); + const { body: locked, stats } = applyQuantumLock(body, ql, ctx); + const result = await run(locked); + if (stats.fragments > 0) return attachQuantumLockStats(result, stats); + return result; +} diff --git a/open-sse/services/compression/strategySelector.ts b/open-sse/services/compression/strategySelector.ts index d47a8d57ac..4d0f85181f 100644 --- a/open-sse/services/compression/strategySelector.ts +++ b/open-sse/services/compression/strategySelector.ts @@ -34,11 +34,12 @@ import { import { resolveAdaptivePlan } from "./adaptiveCompression/resolveAdaptivePlan.ts"; import type { AdaptiveTelemetry } from "./adaptiveCompression/types.ts"; import type { RiskGateConfig } from "./riskGate/riskGate.ts"; +import { resolveRiskGate, withRiskGate, withRiskGateAsync } from "./riskGate/strategyWrap.ts"; import { - resolveRiskGate, - withRiskGate, - withRiskGateAsync, -} from "./riskGate/strategyWrap.ts"; + withCompressionEntrypointGuards, + withCompressionEntrypointGuardsAsync, +} from "./entrypointWrap.ts"; +export { resolveCacheAwareConfig } from "./cacheAwareConfig.ts"; // Re-export so existing importers (resolver test + chatCore dynamic import) keep resolving. export { planFromHeader, formatCompressionMeta, buildNamedComboLookup }; @@ -226,32 +227,6 @@ export function selectCompressionStrategy( .mode as CompressionMode; } -/** - * #3890: honor the cache-aware `skipSystemPrompt` decision that `getCacheAwareStrategy` - * already computes but that `selectCompressionStrategy` (which can only return a mode - * string) previously discarded. In a caching context the system prompt is part of the - * cacheable prefix, so compressing it breaks the upstream prompt cache. This forces - * `preserveSystemPrompt` on for caching requests even when the operator turned it off, - * and leaves non-caching requests untouched. - */ -export function resolveCacheAwareConfig( - config: CompressionConfig, - body?: Record, - context?: CachingDetectionContext -): CompressionConfig { - if (!body) return config; - const ctx = detectCachingContext(body, context); - // Only `skipSystemPrompt` is consumed here, and it depends solely on `ctx.isCachingProvider` - // (NOT on the strategy arg — see getCacheAwareStrategy), so the stored `defaultMode` is a safe - // input even though it may be "off" for a panel-configured install. If getCacheAwareStrategy is - // ever extended to key `skipSystemPrompt` on the mode, pass the resolved effective mode instead. - const cacheAware = getCacheAwareStrategy(config.defaultMode, ctx); - if (cacheAware.skipSystemPrompt && config.preserveSystemPrompt === false) { - return { ...config, preserveSystemPrompt: true }; - } - return config; -} - export function applyCompression( body: Record, mode: CompressionMode, @@ -268,9 +243,11 @@ export function applyCompression( bailout?: BailoutConfig; /** Risk-gate mask/restore wrapper (opt-in, default off). Read via resolveRiskGate. */ riskGate?: RiskGateConfig; + /** Force/override the caching gate (studio dry-run, or chatCore's resolved context). */ + cachingContext?: CachingDetectionContext; } ): CompressionResult { - return withRiskGate(body, resolveRiskGate(options), (b) => runCompression(b, mode, options)); + return withCompressionEntrypointGuards(body, options, (b) => runCompression(b, mode, options)); } function runCompression( @@ -283,6 +260,7 @@ function runCompression( principalId?: string; bailout?: BailoutConfig; riskGate?: RiskGateConfig; + cachingContext?: CachingDetectionContext; } ): CompressionResult { if (mode === "off") { @@ -417,6 +395,24 @@ export async function applyCompressionAsync( config?: CompressionConfig; principalId?: string; onEngineStep?: (step: StackedCompressionStep) => void; + cachingContext?: CachingDetectionContext; + } +): Promise { + return withCompressionEntrypointGuardsAsync(body, options, (b) => + runCompressionAsync(b, mode, options) + ); +} + +async function runCompressionAsync( + body: Record, + mode: CompressionMode, + options?: { + model?: string; + supportsVision?: boolean | null; + config?: CompressionConfig; + principalId?: string; + onEngineStep?: (step: StackedCompressionStep) => void; + cachingContext?: CachingDetectionContext; } ): Promise { if (mode === "stacked") { diff --git a/open-sse/services/compression/types.ts b/open-sse/services/compression/types.ts index 3836a68f81..01c33e35ae 100644 --- a/open-sse/services/compression/types.ts +++ b/open-sse/services/compression/types.ts @@ -14,6 +14,7 @@ import type { ContextBudgetConfig } from "./adaptiveCompression/types.ts"; import type { FidelityGateConfig } from "./fidelityGate.ts"; import type { RiskGateConfig } from "./riskGate/riskGate.ts"; import type { RiskGateStats } from "./riskGate/riskGateStep.ts"; +import type { QuantumLockConfig, QuantumLockStats } from "./quantumLock/quantumPatterns.ts"; // Re-export so consumers that already import from this module (e.g. src/lib/db/compression.ts) // can get ENGINE_IDS without a second bare `@omniroute/open-sse/...engineCatalog.ts` specifier. @@ -148,6 +149,8 @@ export interface CompressionConfig { comboOverrides: Record; compressionComboId?: string | null; stackedPipeline?: CompressionPipelineStep[]; + /** Opt-in QuantumLock cache-prefix stabilization (default off). */ + quantumLock?: QuantumLockConfig; /** Opt-in per-step fidelity gate (default disabled). */ fidelityGate?: FidelityGateConfig; /** Opt-in risk-gate pre-pass: shields sensitive spans from compression (default disabled). */ @@ -247,6 +250,8 @@ export interface CompressionStats { rejected?: boolean; rejectReason?: string; }>; + /** Present only when QuantumLock stabilized ≥1 fragment this run. */ + quantumLock?: QuantumLockStats; } export interface CompressionResult { diff --git a/src/app/(dashboard)/dashboard/compression/studio/PlayView.tsx b/src/app/(dashboard)/dashboard/compression/studio/PlayView.tsx index a0f38ba002..597c238b6e 100644 --- a/src/app/(dashboard)/dashboard/compression/studio/PlayView.tsx +++ b/src/app/(dashboard)/dashboard/compression/studio/PlayView.tsx @@ -6,6 +6,7 @@ import { DiffPane } from "./DiffPane"; import { EncoderComparisonTable } from "./EncoderComparisonTable"; import { PlaygroundInput, LANE_ENGINES } from "./PlaygroundInput"; import { RiskGateBadge } from "./RiskGateBadge"; +import { QuantumLockBadge } from "./QuantumLockBadge"; export interface PlayViewProps { text: string; onText: (t: string) => void; @@ -47,6 +48,7 @@ export function PlayView({ text, onText, laneEngines = LANE_ENGINES }: PlayViewP const [selectedLane, setSelectedLane] = useState(null); const [fidelityGate, setFidelityGate] = useState(false); const [riskGate, setRiskGate] = useState(false); + const [quantumLock, setQuantumLock] = useState(false); const { batch, loading, run } = usePreviewCompression(); const messages = [{ role: "user", content: text }]; const toggle = (e: string) => @@ -59,6 +61,7 @@ export function PlayView({ text, onText, laneEngines = LANE_ENGINES }: PlayViewP fidelityGate, fuzzyDedup, riskGate, + quantumLock, }); const activeDiff = resolveActiveDiff(batch, selectedLane); return ( @@ -77,13 +80,16 @@ export function PlayView({ text, onText, laneEngines = LANE_ENGINES }: PlayViewP onToggleFuzzy={() => setFuzzyDedup((v) => !v)} riskGate={riskGate} onToggleRisk={() => setRiskGate((v) => !v)} + quantumLock={quantumLock} + onToggleQuantum={() => setQuantumLock((v) => !v)} />
{batch?.combined && (
- Fluxo combinado — {active.join(" → ")} + Fluxo combinado — {active.join(" → ")}{" "} +
diff --git a/src/app/(dashboard)/dashboard/compression/studio/PlaygroundInput.tsx b/src/app/(dashboard)/dashboard/compression/studio/PlaygroundInput.tsx index c2ed9ac0bc..43150f3dcc 100644 --- a/src/app/(dashboard)/dashboard/compression/studio/PlaygroundInput.tsx +++ b/src/app/(dashboard)/dashboard/compression/studio/PlaygroundInput.tsx @@ -1,7 +1,7 @@ "use client"; export const LANE_ENGINES = ["session-dedup", "ccr", "lite", "rtk", "ionizer", "headroom", "caveman", "aggressive", "ultra"] as const; -export interface PlaygroundInputProps { text: string; onText: (t: string) => void; active: string[]; onToggleActive: (engine: string) => void; onRun: () => void; loading: boolean; fidelityGate: boolean; onToggleFidelity: () => void; fuzzyDedup: boolean; onToggleFuzzy: () => void; riskGate: boolean; onToggleRisk: () => void; } -export function PlaygroundInput({ text, onText, active, onToggleActive, onRun, loading, fidelityGate, onToggleFidelity, fuzzyDedup, onToggleFuzzy, riskGate, onToggleRisk }: PlaygroundInputProps) { +export interface PlaygroundInputProps { text: string; onText: (t: string) => void; active: string[]; onToggleActive: (engine: string) => void; onRun: () => void; loading: boolean; fidelityGate: boolean; onToggleFidelity: () => void; fuzzyDedup: boolean; onToggleFuzzy: () => void; riskGate: boolean; onToggleRisk: () => void; quantumLock: boolean; onToggleQuantum: () => void; } +export function PlaygroundInput({ text, onText, active, onToggleActive, onRun, loading, fidelityGate, onToggleFidelity, fuzzyDedup, onToggleFuzzy, riskGate, onToggleRisk, quantumLock, onToggleQuantum }: PlaygroundInputProps) { return (